Communication temps réel avec Server-Sent Events (SSE)

Communication temps réel avec Server-Sent Events (SSE)

Lorsque l’on souhaite mettre en place une communication en temps réel entre nos applications et nos APIs, on pense souvent à l’utilisation des web sockets, mais ce n’est pas toujours la bonne solution. Il existe une autre fonctionnalité souvent méconnue qui se nomme SSE pour Server-Sent Events.

Note : Avant de poursuivre, je tiens à préciser que les exemples de cet article sont fonctionnels, mais sont simplifiés au maximum pour se concentrer sur la mise en oeuvre de Server-Sent Event, je pense notamment à la partie qui concerne la mise en place coté back-end. Je ne présenterais pas l’accès à la base de données ni comment architecturer correctement l’application, je préfère me concentrer sur les concepts clés c’est pourquoi certains raccourcis seront pris, libre à vous d’adapter en fonction de vos projets. J’ai tout de même créé un projet complet qui est disponible sur GitHub si vous êtes intéressé.

Pourquoi mettre en place du temps réel ?

De nos jours, la plupart des sites web ou applications que vous utilisez quotidiennement que ce soit Twitter, Facebook, Discord ou encore Snapchat ont toutes un point en commun : ils proposent une expérience temps réel aux utilisateurs.

Si je vous dis “FOMO” ça ne vous dit peut-être rien, mais c’est l’acronyme de “Fear Of Missing Out” que l’on peut traduire par la peur de rater quelque chose. Bon, on ne va pas faire un cours de marketing, mais ce sentiment de peur est très utilisé par les sociétés pour vous rendre dépendants à leurs applications comme c’est le cas avec les réseaux sociaux. 

Nos applications se doivent donc d’être interactives et transmettre de nouvelles informations aux utilisateurs sans intervention de leur part. Il est aujourd’hui impensable de devoir recharger une page web pour mettre à jour celle-ci.

Des solutions existent…

Nous devons donc mettre en place un moyen de communication en “temps réel” entre nos applications et nos APIs. De nombreuses solutions existent, parmi les plus connues nous avons :

  • Polling : Cette solution vous l’avez tous utilisée, notre application va interroger toutes les x secondes notre API pour voir si de nouvelles données sont disponibles;
  • Long polling : C’est une variante de la précédente solution. La différence est que notre API va cette fois-ci garder la connexion ouverte avec notre application et renvoyer une réponse une fois que de nouvelles données sont disponibles;
  • Web sockets : Cette fois-ci une connexion persistante est établie entre notre application et notre API. Les échanges peuvent se faire simultanément dans les deux sens (full duplex).

Les deux premières solutions présentes de nombreux inconvénients notamment une consommation en bande passante et en ressources importante, c’est pourquoi on pense souvent à l’utilisation des web sockets. Mais l’on a pas toujours besoin d’une communication bidirectionnelle, c’est le cas par exemple pour :

  • Afficher le compteur des “j’aime” sur les réseaux sociaux;
  • Mettre à jour l’affichage du stock d’un produit;
  • Afficher les résultats d’événements sportifs;
  • Recevoir des notifications;
  • etc.

Nous allons donc voir l’utilisation d’une solution trop peu méconnue, vous vous en doutez je parle de Server-Sent Events.

Il y a bien longtemps…

Pour la petite histoire, la technologie Server-Sent Events est apparue en 2006 en tant que fonctionnalité expérimentale du navigateur Opéra, c’est à dire deux ans avant les web sockets qui sont apparus quant à eux en 2008. Elle a par la suite été standardisée au sein de HTML 5. Autant dire que ça ne date pas d’hier !

Comment ça fonctionne ?

Tout comme web sockets, la connexion entre le client et le serveur est persistante, mais contrairement à celui-ci, Server-Sent Event établi une communication unidirectionnelle, les messages transitent uniquement du serveur vers le client.

Server-Sent Event repose entièrement sur le protocole HTTP contrairement à web socket qui est un protocole à part entière. La communication se fait comme suit :

  1. Le client se connecte sur le serveur via une URL;
  2. Le serveur maintient la connexion ouverte et envoie une réponse au client avec le header Content-Type: text/event-stream;
  3. Dès que des données sont disponibles, le serveur les envoie au client.
  4. Si la connexion est perdue avec le serveur, le client envoie une demande de reconnexion au serveur avec le header  Last-Event-ID qui permet d’indiquer au serveur qu’elle est le dernier message reçu;
  5. Le client ou le serveur peut mettre fin à la connexion.
Communication Server-Sent Event
Communication Server-Sent Event

Format des messages

Contrairement aux web sockets dont le type de données peut être soit du binaire, soit une chaîne de caractères encodée en UTF-8, Server-Sent Event ne gère que le dernier cas. De plus, les messages doivent respectés un format précis décrit dans la norme :

Chaque message est constitué d’une paire clé/valeur séparée par les deux points : et terminé par un saut de ligne \n. Le message doit lui même être terminé par deux sauts de ligne \n\n

Voyons voir à quoi correspond chacune des paires clé/valeur :

  • id (optionnel) : L’ID unique du message, utilisé par le client lors d’une reconnexion (avec le header Last-Event-ID) pour éventuellement récupérer les données manquantes;
  • event (optionnel) : Le type de message reçu permettant notamment au client de séparer ses traitements en fonction du type de message, la valeur par défaut est message;
  • retry (optionnel) : Le délai en millisecondes avant une tentative de reconnexion au serveur de la part du client;
  • data (obligatoire) : Le contenu du message qui peut être du texte ou du JSON.

Implémentation

Voyons dès maintenant l’implémentation de Server-Sent Event côté client dans un premier temps et côté serveur dans un second temps.

Côté client

La mise en place de Server-Sent Event côté client est extrêmement simple. En effet, les navigateurs proposent la classe EventSource, il suffit de créer une nouvelle instance en passant en paramètre du constructeur l’URL de notre source :

Si la connexion s’est bien déroulée, un événement open est déclenché. Il est possible d’enregistrer un gestionnaire pour cet événement en passant par la méthode onopen (coucou l’absence de camelcase…) ou via la méthode addEventListener :

Il est également possible de vérifier l’état de la connexion via la propriété readyState de l’instance d’EventSource qui a pour valeur :

  • 0 (ou EventSource.CONNECTING) : Connexion ou reconnexion en cours;
  • 1 (ou EventSource.OPEN) : Connecté;
  • 2 (ou EventSource.CLOSE) : Connexion fermée.

Si une erreur se produit, nous pouvons enregistrer un gestionnaire d’événement via la méthode onerror (recoucou l’absence de camel case…) ou via la méthode addEventListener afin de traiter cette erreur :

Récupération des messages

Pour récupérer les messages envoyés par le serveur, rien de plus simple, il suffit de faire appel à la méthode onmessage (décidément l’absence de camel case…):

Le contenu du message est disponible dans la propriété data de l’événement.

Par contre comme nous l’avons vu tout à l’heure, le serveur peut définir des types de messages via la propriété event permettant au client de séparer ses traitements en fonction du type de message. 

Avec la méthode onmessage il est uniquement possible de récupérer les messages ayant le type par défaut message. Pour récupérer les autres types de messages, il suffit d’enregistrer un gestionnaire d’événements pour chacun des types :

Lors de la réception d’un message possédant un id, le client assigne la valeur de cette id à la propriété lastEventId de notre instance d’EventSource. Nous allons voir juste après à quoi sert cette propriété.

Gestion de la reconnexion

Server-Sent Event fournit un mécanisme de reconnexion automatique en cas de déconnexion avec le serveur. Le client s’occupe automatiquement de la reconnexion après quelques secondes ou via un délai défini arbitrairement par le serveur via un message reçu avec la propriété retry définie.

Lors de la reconnexion au serveur, le client envoi via le header Last-Event-ID, la valeur de la propriété lastEventId, contenant l’id du dernier message reçu , permettant au serveur d’envoyer les éventuels messages non reçus par le client.

Fermeture de la connexion

Pour fermer la connexion avec le serveur, il suffit de faire appel à la méthode close :

Côté serveur

Il existe déjà des librairies implémentant Server-Sent Event (sse.js ou express-sse) mais le but de cet article est de comprendre comment fonctionne Server-Sent Event et rien de tel que de mettre les mains dans le cambouis, bref commençons !

Gestion de la connexion

Commençons dans un premier temps par gérer la connexion d’un client à notre serveur. Pour cela nous allons créer une classe SSEClient :

Bon jusque là rien de bien compliqué hein ? Et bien, dites-vous que l’on a presque terminé, c’est vraiment aussi simple que ça !

Création du serveur HTTP

Créons à présent notre serveur HTTP à l’aide d’Express et ajoutons une route permettant à un client de se connecter via Server-Sent Event :

Testons avec le client 

Vérifions que tout cela fonctionne en nous connectons à notre route avec notre navigateur web :

Vous pouvez utiliser la console de votre navigateur pour voir les messages reçus. Sur Chrome, il suffit de se rendre dans l’onglet “Network” puis “EventStream”.

Affichage des messages reçus dans la console du navigateur
Affichage des messages reçus dans la console du navigateur

Centraliser la gestion des connexions

Pour le moment, notre exemple est tout simple, dès qu’un client se connecte on lui envoie un message de bienvenue. Maintenant, comment gérer le cas où l’on souhaite envoyer des messages à une liste de clients en particulier ? 

Nous allons ajouter une classe SSEManager permettant de gérer les connexions et l’envoie des messages aux clients :

L’idée est que notre manager soit accessible au sein de notre application, plusieurs solutions s’offrent à nous comme l’injection de dépendances, l’utilisation d’un conteneur de dépendances, la création d’un singleton que nous importons via l’instruction require, etc.

Nous allons utiliser sur une solution très simple afin de rester concentrer sur le fonctionnement de notre manager, libre à vous par la suite d’architecturer votre application comme bon vous semble.

Créons une nouvelle instance de notre manager et enregistrons là dans notre application Express afin que celui-ci soit accessible dans nos routes :

Créons ensuite notre route :

Testons maintenant tout ça côté client :

À chaque connexion/déconnexion, le compteur de client se met à jour.

Sécurité

On a vu dans deux précédents articles comment sécuriser une API REST, je vous invite à aller les lire, je vous mets les liens ici : Sécuriser une API REST (1/3) : Théorie et Sécuriser une API REST (2/3) : Implémentation en Node.js.

Outre le fait qu’il est indispensable d’utiliser une connexion sécurisée via l’utilisation de HTTPS, il est également nécessaire d’authentifier un utilisateur lorsque l’on souhaite avoir certaines routes privées de notre API utilisant Server-Sent Event.

Dans le cas de l’utilisation de cookies, il suffit de définir l’option withCredentials à true lors de l’instanciation de la classe EventSource :

Mais comment faire pour définir nous même des en-têtes HTTP par exemple l’en-tête Authorization avec comme valeur un JWT ? Et bien ce n’est malheureusement pas possible avec la classe native EventSource. Il est possible néanmoins d’utiliser des librairies permettant d’envoyer les en-têtes HTTP, mais cela sort du cadre du standard Server-Sent Event.

Une autre solution consiste à récupérer un token, que l’on va appeler “stream token”, à l’aide d’un JWT en effectuant une requête HTTP sur une route de notre API, puis de se connecter sur le flux privé à l’aide de celui-ci.

Authentification via un token

Authentification via un “stream token”

Implémentons donc cette solution. Créons notre route permettant de récupérer le “stream token” à l’aide d’un JWT, nous n’allons pas revenir sur comment récupérer un JWT je vous invite à aller lire l’article qui lui est dédié :

Créons ensuite un middleware streamToken permettant de récupérer un utilisateur à partir d’un “stream token”:

Modifions notre précédente route en utilisant cette fois-ci le middleware que l’on vient de créer :

Terminons avec la partie coté client, récupérons le “stream token” et connectons nous :

Le “stream token” possède une durée de validité et il est également possible, pour plus de sécurité, de supprimer celui-ci une fois la connexion établie. Le souci avec cette approche est qu’en cas de déconnexion, la reconnexion automatique sera confrontée à une erreur 401 car le “stream token” n’existe plus. Il sera alors nécessaire d’intercepter cette erreur côté client et de procéder à une nouvelle connexion afin de récupérer un “stream token” valide.

Tout cela peut se réaliser en ajoutant un gestionnaire d’événement d’erreur :

Pour finir

On a vu tout au long de cet article qu’il était simple de mettre en place Server-Sent Event aussi bien coté back que front.

Il y a de nombreux cas d’utilisations de Server-Sent Event, par exemple Facebook l’utilise pour mettre à jour en temps réels les réactions et les commentaires sur les vidéos en direct. On pense souvent à l’utilisation de web sockets alors que ce n’est pas toujours la meilleure solution notamment lorsque l’on souhaite uniquement recevoir des informations du serveur. 

J’espère vous avoir fait découvrir cette fonctionnalité qui est malheureusement trop méconnue de la plupart des développeurs.

Pour les plus curieux, un petit projet, qui reprend ce que l’on vient de voir dans cet article, est disponible sur GitHub, allez y jeter un œil !

Développeur back (nodejs & php), je fais aussi du front (react). Je partage mes connaissances et ma passion au travers de mes articles. N'hésitez pas à me suivre sur Twitter.

1 commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.