Implementing Push Technology Using Server-Sent Events
Making Ajax calls using the XmlHttpRequest object is a well established technique for generating dynamic server requests. However, Ajax does not allow the server to directly initiate data transfers to the client – a technique referred to as push technology. That’s where the server-sent events API comes into the picture. Specializing in push technology, server-sent events transmit data to clients as a continuous stream, referred to as an event stream, over a connection which is kept open. And, by maintaining an open connection, the overhead of repeatedly establishing a new connection is eliminated.
Comparison to WebSockets
Many people are completely unaware that server-sent events exist. This is because they are often overshadowed by the more powerful WebSockets API. While WebSockets enable bidirectional full duplex communication between the client and server, server-sent events only allow messages to be pushed to the client from the server. Applications that require near real-time performance, or two-way communication are probably better suited for WebSockets.
However, server-sent events also have certain advantages over WebSockets. For example, server-sent events support custom message types and automatic reconnection for dropped connections. These features can be implemented in WebSockets, but they are available by default with server-sent events. WebSockets applications also require servers that support the WebSockets protocol. By comparison, server-sent events are built atop HTTP and can be implemented in standard web servers.
Detecting Support
Server-sent events are relatively well supported, with Internet Explorer being the only major browser that does not yet support them. However, as long as IE lags behind, it will remain necessary to provide feature detection. On the client side, server-sent events are implemented using the EventSource
object – a property of the global object. The following function detects whether or not the EventSource
constructor is available in the browser. If the function returns true
, then server-sent events can be used. Otherwise, a fallback mechanism such as long polling should be used.
function supportsSSE() {
return !!window.EventSource;
}
Connecting
To connect to an event stream, call the EventSource
constructor, as shown below. You must specify the URL of the event stream that you are attempting to subscribe to. The constructor will automatically take care of opening the connection.
EventSource(url);
The onopen Event Handler
When a connection is established, the EventSource
‘s onopen
event handler is invoked. The event handler takes the open
event as its only argument. A generic onopen
event handler is shown in the following example.
source.onopen = function(event) {
// handle open event
};
EventSource
event handlers can also be written using the addEventListener()
method. This alternative syntax is preferred to onopen
because it allows multiple handlers to be attached to the same event. The previous onopen
event handler has been rewritten below, using addEventListener()
.
source.addEventListener("open", function(event) {
// handle open event
}, false);
Receiving Messages
The client interprets an event stream as a series of DOM message
events. Each event that is received from the server causes the EventSource
‘s onmessage
event handler to be triggered. The onmessage
handler takes a message
event as its only argument. The following example creates an onmessage
event handler.
source.onmessage = function(event) {
var data = event.data;
var origin = event.origin;
var lastEventId = event.lastEventId;
// handle message
};
The message
event contains three important properties ― data
, origin
, and lastEventId
. As the name implies, data
contains the actual message data, in string format. The data could potentially be a JSON string, which can be passed to the JSON.parse()
method. The origin
property contains the event stream’s final URL after any redirects. The origin
should be checked to verify that messages are only received from expected sources. Finally, the lastEventId
property contains the last message identifier seen in the event stream. The server can attach identifiers to individual messages using this property. If no identifier was ever seen, then lastEventId
will be the empty string.
The onmessage
event handler can also be written using the addEventListener()
method. The following example shows the previous onmessage
event handler, rewritten to use addEventListener()
.
source.addEventListener("message", function(event) {
var data = event.data;
var origin = event.origin;
var lastEventId = event.lastEventId;
// handle message
}, false);
Named Events
A single event stream can specify various types of events by implementing named events. Named events are not handled by the message
event handler. Instead, each type of named event is processed by its own unique handler. For example, if an event stream contained events named foo
, then the following event handler would be required. Notice that the foo
event handler is identical to the message
event handler, with the exception of the event type. Of course, any other types of named messages would require separate event handlers.
source.addEventListener("foo", function(event) {
var data = event.data;
var origin = event.origin;
var lastEventId = event.lastEventId;
// handle message
}, false);
Handling Errors
If a problem occurs with the event stream, the EventSource
‘s onerror
event handler is triggered. A common cause of errors is a disrupted connection. Although the EventSource
object automatically attempts to reconnect to the server, an error event is also fired upon disconnection. The following example shows an onerror
event handler.
source.onerror = function(event) {
// handle error event
};
Of course, the onerror
event handler can also be rewritten using addEventListener()
, as shown below.
source.addEventListener("error", function(event) {
// handle error event
}, false);
Disconnecting
An EventSource
connection can be terminated by the client at any time by calling the close()
method. The syntax for close()
is shown below. The close()
method does not take any arguments, and does not return any value.
source.close();
Connection States
The state of an EventSource
connection is stored in its readyState
property. At any point during its lifetime, a connection can be in one of three possible states – connecting, open, and closed. The following list describes each state.
- Connecting – When an
EventSource
object is created, it initially enters the connecting state. During this time, the connection is not yet established. AnEventSource
will also transition into the connecting state if an established connection is lost. ThereadyState
value for anEventSocket
in the connecting state is 0. This value is defined as the constantEventSource.CONNECTING
. - Open – An established connection is said to be in the open state.
EventSource
objects in the open state can receive data. AreadyState
value of 1 corresponds to the open state. This value is defined as the constantEventSource.OPEN
. - Closed – An
EventSource
is said to be in the closed state if a connection is not established and it is not attempting to reconnect. This state is typically entered by calling theclose()
method. AnEventSource
in the closed state has areadyState
value of 2. This value is defined as the constantEventSource.CLOSED
.
The following example shows how the readyState
property can be used to inspect an EventSource
connection. To avoid hard coding the readyState
values, the example makes use of the state constants.
switch (source.readyState) {
case EventSource.CONNECTING:
// do something
break;
case EventSource.OPEN:
// do something
break;
case EventSource.CLOSED:
// do something
break;
default:
// this never happens
break;
}
Conclusion
This article has covered the client aspect of server-sent events. If you’re interested in learning more about server-sent events, I recommend reading The Server Side of Server-Sent Events. I’ve also written a more hands on article that covers Server-Sent Events in Node.js. Enjoy!