Introduction
A RESTful WCF service is able to accept and process ordinary HTTP requests including requests from browsers, thus effectively acting as a a lightweight web server. In web applications, it is often desirable to notify the browser about changes and events that take place on the server. Normally, this is not a trivial task since the HTTP protocol does not support callbacks. There are three commonly used techniques to provide callback notifications, namely, polling, permanently open socket between server and client, and prolonged HTTP request a.k.a. AJAX Push or Comet. All these techniques have many flavors, and boundaries between them are blur. Polling is preferred when requirements to scalability and throughput are high. This is also the simplest technique. But when the frequency of events on the server side considerably varies, polling causes either unnecessary network overload or unwanted delay in user information update. The permanently open socket technique requires a lot of custom code for its implementation and processing of data transmitted through the socket. More importantly, this technique needs an open "non-standard" communication port on the client side. This dictates reconfiguration of the client firewall that may be problematic.
The Comet technique seems less laborious compared to the permanently open socket method since it allows more natural usage of standard mechanisms (in our case, RESTful WCF). The Comet is more complex and less scalable than polling. But the whole idea of asynchronous callback notifications is about efficiency is cases when the frequency of the server events is unpredictable and varies within a broad range. Otherwise, preference should be given to polling for its simplicity and scalability. So Comet may be the right choice, for example, for control and management systems with random events and a limited number of simultaneous users.
Background
Wikipedia provides the following brief introduction to the Comet technology:
Comet is a web application model in which a long-held HTTP request allows a web server to push data to a browser, without the browser explicitly requesting it. Comet is an umbrella term, encompassing multiple techniques for achieving this interaction. All these methods rely on features included by default in browsers, such as JavaScript, rather than on non-default plug-ins. The Comet approach differs from the original model of the web, in which a browser requests a complete web page at a time.
The use of Comet techniques in web development predates the use of the word Comet as a neologism for the collective techniques. Comet is known by several other names, including AJAX Push, Reverse AJAX, Two-way-web, HTTP streaming, and HTTP server push, among others.
This article attempts to provide simple reusable .NET classes and JavaScript functions to enable the Comet technique for asynchronous web client notification.
Design and Code
The asynchronous notification effect with the Comet approach is achieved by a combination of RESTful WCF service and the XMLHttpRequest
JavaScript object embedded into an HTML form in the browser.
The server part of the suggested Comet mechanism consists of a RESTful WCF service class CometSvc
implementing the interface ICometSvc
and the class Comet
. Together, they constitute the CometSvcLib
assembly. The code for the ICometSvc
, CometSvc
and Comet
types is given below:
namespace CometSvcLib
{
[ServiceContract(SessionMode = SessionMode.NotAllowed)]
public interface ICometSvc
{
[OperationContract]
[WebInvoke(Method = "GET", UriTemplate = CometSvc.NOTIFICATION +
"/{clientId}/{dummy}", BodyStyle = WebMessageBodyStyle.Bare)]
Message Notification(string clientId, string dummy);
}
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple,
InstanceContextMode = InstanceContextMode.PerCall)]
public class CometSvc : ICometSvc
{
public const string NOTIFICATION = "/Notification";
private AutoResetEvent ev = new AutoResetEvent(false);
public Message Notification(string clientId, string dummy)
{
string arg;
HttpStatusCode statusCode = HttpStatusCode.OK;
if (Comet.dlgtGetResponseString != null)
{
Comet.RegisterCometInstance(clientId, this);
if (ev.WaitOne(Comet.timeout))
{
lock (typeof(Comet))
{
arg = Comet.dlgtGetResponseString(clientId);
}
}
else
arg = string.Format("Timeout Elapsed {0}", clientId);
Comet.UnregisterCometInstance(clientId);
}
else
{
arg = "Error: Response generation is not implemented";
Console.WriteLine(arg);
statusCode = HttpStatusCode.InternalServerError;
}
return Comet.GenerateResponseMessage(arg, statusCode);
}
internal void SetEvent()
{
ev.Set();
}
}
}
namespace CometSvcLib
{
public class Comet : IDisposable
{
private WebServiceHost hostComet = null;
private static Dictionary<string, CometSvc> dctSvc =
new Dictionary<string, CometSvc>();
public delegate string GetResponseStringDelegate(string clientId);
public static GetResponseStringDelegate dlgtGetResponseString = null;
public static TimeSpan timeout = new TimeSpan(0, 0, 55);
public Comet()
{
hostComet = new WebServiceHost(typeof(CometSvc));
hostComet.Open();
}
public string CometSvcAddress
{
get
{
return string.Format("{0}{1}",
hostComet.BaseAddresses[0], CometSvc.NOTIFICATION);
}
}
public void Close()
{
Dispose();
}
public void Dispose()
{
if (hostComet != null && hostComet.State ==
CommunicationState.Opened)
hostComet.Close();
}
internal static void RegisterCometInstance(string clientId,
CometSvc cometSvc)
{
lock (typeof(Comet))
{
Comet.dctSvc[clientId] = cometSvc;
}
}
internal static void UnregisterCometInstance(string clientId)
{
lock (typeof(Comet))
{
if (dctSvc.ContainsKey(clientId))
Comet.dctSvc.Remove(clientId);
}
}
public static void SetEvent(string clientId)
{
CometSvc cometSvc;
lock (typeof(Comet))
{
dctSvc.TryGetValue(clientId, out cometSvc);
}
if (cometSvc != null)
cometSvc.SetEvent();
}
public static Message GenerateResponseMessage(string strResponse,
HttpStatusCode statusCode)
{
XmlReader xr;
try
{
xr = XmlReader.Create(new StringReader("<i>" + strResponse + "</i>"));
}
catch
{
xr = XmlReader.Create(new StringReader("<i>Wrong Response.</i>"));
}
Message response =
Message.CreateMessage(MessageVersion.None, string.Empty, xr);
HttpResponseMessageProperty responseProperty =
new HttpResponseMessageProperty();
responseProperty.Headers.Add("Content-Type", "text/html");
response.Properties.Add(HttpResponseMessageProperty.Name,
responseProperty);
(response.Properties.Values.ElementAt(0) as
HttpResponseMessageProperty).StatusCode = statusCode;
return response;
}
}
}
It is essential to note that the service contract of ICometSvc
has SessionMode.NotAllowed
, and the service behavior of CometSvc
is defined with ConcurrencyMode.Multiple
and InstanceContextMode.PerCall
. These definitions ensure that on each call for the service, a new instance of the CometSvc
type will be created and its method CometSvc.Notification()
will run as soon as it will be called.
The flow of the sample for this article is as follows. When the user browses http://[host_URI]:[port]/FormSvc, the method FormSvc.Init()
of the FormSvc
RESTful WCF service is called (action 1 in the figure above). The method FormSvc.Init()
starts a new thread and initiates in this thread some useful processing. Then, FormSvc.Init()
generates an HTTP response message (with the static method Comet.GenerateResponseMessage()
called) and returns the message to the browser (action 2 in the figure). The response message contains an HTML page (content of the FormTemplate.html file where the {URI} and {ID} placeholders are replaced with the CometSvc
service URI and the ID of the client, respectively). The browser displays the HTML page. This page contains a form and JavaScript section. The JavaScript code creates the XMLHttpRequest
object. This object creates and sends the HTTP request to the server with its methods open()
and send()
, and provides a callback method for the server response with its onreadystatechange
property. To provide notification, the XMLHttpRequest
object sends the HTTP request to http://[host_URI]:[port]/CometSvc/Notification/{ID}/[dummy_count] (action 3 in the figure). The last element of the request URI is some changeable integer to distinguish between the requests. It is required because the browser may not send two adjacent identical requests. This request is received by the method CometSvc.Notification()
of the CometSvc
RESTful WCF service. The CometSvc
service was created and opened in the constructor of the Comet
class, whose static instance in turn is created in the static constructor of FormSvc
.
The method registers the current instance of CometSvc
by placing it in some look-up table (Dictionary<>
in our sample) with the client ID as the key. Then, the execution of the method is suspended with ev.
WaitOne(Comet.timeout)
, where ev
is an object of the AutoResetEvent
type. CometSvc.Notification()
will be resumed either after ev.Set()
or when the timeout of the WaitOne()
method elapses. In either case, the entry for this instance of the CometSvc
type is removed from the look-up table. Timeout for the waitable event object should not exceed sendTimeout
of the service provided in the configuration file. Meanwhile, the above mentioned "some useful processing" running in a separate thread in the FormSvc
object gets an event about which the client should be notified. To inform the client, the waitable event object for a given client is set by the "useful processing", resuming the CometSvc.Notification()
method execution. The waitable event object that should be set is found in the look-up table by the client ID.
After execution of the CometSvc.Notification()
method is resumed, the delegate Comet.dlgtGetResponseString(clientId)
is called. This delegate is assigned in the FormSvc
class (in the current implementation, the delegate is static and it is assigned in the static constructor of the FormSvc
class). The delegate method generates the response string for the HTTP response message provided by a subsequent call of the Comet.GenerateResponseMessage()
method. CometSvc.Notification()
sends such a response back to the browser (action 4 in the figure) and returns. When the callback (function assigned to the onreadystatechange
property of the XMLHttpRequest
object) is called, the HTTP connection gets closed by the browser. To provide continuous notification, the callback creates a new XMLHttpRequest
object, opens an HTTP connection, and sends an HTTP request with a recursive call to the doXhr()
JavaScript function.
Sample
In the code sample, useful processing is simulated with repeated calls to an AutoResetEvent
object with random timeout. The range of the timeout "embraces" the value of the notification Comet.timeout
. So, during the sample work, in some cases, the notification event takes place within Comet.timeout
, and a notification is sent to the browser. In other cases, Comet.timeout
expires before the notification event, and some non-informative response is sent to the browser. The notification message (in our case, this is time on the server) is displayed in the text box in the HTML form, whereas the non-informative response is simply ignored.
The code sample is very simple with very little code. It may be tested locally by just running the Host.exe application and then navigating the local browser to the http://localhost:8700/FormSvc URI (in this case, in the above definition, [host_URI] = localhost and [port] = 8700). To test the sample from a remote machine, in the configuration file Host.exe.config, localhost should be replaced with the host machine IP address; the firewall on the host machine should be reconfigured to allow inbound calls to port 8700, and the host machine IP address should be defined as a trusted site for the client's browser.
Discussion
The essential element of the presented technique is the suspension of the WCF service method execution. It means suspension of quite a few service threads. So, implementing this approach, we have to keep in mind that the number of available WCF service threads is generally limited. This number may be adjusted by configuring the service throttling parameter.
In the current implementation of CometSvcLib
, synchronization locks are used in several places. This is done mainly for the look-up table protection, because this table is accessed from different threads. As we know, threads synchronization should be reduced on the server as much as possible for the sake of performance. So, in cases with many clients, when performance is critical, some tricks to avoid this synchronization may be applied. For example, when the maximal number of clients may be estimated in advance, the look-up table may be implemented as a simple array, and the client ID may be the index of the appropriate element of the array. In this case, there is no need for multi-threaded access synchronization.
As it was mentioned above, in the sample, the timeout duration was chosen to have statistically equal amount of informative and non-informative notifications. In real applications, the amount of non-informative notifications should be minimized. So the timeout should be as long as possible, being close (but little less) to the configuration sendTimeout
parameter. But with a very long timeout, the probability that the browser will be closed during the timeout and therefore further processing and suspension of notification for this client becomes unnecessary, rises. Thus, a reasonable trade-off should be assumed for the timeout duration.
Conclusions
A simple implementation of the server-to-client notification technique for a web application based on RESTful WCF was presented. The usage of RESTful WCF considerably simplifies implementation and reduces the amount of required code.
Thanks
I'd like to express my deep gratitude to a friend and colleague of mine Michael Molotsky for his valuable suggestions and useful discussion in the scope of this article and on many other things. :)