RSS

Proposal for Native Comet Support for Browsers

25 Sep

What if I had commit privileges on all the major browsers, the competence to add functionality in all the code bases, and wanted to add native Comet support in a form that was easily accessible to developers? What if we could use a native API that didn’t require the hacks of frame-based streaming, and didn’t face the cross-site and streaming limitations of XMLHttpRequest? Could we access Comet in a simple JavaScript API that works in a manner that servers could easily deliver messages to? I believe we could, and we could not only have easier Comet interaction, but also significantly improve the performance characteristics of both Ajax and Comet interaction in the process. I will describe a native implementation that not only provides simple techniques for Comet, but also provides some very interesting performance enhancements for general Ajax requests.

The first question is what level in the communication stack should native Comet be implemented at? HTTP is not mandated: Comet could be implemented at the TCP level or UDP could even be used. However, eliminating HTTP renders a vast number of application servers obsolete in their ability to communicate with the browser. Most web applications are built around HTTP. Requiring a different TCP or UDP protocol would negate substantial efforts to create open-connection capabilities in existing application servers, and would require significant architectural changes. A native Comet implementation should use HTTP-based Comet. With widespread investment and adoption, HTTP should be leveraged as much as possible to provide the foundation for Comet.

A second question would be what type of JavaScript interface to use. I am suggesting simply using the existing and familiar XMLHttpRequest API with some simple additions to achieve a powerful range of Comet capabilities. Obviously a new API could be used, but the existing XMLHttpRequest is widely adopted and understood. The XMLHttpRequest API provides comprehensive control and access over HTTP messaging.

Server Streamed Messaging

HTTP provides a streaming mechanism, but browser limitations and lack of message partitioning conspire to make it a shaky foundation for Comet. If all browsers properly implemented the interactive mode of XMLHttpRequest, content could be streamed to deliver asynchronous data from the server. However, IE doesn’t support this, and the browsers that do support streaming simply create an ever increasing string of content, continually devouring memory and requiring ad-hoc message partitioning. One could provide server initiated messaging by simply allowing ad hoc HTTP responses to be delivered on the TCP/IP connection without any requirements on correlated HTTP requests. However, this is clearly not allowed by the HTTP specification. Firefox has implemented support for the multipart/x-mixed-replace content type, which provides streaming with clear message partitioning without unbounded strings. While this is effective, this content type is semantically simply incorrect for many Comet usages. Often Comet is subscribing to a series of events, not resources that replace each other. If chat messages are being sent from a server, they are not intended to supersede the prior messages, but rather add to them. The multipart/x-mixed-replace content type is great for streaming web cams, but it is simply not the ideal solution for Comet. HTML 5 proposes to use server-sent events with the application/x-dom-event-stream content type to send DOM events. This approach is certainly not without merit. However, JavaScript interaction with a server requires routing all messages through a DOM element, and servers must place the data in a special DOM event formatting style. Handling these types of server-sent events would be very awkward with the XMLHttpRequest object, requiring a number of altered or new interface points.

In my last article, I pointed out the technique of encapsulating HTTP messages into content. I originally suggested the use of the multipart/digest content type, but after further thought, I believe that the message/http content type is actually more semantically correct since it directly indicates that the content will contain HTTP message(s), and this content type is also more compact because it doesn’t require boundaries. A native implementation could easily handle response streams with the XMLHttpRequest object in the same manner as Firefox handles a response with the multipart/x-mixed-replace content type. The message/http content type would not only be semantically correct, but headers can be included in each message to indicate status codes, request correlation, caching information, and more. This can also provide the means for RESTful semantics for Comet messages. The message/http content type could easily be handled by the XMLHttpRequest in the same way as Firefox handles the multipart/x-mixed-replace content type. Each new HTTP response in the stream could trigger the onreadystatechange handler and the content and response headers could easily be accessed with the standard XMLHttpRequest API.

Another advantage to encapsulating HTTP responses in a content stream is that gzip can be applied to compress the full inner HTTP responses. Normally gzip does not compress headers (since the headers negotiate the gzip), but with encapsulated HTTP responses, the outer response can negotiate and handle the gzip and the inner HTTP responses will be fully compressed (headers and content). HTTP headers are highly compressible when multiple responses are delivered in the same stream, meaning that this delivery mechanism can be highly efficient and compact on the wire. Each HTTP response may only have several bytes of overhead per message.

Client-Delivered Streaming

While there are several ways for JavaScript clients to consume server-delivered streaming data in existing browsers, there is simply no mechanism for client-delivered streaming. Once a browser sends request data to a server, it is basically impossible for JavaScript to coerce the browser to send any more data on that connection until a response is received and finished. The one exception is through HTTP pipelining, however HTTP pipelining is very inflexible because it requires responses to be received in the same order as requests. In a true asynchronous messaging system, a client should be able to deliver messages without placing constraints on the order of messages received from the server. The inability of a client to send data on an open HTTP connection is particularly problematic for Comet clients. A Comet connection may stay open for an extended period of the time, and the whole time that connection is essentially limited to one-way communication (the client is mute on that connection until the HTTP conversation has finished). Once request has been sent on an open connection, if the client needs to send additional data to the server (such as a request to subscribe to an additional resource) a new connection must be used. Creating new TCP connections is slow, and with the browser’s two connection limit, requests may be queued. Lifting the two-connection limit on browsers may seem attractive, but connections utilize valuable resources on the server, and using existing TCP connections is more efficient. By utilizing chunked encoding in HTTP requests, a native Comet implementation could also add support for client-delivered streaming. This ability could be accessed by adding a sendChunk method to the XMLHttpRequest API. Calling sendChunk instead of the normal send method would indicate a request transfer encoding of chunked, and it would send a chunk of data in the content stream and leave the content stream open for future chunks to be sent. The connection could always be terminated with the standard send method.

Once again, the message/http content type can be utilized to encapsulate multiple HTTP messages in this chunked content stream. This provides a standards-based way to partition messages in the client sent content stream, and follows the well understood HTTP protocol for sending messages. This capability can be achieved by manually constructing HTTP request messages and calling sendChunk. However, to simplify usage, an addHttpChunk method could be added to the XMLHttpRequest API. The addHttpChunk would take a single argument: an inner XMLHttpRequest object to be delivered inside the outer XMLHttpRequest object’s content stream. When a user calls the XMLHttpRequest.addHTTPChunk method, the provided inner XMLHttpRequest object would be associated with the outer XMLHttpRequest object. When a the send method was called on the inner XMLHttpRequest object, the HTTP message would be delivered as a data chunk in the containing message/http content stream. Multiple messages (HTTP requests) could then be delivered in a single request stream and a single connection. Responses could be correlated to requests by the Content-Location header, and the onreadystatechange handler could be called for inner XMLHttpRequests when the containing HTTP response content stream received a correlated HTTP response. Below is an example of the usage:


var cometXHR = new XMLHttpRequest;
cometXHR.open('POST','comet',true);

var xhr1 = new XMLHttpRequest;
cometXHR.addHttpChunk(xhr1);
xhr1.onreadystatechange=function(){...}
xhr.open('GET','/ticker1',true);
xhr.send();

var xhr2 = new XMLHttpRequest;
cometXHR.addHttpChunk(xhr2);
xhr2.onreadystatechange=function(){...}
xhr.open('GET','/ticker2',true);
xhr.send();

This would asynchronously send and receive embedded HTTP messages utilizing a single HTTP connection.

The ability to stream HTTP requests over a single connection does not solely benefit Comet applications. Standard pull-based Ajax application can benefit from this capability as well. In existing browsers, when multiple Ajax calls are made, each request must either create a new connection (unless there is an unused keep-alive connection available) or wait until a response is received on an existing connection. With request streaming, successive Ajax requests can be continuously delivered to the server without unnecessary delay and without creating new connections.

By combining the client-delivered streaming capabilities with the handling and partitioning of server-delivered streaming, a client could truly have asynchronous two-way communication with a server. A client could asynchronously send messages and receive messages from the server on the same HTTP/TCP connection.

Resource Subscription

A very simple resource subscription technique could also be included in the XMLHttpRequest API. Without any API additions, one could use my proposed When-Modified-After header. A client could trigger resource retrieval and subscription by simply including a When-Modified-After header with a beginning-of-the-epoch time (Wed, 1 Jan 1970 00:00:00 GMT) and indicating multipart response parsing (with the multipart property). The server could then return a response with a message/http content stream, and then immediately return the resource in a nested HTTP response in the entity content stream. The server could then keep the connection open and send any further modifications in the entity content stream as additional HTTP responses. Resource update HTTP responses could be a 200 with the entire new resource representation, 206 with an updated range, or 410 to indicate the resource was deleted. The XMLHttpRequest object could then call the onreadystatechange handler with each resource update, just as when the first HTTP response arrived. The XMLHttpRequest API could be augmented with a subscribe property, which when set to true could automatically include the When-Modified-After header. Developers would then have a very simple method for sending subscription requests using a familiar API.

Cached Resource until Updated

With a native modification to the browser in combination with the When-Modified-After header, an implementation could provide an interesting alternate behavior, which I believe is much more powerful than the simple approach above. When a XMLHttpRequest is made with the subscribe property set to true, and a cached copy of the resource is available, the browser could immediately return the cached copy and send a request to the server with the When-Modified-After header set to the date of the cached copy of the resource. The client would then immediately have access to the resource data without even needing to wait for a request and response. If the server has a newer copy of the resource than the cached copy, the resource would be immediately delivered to the client and the client would receive the update as soon as the HTTP response was received by the client. The fact that the client chose to subscribe to the resource indicates that the client code is prepared to handle updates. This behavior takes advantage of this fact to allow immediate access to cached data while waiting for a server copy of the data, which is treated as an update. Furthermore, if the cached copy is fresh, the server does not need to send a 304 response, as it can simply block the connection and wait for a future modification. In the meantime the client can happily continue with the cached fresh copy of the resource data, while listening for any updates from the server. This can provide a substantial improvement in performance of incrementally rendering data in applications, especially in the face of poor or broken network connections. This provides a capability reminiscent of that suggested by the stale-while-revalidated and stale-if-error headers used by Yahoo. A client can act like an offline client, immediately showing cached resource data until network response demonstrates otherwise. This provides a much more rapid and seamless approach to handle network uncertainty than waiting for a network problem before resorting to cached/offline resource handling.

Flexible Low or High Level Interaction

This API provides flexibility and means for developers to interact with Comet at various different levels. Developers could easily use this API to create their own low-level-customized high-speed Comet protocol since all the capabilities of HTTP can be controlled and accessed: method, request and response headers, request and response entities, and status codes. Bayeux could easily be implemented with this API. Developers could use the sendChunk method to create their own custom stream of messages, and directly parse responseText on incremental updates to do their own message partitioning. The API also allows developers to work at a higher level, simply issuing resource subscription requests with the subscribe property and delivering and receiving messages as HTTP messages. An important characteristic of HTTP/REST that is preserved by this API is that it does not force any inter-connection statefulness. By using the When-Modified-After header, subscriptions can be issued without requiring the server to maintain a session to remember subscriptions. An HTTP user agent should not force stateful interconnection session handling. Subscriptions are all local to an HTTP connection. When the connection is broken, a server may discard any subscription information related to that connection. This is a very important concept for utilizing shared-nothing (or at least shared-little) principles to easily create simple scalable servers. Developers are still free to create Comet communications that utilize stateful sessions when desired.

The When-Modified-After header is also advantageous in unreliable connection situations. If an application temporarily goes offline or if an underlying TCP connection is broken, the When-Modified-After header allows a client to easily resume a subscription from the last time of an update. To resume a subscription, the client simply sends a request with the When-Modified-After header set to the time of the last update. The server can simply compare the resource date with the When-Modified-After header date to determine if a resource needs to be sent. It does not need to keep individual event queues for each client in order to resume a connection.

Compatibility with Existing Capabilities

Even if my fantasy world of having commit privileges to all the browser code bases really did exist, adoption does not happen overnight. We would still be faced with dealing with existing browser implementations. However, this proposal for native Comet implementation using HTTP standards does not exclude current browsers from participation in the suggested Comet communication. Current browsers would not realize the performance benefits of client-delivered streaming, consistent server-delivered streaming handling, and cached until updated responses. However, all of the suggested communication techniques including message/http requests and responses and the When-Modified-After header are fully accessible with the existing capabilities of XMLHttpRequest. This means servers could easily deliver Comet communication without requiring separate protocols for newer implementations and existing implementations. Comet communication can remain consistent, with native implementations driving simple APIs and enjoying major performance benefits. Existing implementations can gracefully degrade, using JavaScript to emulate APIs, manually batching requests, and aborting and recreating HTTP connections in order to send new subscription requests and resume connections in browsers that do not support XMLHttpRequest interactive mode (IE). I am hoping to actually create a JavaScript implementation of this approach and make it available.

A native implementation that augmented these XMLHttpRequest capabilities would also be backwards compatible in behavior since all the new behaviors require opt-in (through sendChunk, addHTTPChunk, and subscribe).

Conclusion

With only a few simple XMLHttpRequest API additions, Comet communication could be realized with a native implementation that supports fast and efficient standards-based two-way asynchronous communication, with true server-delivered HTTP streaming and simple subscription requests, with the added benefit of client-delivered streaming and cached resource until updated capability. Developers could create Comet applications with a standard API without hacks. Communication could be standards-based, allowing current servers to easily implement the necessary features to handle communication.

Notes

To understand the When-Modified-After header, it is worth comparing it to the If-Modified-Since header. The behavior is similar. If the server’s resource is newer than the header’s date, the server immediately returns the server’s copy. When the resource matches or is older than the header’s date, the If-Modified-Since header says that the server should return a 304 Not Modified, whereas the When-Modified-After header says the server should wait until a modification occurs. The When-Modified-After header also suggests that when possible (when the client declares that it Accepts message/http with content negotiation) the connection should stay open indefinitely so as to receive multiple update responses.

Advertisements
 
Leave a comment

Posted by on September 25, 2008 in COMET

 

Tags: , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: