Server-Sent Events: An Overlooked Browser Feature

by | Feb 14, 2024

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.

StrengthsWeaknesses
Server-Sent Events
  • uses the native HTTP/S protocols
  • lightweight, no additional overheads
  • will not get blocked by firewalls
  • no more than 6 connections can be opened up for the same subdomain (only on HTTP/1, HTTP/1.1)
  • only allows unidirectional communication (server > browser)
  • limits the usage in a web worker context to shared and dedicated workers (not service workers)
WebSockets
  • allows bidirectional communication (server <> browser)
  • has a lot of libraries and frameworks with built-in support for it
  • can be accessed in any context (web, web workers) on the major browsers
  • the elevated protocol might get blocked by a firewall
  • small overhead for each connection
  • can be more complex to setup
  • securing websocket communications can be really hard

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 EventSourceobject 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.

You May Also Like…

Releasing Icinga DB v1.2.1

Releasing Icinga DB v1.2.1

Today we are releasing a new version of Icinga DB, version 1.2.1, a maintenance release that addresses HA issues and...

Subscribe to our Newsletter

A monthly digest of the latest Icinga news, releases, articles and community topics.