Creating Comet applications with ASP.NET
Sometimes it’s useful to hijack a particular technology to do something it was never intended to do. Comet does exactly that, and it’s not for the faint of heart. In a nutshell, “Comet-style” applications use features of the HTTP request-response communication model to morph it into a streaming context whereby the server can essentially push data to the client (instead of the client request data from the server). Wikipedia puts it well:
Comet is a programming technique that enables web servers to send data to the client without having any need for the client to request it. It allows creation of event-driven web applications which are hosted in the browser.
When faced with the need for a real-time web application, there will always be trade-offs between performance and scalability. One possibility is to implement a pull architecture that periodically polls the sever. The benefits include higher scalability per server with the trade-off of increased latency and lower performance (in terms of information availability). On the other side of the coin is a persistent connection which allows the server to push streaming data to the client. Benefits include great performance (low latency), but low scalability since every client connection is held open for long periods of time which today’s web servers are not optimized for. Lightstreamer.com has a good white paper describing the pros and cons of the various approaches (including some hybrid methods), and, of course, why theirs is the best and you should buy it. Truth be told, I think they have a very compelling product offering. (I haven’t tried it, though, just read about it.)
So how can a Comet ASP.NET application scale? Very poorly. Since each client connection holds open a dedicated thread from the ASP.NET thread pool, there are really only two ways to scale:
- Increase the number of threads in the thread pool to the maximum. This is done in the <processModel> element located under <system.web>. Set the maxWorkerThreads attribute to the max of 100. This is a per-CPU value, so assuming you ran this on a quad-processor server, your application has the potential to scale to 400 concurrent users per server. Assuming you grab a barebones Dell PowerEdge 4-proc server for $10,000 USD, you’ll be paying $25 per concurrent user. Fantastic numbers! Not.
- Add more servers – in real-time applications it’s typically not feasible for each client connection to end up on different servers. Once the initial connection has been made, the client is “pinned”. In cases like these, additional servers can be added but they have to live behind some “sticky load balancing” routers.
The other option is to write a custom web server that is optimized for concurrent connections. I wonder if WCF could handle the challenge, and while it seems like it could do better than IIS+ASP.NET, it appears you wouldn’t necessarily get a huge bang for your buck. Some real-world testing would be much more definitive however. Here’s a few links talking about streaming data back over an HTTP connection with WCF:
Just for fun I’ve created a very brief ASP.NET sample application that demonstrates a basic Comet implementation. It streams the current server time back to the web browser, which just displays the value. Most of the work is in javascript on the client page. The key ASP.NET components are simply the disabling of output buffering, and flushing the response stream as appropriate. I didn’t use any “fancied up” javascript libraries so as not to exclude anybody who isn’t familiar with any particular library.
And now, after having bashed my own approach, here it is. Create a new Web Site in Visual Studio 2005. Add a page called simply “Service.aspx” and put this code in the code-behind:
public static string Delimiter = "|"; protected void Page_Load(object sender, EventArgs e) { Response.Buffer = false; while (true) { Response.Write(Delimiter + DateTime.Now.ToString("HH:mm:ss.FFF")); Response.Flush(); // Suspend the thread for 1/2 a second System.Threading.Thread.Sleep(500); } // Yes I know we'll never get here, // it's just hard not to include it! Response.End(); }
Add a new HTML page to the project (I called mine “ajax.html”) and overwrite the contents with this: (Explanation to follow.)
(NOTE: The formatting got screwed up during a blog import to WordPress – download the zip file linked below for “readable” code.
)
<html xmlns="http://www.w3.org/1999/xhtml" ><head> <title>Comet AJAX Sample</title> <script language="javascript"> function getData() { loadXMLDoc("Service.aspx"); }var req = false; function createRequest() { // branch for native XMLHttpRequest object if(window.XMLHttpRequest && !(window.ActiveXObject)) { try { req = new XMLHttpRequest(); } catch(e) { req = false; } // branch for IE/Windows ActiveX version } else if(window.ActiveXObject) { try { req = new ActiveXObject("Msxml2.XMLHTTP"); } catch(e) { try { req = new ActiveXObject("Microsoft.XMLHTTP"); } catch(e) { req = false; } } } } function loadXMLDoc(url) { try { if (req) { req.abort(); req = false; } createRequest(); if (req) { req.onreadystatechange = processReqChange; req.open("GET", url, true); req.send(""); } else { alert('unable to create request'); } } catch (e) { alert(e.message); } } function processReqChange() { if (req.readyState == 3) { try { ProcessInput(req.responseText); // At some (artibrary) length // recycle the connection if (req.responseText.length > 3000) { lastDelimiterPosition = -1; getData(); } } catch (e) { alert(e.message); } } } var lastDelimiterPosition = -1; function ProcessInput(input) { // Make a copy of the input var text = input; // Search for the last instance of the delimiter var nextDelimiter = text.indexOf('|', lastDelimiterPosition+1); if (nextDelimiter != -1) { // Pull out the latest message var timeStamp = text.substring(nextDelimiter+1); if (timeStamp.length > 0) { lastDelimiterPosition = nextDelimiter; ProcessTime(timeStamp); } } } function ProcessTime(time) { var out = document.getElementById('outputZone'); out.innerHTML = time; } </script></head><body onload="getData()"> <b>Server Time:</b> <span id="outputZone"></span></body></html>
It works by making a connection to the server and processing the response as it streams back. “getData()” will initiate an AJAX request for the service page, which will continue to stream data back. When new data is available, the client parses out the new data beginning from the last location in the response that was processed. If the response text gets too large, the connection is severed and a new one made (semi-polling — one of the hybrid approaches) as processing an ever increasing response body can ultimately slow down the web browser unnecessarily.
There is a lot more to a successful Comet implementation, obviously, and I encourage you to read the information available at Wikipedia including the reference links.
Download the 3 files (ajax.html, Service.aspx, Service.cs) here: ASP.NET Comet Example.zip