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
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
{
private static CometStateManager stateManager;
static DefaultChannelHandler()
{
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)
{
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)
{
}
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;
public long MessageId
{
get { return this.messageId; }
set { this.messageId = value; }
}
public object Contents
{
get { return this.contents; }
set { this.contents = value; }
}
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();
CometClient cometClient =
DefaultChannelHandler.StateManager.GetCometClient(clientPrivateToken);
chatMessage.From = cometClient.DisplayName;
chatMessage.Message = message;
DefaultChannelHandler.StateManager.SendMessage(
"ChatMessage", chatMessage);
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.