Saturday, October 9, 2010

AJAX Push in iOS, Safari, and Chrome with Server-Sent Events

One of the many new APIs of HTML5 is Server-Sent Events. Server-Sent Events are a lot like long-polling. They work like this: establish a connection to the server from the client, send data from the server to the client in pieces, and if the connection is severed re-establish the connection from the client to the server and continue sending event data from the server. Both long-polling and Server-Sent Events are one-way, server-to-client messaging, so if you need to send data TO the server, you will need to fall back on an XMLHttpRequest, and the message will be sent on a separate connection.

The main difference between Server-Sent Events and long-polling is Server-Sent Events are handled directly by the browser, and all the user has to do is listen for the messages from the server, no worries about re-establishing the connection, the browser handles that part of the work. Server-Sent Events are really simple. Client-side javascript looks like this:

var source = new EventSource("/path/to/my/event-stream-handler.php"); 
//or whatever CGI you are using

source.onmessage = function(event) {
    //do some thing with, it is a string

The EventSource class is where the work gets performed on the client side. The server-sent event stream syntax is really simple too, all you have to do is respond to an event request with the "Content-Type" header set to "text/event-stream", and start sending event streams whenever you are ready. A single event stream is written:

data: My event data\n\n

That is "data: ", followed by the string data you are sending as your event message, followed by 2 new line feeds (\n). Optionally, an event stream can span multiple lines if written like so:

data: The first line\n
data: The second line\n\n

That makes it easy to send long JSON messages without breaking the syntax, or some other long message. You will notice that there is only one (1) new line feed after the first line of the message, this is to let the client know that the message is longer, and will be continued on the next line, but the next line still begins with "data: ". Your server can end lines with 2 carriage-return/new line feed combos, or 2 carriage-returns as well.

"Great, that seems really simple, but what browsers support this"

Good News! Chrome, Safari, and YES! Safari Mobile on iOS4 already support Server-Sent Events. So you can start using this today. There isn't a lot of documentation for it out there though. Of course, Chrome and Safari both support the WebSocket API, so you may choose that over this for the larger screen devices. But being able to perform AJAX Push from an iOS device without using a hacked long-polling trick, that is AWESOME!


Client-side handling is so simple, just set the "onmessage" attribute to your callback handler, or use the "addEventListener" method like so:

source.addEventListener("message", function(event) {
    //append the data to the body if you like
    document.body.innerHTML += + "<br>";

}, false);

The server side part is simple too. Here is an example in PHP:

if ($_SERVER['HTTP_ACCEPT'] === 'text/event-stream') {
    //send the Content-Type header
    header('Content-Type: text/event-stream');
    //its recommended to prevent caching of event data
    header('Cache-Control: no-cache');
    //send the first event stream immediately
    echo "data: This is the first event\n\n";
    //flush the output

    $i = 5;
    //create a loop to output more event streams
    while (--$i) {
        //pause for 1 second
        //emit an event stream
        $time = date('r');
        echo "data: The server time is: {$time}\n\n";
        //flush the output again

That example responds with 5 separate event streams, each one (1) second apart, to a single client request then terminates. The browser will then re-establish the connection repeatedly after each request terminates. It is important to call flush() after each event stream is output in order to force the server to send the data that it has in the buffer, but this may not work on Apache if gzip or deflate encoding are enabled, or any other server that buffers output data. If your server doesn't flush() data, the browser will end up receiving all the events at once, after the script terminates, like in this demo (yeah, I know, I need a different host).

To cancel an EventSource from the server side respond with a Content-Type header other than "text/event-stream" or with a HTTP status code like 404 Not Found. That should make the browser stop requesting to re-establish the connection.

A great server for handling these types of connections is node.js.With node.js you can handle the incoming event stream requests in a single process allowing you pipe event streams to multiple clients without using some external messaging system to communicate between threads or processes, and the client can maintain the connection to the server as long as it would like, because node.js handles a lot of simultaneous connections at once (I have heard up to 20,000 and it's fast :-)). But you can also use Ruby EventMachine or Python's Twisted to accomplish similar results.


Opera also supports a different version of Server-Sent Events through a DOM element "event-source" and using the "application/x-dom-event-stream" MIME type instead of "text/event-stream". Opera's implementation is based on an older version of the spec I think but don't know, because the event stream syntax is slightly different as well. The event stream syntax in Opera is:

Event: event-name\n
data: event-data\n\n

More information on Opera's implementation can be found at

Check out the source from the demo above, includes a node.js demo example as well.

My Conclusions

Server-Sent Events are ready for use in iOS web apps, and are easy to implement using your existing server resources, so why not use them. Sure, asynchronous operations on multiple concurrent connections can make the whole process seem a lot easier, but we don't need that.I find this subject intriguing, so hopefully I'll follow this up with more experiments later.

1 comment: