Event Source Interface, an unidirectional alternative to Sockets in JavaScript
Felippe Regazio
Posted on February 17, 2020
A Web Socket is a computer communication protocol that provides a bidirectional communication between the server and the client. Its specially useful if you need a persistent connection for long time running tasks with decision flow, games, chats, etc. and its pretty faster if compared to polling. In a socket you can send information to your server anytime and wait for the response and vice-versa. Also a socket supports many connections, so the same socket can have multiple-clients sharing the same channel.
But sometimes you don't need to exchange information with the server in that fashion, you just need to be notified by the server about something, for exemple a social network status or keep monitoring a long running task. In these cases, you could use a socket anyway but would be a waste of resources. You could also sending many requests to a server endpoint, but would be harmful for your performance. Would be better to use the Event Source Interface.
Unlike WebSockets, server-sent events are unidirectional; that is, data messages are delivered in one direction, from the server to the client (such as a user's web browser). That makes them an excellent choice when there's no need to send data from the client to the server in message form. For example, EventSource is a useful approach for handling things like social media status updates, news feeds, or delivering data into a client-side storage mechanism like IndexedDB or web storage | https://developer.mozilla.org/en-US/docs/Web/API/EventSource
For the backend, we will create an endpoint for the Event Source. This will be our Event Emitter, and it must follow a pre-defined structure. We will need to set some headers:
Content-Type: text/event-stream
Cache-Control: no-cache
And the data must be sent on the body like this:
data: some string
Thats all. Traducing it to PHP, you would have something like this:
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
function send_message ($message) {
echo "data: " . json_encode($message) . PHP_EOL;
echo PHP_EOL;
ob_flush();
flush();
}
I'll use PHP for that purpose but you can write your backend in any language that you prefer.
On the code above we are setting the proper headers, and a function that flushes the data: "{...}"
on the body. The message don't need to be a encoded json, it can be a plain string, but lets encode it to be more scalable.
The EventSource connection will be persistent. The client request activates the backend, and it keeps re-starting the server script every time that it ends till you explicitly tells your client to stop.
Lets write a backend that counts to 50 waiting 2 seconds between every number. Before pass to the next number, we will send our current index to our client.
Here's the entire PHP script for that:
<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
function send_message ($message) {
echo "data: " . json_encode($message) . PHP_EOL;
echo PHP_EOL;
ob_flush();
flush();
}
for ($i = 0; $i < 50; $i++) {
send_message($i + 1);
sleep(2);
}
send_message('done');
If you access a file with that code, you will see the data being flushed on the page. Every number is an event being sent from the server to the client. Now we need to listen those events on our application and handle them as we prefer. Thats a job for the Event Source Interface.
The client will be pretty simple. We will create a new EventSource using its constructor, point it to our backend script and start to listen the messages.
The events emitted by the EventSource instance are onopen
, onmessage
and onerror
. They are pretty descriptive and our JavaScript must be quite simple. We will create our EventSource instance, listen to the events from the server and execute a function to handle those events properly.
// here we are defining the backend endpoint
const EVENT_SOURCE_ENDPOINT = 'backend/event_server.php';
// instantiating the EventSource and pointing it to our endpoint
const ServerEvents = new EventSource(EVENT_SOURCE_ENDPOINT);
// listening to the connection with the server
ServerEvents.addEventListener('open', e => {
handleServerConnection();
});
// listening to server messages
ServerEvents.addEventListener('message', e => {
const data = JSON.parse(e.data);
handleServerMessage(data);
});
// listening to errors
ServerEvents.addEventListener('error', e => {
handleServerError(e);
});
The comments on the JS code above will must be enough to give a pretty notion about whats happening. The server will be sending messages to our client application which is listening to them. Every time a message is delivered, the client listens the event source event and runs our handle.
The application will be still available for the user and those functions will be running asynchronously executing the handlers always when the event happens. This is just my way to handle it, you could just write the code inside the events callbacks and do whatever you want if you prefer.
Here is the complete JS example:
(function () {
// here we are defining the backend endpoint
const EVENT_SOURCE_ENDPOINT = 'backend/event_server.php';
// instantiating the EventSource and pointing it to our endpoint
const ServerEvents = new EventSource(EVENT_SOURCE_ENDPOINT);
// listening to the connection with the server
ServerEvents.addEventListener('open', e => {
handleServerConnection();
});
// listening to server messages
ServerEvents.addEventListener('message', e => {
const data = JSON.parse(e.data);
handleServerMessage(data);
});
// listening to errors
ServerEvents.addEventListener('error', e => {
handleServerError(e);
});
// ------------------------------------------------------
// append a string (msg) on our <pre> element
uiRenderMessage = (msg) => {
document.getElementById('server-messages').append(`${msg}\n`);
}
// show the connected message when connect to the server
handleServerConnection = () => {
uiRenderMessage('A connection with server has been established\n');
}
// handle the messages received by the server
handleServerMessage = msg => {
uiRenderMessage(`Server said: ${msg}`);
if (msg === 'done') {
// if you don't handle a closing message, the process will
// start all over again.
uiRenderMessage('\n');
ServerEvents.close();
}
}
handleServerError = evt => {
uiRenderMessage('An unexpected error occurred :(');
console.error(evt);
}
})();
Once the backend script is started by the client requisition, you must send a close message on the end of the process and use it to close your EventSource on the client. If you don't do that, the process will keep repeating all over and over again. This would result in our application starting to count till 50 again on every end. If you look at the handleServerMessage() function, we wait for the 'done' message to know when the server has all the work done. Depending on your usage, you wont
need a close flag.
Quick Tip: When instantiating the EventSource, you can send some data to the backend by attaching it on the URL GET Params. Its not a form of communication since you can send the data only one time, but its useful anyway. In the case of this example, you would add the data on the GET Params of the EVENT_SOURCE_ENDPOINT.
I created a repo on GitHub with this example running. You must clone the repo, and access the index.html. Its a stupidly simple example with 3 main files with not more then 20 lines each. You will see a simple page with a text area (to check the page availability), and an element showing the server messages appearing one by one, counting till 50 as the server updates them.
If you don't have the LAMP stack on your computer to test it but have PHP, you can go to the "backend" folder on the repo and run:
php -S 0.0.0.0:9999
This will run a built-in php server on that folder.
Now change the EVENT_SOURCE_ENDPOINT on the JS file to "http://localhost:9999/event_server.php".
With that pattern is possible to construct a lot of cool things, i guess. Happy code. Thats all folks!
Cover Photo by Tian Kuan on Unsplash
Posted on February 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
July 21, 2024