Click here to Skip to main content
15,879,096 members
Articles / Web Development / ASP.NET
Article

Scalable COMET Combined with ASP.NET - Part 2

Rate me:
Please Sign up or sign in to vote.
4.91/5 (18 votes)
11 Jul 2008CPOL9 min read 179.3K   3.2K   65   89
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:

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

XML
<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:

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

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

JavaScript
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:

JavaScript
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:

C#
[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:

JavaScript
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:

C#
[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.

JavaScript
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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionThis Library + ASP .NET MVC 3 Pin
setzamora2-Sep-14 21:57
setzamora2-Sep-14 21:57 
QuestionExternal client Pin
szydera8822-Jul-12 0:38
szydera8822-Jul-12 0:38 
Generalnice work!! Pin
Member 78815188-Jul-12 10:20
Member 78815188-Jul-12 10:20 
GeneralGreat Article *5 Pin
vianika9117-Nov-11 16:18
vianika9117-Nov-11 16:18 
QuestionDoes this scale well? Pin
HemanshuBhojak12327-Jul-11 23:55
HemanshuBhojak12327-Jul-11 23:55 
QuestionGreat code! Pin
Zane Kaminski27-Jul-11 11:27
Zane Kaminski27-Jul-11 11:27 
GeneralSuperb code snippet for chat messenger Pin
kudanthaivasanth2-Jun-11 6:23
kudanthaivasanth2-Jun-11 6:23 
GeneralWhere is the latest code? Pin
Exvance114-Apr-11 4:41
Exvance114-Apr-11 4:41 
GeneralRe: Where is the latest code? Pin
zaxscd_zaxscd27-Nov-11 4:37
zaxscd_zaxscd27-Nov-11 4:37 
GeneralSorting Pin
MisterIsk3-Nov-10 3:28
MisterIsk3-Nov-10 3:28 
QuestionHow would you spin off multiple sockets from the asynchronous handler? Pin
Bill SerGio, The Infomercial King7-Oct-10 4:45
Bill SerGio, The Infomercial King7-Oct-10 4:45 
GeneralMy vote of 5 Pin
hbrenes24-Sep-10 12:50
hbrenes24-Sep-10 12:50 
GeneralMy vote of 5 Pin
rpk67u28-Jun-10 0:41
rpk67u28-Jun-10 0:41 
GeneralI get an error when the app is publishet at my winhost.com hosting "Comet Client does not exist" Pin
alexdg046-Oct-09 19:57
alexdg046-Oct-09 19:57 
GeneralI want to show how many users are connected to the comet client Pin
eguru166-Oct-09 18:17
eguru166-Oct-09 18:17 
GeneralCould not load type from assembly error. Pin
datsun_801-Jul-09 6:28
datsun_801-Jul-09 6:28 
GeneralRe: Could not load type from assembly error. Pin
datsun_801-Jul-09 8:41
datsun_801-Jul-09 8:41 
GeneralRe: Could not load type from assembly error. Pin
James Simpson23-Jul-09 5:26
James Simpson23-Jul-09 5:26 
Generalapplication running in vs 2008 development server but not in IIS Pin
mohammad7213-May-09 9:55
mohammad7213-May-09 9:55 
GeneralRe: application running in vs 2008 development server but not in IIS Pin
James Simpson15-May-09 10:01
James Simpson15-May-09 10:01 
Your IIS configuration is probably wrong.

1) ensure the handlers are correct, ensure its in the correct part of web.config (i.e for ii6/7 its different)(
2) Its been written to run from root of a website, there is a "/" in the path in the chat javascript, check its that

James

James Simpson
Web Solutions Developer
www.methodworx.com

GeneralRe: application running in vs 2008 development server but not in IIS Pin
CuongNX13-Apr-10 16:28
CuongNX13-Apr-10 16:28 
GeneralRe: application running in vs 2008 development server but not in IIS [modified] Pin
hantique21-Oct-10 16:02
hantique21-Oct-10 16:02 
GeneralRe: application running in vs 2008 development server but not in IIS Pin
mehrdadc4821-Apr-12 0:20
mehrdadc4821-Apr-12 0:20 
GeneralDownload from Pin
James Simpson1-May-09 9:39
James Simpson1-May-09 9:39 
QuestionRe: Download from Pin
yaron1123-May-09 11:59
yaron1123-May-09 11:59 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.