"Dream as if you'll live forever, live as if you'll die today (James Dean)"
(ASP).NET - C# - System Architecture - and more...

Implementing SSE using SignalR with Hubs

Posted under SignalR | HTML5 |

In a previous post, I had a look at how to access a SignalR server from an SSE client (EventSource) implementation when the server uses Persistent Connections.

Now I want to extend this by using SignalR Hubs implementations and see what the differences are for the client. 

SignalR Hubs

As you may know by now, SignalR provides two levels of abstractions. The first one, Persistent Connections, is a low-level abstraction on top of the transport protocol and may look similar to development against abstract sockets.

The second level are Hubs. Hubs take advantage of dynamic typing in C# and JavaScript and are recommended when needing to send different types and structures of messages.

Needless to say, the team is pushing the Hub programming model in front of the Persistent Connections model. 

Server implementation

The server implementation for hubs is slightly different, but just as easy to accomplish.

My static NotificationService class will now take an IHubConnectionContext as parameter. Note that they use now the context of Clients instead of Connections.

public class NotificationService
{
    private readonly static Lazy<Notification> instance = 
         new Lazy<Notification>(() =>         
         new Notification(GlobalHost.ConnectionManager.GetHubContext<NotificationHub>().Clients));

    private NotificationService(IHubConnectionContext clients)
    {
        this.Clients = clients;
    }

    public static NotificationService Instance { get { return instance.Value; } }

    private IHubConnectionContext Clients { get; set; }

    public void Notify(string message)
    {
        Clients.All.notify(message);
    }
}

In the Notify method we don’t Broadcast anymore to all connections, but call a dynamic method on all Clients. These clients can now “listen” to this specific method to receive the message.
This way, we can actually call as many methods as we want and let the clients decide what to do with it.

The actual hub implementation is a class that derives from the abstract Hub class.

[HubName(“notifications”)]
public class NotificationHub : Hub
{
    private readonly NotificationService notification;
 
    public NotificationConnection() : this(NotificationService.Instance){}
    public NotificationConnection(NotificationService notification)
    {
        this.notification = notification;
    }
 
    protected override Task OnConnected()
    {
        return base.OnConnected();
    }
 
    protected override Task OnReconnected()
    {
        return base.OnReconnected();
    }
 
    protected override Task OnDisconnected()
    {
        return base.OnDisconnected();
    }
}

As you can see, the methods OnConnected, OnReconnected and OnDiconnected do not contain any parameters. You can now access the caller context information through the Context property of the Hub class.

public class HubCallerContext
{
    public HubCallerContext(IRequest request, string connectionId);

    public string ConnectionId { get; }
    public NameValueCollection Headers { get; }
    public NameValueCollection QueryString { get; }
    public IRequest Request { get; }
    public IDictionary<string, Cookie> RequestCookies { get; }
    public IPrincipal User { get; }
}

Once again the last part is mapping the Hubs to the Routes.

RouteTable.Routes.MapHubs();

By default, it will map the hubs to the URL /SignalR/{hubname} where the hubname is specified in the HubName atttribute of the class. However, you can change the default URL in the MapHubs overload.

Client implementation

You need to import the dynamic created javascript proxy before accessing the hubs in the browser.

<script src="signalr/hubs" type="text/javascript"></script>

After that, the following code will start listening to the specified hub and handles the messages received on the notify method.

var hub = $.connection.notifications; // note this is the name of your hub
$.connection.hub.start();
hub.client.notify = function (msg) {  // register to the specified method to handle the messages
    showNotification(msg);
};

Now let’s look with Fiddler what happens in this case.
First we have the same negotiate as we had with Persistent Connections.
However, the connect URL has now an extra parameter, providing the name of the hub to which we wish to connect to.

 

However, when looking at the traffic generated with fiddler, there is a little bit more going on underneath. Let’s analyze.

GET /notifications/negotiate
GET /notifications/connect?transport=serverSentEvents&connectionToken=pQ5GxUVJopaCyEnz8EkWeekzjlV0Q6ABl6Yugx8MZy5pbBEf_
                                TGS9XGZpa8jMbfLjGh4Fj6Hwi6Z8i6EhXbALz5clP0t7ebLI_l4iSiBVqLtoV725gYD2hDj9U4Ma_D0
                                &connectionData=%5B%7B%22name%22%3A%22notifications%22%7D%5Dtid=4

The second request has one extra parameter that contains the name of the hub we want to connect to. The others are the same as when we tried to connect to a Persistent Connection implementation.

transport: the transport implementation we wish to use (in our case serverSentEvents)
connectionToken: as previous provided by the server through the negotiate request
connectionData: an URL-encoded json object with the hub name to connect to ==> [{"name":"notifications"}]
tid: a random generated identifier (not sure yet what this is used for)  

Client implementation using EventSource

We have once again all the information required to make a connection directly using the EventSource object.

var connectionToken = null;
var eventSource = null;
$.get('/signalr/negotiate', function (data) {
    connectionToken = data.ConnectionToken;
    eventSource = new EventSource("/signalr/connect?transport=serverSentEvents&connectionToken=" 
          + connectionToken + "&connectionData=%5B%7B%22name%22%3A%22notifications%22%7D%5D&tid=" 
          + Math.floor(Math.random() * 11));
}, false);

Now we can have an event listener again on ‘message’.

eventSource.addEventListener('message', function (e) {
         showNotification(e.data);
}, false); 

Let us look now at the stream sent from the server and in what format the message is sent.

13
data: initialized

a
data: {}

46
data: {"C":"R,1C|k,0|l,0|m,0","M":[{"H":"notifications","M":"notify","A":["this is a message"]}]}

49
data: {"C":"R,1C|k,0|l,0|m,0","M":[{"H":"notifications","M":"notify","A":["this is another message"]}]}

49
data: {"C":"R,1C|k,0|l,0|m,0","M":[{"H":"notifications","M":"notify","A":["this is another message"]}]}

a
data: {}

0

As you can see, the format of the actual message is a little extended and contains now also the name of the hub and the name of the method being called.
This is very interesting to know because it will allow the client to filter out the different types of messages being sent by the server, however it is not as elegant as the SSE standardizations describe.

Our client-parsing will now look like this.

eventSource.addEventListener('message', function (e) {
   // e.data == {"C":"R,1C|k,0|l,0|m,0","M":[{"H":"notifications","M":"notify","A":["this is another message"]}]}
   var data = JSON.parse(e.data);
   if (data.C == null) { return; } 
 
   showNotification(data.M[0].A[0]);
}, false);

This does means for non-Microsoft clients that the client does need to understand very well the message format sent from SignalR. Making it harder to create true cross-platform solutions.

Closing the connection

Closing the connection using the SignalR jquery implementation is just calling stop on the hub object.

    $.connection.hub.stop();

Closing from our SSE EventSource object is the same as with Persistent Connections.

eventSource.close();
$.ajax('/signalr/abort?transport=serverSentEvents&connectionToken=' + connectionToken,
{
   type: 'POST',
   contentType: 'application/json; charset=UTF-8'
});

Conclusion

There is not much difference between the Hubs and Persistent Connections implementations. at least not when it comes from the point of a pure SSE client side.

The advantage of calling against Hubs is that you can receive different messages, but you need to implement the logic of parsing the generic message yourself. The use of the SSE filters will not work here.

I’m afraid, if you use SignalR on the server, you cannot proclaim that you’re SSE compatible. Non the less, SignalR is a great framework if you can implement it both on client and server.

Comments (0)

Sorry, due to immense comment spam on my site, comments are completely close for everyone until I find a way to fight them.
Disclaimer: The opinions expressed herin are my own personal opinions and do not represent my employer's view in any way.
© 2012 Ronald Rosier.     Creative Commons License
Title
content stuff to be said.