In an ever growing world of libraries, frameworks and content packagers, we tend to loose the focus on already available native technology, which comes shipped with tools that we use on a daily basis. One such feature goes by the name of Server-Sent Events and it has been around for more than a decade, whilst being supported by all major browsers.
This article attempts to shed light on this lesser known functionality and how it can be an adequate solution to some of the more common requirements of web applications that are displaying data in real-time.
From Ancient to Modern
But for now, let’s go back in time for a quick while and evaluate how browsers used to communicate new data towards browsers. In the earliest stages of websites filled with dynamic data, the so-called “long polling” and “streaming” techniques were used in a lot of forms. They were categorized under the Comet model.
Whatever way the implementations differentiated from each other, the idea behind it stayed the same.
Long polling
After the initial load of the website, a subsequent request would be made in form of a XML HTTP Request (XHR) or through user-agent based redirections like HTML’s http-equiv meta tag or HTTP’s Location header. On the receiving end, the server made sure to not return those requests immediately but only after the data on its side changed.
Streaming
Opening up a persistent connection towards the server, which would then be followed by serving the response in chunks, enabled a technically infinite response stream. Those chunks could then be individually processed and acted upon by the receiving browser.
This opened up the possibility to display new state changes towards browsers with very little delay. But it also had its negatives, as the server would need to keep more connections opened up and thus could not free up the associated resources. The overhead of the tcp handshakes and the needed memory also played a role in terms of the server load.
As the internet evolved and more and more web applications turned from static sites towards user-interactive interfaces, the need for a better way to exchange such data became more important. In 2008, this demand led to a proposal towards the HTML5 specification, in which persistent TCP connections should get properly integrated and respected by all of the web clients.
This was the turning point for today’s functionalities like WebSockets and Server-Sent Events.
WebSockets vs Server-Sent Events
When comparing the two available solutions that address this demand, we should first examine their strengths and weaknesses.
Strengths | Weaknesses | |
Server-Sent Events |
|
|
WebSockets |
|
|
As you can see, both of them have their right to exist. It should however be noted that the SSE technology might be a better fit for simple server to browser communication streams, as it makes use of the well performing and secure hypertext transfer protocol, without adding another layer of abstractness to it as the web sockets do.
The Usage Of Server-Sent Events
On the client side, a simple EventSource
object can be created. The incoming events are triggered on the EventSource
object itself.
Bindings can be achieved by using the onmessage(), onopen() and onerror()
methods on the object, as well as adding an event listener to it.
Example: Client (JavaScript)
let source = new EventSource('/example/directive/listen'); source.addEventListener('open', (event) => { self.console.log(`connection to '${source.url}' established`); }); source.addEventListener('error', (event) => { self.console.error("received an error: ", event); }); source.addEventListener('message', (event) => { self.console.log("received a generic event with data: ", event.data); }); source.addEventListener('foo', (event) => { // prints "received a custom event 'foo' with data: bar" self.console.log("received a custom event 'foo' with data: ", event.data); });
If we want to match the client implementation on the server side, we need to make sure to return a chunked event-stream with the mime type application/event-stream
.
There are plenty of languages to program in, but if we were to use a minimalistic ReactPHP approach, we would just need to initialize our HTTP connection, followed by serving our events to the current listeners (clients).
Example: Server (PHP with the ReactPHP library)
use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\Loop; use React\EventLoop\LoopInterface; use React\Http\HttpServer; use React\Http\Message\Response; use React\Socket\SocketServer; final class Sender { /** @var LoopInterface $mainLoop */ private $mainLoop; /** @var SocketServer $socket */ private $socket; /** @var HttpServer $http */ private $http; public function __construct() { // get main react loop $this->mainLoop = Loop::get(); // spawn socket $this->socket = new SocketServer('[::]:9001', [], $this->mainLoop); // spawn http server wrapper $this->http = new HttpServer(function (ServerRequestInterface $_) { return new Response( StatusCodeInterface::STATUS_OK, [ "Content-Type" => "text/event-stream; charset=utf-8", "Cache-Control" => "no-cache", "X-Accel-Buffering" => "no" ], 'event: foo' . PHP_EOL . 'data: bar' . PHP_EOL . PHP_EOL ); }); // attach http wrapper to socket server $this->http->listen($this->socket); } } new Sender();
This could obviously be further expanded to include proper authentication, the usage of ReactPHP’s ThroughStream, you name it.
Final thoughts
Hidden treasures can be found in various places, but we may not notice them due to our routines and experiences. Therefore, it is always worthwhile to step out of our comfort zone and consider new approaches when solving problems.
Like in my case, where I have worked with WebSockets before but never actually researched native alternatives for use-cases when there’s only a need for a one-directional communication.