WhaTWG Living standard:
http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#server-sent-events
W3C Draft:
http://dev.w3.org/html5/eventsource/Imagine you’re building a simple stock ticker application. You have a server resource that publishes the APIs you need for getting stock values, so it’s quite easy to get things started. But how do you get updates to the stock values? How does the server let your client application know that a stock’s value has changed?
This is probably the canonical use case for Server-sent Events: a situation in which the server needs to inform the client that something has happened. Because HTTP as a standard only defines stateless communication, and thus only clients can initiate requests to servers, there was no way for a server to send a message to a client without the client first asking for one. Server-sent Events is one of the ways that HTML5 addresses this issue, in the specific case of one-way communication from the server to the client.
In the past, people wrote simple polling timers into their scripts that would essentially ask the server
“Is there anything new?” on a timer, as shown in Listing 3-1.
■Note The examples in this section will need to be run from a server, rather than loaded directly from your
filesystem.
Listing 3-1. An Example Polling Script
<!DOCTYPE HTML>
<html>
<head>
<title>The HTML5 Programmer's Reference</title>
</head>
<body>
<script>
// This is a mock API that simply returns the same JSON structure // every time the URL is requested. This JSON structure has a single // property, isChanged, which is set to false.
var strUri = './example3-1.json';
// This is how often we'll query the mock API, in milliseconds.
var timerLength = 1000;
/**
* Fetch an update from a web service at the specified URL. Initiates an * XMLHttpRequest to the service and attaches an event handler for the success * state.
* @param {string} strUrl The URL of the target web service.
*/
function fetchUpdates(strUrl) {
// Create and configure a new XMLHttpRequest object.
var myXhr = new XMLHttpRequest();
myXhr.open("GET", strUrl);
// Register an event handler for the readyStateChange event published by // XMLHttpRequest.
myXhr.onreadystatechange = function() { // If readyState is 4, request is complete.
if (myXhr.readyState === 4) { handleUpdates(myXhr.responseText);
} };
// Everything is configured. Send the request.
myXhr.send();
} /**
* Handle an update from the mock API. Parses the JSON returned by the API and * looks for changes.
* @param {string} jsonString A JSON-formatted string.
*/
function handleUpdates(jsonString) { // Parse the JSON string.
var jsonResponse = JSON.parse(jsonString);
if (jsonResponse.isChanged) {
// Handle changes here, probably by checking the structure to determine // what changed and then route the change to the approprate part of the app.
console.log('change reported.');
} else {
console.log('no changes reported.');
} }
// Everything is all set up: we have a function for fetching an update and a // function to handle the results of an update query. Now we just have to kick // off everything. Using setInterval, we will call our fetchUpdates method every // timerLength milliseconds. We can cancel the timer by calling the
// cancelInterval(pollTimer) method.
var pollTimer = setInterval(fetchUpdates.bind(this, strUri), timerLength);
</script>
</body>
</html>
This example does a lot of setup before it starts the polling process. Begin by defining the URL for the mock API, which is just a file you have created on the filesystem. You also define how often you want to poll your mock API. Then create a function for fetching an update. This is the method you will call with the timer, and it initiates the XMLHttpRequest (XHR for short) to the mock service. The XHR will publish readyStateChange events as it communicates with the server. By looking at the readyState property of the XHR object, you can tell what state the query is in: still talking with the server, or done talking, or even if an error has occurred. So add an event handler to the XHR object that will be called each time a readyStateChange event occurs. In this case you’re using an inline function to keep the code simple, though you could have defined it outside of this code block and referred to it by name. The event handler checks the readyState property and, if it is in the correct state, it calls the handleResponse function. That function takes the JSON-formatted string that you fetched from your mock API and processes it accordingly.
This is a pretty unsophisticated example, but it demonstrates the basics of using a timer to regularly poll a web service. Using the methods setInterval and cancelInterval it’s easy to start and stop timers. If your application will need multiple timers, you can build a constructor with convenience methods for creating, starting, stopping, pausing, and disposing of them. You can rapidly build up a lot of code just around creating and managing your timers.
And if you think about it, simple timers aren’t really a good way of handling this problem. What if there is a temporary network problem and one of the calls to fetchUpdates takes longer than a second? In that case, another call to fetchUpdates would be executed before the first has returned, resulting in two active and pending calls to the server. Depending on the network conditions, the second call could return before the first. This situation is referred to as a race condition, because the two pending calls are essentially in a race to see which one completes first.
Fortunately this race condition is fairly easy to fix: instead of having a timer fire off requests regardless of external limitations, alter the handleUpdates method so that the last thing it does is schedule the next call to fetchUpdates. That way you won’t ever have more than one call happening and the race condition is eliminated. The script would change as shown in Listing 3-2 (the surrounding HTML remains the same).
Listing 3-2. Eliminating the Race Condition from Example in Listing 3-1 // This is a mock API that simply returns the same JSON structure // every time the URL is requested. This JSON structure has a single // property, isChanged, which is set to false.
var strUri = './example3-1.json';
// This is how often we'll query the mock API, in milliseconds.
var timerLength = 1000;
// Reference to the currently active timer.
var pollTimeout;
/**
* Fetch an update from a web service at the specified URL. Initiates an * XMLHttpRequest to the service and attaches an event handler for the success * state.
* @param {string} strUrl The URL of the target web service.
*/
function fetchUpdates(strUrl) {
// Create and configure a new XMLHttpRequest object.
var myXhr = new XMLHttpRequest();
myXhr.open("GET", strUrl);
// Register an event handler for the readyStateChange event published by // XMLHttpRequest.
myXhr.onreadystatechange = function() { // If readyState is 4, request is complete.
if (myXhr.readyState === 4) { handleUpdates(myXhr.responseText);
} };
// Everything is configured. Send the request.
myXhr.send();
} /**
* Handle an update from the mock API. Parses the JSON returned by the API and * looks for changes, and then initiates the next query to the mock service.
* @param {string} jsonString A JSON-formatted string.
*/
function handleUpdates(jsonString) { // Parse the JSON string.
var jsonResponse = JSON.parse(jsonString);
if (jsonResponse.isChanged) {
// Handle changes here, probably by checking the structure to determine // what changed and then route the change to the approprate part of the app.
console.log('change reported.');
} else {
console.log('no changes reported.');
}
// Schedule the next polling call.
pollTimeout = setTimeout(fetchUpdates.bind(this, strUri), timerLength);
}
// Initiate polling process.
fetchUpdates(strUri);
The changes from the previous version of the script have been bolded. This version of the code eliminates the race condition, because only one call to fetchUpdates will be active at any given time.
However, you could now be polling the server at an unpredictable rate.
It is possible to program around these problems as well, but handling all of the edge cases well can be difficult, and you will end up with a significant amount of code, all just to intelligently handle polling a web service.
Ideally, this sort of communication should be a feature of the browser, and that’s what the new Server-sent Events feature does. Server-Server-sent Events has the browser handle all of the details of connecting to the server and polling it for events, and lets you leave behind timer-based polling scripts and all of the problems they entail. Server-sent Events provide a way for you to open a channel to the server, and then attach event listeners to that channel to handle various event types that the server will publish. The browser handles everything else for you.
To use Server-sent Events, you need not only a client that can open a channel to a web service and handle the events that occur on that channel; you also need a server that will handle incoming channel subscriptions correctly and publish correctly formatted events.