Cannot have many tabs open with SignalR

I've been quite fond of a library known as SignalR. The library wraps several different communication protocols: Websockets, server-sent-events, and long-polling. You don't really need to know exactly what those are. Just know that they are each a form of continuous communication with the client browser directly from the server. SignalR allows real-time communication with the client. The most common incarnation of this ability comes in the form of a chat program.

Recently I implemented a SignalR powered notification utility at work. It worked amazingly! We were able to set real-time notifications and have them sent to users on the site even if they hadn't refreshed the page. This allowed us to do countdown timers before shutting down the site for a release, as well as many other things. There is just one issue with this functionality. Browsers have a maximum number of concurrent connections, usually around 6. After opening 6 or more tabs in the browser, all pointing to the same site, subsequent tabs would simply refuse to load.

The reason for this behavior is the SignalR connections. Six tabs means six continuous connections to the server. At this point the user has essentially eaten up every allowed connection to our site through her browser. Most requests are not continuous so a maximum of 6 concurrent requests is plenty to handle a page load as well as many ajax requests in seconds. Our powerusers were opening upwards of 20 tabs to get work done quicker. Up until now this hasn't been an issue because no concurrent connections to our site existed.

Setting aside the obvious user experience design flaw that is causing our users to think they need 20 tabs open, I had to find a more immediate fix. I came to the conclusion that the only way to fix this is to limit the number of concurrent connections to 3. If I could limit them to 3 then the remaining 3 would be available to service other requests. Most users with only 1 or 2 tabs open wouldn't notice the difference and the powerusers would just miss out on the SignalR goodies for all tabs after their third.

The issue with limiting these connections is that it can be difficult to know on the server that a connection is from the same user. SignalR hub classes do not have access to session, only cookies. Cookies would be plenty if I didn't need to track non-authenticated users; for that I need access to session IDs. SignalR can't have access to session or else you would completely break the asynchronous architecture of SignalR which kind of defeats the purpose. If only I could just get the user's session ID.

Lucky for us, once a SignalR hub is started we get a connection ID that can be accessed from the client. Here is what I did to track the users' SignalR connections:

  1. I created an Entity called HubConnection that will be stored in the database.
public class HubConnection
{
    public int Id { get; set; }
    public string ConnectionId { get; set; }
    public string SessionId { get; set; }
}
  1. Next I created a simple action method to register connections. Notice that I did not put this on a SignalR hub.
public int RegisterHubConnection(string connectionId)
{
	var hubConnection = new HubConnection
	{
        ConnectionId = connectionId,
        SessionId = Session.SessionID
    };
    _dataBucket.HubConnectionRepository.Add(hubConnection);
    _dataBucket.SaveChanges();
    return _dataBucket.HubConnectionRepository.GetList(Session.SessionID).Count;
}

Because it's just a simple action method that will be hit with a plain AJAX request I have unfettered access to session :D.

  1. Lastly, on the client I start the hub and set up a callback function where I call my register action method.
$.connection.hub.start().done(function () {
	$.ajax({
		url: '/Api/Notification/RegisterHubConnection',
		data: { connectionId: $.connection.hub.id },
		type: 'post',
		success: function (connectionCount) {
			if (connectionCount > 3) {
				console.log('Too many broswer tabs open. Shutting down SignalR connection.');
				$.connection.hub.stop();
			}
		}
	});
});

After my action method adds the new hub connection it returns the number of hub connections stored in the database. If the number of connections is greater than 3 then I stop the current SignalR hub on the client, killing the concurrent connection.

  1. That's all fine, but you might ask how we remove connections stored in the database. To do this we need to go to our actual SignalR hub and make it inherit from an interface called IDisconnect and implement the Disconnect() method.
public class NotificationHub : Hub, IDisconnect
{
	IDataBucket _dataBucket;

	public NotificationHub(IDataBucket dataBucket)
	{
	    _dataBucket = dataBucket;
	}

	public Task Disconnect()
	{
		var hubConnection = _dataBucket.HubConnectionRepository.Get(Context.ConnectionId);
		_dataBucket.HubConnectionRepository.Remove(hubConnection);
        _dataBucket.SaveChanges();
		return null;
	}
}

The disconnect method is triggered when SignalR detects that it lost connection to a client. Because this method is triggered by the server (after it hasn't heard from a client in a while) we don't have access to much information about the client that disconnected. Luckily we have access to the client's connection ID and we stored it in the database alongside our session ID. All we have to do in this method is go to the database and delete the record with the connection ID.

Essentially we have created a database table that will hold records representing only connected clients. Once a client leaves then the record is removed. It's a slightly more involved solution than I had hoped for but it did the trick. Clients now only have SignalR active in their first 3 tabs and all subsequent tabs are free to load without issue.