SignalR is Amazing!

I have to share a library with you. It's just so awesome that it can't stay contained. It's called SignalR. For a couple years I've been searching for a simple workable .NET solution to an AJAX chat application. On the surface an AJAX chat is very simple, but the way I (and many others) have been doing it is not the least bit scalable.

The Problem

The way I had attempted to structure my AJAX chat a couple years ago was with two basic functions. First I had the event to send a message to the chat.

$('#sendButton').click(function ()
{
    var message = $('#chatBox').val();
    $.ajax({
        url: '/chat/send',
        data: { message: message },
        type: 'POST'
    });
}

Simple enough to send, but then to receive new messages I had code that ran at an interval.

setInterval(function ()  
{
    $.ajax({
        url: '/chat/messages',
        data: { lastMessageReceived: id },
        success: function (response)
        {
            $('#chatDisplay').append(response);
        }
    });
}, 7000);

This may seem like a decent solution at first, but drop a hundred users in your chat and you'll soon realize that your server is getting greatly overwhelmed. This is because every 7 seconds a new request is made to the server. Even if the person walks away from their computer for a while their browser will continue to hit the server every 7 seconds. The load put on the server fielding these requests grows exponentially as more users enter the chat. It simply just wasn't practical.

The Solution

After a bit of research I found that the solution was simple, or rather the concept of the solution was simple. Obviously the best way to conceive a chat application would be to give the server the ability to call the client when it has something for it, rather than have the client poll the server every few seconds. Fortunately the web has already embraced this concept several different ways. The first is called long polling or push. The basic concept is simple; the client polls the server like normal but the server hangs onto the request for as long as it can and does not send a response. When it has something to send to the client it then uses that open request to send the data. The client receives its response and it starts another request. Another option is to use a relatively new protocol called Web Sockets that is part of the HTML 5 specification. Web sockets, as you might imagine, are just sockets tailored for the web. They provide full two-way real-time communication between client and server.

The downside to all of this is that ASP.NET and IIS are not outfitted to handle these types of things. It was designed to handle many requests rapidly. It was never intended to hang on to hundreds or even thousands of concurrent requests for a long period of time. So even though I knew of potential conceptual solutions to my issue I was still limited by my tools. I began to look into things like node.js and some other tech before finally just concluding that it wasn't worth the effort. Recently though, I've discovered SignalR and I have to tell you that it's pretty much amazing. I'm still conducting my examination of its inner-workings but on the surface it is completely intuitive and effortless.

SignalR

SignalR is a pretty intelligent library written for .NET. It takes advantage of whatever technologies your browser is capable of; if it can do web sockets then it does that, but if not it will fall back on older techniques like long polling. So in the end this works even as far back as IE 7 which is pretty impressive. It runs separate from ASP.NET so it somehow gets around some key issues that would otherwise hinder the library's ability to do what it does, such as session. Luckily though, you do have access to the user's authentication ticket. It's insanely simple to setup and it starts working right away. I was honestly blown away at how simple it is to setup, despite it's frustratingly sparse documentation. I was going to just do a brief walk through here but I think I'd like to have a little more fun with it. Let's go ahead and setup a chat application right here on Code Tunnel. I will walk through the process as I set it up.

1) I'm going to open up the Code Tunnel solution on my machine and use NuGet to install SignalR as that is the easiest way to get started.

2) In the NuGet package manager search for "SignalR" and install it. You'll notice that it has dependencies on SignalR.Hosting.AspNet and SignalR.js which it will automatically install for you. You'll also see a few other things related to SignalR such as SignalR.Client and SignalR.Ninject. SignalR.Client allows you to interact with your SignalR service from a .NET application instead of javascript which I'm sure I will find useful one day. SignalR has it's own dependency resolver, but you can install SignalR.Ninject which overrides the dependency resolver to use your Ninject kernel if you already have one for your application.

3) Once installed in your application you'll be surprised how quickly we get our chat app up and running. First we need to create what is called a "hub". It's a very simple class that derives from SignalR.Hubs.Hub; To keep this simple I just added a new ChatController to the Code Tunnel project and put my SignalR hub class in the same file.

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Web;  
using System.Web.Mvc;  
using SignalR.Hubs;

namespace CodeTunnel.UI.Controllers  
{
    public class ChatController : Controller
    {        
        public ActionResult Index()
        {
            return View();
        }

    }

    public class ChatHub : Hub
    {

    }
}

4) A SignalR hub operates somewhat like an MVC controller. All public methods on the hub are accessible by the client. However, it's a little more than that. The client can actually directly invoke the methods on the hub almost as if they were javascript functions. In a bit I will setup the hub, but first I need to setup the chat controller with a view and some basic functionality for the user to pick a nickname. I set up a very simple route in my Global.asax file and two basic action methods.

[HttpGet]
public ActionResult Index()  
{
    return View();
}

[HttpPost]
public ActionResult Index(string nickname)  
{
    ViewBag.Nickname = nickname;
    return View();
}

I then threw up a simple view that either asks for a nickname, or puts you in the chat if one has already been supplied.

@{
    ViewBag.Title = "SignalR Chat Application";
}
<script type="text/javascript" src="@Url.Content("~/scripts/jquery.signalR.min.js")" />  
<script src="signalr/hubs" type="text/javascript"></script>  
<style type="text/css">  
     #chatDisplay
    {
        width: 900px;
        background-color: #CCEECC;
        height: 200px;
    }
    #message
    {
        width: 500px;
    }
</style>

@if (ViewBag.Nickname == null)
{
    using (Html.BeginForm())
    {
        <div>
            <h2>Welcome to the Code Tunnel SignalR Chat example!</h2>
            <br />
            Please enter a nickname to be used in the chat.<br />
            @Html.TextBox("nickname", "Anonymous", new { style = "width: 250px;" }) <input type="submit" id="submit" value="Submit" />
        </div>
    }
}
else  
{
    using (Html.BeginForm("Index", "Chat", FormMethod.Post, new { id = "chatForm" }))
    {
        <div>
            <div id="chatDisplay">
            </div>
            <br />
            @ViewBag.Nickname: @Html.TextBox("message") <input type="submit" id="send" value="Send" />
        </div>
    }
}

5) Now that I have a basic user interface I can setup the SignalR goodness. Let's start with the hub class I created earlier. I'm going to add one very basic method to that class.

public class ChatHub : Hub  
{
    public void Send(string message)
    {
        Clients.addMessage(string.Format("<div>{0}: {1}</div>", Caller.nickname, message));
    }
}

6) Believe it or not that's it for the server. Now on the client side we need a little bit of code. First we have to instantiate a variable that will represent our server-side hub and allow us to call the method we defined.

var chatHub = $.connection.chatHub;  

This gives us access to our hub methods as functions on the chatHub object. But we're not quite done. We need to define the addMessage function that the Send method is calling on all the connected clients.

chatHub.addMessage = function(message) {  
    $('#chatDisplay').append(message);
};

One other nice aspect of the chatHub object is that it acts as a state manager, allowing you to pass variables back and forth between client and server. This makes it easy to tell our hub what our nickname is without having to add a bunch of method parameters. This makes our code nice and clean because the nickname won't change throughout the chat session so there is no reason to keep passing it as a method argument.

chatHub.nickname = '@ViewBag.Nickname';  

7) The final step is just to intercept the form submit for the chat and make it call our send method on the hub. The reason I did a form rather than a simple button is because by default pressing enter while an element in the form is focused will submit the form. This allows us to handle both the submit button and the enter key in the text box with one event rather than catching the keydown event for the text box and a click event for a button. But really you could have done it either way.

$('#chatForm').submit(function (e) {
    e.preventDefault();
    chatHub.send($('#nickname').val(), $('#message').val());
});

Once that's done we can go ahead and start the hub.

$.connection.hub.start();

In the end my view looks like this:

@{
    ViewBag.Title = "SignalR Chat Application";
}
<script type="text/javascript" src="@Url.Content("~/scripts/jquery.signalR.min.js")"></script>  
<script type="text/javascript" src="@Url.Content("~/signalr/hubs")"></script>  
<style type="text/css">  
    #chatDisplay
    {
        width: 900px;
        background-color: #CCEECC;
        height: 200px;
        color: #000;
        border-radius: 10px;
        padding: 5px;
    }
    #message
    {
        width: 500px;
    }
</style>

@if (ViewBag.Nickname == null)
{
    using (Html.BeginForm())
    {
        <div>
            <h2>Welcome to the Code Tunnel SignalR Chat example!</h2>
            <br />
            Please enter a nickname to be used in the chat.<br />
            @Html.TextBox("nickname", "Anonymous", new { style = "width: 250px;" }) <input type="submit" id="submit" value="Submit" />
        </div>
    }
}
else  
{
    <script type="text/javascript">
        $(function ()
        {
            var chatHub = $.connection.chatHub;
            chatHub.addMessage = function (message)
            {
                $('#chatDisplay').append(message);
            };
            chatHub.nickname = '@ViewBag.Nickname';
            $('#chatForm').submit(function (e)
            {
                e.preventDefault();
                chatHub.send($('#message').val());
            });
            $.connection.hub.start();
        });
    </script>

    using (Html.BeginForm("Index", "Chat", FormMethod.Post, new { id = "chatForm" }))
    {
        <div>
            <div id="chatDisplay">
            </div>
            <br />
            @ViewBag.Nickname: @Html.TextBox("message") <input type="submit" id="send" value="Send" />
        </div>
    }
}

Chev

Read more posts by this author.

comments powered by Disqus