Anthyme Caillard

June 06, 2012

This article presents the long polling pattern and shows a way to implement it with a Web real-time chat.

Introduction

The purpose of real time programming

Imagine you have distant users and you want that these users see the same state of a view and interact with it in real time (a chat, a dashboard, whatever you want), how can you do it ?

There is a development model named Comet programming that allowing these kinds of functionalities and I will show you one of its patterns: the long polling.

The timed polling technique

Inexperienced developers may use a timer on the client that refresh the view by polling the data every second (or 500 ms or less), it seems to be good but it's not really efficient.
If you have 100 users you have 100 (200 or more) calls per second even if there is no new data to load.
You can increase the time between 2 calls but your will lose responsiveness, even 1 second to have your screen refreshed can seem too slow in a lot of situations where you want or need real time responses.

The pushing technique

The solution is to "push" some information from your server to your client. Push can be used to send data or "events" to the client.
When needed (and only when needed) the server sends information to the client directly, each call is useful and the latency is near 0 (depending on workload and bandwidth).
Push is natively possible with WCF DualHttpBinding and NetTcpBinding but there are some limitations:

You need to open port dedicated to this communication (NetTcpBinding)

Some networks (firewalls) don't allow connection from server to the client (even with the Http protocol)

You can't (currently) push data to a browser so it can't be used for web development

The long polling technique

One solution (among others) is to use the long polling technique that "simulate" Push.
The long polling is in fact a solution near timed polls but give an "experience" like push let see how it proceed:

The user launch the application or go to the website

Load (or return) your view with initialized data

On the client side

Call the server if there are modifications since a timestamp

on the server side

If they are modifications, return it immediately

Else block the client call and register for modifications with a timeout (for example 30s)

On modification event, return the new data

If the timeout is finish return an empty modifications list

On the client side with the return of the call

If they are modification apply them to the view and update your timestamp

Finnaly recall the server if there are modifications (step 1)

This approach permit two things:
- When there are modifications on the server they instantaneously return to your client (like a push).
- You have a maximum of "useless" calls of n users / t timeout (with 100 users and 30s timeout = 3.3 call/s instead of 100 or 200 for 1 s / 500 ms timed polling)

There is one drawback versus real push: your request is blocked on the server side, so one thread is blocked for each user. You have to think about your number of users and threads that your webserver can allow.

Now let's code an example of long polling screen, a simple multi user web chat.

As you can see I use the WebInvoke attribute to create a REST service that return JSON.

Notifications

Now I will create a base Notification class that represent a event dedicated to a user at a specific date.
The MessageNotification class represents a new message in the chat. You can also imagine a UserJoinNotification, to show when a user join the chat.

Chat engine

Now I create a ChatEngine class that represents the chat session.
It is a singleton class and contains the user sessions.
You can send message on this object with the SendMessage method, it simply creates new message notifications on the user sessions.

Web service implementation

Now let's see the implementation of the service.

I create a helper method "GetSession" that return a UserSession by a SessionKey.
The CreateSession method create a SessionKey and an User with a "fake autoinc" id and register it to the ChatEngine.
The SendMessage method simply calls the SendMessage method of the ChatEngine with a DateTime created on the server side.

And the main method of the long polling pattern: the GetUpdates method
The principle is to create an EventWaitHandle object and wait for a timeout (lesser than the client timeout).
A subscribe to the modification of the UserSession simply stop the EventWaitHandle.

And at the end of the method I return the new notifications and the synchronization time point ticks.

The result

Final words

Ok it's a draft and you can imagine a lot of optimizations and features (disconnection clear the notification collection ...). I have advices for you:

Don't create multiple long polling calls per page (and per user if possible) because it block a call on your webserver and it can be a big problem of thread/memory usage.
A solution can be a multiplication of data present in the GetUpdatesResult object (like user joining/leaving the chat, everything you need in the page).

If you have a complex application, create an event approach
Rename the GetUpdates method to "GetNotifications" and then publish notifications to the different parts (modules) of your application with an event aggregator (like amplify). These parts can then retreive information they need like a GetMessagesFrom(ticks) for the message part, GetUserJoinFrom(ticks) for the user list part, etc.

Websocket: It is a new html 5 technology that allows real pushes to your client. At this moment the technology is not supported in a lot a browser, you should wait a little before using it.

SignalR:
It is a new framework to push data to your web client. It uses websockect if available else long polling automatically wrapped in the SignalR API.
It's not totally stable, nor integrated in the WCF stack (too bad) and you have less control but it's really really easy to push data to your client with this framework. You should try it.
I will probably create an article with SignalR if it's useful (there are already articles on SignalR uses).