Appearance
Server-Sent Events
Principe
Un Server-Sent Event (ou SSE, événement envoyé côté serveur) est une stratégie qui repose sur le protocole HTTP et qui permet de tirer parti du protocole TCP lui-même afin d'optimiser les échanges récurrents et permanent entre le client et le serveur.
Il est bien important de respecter plusieurs choses afin de pouvoir utiliser les SSE.
En premier lieu, comme cette stratégie repose sur le protocole HTTP, il est important de faire en sorte que la connexion soit permanente, pour palier à la limitation de ce dernier.
Heureusement, depuis la version 2 du protocole HTTP, nous pouvons conserver le tunnel de connexion sous-jacent TCP lié à la connexion HTTP par l'utilisation de l'en-tête Connection avec la valeur keep-alive. C'est un point clé qui va nous permettre de communiquer via un tunnel de connexion permanent avec le client.
Il faut aussi noter que l'utilisation de cette stratégie est régie par un format particulier. En effet, le client s'attend à recevoir des informations dans le corps de la réponse dans un format strict.
Nous pouvons par exemple envoyer des données dans le corps de la réponse formatté comme ci-dessous.
data: Hello, world!
En faisant cela de cette façon, cela revient à envoyer en réalité ceci.
event: message
data: Hello, world!
Nous envoyons ici un événement intitulé message, et un contenu texte qui a le contenu Hello, world!.
Si nous souhaitons envoyer un autre événement dont le nom est différent, nous pouvons le rajouter nous-même de cette façon.
event: notification
data: Soldes Surprise, -15% pendant 30mn sur tout le site !
Notez également que le retour à la ligne final n'est pas annodin, il permet de séparer les différents événements, puisqu'il est possible pour un seul et même tunnel HTTP d'envoyer plusieurs événements.
event: message
data: Hello, world!
event: notification
data: Soldes Surprise, -15% pendant 30mn sur tout le site !
Code côté serveur
Côté serveur, il y a la nécessité d'ajouter du code afin de permettre au client de pouvoir communiquer selon la même stratégie.
typescript
import express from "express";
const server = express();
server.get("/apple", (request, response) => {
response.set("Connection", "keep-alive");
response.set("Cache-Control: no-cache");
response.write("event: message\ndata: PRC: 12; VLT: 0.7; VLM: 2637\n\n");
});
server.listen(8000, "0.0.0.0", () => {
console.log("Server started");
});
Ceci est le code entier qui nous permet côté serveur de répondre aux requêtes des clients qui souhaiteraient récupérer le cours d'une action Apple par exemple en temps réel.
Attention
Nous n'avons pas ajouté le code nécessaire pour pouvoir récupérer à chaque fois qu'une données est modifiée en base de données, sa nouvelle valeur et l'envoyer en réponse du tunnel SSE pour ne pas alourdir l'exemple.
Comme vous pouvez le voir, nous avons bien ajouté l'en-tête Connection: keep-alive, ainsi qu'un second en-tête Cache-Control: no-cache. Ce dernier n'est en théorie pas nécessaire, mais il est toujours intéressant à avoir pour éviter que le navigateur ne mette en cache à tort le résultat de notre réponse HTTP.
typescript
import express from "express";
const server = express();
server.get("/apple", (request, response) => {
response.set("Connection", "keep-alive");
response.set("Cache-Control: no-cache");
response.write("event: message\ndata: PRC: 12; VLT: 0.7; VLM: 2637\n\n");
});
server.listen(8000, "0.0.0.0", () => {
console.log("Server started");
});
Ensuite, nous avons envoyé une réponse via la méthode write, et non pas send ou end.
Il ne faut surtout pas utiliser les deux dernières méthodes response.send ou response.end. En effet, si nous l'avions fait, le tunnet de connexion HTTP aurait été coupé, puisque le serveur indique au client d'envoyer la dernière partie de la réponse et de s'arrêter.
Bon à savoir
Si vous vous souvenez du fonctionnement du protocole HTTP, vous vous souvenez alors qu'une requête HTTP précède toujours une réponse HTTP, c'est pour cela que le serveur peut envoyer autant de corps de réponse qu'il le souhaite, sans jamais coupé la connexion HTTP ou rendre cette dernière invalide puisqu'elle respecte l'ordre dans lequel les en-têtes de requêtes, le corps de requête, les en-têtes de réponse et le corps de réponse doivent se situer.
C'est la subtilité du protocole HTTP sur laquelle se repose les SSE : tant que la réponse n'est pas finie, le client continue de recevoir des données, même passé un certain temps.
C'est donc pour cela que la méthode response.write est utilisée ici à la place car elle ne ferme pas le tunnel de connexion, et les données sont envoyées au fur et à mesure que le serveur continue d'en envoyer.
typescript
import express from "express";
const server = express();
server.get("/apple", (request, response) => {
response.set("Connection", "keep-alive");
response.set("Cache-Control: no-cache");
response.write("event: message\ndata: PRC: 12; VLT: 0.7; VLM: 2637\n\n");
});
server.listen(8000, "0.0.0.0", () => {
console.log("Server started");
});
Nous pouvons évidemment changer le nom de l'événement si nous le souhaitons.
typescript
import express from "express";
const server = express();
server.get("/apple", (request, response) => {
response.set("Connection", "keep-alive");
response.set("Cache-Control: no-cache");
response.write("event: pricing\ndata: PRC: 12; VLT: 0.7; VLM: 2637\n\n");
});
server.listen(8000, "0.0.0.0", () => {
console.log("Server started");
});
Et nous pouvons bien entendu renvoyer autre chose que des données sous la forme de chaîne de caractères, par exemple des données au format JSON.
typescript
import express from "express";
const server = express();
server.get("/apple", (request, response) => {
const data = JSON.stringify({
price: 12,
volatility: 0.7,
volume: 2637
});
response.set("Connection", "keep-alive");
response.set("Cache-Control: no-cache");
response.write(`event: pricing\ndata: ${data}\n\n`);
});
server.listen(8000, "0.0.0.0", () => {
console.log("Server started");
});
Code côté client
Du côté du serveur, nous avons une API Web qui nous permet de consommer cette stratégie sans avoir à analyser et parser toute la réponse à chaque fois qu'un morceau d'événement est reçu.
C'est l'API Web EventSource que nous utiliserons alors. Voici un exemple de son utilisation.
typescript
const eventSource = new EventSource("share.finances.com/apple");
eventSource.addEventListener("message", () => {
console.log("Un message est reçu !");
});
Comme vous le voyez ici, nous avons instancié un nouvel objet EventSource, et écouté un événement message depuis ce même objet. Cela est très similaire à ce que vous connaissez déjà sur la manière d'écouter un événement du DOM.
Si nous souhaitons écouter un événement en particulier, il nous suffit simplement de changer le nom de l'événement.
typescript
const eventSource = new EventSource("share.finances.com/apple");
eventSource.addEventListener("pricing", () => {
console.log("Un message est reçu !");
});
Et si nous souhaitons récupérer les données, nous pouvons le faire en renseignant un nom d'argent dans la fonction qui a été passé en second argument à notre objet eventSource.
typescript
const eventSource = new EventSource("share.finances.com/apple");
eventSource.addEventListener("pricing", (event) => {
console.log(event.data);
});
Attention
Le type de données de la propriété event.data sera TOUJOURS une chaîne de caractères, il est donc important que vous transformiez explicitement cette chaîne de caractères vers le type de données auquel vous vous attendez.
Nous pouvons transformer les données reçu vers un type de données qui nous intéresse, par exemple au format JSON.
typescript
const eventSource = new EventSource("share.finances.com/apple");
eventSource.addEventListener("pricing", (event) => {
const pricing = JSON.parse(event.data);
console.log(pricing.price);
console.log(pricing.volatility);
console.log(pricing.volume);
});
Astuce
Vous pouvez vous aider de librairie comme Zod pour vous aider à parser les données que vous recevez, et vous assurer que les données reçues respectent bien la structure auquel vous vous attendez, en plus de pouvoir vous permettre de typer votre code bien mieux si vous utilisez du TypeScript.
Avantage
L'avantage indéniable du protocole SSE est sa simplicité de mise en place pour pouvoir bénéficier d'un tunnel de connexion permanent, tout en paliant aux inconvénients du Polling qui se voient obligés d'ouvrir et fermer une connexion HTTP à chaque fois pour pouvoir rafraîchir les données.
De plus, si jamais les données ne sont pas rafraîchit (par exemple, une salle de marché qui ferme entre 22h30 et 7h30), il n'y aura aucun événement envoyé depuis un serveur, contrairement au Polling qui va envoyer des événements à intervalles régulier, pour finalement n'avoir que la même donnée à chaque fois.
Les SSE sont également trivial à mettre en place. Nous avons d'ailleurs montré un exemple en utilisant la plateforme Node.js, mais vous auriez pu tout à fait implémenter cela en PHP (moins pertinent mais tout à fait possible), en Go, en Rust, en Python et de manière générale, dans n'importe quel langage qui implémente le protocole HTTP.
Inconvénient
L'inconvénient majeur par rapport au Polling et au Long Polling est qu'il nécessite la mise en place d'un code côté serveur.
Cela est tout de même à relativiser, dans la plupart des projets, nous avons le contrôle sur le client et le serveur, néanmoins c'est un inconvénient à garder en tête si les projets sont amenés à évoluer vers des solutions SaaS par exemple qui fournissent les données.
L'autre inconvénient évident est qu'il repose toujours sur le protocole HTTP qui n'a pas été conçu pour ce genre de problématique et cas d'utilisation, malgré les évolutions du protocole HTTP/2 et son ajout d'en-tête comme Connection: keep-alive.
Contrairement à des solutions comme le protocole WebSocket qui sont des protocoles dediés pour les échanges en temps réel.