WebSockets vs Server-Sent Events
Sometimes we need information from our servers instantaneously. For this sort of use case our usual AJAX request/response doesn’t cut it. Instead, we need a push-based method like WebSockets, Long Polling, Server-Sent Events (SSE) and — more recently — HTTP2 push. In this article, we compare two methods for implementing realtime — WebSockets and Server-Sent Events (SSE), with information on which to use and when.
In brief: The what and why of WebSockets
Around the middle of 2008, the limitations of using continuous two-way server/ browser interaction system Comet were being felt particularly keenly by developers Michael Carter and Ian Hickson. Through collaboration on IRC and W3C mailing lists, they hatched a plan to introduce a new standard for modern realtime on the web via bi-directional communication. And so WebSockets came to be. The idea made its way into the W3C HTML draft standard and, shortly after, Michael Carter wrote an article introducing the Comet community to WebSockets. In 2010, Google Chrome 4 was the first browser to ship full support for WebSockets, with other browser vendors following suit over the course of the next few years. In 2011, RFC 6455 — The WebSocket Protocol — was published to the IETF website.
In a nutshell, WebSockets are a thin transport layer built on top of a device’s TCP/IP stack. The intent is to provide what is essentially an a TCP communication layer to web applications that’s as close to raw as possible, bar a few abstractions to eliminate certain security-based complications and other concerns. A full overview of WebSockets can be found elsewhere on the Ably Engineering blog.
An overview of Server-Sent Events (SSE)
SSE is based on something called Server-Sent DOM Events, which was first implemented in Opera 9. The idea is simple: a browser can subscribe to a stream of events generated by a server, receiving updates whenever a new event occurs. This led to the birth of the popular EventSource interface, which accepts an HTTP stream connection and keeps the connection open while retrieving available data from it.
The connection is kept open (until it receives an instruction to be closed) by calling EventSource.close(). SSE is a standard describing how servers can initiate data transmission towards clients once an initial client connection has been established. It provides a memory-efficient implementation of XHR streaming. Unlike a raw XHR connection, which buffers the full received response until the connection is dropped, an SSE connection can discard processed messages without accumulating all of them in memory. SSE is designed to use the JavaScript EventSource API to subscribe to a stream of data in any popular browser. Through this interface, a client requests a particular URL to receive an event stream. SSE is commonly used to send message updates or continuous data streams to a browser client. In summary, a server-sent event is when updates are pushed (rather than pulled, or requested) from a server to a browser.
Weighing up the two
A) WebSockets
Advantages
- WebSockets offer bi-directional communication in realtime.
- WebSockets generally do not use ‘XMLHttpRequest’, and as such, headers are not sent every-time we need to get more information from theserver. This, in turn, reduces the expensive data loads being sent to the server.
- WebSocket connections can both send and receive data from the browser. A chat app is a good example of a basic application that could use WebSockets.
- WebSockets can transmit both binary data and UTF-8
Potential stumbling blocs
- When connections are terminated WebSockets don’t automatically recover — this is something you need to implement yourself, and is part of the reason why there are many client-side libraries in existence.
- Note that browsers older than 2011 don’t support WebSocket connections.
- Some enterprise firewalls with packet inspection have trouble dealing with WebSockets (notably SophosXG Firewall, WatchGuard, McAfee Web Gateway).
B) SSE
Advantages
- Transported over simple HTTP instead of a custom protocol
- Can be poly-filled with javascript to”backport”SSE to browsers that do not support it yet.
- Built-in support for re-connection and event-id
- No trouble with corporate firewalls doing packet inspection
- Useful for apps that enable one-way communication of data, eg live stock prices
Potential stumbling blocks
- SSE is limited to UTF-8, and does not support binary data.
- SSE is subject to limitation with regards to the maximum number of open connections. This can be especially painful when opening various tabs as the limit is per browser and set to a very low number (6).
- SSE is mono-directional
WebSockets vs SSE: Which is best?
This is largely a question of technical debt, which, rather than being categorically a ‘bad thing’, can sometimes be leveraged and/ or save time in the short term. WebSockets are undoubtedly more complex and demanding than SSEs, and require a bit of developer input up front. For this investment, you gain a full-duplex TCP connection that is useful for a wider range of application scenarios. For example WebSockets tend to be preferable for use cases such as multi-player games or location-based apps. Where SSE + AJAX can technically be used to achieve these, this might cause the domain to get multiplexed as the AJAX requests aren’t really in sync.
SSE is a simpler and faster solution, but it isn’t extensible: if your web application requirements were to change, the likelihood is it would eventually need to be refactored using… WebSockets. Although WebSocket technology presents more upfront work, it’s a more versatile and extensible framework, so a better option for complex applications that are likely to add new features over time. Once you’ve decided which is best for you — you can get started building.
WebSockets and SSE: Getting started
A) Open Source solutions
Open sourcing WebSockets
On the WebSocket front there are two primary classes of WebSocket libraries: those that implement the protocol and leave the rest to the developer, and those that build on top of the protocol with various additional features commonly required by realtime messaging applications, such as restoring lost connections, pub/sub and channels, authentication, authorization, etc. The latter variety often requires that their own libraries be used on the client-side, rather than just using the raw WebSocket API provided by the browser. As such, it becomes crucial to make sure you’re happy with how they work and what they’re offering. You may find yourself locked into your chosen solution’s way of doing things once it has been integrated into your architecture, and any issues with reliability, performance, and extensibility may come back to bite you later (back to the user specific tech debt weigh up, as mentioned above). Let’s take a look at how WebSockets can be implemented.
The server-side — Using WebSockets on the server
ws is a”simple to use, blazing fast and thoroughly tested WebSocket client and server for Node.js”. It is a barebones implementation, designed to do all the hard work of implementing the protocol. However, bear in mind additional features such as connection restoration, pub/sub, and so forth, are concerns you’ll have to manage yourself (see the end of this article for scalability concerns).
Server (Node.js):
const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); wss.on('connection', function connection(ws) { ws.on('message', function incoming(message) { console.log('received: %s', message); }); ws.send('something'); });
The client-side — Using WebSockets in the browser
The WebSocket API is defined in the WHATWG HTML Living Standard and is pretty trivial to use. Constructing a WebSocket takes just one line of code:
//JS const ws = new WebSocket('ws://example.org');
Note the use of ‘ws’ where you’d normally have the ‘http’ scheme. There’s also the option to use wsswhere you’d normally use ‘https’. These protocols were introduced in tandem with the ‘WebSocket’ specification, and are designed to represent an HTTP connection that includes a request to upgrade the connection to use WebSockets. Creating the ‘WebSocket’ object doesn’t do a lot by itself. The connection is established asynchronously, so you’d need to listen for the completion of the handshake before sending any messages, and also include a listener for messages received from the server:
ws.addEventListener('open', () => { // Send a message to the WebSocket server ws.send('Hello!'); }); ws.addEventListener('message', event => { // The `event` object is a typical DOM event object, and the message data sent // by the server is stored in the `data` property console.log('Received:', event.data); });
There are also ‘error’ and ‘close’ events. WebSockets don’t automatically recover when connections are terminated — this is something you need to implement yourself, and is part of the reason why there are many client-side libraries in existence. While the ‘WebSocket’ class is straightforward and easy to use, it is still just a basic building block. Support for different subprotocols or additional features such as messaging channels need to be implemented separately.
The open source route to SSE
SSE was initially built on the ‘EventSource’ API, which is quite simple and straightforward, and as such, open-source SSE solutions are uncommon. Nevertheless, because of backward compatibility with older browsers, there are some few libraries which are used as poly-fills. Below are a few of them:
Here’s how SSE can be implemented.
Implementing SSE server-side
‘express-sse’ is an Express middleware for quick’n’easy server-sent events. It is meant to keep things simple if you need to send server-sent events without too many complications and fallbacks.
var SSE = require('express-sse'); var sse = new SSE(); app.get('/stream', sse.init); // we can always call sse.send from anywhere the sse variable is available, and see the result in our stream. let content = 'Test data at ' + JSON.stringify(Date.now()); sse.send(content);
If you were to implement this without any library, we would have something like this:
app.get('/stream', (req, res)=>{ res.status(200).set({ "connection": "keep-alive", "content-type": "text/event-stream" }); res.write(`data: Hello there \n\n`); });
Looking at the code above, notice three things:
- ‘Text/event-stream content’ type header. This is what browsers look for to confirm an event stream.
- ‘Keep-alive’ header. This tells the browser not to close the connection.
- The double new-line characters at the end of data. This indicates the end of the message.
Client-side: Using SSE in the browser
SSE in the browser is implemented using the ‘EventSource’’s API. We need to pass in the URL to our steam to ‘EventSource’, and then tell it what to do when it receives a new message. For our use-case, we will simply log the message.
SSE, WebSockets and open-source — levels of relevance
Earlier in this article (see pros and cons section) we demonstrated that WebSockets, although harder to get your head around) might be the better choice if you foresee your app working at scale. To recap, this was due to the fact WebSockets offer a full-duplex TCP connection, which can be be used to further a wider range of realtime application scenarios, ie beyond purely push-based.
The same goes for the open source vs hosted solution choice. Open source is great in the initial stages, but it soon becomes clear when your use case has outgrown the capacity of open-source solutions. Considerations that might end up taking up a lot of developer time, should you go down the open source route, include:
…the list goes on. To talk about scaling your realtime application using Ably, the world’s fastest and most reliable data stream network, get in touch, or read more about important realtime engineering problems and solutions.
provides cloud infrastructure and APIs to help developers simplify complex realtime engineering. We make it easy to power and scale realtime features in apps, or distribute data streams to third-party developers as realtime APIs.
Originally published at https://www.ably.io on September 30, 2019.