Is there such a thing as a server push over HTTP? The short answer is no. The long answer is that you can emulate sever push over HTTP and create an illusion to the user that the server is able to push data to the browser as and when new data is available on the server side. In this post I would like to give a quick demo on how this can be done using XMLHttpRequest. Note that server push like this has some pitfalls and you need to carefully design a protocol for creating and maintaining connections, and need a scalable server.
[Click here to jump the demo.]
I was intrigued when I found that some Ajax toolkits are now supporting server push. I was intrigued because there is no such thing as server push with HTTP. To me, with a true server push protocol, the server should be able to wake up the browser and send some data any time. But HTTP is a client-initiated synchronous protocol. Clients open connections when they want, and either the server or the client or any intermediary can close the connection any time. Clients may be able to open persistent connections (HTTP 1.1 style), but those connections are also short-lived. Persistent connections are designed for reducing connection latency problems, and not for server push. More importantly, the server can not arbitrarily push data to the client unless the client opens a connection with the server first.
Although a true server push is not possible over HTTP, you can emulate one by using polling to open connections, and long lived connections to transfer data almost continuously. Some folks now call this technique Comet. The details to make this work across all browsers are somewhat complex, so I chose to demonstrate this technique that works just in Firefox 1.5.
In this model, the browser leaves a connection open with the server, to let the server return data in chunks. However, since the server can’t leave the connection open forever, and since connections may get dropped any time, you also need to design a protocol that can reinitiate a connection when required. In the demo below, I use client-side polling plus long-lived connections to emulate server push.
Server Push Demo
An obvious example for server push is browser based chat. Instead of writing a true multi-user chat app, I decided to limit the push to the user that initiated the chat. This is like to chatting with yourself.
In this demo, when you type in a message in the chat area, and press enter, the browser sends the message to the server, and then initiates another long-lived connection (which I call a push channel). The server pushes new chat messages via the push channel to the browser.
To work within the limits of the bandwidth and process limitations from my ISP, in this demo, I keep the push channel alive for just about two minutes. Within those two minutes, if the server detects no activity for about fifteen seconds, it ends the connection. In either case, the browser detects the end of connections, and reconnects when you start sending messages again.
To try this demo, start entering messages in the textarea. Remember to press enter after each message. The messages entered will start to appear below the chat area almost immediately.
[This demo requires Firefox 1.5]
Behind the Scenes
In this demo, I use two XMLHttpRequests – one is a regular request to submit messages to the server, while the second one is the push channel to get messages from the server.
The first XMLHttpRequest is easy. When you press enter, the browser initiates this request, and sends the message entered to the server. It then waits for the server to acknowledge the message. The server processes this message and acknowledges by either asking the browser to initiate the push channel, and a simple OK.
The push channel is based on a technique described in Server Push and Server Sockets. In this technique, the push channel returns a multipart response with content type
multipart/x-mixed-replace. Whenever a new chat message arrives, the server writes a MIME part containing the message. Here is an example message:
HTTP/1.1 200 OK Date: Sun, 23 Apr 2006 19:19:11 GMT Content-Type: multipart/x-mixed-replace;boundary="goofup101" --goofup101 Content-type: text/xml <div> <div class='message'>You said: Knock knock</div> </div> --goofup101 Content-type: text/xml <div> <div class='message'>You said: Who's there?</div> </div> --goofup101--
On the browser side, we need to be able to process each part without waiting for the complete multipart message to arrive. This can be done via the onload handler of XMLHttpRequest. Here is an example. The onload handler in this example gets called whenever a new chat message arrives.
var channel = ...; // Create XMLHttpRequest
channel.multipart = true;
channel.onload = function(e)
message = this.responseXML;
message = this.event.target.responseXML;
// Process the message }; ... channel.open('GET', url, true); channel.send(null); ...
These are the easier problems to solve. For the server push to work effeciently and effectively, managing connections is critical for both the browser and the server.
For instance, you can start by leaving the connection open for a long duration on the server side. This might work on a low-volume server with a handful of concurrent users. On most web servers, maintaining a connection requires maintaining a thread or even a process per connection, and there are physical limits on how many threads or processes you can have at any given time. For instance, my ISP limits the number of processes to under thirty. So these limits impose a physical constraint on the scalability of the server.
Another problem to solve is dropped or terminated connections. To keep getting messages, the browser will have to detect whether a connection is open or not, and then reestablish a connection. The server has a similar problems to solve. It will have to detect duplicate connections from the same client. For instance, in most browsers, you can terminate all open connections by simply pressing the escape key, and then reinitiate a connection by entering new chat messages. By doing this, you are leaving a thread/process running on the server side. The web server may ultimately terminate such open threads/processes, but if not well-designed, your server side app may be thinking that it is pushing data when in fact the client is not connected to it. Furthermore, the server will have to detect inactivity from the user side, and close the connection when it detects long periods of inactivity.
In order to address some of these problems, I developed a simple protocol which can help the browser and server recover from these in a timely manner, and without losing any data. This protocol is specific to the chat app, and other apps may require more sophisticated or elaborate protocol (remember – mine was a weekend project).
Here is my chat protocol.
Browser Server | | | Send a message | |------------------------------------>| | Connect | |<------------------------------------| | Open channel | |------------------------------------>| | | | Send a message | |------------------------------------>| | OK | |<------------------------------------| | | | Send a message | |------------------------------------>| | OK | |<------------------------------------| | | | Closing channel | |<------------------------------------|
In this protocol, the server tells the browser when to open the push channel, and when the server is about to close the push channel. For instance, when the browser submits the first message, the server detects that this client has no channel open, and sends a message back asking it to open the channel. Similarly, when the browser detects inactivity or timeout, it sends a message via the push channel that it is about to close the channel. That is, the last part to arrive in the multipart/x-mixed-replace message includes a status flag telling the browser that the server will terminate the connection after this part. This protocol could be extended to deal with the possible case of multiple open channels from the same browser.
Since the multipart support for XMLHttpRequest is currently limited too Mozilla/Firefox browsers, a solution for other browsers requires use of hidden iframes as a channel to download data from the server. In any case, solving this problem on the browser side is just part of the deal. Server push like the one demonstrated above needs a careful design on the server side as well. If implemented badly, server push could seriously constrain resources on the server side. Today most servers limit the number of connections to the number of thread/process a server can handle concurrently. For instance, the servlet model in J2EE requires that a thread be managed for each incoming connection, and most implementations assume that connections are short lived. There is the server side opponent for server push.