Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Scalable COMET Combined with ASP.NET - Part 2

0.00/5 (No votes)
11 Jul 2008 3  
A chat application demonstrating a reusable API for COMET and ASP.NET (following on from a previous article).

Introduction

If you have read my previous article Scalable COMET Combined with ASP.NET, then you should understand what I was trying to achieve. I explained COMET and how to get the best scalable performance from ASP.NET; however, I think the previous article was a little too close to the wire. It demonstrated the technique well enough, but did not really contain any useful code. So, I thought I would write an API that wrapped up the functionality of the previous article into a neat set of classes that can be included in a typical web project, giving you the opportunity to leverage (and test) the idea.

I'm not going to go into much detail about the threading model, because it is pretty much covered in the previous article; I'm just going to cover the API and how to use it in your web applications.

I decided I would write a lightweight messaging API that is similar to the Bayeux protocol in the way it exchanges messages; however, it is not an implementation of this protocol as I believe it was overkill for what was required to get this API to work, and it is also only a draft.

My original article stated I would put together a Tic-Tac-Toe game; unfortunately, I figured the idea would be easier demonstrated with a simple chat application. The application uses a COMET channel to receive messages, and a WCF service to send messages.

The basic chat application

chat.PNG

Glossary of Terms

Below is a list of terms that I use in this document, and what they are meant to describe:

  • Channel - This is an end point that a COMET client can connect to. Any messages sent to the client must be delivered through a channel.
  • Timeout - This is when a client has been connected to a channel for a predefined amount of time and no messages have been received. A client can reconnect when they "timeout".
  • Idle Client - This is the time frame that a client has not been connected to the server, an idle client will be disconnected after a predefined time.
  • Message - A JSON message that is sent through a channel to a client.
  • Subscribed - A client that is subscribed to a channel. They are connected and ready to receive messages.

The Core Project

The core project contains all the classes required to enable COMET in your ASP.NET application. The code is very close in design to the code in the original article, but I have extended the functionality to enable the transmission of generic messages between the clients and the server.

The main class that controls the COMET mechanism is CometStateManager. This class manages a single channel within your application. This class aggregates an ICometStateProvider instance that manages the state in a particular way for your application. In the API, there is a built-in InProcCometStateProvider implementation that stores the state within the server's memory. Obviously, this is no good for load balanced environments, but one could implement a custom provider that uses a DB, or a custom state server.

To expose your channel to the outside world, it needs to be wrapped up in an IHttpAsyncHandler implementation. I actually attempted to use the asynchronous model within WCF, but found that it did not release the ASP.NET worker threads the same way as the asynchronous handler, which is a bit of a shame, and totally unexpected.

The code below demonstrates how you would setup an IHttpAsyncHandler to provide an end point for your COMET channel:

public class DefaultChannelHandler : IHttpAsyncHandler
{
    //    this is our state manager that 
    //    will manage our connected clients
    private static CometStateManager stateManager;

    static DefaultChannelHandler()
    {
        //    initialize the state manager
        stateManager = new CometStateManager(
            new InProcCometStateProvider());
    }

    #region IHttpAsyncHandler Members

        public IAsyncResult BeginProcessRequest
        (HttpContext context, AsyncCallback cb, object extraData)
        {
            return stateManager.BeginSubscribe(context, cb, extraData);
        }
    
        public void EndProcessRequest(IAsyncResult result)
        {
            stateManager.EndSubscribe(result);
        }
    
        #endregion

        #region IHttpHandler Members
    
        public bool IsReusable
        {
            get { return true; }
        }
    
        public void ProcessRequest(HttpContext context)
        {
            throw new NotImplementedException();
        }
    
        public static CometStateManager StateManager
        {
            get { return stateManager; }
        }
    
        #endregion
}

The above code is pretty simple. We have a static instance of our CometStateManager that is constructed with an implementation of ICometStateProvider. In this example, we use the built-in InProcCometStateProvider implementation.

The rest of the implementation of the class simply maps the BeginProcessRequest and EndProcessRequest methods to the BeginSubscribe and EndSubscribe methods of our CometStateManager instance.

We also need the entry in the web.config file that enables the handler.

<add verb="POST" 
     path="DefaultChannel.ashx" 
     type="Server.Channels.DefaultChannelHandler, Server" />

That's it, the channel is now ready to be subscribed to by a client.

The CometClient Class

The channel needs to keep track of clients, each client is represented in some sort of cache by an instance of the CometClient class. We don't want any old client connecting to the server or subscribing to channels without some sort of authentication, so we would implement an authentication mechanism, maybe a standard ASP.NET login form, or possibly a WCF call to a service that can validate some credentials and then initialize a client in our channel.

The code below shows the login action of the default.aspx file in the included chat application:

protected void Login_Click(object sender, EventArgs e)
{
    try
    {
        DefaultChannelHandler.StateManager.InitializeClient(
            this.username.Text, this.username.Text, this.username.Text, 5, 5);

        Response.Redirect("chat.aspx?username=" 
                          + this.username.Text);
    }
    catch (CometException ce)
    {
        if (ce.MessageId == CometException.CometClientAlreadyExists)
        {
            //  ok the comet client already exists, so we should really show
            //  an error message to the user
            this.errorMessage.Text = 
                "User is already logged into the chat application.";
        }
    }
}

We are not validating a password or anything, we are simply taking the username directly from the page and using that to identify our client. A COMET client has two tokens that are supplied by the consumer of the API:

  • PrivateToken - This is the token which is private to the client, the token that is used to subscribe to messages for that client.
  • PublicToken - This is the token which is used to identify the client to other clients. This is typically used when sending messages to a specific client.

The reason why we use a public and private token is because the private token can be used to subscribe to a channel and receive messages for that user. We don't want any other client to be able to do that apart from the original client (e.g., we don't want the messages spoofed!). Because of this reason, we use the public token if we wanted to send messages between clients.

I have also included a DisplayName property on the client that can be used to store a username; this has been added just for simplicities sake.

To setup a client in the channel, you need to call InitializeClient. This is shown above. This method takes the following parameters:

  • publicToken - The public token of the client
  • privateToken - The private token of the client
  • displayName - The display name of the client
  • connectionTimeoutSeconds - The amount of seconds a connected client will wait for a message until it responds with a timeout message
  • connectionIdleSeconds - The amount of seconds the server will wait for a client to reconnect before it kills the idle client

In the above example, InitializeClient is called, specifying the username from the form as the publicToken, privateToken, and displayName. Although this is not very secure, it is good enough for the example. To make this more secure, I could have generated a GUID for the privateToken, and kept the public token as the username.

The InitializeClient call will then call through to the ICometStateProvider.InitializeClient with a newly initialized CometClient class, and expect it to store it in a cache.

With the CometClient now available in the channel, clients may subscribe to the channel using their privateToken.

Client-Side JavaScript

To enable the client-side functionality, there is a WebResource located in the core project, Scripts/AspNetComet.js that contains all the JavaScript needed to subscribe to the channel (and a public domain JSON parser from here). To make things easier, I have included a static method on CometStateManager called RegisterAspNetCometScripts, which accepts a Page as a parameter and registers the script on that page.

protected void Page_Load(object sender, EventArgs e)
{
    CometStateManager.RegisterAspNetCometScripts(this);
}

With this call in place, we are free to use the very basic client-side API that is available to us. The example below is taken from chat.aspx in the web project, and shows how you can subscribe to a particular channel once a client has been initialized.

var defaultChannel = null;
    
function Connect()
{
    if(defaultChannel == null)
    {
        defaultChannel = 
             new AspNetComet("/DefaultChannel.ashx", 
               "<%=this.Request["username"] %>", 
               "defaultChannel");
        defaultChannel.addTimeoutHandler(TimeoutHandler);
        defaultChannel.addFailureHandler(FailureHandler);
        defaultChannel.addSuccessHandler(SuccessHandler);
        defaultChannel.subscribe();
    }
}

All the functionality for the client-side API is wrapped up in a JavaScript class called AspNetComet. An instance of this class is used to track the state of a connected client. All that is required to subscribe is the URL of the COMET end point handler, the privateToken of the CometClient, and an alias that is used to identify the channel on the client. Once we have constructed an instance of AspNetComet, we setup a bunch of handlers that are called at specific times during the COMET life cycle.

  • addTimeoutHandler - Adds a handler that is called when a client has waited for a predefined amount of time and no messages have been received.
  • addFailureHandler - Adds a handler that is called when a COMET call fails; examples of failures would be the COMET client is not recognised.
  • addSuccessHandler - Adds a handler that is called for every message that is sent to the client.

The following code shows the signatures of each handler method:

    function SuccessHandler(privateToken, channelAlias, message)
    {
        // message.n - This is the message name
        // message.c - This is the message contents
    }

    function FailureHandler(privateToken, channelAlias, errorMessage)
    {
    }

    function TimeoutHandler(privateToken, channelAlias)
    {
    }

The message parameter of the SuccessHandler is an instance of the CometMessage class. The code below shows the class and its JSON contract:

[DataContract(Name="cm")]
public class CometMessage
{
    [DataMember(Name="mid")]
        private long messageId;
        [DataMember(Name="n")]
        private string name;
        [DataMember(Name="c")]
        private object contents;

        /// <summary>
        /// Gets or Sets the MessageId, used to track 
        /// which message the Client last received
        /// </summary>
        public long MessageId
        {
            get { return this.messageId; }
            set { this.messageId = value; }
        }

        /// <summary>
        /// Gets or Sets the Content of the Message
        /// </summary>
        public object Contents
        {
            get { return this.contents; }
            set { this.contents = value; }
        }

        /// <summary>
        /// Gets or Sets the error message if this is a failure
        /// </summary>
        public string Name
        {
            get { return this.name; }
            set { this.name = value; }
        }
}

Sending a Message

In the chat web application, I have included an AJAX-enabled WCF web service that acts as the end point for the "Send Message" functionality of the chat application. The code below shows the client-side event handler for the click of the Send Message button:

function SendMessage()
{
    var service = new ChatService();
        
        service.SendMessage(
          "<%=this.Request["username"] %>",
          document.getElementById("message").value,
            function()
             {
                document.getElementById("message").value = '';
            },
            function()
            {
                alert("Send failed");
            });

}

The code constructs an instance of the ChatService client-side object that is created by the ASP.NET Web Service framework, then just calls the SendMessage method, passing over the privateToken of the client and their message.

The server code for SendMessage then takes the parameters, and writes a message to all the clients; the code below demonstrates this:

[OperationContract]
public void SendMessage(string clientPrivateToken, string message)
{
    ChatMessage chatMessage = new ChatMessage();

    //
    //  get who the message is from
    CometClient cometClient = 
      DefaultChannelHandler.StateManager.GetCometClient(clientPrivateToken);

    //  get the display name
    chatMessage.From = cometClient.DisplayName;
    chatMessage.Message = message;

    DefaultChannelHandler.StateManager.SendMessage(
      "ChatMessage", chatMessage);

    // Add your operation implementation here
    return;
}

This method looks up the CometClient from the private token, and then creates a ChatMessage object that is used as the content of the message that is sent to each connected client using the SendMessage method on the CometStateManager instance. This will trigger any connected client to the callback on the SuccessHandler method contained in chat.aspx, which writes the message to the chat area on the page.

function SuccessHandler(privateToken, alias, message)
{
    document.getElementById("messages").innerHTML += 
      message.c.f + ": " + message.c.m + "<br/>";
}

Using the Code

The website included in the solution will execute without any configuration changes, then just connect a few clients to the application login using a desired username, and chat. Messages should be received in real-time, and appear instantly to each user.

Using the API will enable you to use a COMET style approach in your AJAX enabled applications. Using WCF can be handy for sending messages to the server, this is all neatly wrapped up for you automatically, then just callback to the connected clients on a COMET channel.

History

  • 11th July 2008 - Created.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here