I've been spending some time exploring asynchrony and scale recently. You may have seen my post about my explorations with node.js and iisnode running node on Windows.
Every application has different requirements such that rules to "make it scale" don't work for every kind of application. Scaling a web app that gets some data and for loops over it is different from an app that calls out to a high-latency mainframe is different from an app that needs to maintain a persistent connection to the server.
The old adage "when all you have it is a hammer everything looks like a nail" really holds true in the programming and web space. The more tools - and the knowledge to use them - the better. That's why I'm an advocate not only of polyglot programming but also of going deep with your main languages. When you really learn LINQ for example and get really good at dynamics, C# becomes a much more fun and expressive language.
Polling is a common example of hammering a screw. Trying to make a chat program? Poll every 5 seconds. Got a really long running transaction? Throw up an animated GIF and poll until eternity, my friend!
Long polling is another way to get things done. Basically open a connection and keep it open, forcing the client (browser) to wait, pretending it's taking a long time to return. If you have enough control on your server-side programming model, this can allow you to return data as you like over this "open connection." If the connection breaks, it's transparently re-opened and the break is hidden from both sides. In the future things like WebSockets will be another way to solve this problem when it's baked.
Persistent Connections in ASP.NET
Doing this kind of persistent connection in a chat application or stock ticker for example hasn't been easy in ASP.NET. There hasn't been a decent abstraction for this on the server or a client library to talk to it.
SignalR is an asynchronous signaling library for ASP.NET that our team is working on to help build real-time multi-user web application.
Isn't this just Socket.IO or nowjs?
Socket.IO is a client side JavaScript library that talks to node.js. Nowjs is a library that lets you call the client from the server. All these and Signalr are similar and related, but different perspectives on the same concepts. Both these JavaScript libraries expect certain things and conventions on the server-side, so it's probably possible to make the server look the way these clients would want it to look if one wanted.
SignalR is a complete client- and server-side solution with JS on client and ASP.NET on the back end to create these kinds of applications. You can get it up on GitHub.
But can I make a chat application in 12 lines of code?
I like to say
"In code, any sufficient level of abstraction is indistinguishable from magic."
That said, I suppose I could just say, sure!
Chat.DoItBaby()
But that would be a lie. Here's a real chat application in SignalR for example:
Client:
var chat = $.connection.chat;
chat.name = prompt("What's your name?", "");
chat.receive = function(name, message){
$("#messages").append("<br>"+name+": "+message);
}
$("#send-button").click(function(){
chat.distribute($("#text-input").val());
});
Server:
public class Chat : Hub {
public void Distribute(string message) {
Clients.receive(Caller.name, message);
}
}
That's maybe 12, could be 9, depends on how you roll.
More details on SignalR
SignalR is broken up into a few package on NuGet:
- SignalR - A meta package that brings in SignalR.Server and SignalR.Js (you should install this)
- SignalR.Server - Server side components needed to build SignalR endpoints
- SignalR.Js - Javascript client for SignalR
- SignalR.Client - .NET client for SignalR
- SignalR.Ninject - Ninject dependeny resolver for SignalR
If you just want to play and make a small up, start up Visual Studio 2010.
First, make an Empty ASP.NET application, and install-package SignalR with NuGet, either with the UI or the Package Console.
Second, create a new default.aspx page and add a button, a textbox, references to jQuery and jQuery.signalR along with this script.
<html >
<head runat="server">
<script src="Scripts/jquery-1.6.2.min.js" type="text/javascript"></script>
<script src="Scripts/jquery.signalR.min.js" type="text/javascript"></script>
</head>
<body>
<form id="form1" runat="server">
<div>
<script type="text/javascript">
$(function () {
var connection = $.connection('echo');
connection.received(function (data) {
$('#messages').append('<li>' + data + '</li>');
});
connection.start();
$("#broadcast").click(function () {
connection.send($('#msg').val());
});
});
</script>
<input type="text" id="msg" />
<input type="button" id="broadcast" />
<ul id="messages"></ul>
</div>
</form>
</body>
</html>
Low Level Connection
Notice we're calling /echo from the client? That is hooked up in routing in Global.asax:
RouteTable.Routes.MapConnection<MyConnection>("echo", "echo/{*operation}");
At this point, we've got two choices of models with SignalR. Let's look at the low level first.
using SignalR;
using System.Threading.Tasks;
public class MyConnection : PersistentConnection
{
protected override Task OnReceivedAsync(string clientId, string data)
{
// Broadcast data to all clients
return Connection.Broadcast(data);
}
}
We derive from PersistentConnection and can basically do whatever we want at this level. There's lots of choices:
public abstract class PersistentConnection : HttpTaskAsyncHandler, IGroupManager
{
protected ITransport _transport;
protected PersistentConnection();
protected PersistentConnection(Signaler signaler, IMessageStore store, IJsonStringifier jsonStringifier);
public IConnection Connection { get; }
public override bool IsReusable { get; }
public void AddToGroup(string clientId, string groupName);
protected virtual IConnection CreateConnection(string clientId, IEnumerable<string> groups, HttpContextBase context);
protected virtual void OnConnected(HttpContextBase context, string clientId);
protected virtual Task OnConnectedAsync(HttpContextBase context, string clientId);
protected virtual void OnDisconnect(string clientId);
protected virtual Task OnDisconnectAsync(string clientId);
protected virtual void OnError(Exception e);
protected virtual Task OnErrorAsync(Exception e);
protected virtual void OnReceived(string clientId, string data);
protected virtual Task OnReceivedAsync(string clientId, string data);
public override Task ProcessRequestAsync(HttpContext context);
public void RemoveFromGroup(string clientId, string groupName);
public void Send(object value);
public void Send(string clientId, object value);
public void SendToGroup(string groupName, object value);
}
High Level Hub
Or, we can take it up a level and just do this for our chat client after adding
<script src="/signalr/hubs" type="text/javascript"></script>
to our page.
$(function () {
// Proxy created on the fly
var chat = $.connection.chat;
// Declare a function on the chat hub so the server can invoke it
chat.addMessage = function (message) {
$('#messages').append('<li>' + message + '</li>');
};
$("#broadcast").click(function () {
// Call the chat method on the server
chat.send($('#msg').val());
});
// Start the connection
$.connection.hub.start();
});
Then there is no need for routing and the connection.chat will map to this on the server, and the server can then call the client back.
public class Chat : Hub
{
public void Send(string message)
{
// Call the addMessage method on all clients
Clients.addMessage(message);
}
}
At this point your brain should have exploded and leaked out of your ears. This is C#, server-side code and we're telling all the clients to call the addMessage() JavaScript function. We're calling the client back from the server by sending the name of the client method to call down from the server via our persistent connection. It's similar to NowJS but not a lot of people are familiar with this technique.
SignalR will handle all the connection stuff on both client and server, making sure it stays open and alive. It'll use the right connection for your browser and will scale on the server with async and await techniques (like I talked about in the node.js post where I showed scalable async evented I/O on asp.net).
Want to see this sample running LIVE?
We've got a tiny tiny chat app running at AppHarbor over at http://chatapp.apphb.com, so go beat on it. There are folks in /join aspnet. Try pasting in YouTubes or images!
It's early, but it's an interesting new LEGO piece for .NET that didn't completely exist before. Feel free to check it out on GitHub and talk to the authors of SignalR, David Fowler and Damian Edwards. Enjoy.
© 2011 Scott Hanselman. All rights reserved.