Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#4.0

Push Messages in RESTful WCF Web Application with Comet Approach

4.91/5 (37 votes)
25 Apr 2011CPOL9 min read 115.4K   4.7K  
The article discusses a RESTful WCF based mechanism for server-to-client asynchronous (Push) notifications in web applications. Execution of WCF service notification method is suspended until either server event or timeout lapse.

Image 1

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:

C#
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. :)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)