February 2013
Volume 28 Number 02
ASP.NET - Building a Simple Comet Application in the Microsoft .NET Framework
By Derrick Lau
Comet is a technique for pushing content from a Web server to a browser without an explicit request, using long-lived AJAX connections. It allows for a more interactive UX and uses less bandwidth than the typical server round-trip triggered by a page postback to retrieve more data. Although there are plenty of Comet implementations available, most are Java-based. In this article I’ll focus on building a C# service based on the cometbox code sample available at code.google.com/p/cometbox.
There are newer methods for implementing the same behavior using HTML5 features such as WebSockets and server-side events, but these are available only in the latest browser versions. If you must support older browsers, Comet is the most-compatible solution. However, the browser must support AJAX by implementing the xmlHttpRequest object; otherwise it won’t be able to support Comet-style communication.
The High-Level Architecture
Figure 1 shows basic Comet-style communication, while Figure 2 depicts the architecture of my example. Comet uses the browser’s xmlHttpRequest object, which is essential for AJAX communication, to establish a long-lived HTTP connection to a server. The server holds the connection open, and pushes content to the browser when available.
Figure 1 Comet-Style Communication
Figure 2 Architecture of the Comet Application
Between the browser and the server is a proxy page, which resides in the same Web application path as the Web page containing the client code and does nothing except forward the messages from browser to server and from server to browser. Why do you need a proxy page? I’ll explain in a bit.
The first step is to select a format for the messages exchanged between the browser and server—JSON, XML or a custom format. For simplicity’s sake, I picked JSON because it’s naturally supported in JavaScript, jQuery and the Microsoft .NET Framework, and can transmit the same amount of data as XML using fewer bytes and, therefore, less bandwidth.
To set up Comet-style communication, you open an AJAX connection to the server. The easiest way to do this is to use jQuery because it supports multiple browsers and provides some nice wrapper functions such as $.ajax. This function is essentially a wrapper for each browser’s xmlHttpRequest object, and neatly provides event handlers that can be implemented to process incoming messages from the server.
Before starting the connection, you instantiate the message to send. To do this, declare a variable and use JSON.stringify to format the data as a JSON message, as shown in Figure 3.
Figure 3 Format the Data as a JSON Message
function getResponse() {
var currentDate = new Date();
var sendMessage = JSON.stringify({
SendTimestamp: currentDate,
Message: "Message 1"
});
$.ajaxSetup({
url: "CometProxy.aspx",
type: "POST",
async: true,
global: true,
timeout: 600000
});
Next, initialize the function with the URL to connect to, the HTTP method of communication to use, the communication style and the connection timeout parameter. JQuery supplies this functionality in a library call named ajaxSetup. I set the timeout in this example to 10 minutes because I’m only building a proof of concept solution here; you can change the timeout setting to whatever you want.
Now open a connection to the server using the jQuery $.ajax method, with the definition of the success event handler as the only parameter:
$.ajax({
success: function (msg) {
// Alert("ajax.success().");
if (msg == null || msg.Message == null) {
getResponse();
return;
}
The handler tests the message object returned to ensure it contains valid information before parsing; this is necessary because if an error code is returned, jQuery will fail and display an undefined message to the user. Upon a null message, the handler should recursively call the AJAX function again and return; I’ve found that adding the return stops the code from continuing. If the message is OK, you simply read the message and write the contents to the page:
$("#_receivedMsgLabel").append(msg.Message + "<br/>");
getResponse();
return;
}
});
This creates a simple client that illustrates how Comet-style communication works, as well as providing a means for running performance and scalability tests. For my example, I put the getResponse JavaScript code in a Web user control and registered it in the codebehind so the AJAX connection opens immediately when the control is loaded onto the ASP.NET page:
public partial class JqueryJsonCometClientControl :
System.Web.UI.UserControl
{
protected void Page_Load(object sender, EventArgs e)
{
string getResponseScript =
@"<script type=text/javascript>getResponse();</script>";
Page.ClientScript.RegisterStartupScript(GetType(),
"GetResponseKey", getResponseScript);
}
}
The Server
Now that I have a client that can send and receive messages, I’ll build a service that can receive and respond to them.
I tried implementing several different techniques for Comet-style communication, including the use of ASP.NET pages and HTTP handlers, none of which were successful. What I couldn’t seem to do was get a single message to broadcast to multiple clients. Luckily, after a lot of research I stumbled across the cometbox project and found it to be the easiest approach. I did some tinkering to make it run as a Windows service so it would be easier to use, then gave it the ability to hold a long-lived connection and push content to the browser. (Unfortunately, in doing so, I wrecked some of the cross-platform compatibility.) Finally, I added support for JSON and my own HTTP content message types.
To get started, create a Windows service project in your Visual Studio solution and add a service installer component (you’ll find the instructions at bit.ly/TrHQ8O) so you can turn your service on and off in the Services applet of the Administrative Tools in Control Panel. Once this is done, you need to create two threads: one that will bind to the TCP port and receive as well as transmit messages; and one that will block on a message queue to ensure that content is transmitted only when a message is received.
First, you must create a class that listens on the TCP port for new messages and transmits the responses. Now, there are several styles of Comet communication that can be implemented, and in the implementation there’s a Server class (see the code file Comet_Win_Service HTTP\Server.cs in the sample code) to abstract these. For simplicity’s sake, however, I’ll focus on what’s required to do a very basic receive of a JSON message over HTTP, and to hold the connection until there’s content to push back.
In the Server class, I’ll create some protected members to hold objects I’ll need to access from the Server object. These include the thread that will bind to and listen on the TCP port for HTTP connections, some semaphores and a list of client objects, each of which will represent a single connection to the server. Of importance is _isListenerShutDown, which will be exposed as a public property so it can be modified in the service Stop event.
Next, in the constructor, I’ll instantiate the TCP Listener object against the port, set it for exclusive use of the port, and then start it. Then I’ll start a thread to receive and handle clients that connect to the TCP listener.
The thread that listens for client connections contains a while loop that continually resets a flag indicating whether the service Stop event was raised (see Figure 4). I set the first part of this loop to a mutex to block on all listening threads to check whether the service Stop event was raised. If so, the _isListenerShutDown property will be true. When the check completes, the mutex is released and if the service is still running, I call the TcpListener.AcceptTcpClient, which will return a TcpClient object. Optionally, I check existing TcpClients to ensure I don’t add an existing client. However, depending on the number of clients you expect, you might want to replace this with a system where the service generates a unique ID and sends it to the browser client, which remembers and resends the ID each time it communicates with the server to ensure it holds only a single connection. This can become problematic, though, if the service fails; it resets the ID counter and could give new clients already-used IDs.
Figure 4 Listening for Client Connections
private void Loop()
{
try
{
while (true)
{
TcpClient client = null;
bool isServerStopped = false;
_listenerMutex.WaitOne();
isServerStopped = _isListenerShutDown;
_listenerMutex.ReleaseMutex();
if (!isServerStopped)
{
client = listener.AcceptTcpClient();
}
else
{
continue;
}
Trace.WriteLineIf(_traceSwitch.TraceInfo, "TCP client accepted.",
"COMET Server");
bool addClientFlag = true;
Client dc = new Client(client, this, authconfig, _currentClientId);
_currentClientId++;
foreach (Client currentClient in clients)
{
if (dc.TCPClient == currentClient.TCPClient)
{
lock (_lockObj)
{
addClientFlag = false;
}
}
}
if (addClientFlag)
{
lock (_lockObj)
{
clients.Add(dc);
}
}
Finally, the thread goes through the list of clients and removes any that are no longer alive. For simplicity, I put this code in the method that’s called when the TCP listener accepts a client connection, but this can affect performance when the number of clients gets into the hundreds of thousands. If you intend on using this in public-facing Web applications, I suggest adding a timer that fires every so often and doing the cleanup in that.
When a TcpClient object is returned in the Server class Loop method, it’s used to create a client object that represents the browser client. Because each client object is created in a unique thread, as with the server constructor, the client class constructor must wait on a mutex to ensure the client hasn’t been closed before continuing. Afterward, I check the TCP stream and begin reading it, and initiate a callback handler to be executed once the read has been completed. In the callback handler, I simply read the bytes and parse them using the ParseInput method, which you can see in the sample code provided with this article.
In the ParseInput method of the Client class, I build a Request object with members that correspond to the different parts of the typical HTTP message and populate those members appropriately. First, I parse the header information by searching for the token characters, such as “\r\n,” determining the pieces of header information from the format of the HTTP header. Then I call the ParseRequestContent method to get the body of the HTTP message. The first step of ParseInput is to determine the method of HTTP communication used and the URL the request was sent to. Next, the HTTP message headers are extracted and stored in the Request object Headers property, which is a Dictionary of header types and values. Once again, take a look at the downloadable sample code to see how this is done. Finally, I load the contents of the request into the Request object’s Body property, which is just a String variable containing all the bytes of the content. The content has yet to be parsed at this point. At the end, if there are any problems with the HTTP request received from the client, I send out an appropriate error response message.
I separated the method for parsing the HTTP request’s content so I could add in support for different message types, such as plain text, XML, JSON and so forth:
public void ParseRequestContent()
{
if (String.IsNullOrEmpty(request.Body))
{
Trace.WriteLineIf(_traceSwitch.TraceVerbose,
"No content in the body of the request!");
return;
}
try
{
First the contents are written to a MemoryStream so, if necessary, they can be deserialized into object types depending on the request’s Content-Type, as certain deserializers only work with streams:
MemoryStream mem = new MemoryStream();
mem.Write(System.Text.Encoding.ASCII.GetBytes(request.Body), 0,
request.Body.Length);
mem.Seek(0, 0);
if (!request.Headers.ContainsKey("Content-Type"))
{
_lastUpdate = DateTime.Now;
_messageFormat = MessageFormat.json;
}
else
{
As shown in Figure 5, I kept the default action of handling XML-formatted messages because XML is still a popular format.
Figure 5 The Default XML Message Handler
if (request.Headers["Content-Type"].Contains("xml"))
{
Trace.WriteLineIf(_traceSwitch.TraceVerbose,
"Received XML content from client.");
_messageFormat = MessageFormat.xml;
#region Process HTTP message as XML
try
{
// Picks up message from HTTP
XmlSerializer s = new XmlSerializer(typeof(Derrick.Web.SIServer.SIRequest));
// Loads message into object for processing
Derrick.Web.SIServer.SIRequest data =
(Derrick.Web.SIServer.SIRequest)s.Deserialize(mem);
}
catch (Exception ex)
{
Trace.WriteLineIf(_traceSwitch.TraceVerbose,
"During parse of client XML request got this exception: " +
ex.ToString());
}
#endregion Process HTTP message as XML
}
For Web applications, however, I highly recommend formatting the messages in JSON as, unlike XML, it doesn’t have the overhead of beginning and cancel tags and it’s natively supported in JavaScript. I just use the Content-Type header of the HTTP request to indicate whether the message was sent in JSON, and deserialize the contents using the System.Web.Script.Serialization namespace JavaScriptSerializer class. This class makes it very easy to deserialize a JSON message into a C# object, as shown in Figure 6.
Figure 6 Deserializing a JSON Message
else if (request.Headers["Content-Type"].Contains("json"))
{
Trace.WriteLineIf(_traceSwitch.TraceVerbose,
"Received json content from client.");
_messageFormat = MessageFormat.json;
#region Process HTTP message as JSON
try
{
JavaScriptSerializer jsonSerializer = new JavaScriptSerializer();
ClientMessage3 clientMessage =
jsonSerializer.Deserialize<ClientMessage3>(request.Body);
_lastUpdate = clientMessage.SendTimestamp;
Trace.WriteLineIf(_traceSwitch.TraceVerbose,
"Received the following message: ");
Trace.WriteLineIf(_traceSwitch.TraceVerbose, "SendTimestamp: " +
clientMessage.SendTimestamp.ToString());
Trace.WriteLineIf(_traceSwitch.TraceVerbose, "Browser: " +
clientMessage.Browser);
Trace.WriteLineIf(_traceSwitch.TraceVerbose, "Message: " +
clientMessage.Message);
}
catch (Exception ex)
{
Trace.WriteLineIf(_traceSwitch.TraceVerbose,
"Error deserializing JSON message: " + ex.ToString());
}
#endregion Process HTTP message as JSON
}
Finally, for testing purposes I added a ping Content-Type that simply responds with a text HTTP response containing only the word PING. This way I can easily test to see if my Comet server is running by sending it a JSON message with Content-Type “ping,” as shown in Figure 7.
Figure 7 Content-Type “Ping”
else if (request.Headers["Content-Type"].Contains("ping"))
{
string msg = request.Body;
Trace.WriteLineIf(_traceSwitch.TraceVerbose, "Ping received.");
if (msg.Equals("PING"))
{
SendMessageEventArgs args = new SendMessageEventArgs();
args.Client = this;
args.Message = "PING";
args.Request = request;
args.Timestamp = DateTime.Now;
SendResponse(args);
}
}
Ultimately, ParseRequestContent is just a string parsing method—nothing more, nothing less. As you can see, parsing XML data is a little more involved because the content has to be written to a MemoryStream first and then deserialized, using the XmlSerializer class, into a class created to represent the message from the client.
To better organize the source code, I create a Request class, shown in Figure 8, that simply contains members to hold the headers and other information sent in the HTTP request in a manner easily accessible within the service. If you wish, you can add helper methods to determine if the request has any content or not, and authentication checks, too. However, I didn’t do this here to keep this service simple and easy to implement.
Figure 8 The Request Class
public class Request
{
public string Method;
public string Url;
public string Version;
public string Body;
public int ContentLength;
public Dictionary<string, string> Headers =
new Dictionary<string, string>();
public bool HasContent()
{
if (Headers.ContainsKey("Content-Length"))
{
ContentLength = int.Parse(Headers["Content-Length"]);
return true;
}
return false;
}
The Response class, like the Request class, contains methods to store the HTTP response information in a manner easily accessible by a C# Windows service. In the SendResponse method, I added logic to attach custom HTTP headers as required for cross-origin resource sharing (CORS), and had those headers loaded from a configuration file so they can be easily modified. The Response class also contains methods to output messages for some common HTTP statuses, such as 200, 401, 404, 405 and 500.
The SendResponse member of the Response class simply writes the message to the HTTP response stream that should still be alive, as the timeout set by the client is quite long (10 minutes):
public void SendResponse(NetworkStream stream, Client client)
{
As shown in Figure 9, the appropriate headers are added to the HTTP response to fit with the W3C specification for CORS. For simplicity, the headers are read from the configuration file so the header contents can be easily modified.
Now I add the regular HTTP response headers and content, as shown in Figure 10.
Figure 9 Adding the CORS Headers
if (client.Request.Headers.ContainsKey("Origin"))
{
AddHeader("Access-Control-Allow-Origin", client.Request.Headers["Origin"]);
Trace.WriteLineIf(_traceSwitch.TraceVerbose,
"Access-Control-Allow-Origin from client: " +
client.Request.Headers["Origin"]);
}
else
{
AddHeader("Access-Control-Allow-Origin",
ConfigurationManager.AppSettings["RequestOriginUrl"]);
Trace.WriteLineIf(_traceSwitch.TraceVerbose,
"Access-Control-Allow-Origin from config: " +
ConfigurationManager.AppSettings["RequestOriginUrl"]);
}
AddHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
AddHeader("Access-Control-Max-Age", "1000");
// AddHeader("Access-Control-Allow-Headers", "Content-Type");
string allowHeaders = ConfigurationManager.AppSettings["AllowHeaders"];
// AddHeader("Access-Control-Allow-Headers", "Content-Type, x-requested-with");
AddHeader("Access-Control-Allow-Headers", allowHeaders);
StringBuilder r = new StringBuilder();
Figure 10 Adding the Regular HTTP Response Headers
r.Append("HTTP/1.1 " + GetStatusString(Status) + "\r\n");
r.Append("Server: Derrick Comet\r\n");
r.Append("Date: " + DateTime.Now.ToUniversalTime().ToString(
"ddd, dd MMM yyyy HH':'mm':'ss 'GMT'") + "\r\n");
r.Append("Accept-Ranges: none\r\n");
foreach (KeyValuePair<string, string> header in Headers)
{
r.Append(header.Key + ": " + header.Value + "\r\n");
}
if (File != null)
{
r.Append("Content-Type: " + Mime + "\r\n");
r.Append("Content-Length: " + File.Length + "\r\n");
}
else if (Body.Length > 0)
{
r.Append("Content-Type: " + Mime + "\r\n");
r.Append("Content-Length: " + Body.Length + "\r\n");
}
r.Append("\r\n");
Here the entire HTTP response message, which was built as a String, is now written to the HTTP response stream, which was passed in as a parameter to the SendResponse method:
byte[] htext = Encoding.ASCII.GetBytes(r.ToString());
stream.Write(htext, 0, htext.Length);
Transmitting Messages
The thread to transmit messages is essentially nothing more than a While loop that blocks on a Microsoft message queue. It has a SendMessage event that’s raised when the thread picks up a message from the queue. The event is handled by a method in the server object that basically calls the SendResponse method of each client, thus broadcasting the message to every browser connected to it.
The thread waits on the appropriate message queue until there’s a message placed on it, indicating the server has some content it wishes to broadcast to the clients:
Message msg = _intranetBannerQueue.Receive();
// Holds thread until message received
Trace.WriteLineIf(_traceSwitch.TraceInfo,
"Message retrieved from the message queue.");
SendMessageEventArgs args = new SendMessageEventArgs();
args.Timestamp = DateTime.Now.ToUniversalTime();
When the message is received, it’s converted into the expected object type:
msg.Formatter = new XmlMessageFormatter(new Type[] { typeof(string) });
string cometMsg = msg.Body.ToString();
args.Message = cometMsg;
After determining what will be sent to the clients, I raise a Windows event on the server indicating there’s a message to be broadcast:
if (SendMessageEvent != null)
{
SendMessageEvent(this, args);
Trace.WriteLineIf(_traceSwitch.TraceVerbose,
"Message loop raised SendMessage event.");
}
Next, I need a method that will build the actual HTTP response body—the contents of the message the server will broadcast to all the clients. The preceding message takes the message contents dumped onto the Microsoft message queue and formats it as a JSON object for transmission to the clients via an HTTP response message, as shown in Figure 11.
Figure 11 Building the HTTP Response Body
public void SendResponse(SendMessageEventArgs args)
{
Trace.WriteLineIf(_traceSwitch.TraceVerbose,
"Client.SendResponse(args) called...");
if (args == null || args.Timestamp == null)
{
return;
}
if (_lastUpdate > args.Timestamp)
{
return;
}
bool errorInSendResponse = false;
JavaScriptSerializer jsonSerializer = null;
Next, I need to instantiate an instance of the JavaScriptSerializer object to put the message contents into JSON format. I add the following try/catch error handling because sometimes there are difficulties instantiating an instance of a JavaScriptSerializer object:
try
{
jsonSerializer = new JavaScriptSerializer();
}
catch (Exception ex)
{
errorInSendResponse = true;
Trace.WriteLine("Cannot instantiate JSON serializer: " +
ex.ToString());
}
Then I create a string variable to hold the JSON-formatted message and an instance of the Response class to send the JSON message.
I immediately do some basic error checking to make sure I’m working with a valid HTTP request. Because this Comet service spawns a thread for each TCP client, as well as for the server objects, I felt it safest to include these safety checks every so often, to make debugging easier.
Once I verify that it’s a valid request, I put together a JSON message to send to the HTTP response stream. Note that I just create the JSON message, serialize it and use it to create an HTML response message:
if (request.HasContent())
{
if (_messageFormat == MessageFormat.json)
{
ClientMessage3 jsonObjectToSend = new ClientMessage3();
jsonObjectToSend.SendTimestamp = args.Timestamp;
jsonObjectToSend.Message = args.Message;
jsonMessageToSend = jsonSerializer.Serialize(jsonObjectToSend);
response = Response.GetHtmlResponse(jsonMessageToSend,
args.Timestamp, _messageFormat);
response.SendResponse(stream, this);
}
To hook it all together, I first create instances of the message loop object and the server loop object during the service Start event. Note that these objects should be protected members of the service class so that methods on them can be called during other service events. Now the message loop send message event should be handled by the server object BroadcastMessage method:
public override void BroadcastMessage(Object sender,
SendMessageEventArgs args)
{
// Throw new NotImplementedException();
Trace.WriteLineIf(_traceSwitch.TraceVerbose,
"Broadcasting message [" + args.Message + "] to all clients.");
int numOfClients = clients.Count;
for (int i = 0; i < numOfClients; i++)
{
clients[i].SendResponse(args);
}
}
The BroadcastMessage just sends the same message to all clients. If you wish, you can modify it to send the message only to the clients you want; in this way you can use this service to handle, for instance, multiple online chat rooms.
The OnStop method is called when the service is stopped. It subsequently calls the Shutdown method of the server object, which goes through the list of client objects that are still valid and shuts them down.
At this point, I have a reasonably decent working Comet service, which I can install into the services applet from the command prompt using the installutil command (for more information, see bit.ly/OtQCB7). You could also create your own Windows installer to deploy it, as you’ve already added the service installer components to the service project.
Why Doesn’t It Work? The Problem with CORS
Now, try setting the URL in the $.ajax call of the browser client to point to the Comet service URL. Start the Comet service and open the browser client in Firefox. Make sure you have the Firebug extension installed in the Firefox browser. Start Firebug and refresh the page; you’ll notice you get an error in the console output area stating “Access denied.” This is due to CORS, where for security reasons, JavaScript can’t access resources outside the same Web application and virtual directory its housing page resides in. For example, if your browser client page is in https://www.somedomain.com/somedir1/somedir2/client.aspx, then any AJAX call made on that page can go only to resources in the same virtual directory or a subdirectory. This is great if you’re calling another page or HTTP handler within the Web application, but you don’t want pages and handlers to block on a message queue when transmitting the same message to all clients, so you need to use the Windows Comet service and you need a way of getting around the CORS restriction.
To do this, I recommend building a proxy page in the same virtual directory, whose only function is to intercept the HTTP message from the browser client, extract all the relevant headers and content, and build another HTTP request object that connects to the Comet service. Because this connection is done on the server, it isn’t impacted by CORS. Thus, through a proxy, you can keep a long-lived connection between your browser client and the Comet service. Moreover, you can now transmit a single message when it arrives on a message queue to all connected browser clients simultaneously.
First, I take the HTTP request and stream it into an array of bytes so I can pass it to a new HTTP request object that I’ll instantiate shortly:
byte[] bytes;
using (Stream reader = Request.GetBufferlessInputStream())
{
bytes = new byte[reader.Length];
reader.Read(bytes, 0, (int)reader.Length);
}
Next, I create a new HttpWebRequest object and point it to the Comet server, whose URL I put in the web.config file so it can be easily modified later:
string newUrl = ConfigurationManager.AppSettings["CometServer"];
HttpWebRequest cometRequest = (HttpWebRequest)HttpWebRequest.Create(newUrl);
This creates a connection to the Comet server for each user, but since the same message is being broadcast to each user, you can just encapsulate the cometRequest object in a double locking singleton to reduce the connection load on the Comet server, and let IIS do the connection load balancing for you.
Then I populate the HttpWebRequest headers with the same values I received from the jQuery client, especially setting the KeepAlive property to true so I maintain a long-lived HTTP connection, which is the fundamental technique behind Comet-style communication.
Here I check for an Origin header, which is required by the W3C specification when dealing with CORS-related issues:
for (int i = 0; i < Request.Headers.Count; i++)
{
if (Request.Headers.GetKey(i).Equals("Origin"))
{
containsOriginHeader = true;
break;
}
}
I then pass the Origin header on to the HttpWebRequest so the Comet server will receive it:
if (containsOriginHeader)
{
// cometRequest.Headers["Origin"] = Request.Headers["Origin"];
cometRequest.Headers.Set("Origin", Request.Headers["Origin"]);
}
else
{
cometRequest.Headers.Add("Origin", Request.Url.AbsoluteUri);
}
System.Diagnostics.Trace.WriteLineIf(_proxySwitch.TraceVerbose,
"Adding Origin header.");
Next, I take the bytes from the content of the HTTP request from the jQuery client and write them to the request stream of the HttpWebRequest, which will be sent to the Comet server, as shown in Figure 12.
Figure 12 Writing to the HttpWebRequest Stream
Stream stream = null;
if (cometRequest.ContentLength > 0 &&
!cometRequest.Method.Equals("OPTIONS"))
{
stream = cometRequest.GetRequestStream();
stream.Write(bytes, 0, bytes.Length);
}
if (stream != null)
{
stream.Close();
}
// Console.WriteLine(System.Text.Encoding.ASCII.GetString(bytes));
System.Diagnostics.Trace.WriteLineIf(_proxySwitch.TraceVerbose,
"Forwarding message: "
+ System.Text.Encoding.ASCII.GetString(bytes));
After forwarding the message to the Comet server, I call the GetResponse method of the HttpWebRequest object, which provides an HttpWebResponse object that allows me to process the server’s response. I also add the required HTTP headers that I’ll send with the message back to the client:
try
{
Response.ClearHeaders();
HttpWebResponse res = (HttpWebResponse)cometRequest.GetResponse();
for (int i = 0; i < res.Headers.Count; i++)
{
string headerName = res.Headers.GetKey(i);
// Response.Headers.Set(headerName, res.Headers[headerName]);
Response.AddHeader(headerName, res.Headers[headerName]);
}
System.Diagnostics.Trace.WriteLineIf(_proxySwitch.TraceVerbose,
"Added headers.");
I then wait for the server’s response:
Stream s = res.GetResponseStream();
When I receive the Comet server’s message, I write it to the original HTTP request’s response stream so the client can receive it, as shown in Figure 13.
Figure 13 Writing the Server Message to the HTTP Response Stream
string msgSizeStr = ConfigurationManager.AppSettings["MessageSize"];
int messageSize = Convert.ToInt32(msgSizeStr);
byte[] read = new byte[messageSize];
// Reads 256 characters at a time
int count = s.Read(read, 0, messageSize);
while (count > 0)
{
// Dumps the 256 characters on a string and displays the string to the console
byte[] actualBytes = new byte[count];
Array.Copy(read, actualBytes, count);
string cometResponseStream = Encoding.ASCII.GetString(actualBytes);
Response.Write(cometResponseStream);
count = s.Read(read, 0, messageSize);
}
Response.End();
System.Diagnostics.Trace.WriteLineIf(_proxySwitch.TraceVerbose,
"Sent Message.");
s.Close();
}
Test the Application
To test your application, create a Web site to hold the sample application pages. Make sure the URL to your Windows service is correct and the message queue is properly configured and useable. Start the service and open the Comet client page in one browser and the page to send messages in another. Type in a message and press send; after roughly 10 ms you should see the message appear in the other browser window. Try this with various browsers—especially some of the older ones. As long as they support the xmlHttpRequest object, it should work. This provides almost real-time Web behavior (en.wikipedia.org/wiki/Real-time_web), where content is pushed to the browser almost instantaneously without requiring action from the user.
Before any new application is deployed, you have to do performance and load testing. To do this, you should first identify the metrics you want to gather. I suggest measuring usage load against both response times and data-transfer size. Additionally, you should test usage scenarios that are relevant to Comet, in particular broadcasting a single message to multiple clients without postback.
To do the testing, I constructed a utility that opens multiple threads, each with a connection to the Comet server, and waits until the server fires a response. This test utility allows me to set a few parameters, such as the total number of users that will connect to my Comet server and the number of times they reopen the connection (currently the connection is closed after the server’s response is sent).
I then created a utility that dumps a message of x number of bytes to the message queue, with the number of bytes set by a text field on the main screen, and a text field to set the number of milliseconds to wait between messages sent from the server. I’ll use this to send the test message back to the client. I then started the test client, specified the number of users plus the number of times the client will reopen the Comet connection, and the threads opened the connections against my server. I waited a few seconds for all the connections to be opened, then went to the message-sending utility and submitted a certain number of bytes. I repeated this for various combinations of total users, total repetitions and message sizes.
The first data sampling I took was for a single user with increasing repetitions but with the response message a consistent (small) size throughout the testing. As you can see in Figure 14, the number of repetitions doesn’t seem to have an impact on system performance or reliability.
Figure 14 Varying the Number of Users
Users | Repetitions | Message Size (in Bytes) | Response Time (in Milliseconds) |
1,000 | 10 | 512 | 2.56 |
5,000 | 10 | 512 | 4.404 |
10,000 | 10 | 512 | 18.406 |
15,000 | 10 | 512 | 26.368 |
20,000 | 10 | 512 | 36.612 |
25,000 | 10 | 512 | 48.674 |
30,000 | 10 | 512 | 64.016 |
35,000 | 10 | 512 | 79.972 |
40,000 | 10 | 512 | 99.49 |
45,000 | 10 | 512 | 122.777 |
50,000 | 10 | 512 | 137.434 |
The times are gradually increasing in a linear/constant manner, which means the code on the Comet server is generally robust. Figure 15 graphs the number of users against the response time for a 512-byte message. Figure 16 shows some statistics for a message size of 1,024 bytes. Finally, Figure 17 shows the chart from Figure 16 in graphical format.All of these tests were done on a single laptop with 8GB of RAM and a 2.4 GHz Intel Core i3 CPU.
Figure 15 Response Times for Varying Numbers of Users for a 512-Byte Message
Figure 16 Testing with a Message Size of 1,024 Bytes
Users | Repetitions | Response Time (in Milliseconds) |
1,000 | 10 | 144.227 |
5,000 | 10 | 169.648 |
10,000 | 10 | 233.031 |
15,000 | 10 | 272.919 |
20,000 | 10 | 279.701 |
25,000 | 10 | 220.209 |
30,000 | 10 | 271.799 |
35,000 | 10 | 230.114 |
40,000 | 10 | 381.29 |
45,000 | 10 | 344.129 |
50,000 | 10 | 342.452 |
Figure 17 User Load vs Response Time for a 1KB Message
The numbers don’t show any particular trend, except that response times are reasonable, remaining at below one second for message sizes up to 1KB. I didn’t bother tracking bandwidth use because that’s affected by the message format. Also, because all testing was done on a single computer, network latency was eliminated as a factor. I could’ve tried it against my home network, but I didn’t think it would be worthwhile because the public Internet is far more complex than my wireless router and cable modem setup. However, because the key point of Comet communication techniques is to reduce server round-trips by pushing content from the server as updated, theoretically half the network bandwidth usage should be reduced through Comet techniques.
Wrapping Up
I hope you can now successfully implement your own Comet-style applications and use them effectively to reduce network bandwidth and increase Web site application performance. Of course, you’ll want to check out the new technologies included with HTML5, which can replace Comet, such as WebSockets (bit.ly/UVMcBg) and Server-Sent Events (SSE) (bit.ly/UVMhoD). These technologies hold the promise of providing a simpler way of pushing content to the browser, but they do require the user to have a browser that supports HTML5. If you still have to support users on older browsers, Comet-style communication remains the best choice.
Derrick Lau is an experienced software development team leader with approximately 15 years of relevant experience. He has worked in the IT shops of financial firms and the government, as well as in the software development sections of technology-focused companies. He won the grand prize in an EMC development contest in 2010 and came in as a finalist in 2011. He is also certified as an MCSD and as an EMC content management developer.
Thanks to the following technical expert for reviewing this article: Francis Cheung