Server-Sent Events démystifiés, avec un cas concret.

jefflefoll

Jean-François Le Foll

Posted on March 11, 2020

Server-Sent Events démystifiés, avec un cas concret.

Préface

Avant d'aller plus loin, expliquons d'abord ce que sont les Server-sent Events (SSE pour les intimes).

Il s'agit d'un canal unidirectionnel permettant au serveur d'envoyer des messages à un navigateur.

C'est une techno ancienne, qui date de 2009-2010 et est disponible au grand public depuis les versions 6 de Chrome et Firefox, mais qui a été un peu mise de coté à l'arrivée des WebSockets.

Alt Text

Objectif ?

Le but de cette démo est de construire un cas concret d'utilisation de SSE.
Beaucoup d'articles ou de démos que j'ai pu trouver ne montrent guère autre chose qu'un event pré-construit envoyé à intervalle régulier, pas grand-chose à voir avec la réalité.

Dans cette démo, nous allons construire une API Rest, qui poussera un message dans un RabbitMQ en mode Fan-out.
De l'autre coté, nous aurons un serveur nodejs qui écoutera les messages et poussera un SSE au bon client de la webapp servi par le nodejs.
Alt Text

Dans l'objet poussé sur l'endpoint, nous avons un client ID, ce qui permet d'avoir plusieurs onglets ouverts sur la web app et ainsi voir que les SSE arrivent seulement au bon client.

Le code :

L'API Rest

Je ne vais pas entrer ici dans les détails de l'API Rest, c'est une API Java (Jooby 2) qui sert de producer RabbitMQ.

La Web App

Il s'agit d'une simple page html avec un Web Component (les dépendances sont chargées directement avec Pika, cela peut prendre quelques secondes).

Pour se connecter à un endpoint SSE, rien de plus simple, il suffit de définir un EventSource qui prend en paramètre l'url de la source.

const sseSource = new EventSource("/event-stream/" + this.clientId);
Enter fullscreen mode Exit fullscreen mode

Dans notre cas, nous ajoutons dans l'url le clientId du client connecté.

Sur cet objet EventSource nous allons venir définir un event listener, le but est de ne réagir qu'aux messages de type notif :

    sseSource.addEventListener("notif", e => {
      this.count = this.count + 1;
      this.notifications = [...this.notifications, e.data];
    });
Enter fullscreen mode Exit fullscreen mode

Ce code nous permet d'incrémenter un compteur et d’alimenter un tableau avec le message de l’événement.
C'est tout pour la partie front.

La partie serveur NodeJs

1) Le point d'entrée SSE

Ici on utilise ExpressJS car il n'y rien de particulier à faire pour mettre en place la ressource SSE :

app.get("/event-stream/:clientId", (req, res) => {
  let clientId = req.params.clientId;

  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive"
  });
  res.write("\n");

  connections[clientId] = res;

  console.log("Connection open for client : " + clientId);

  req.on("close", () => {
    delete connections[clientId];
  });
});
Enter fullscreen mode Exit fullscreen mode

Ici nous définissons notre ressource GET "/event-stream/:clientId" avec un paramètre clientId qui nous permet de stocker l'objet response sur lequel envoyer les messages :

    connections[clientId] = res;
Enter fullscreen mode Exit fullscreen mode

Mais avant de stocker cette réponse, il faut la "configurer" pour le SSE :

  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive"
  });
Enter fullscreen mode Exit fullscreen mode

Pour résumer cette partie, à chaque fois qu'un client fait un GET sur notre ressource SSE, nous stockons dans un objet sont clientId et l'objet response afin de lui pousser des messages ultérieurement.

2) Le consumer RabbitMQ

Afin de pouvoir se connecter sur le RabbitMQ, nous allons avoir besoin de la dépendance vers la librairie amqplib

   const amqp = require("amqplib/callback_api"); // => fonctionne avec des callback
// ou
   const amqp = require("amqplib");  // => fonctionne avec des promesses
Enter fullscreen mode Exit fullscreen mode

Au démarrage du serveur nodejs, on ouvre une connexion vers RabbitMQ

amqp.connect("amqp://localhost", (error0, connection) => {})
Enter fullscreen mode Exit fullscreen mode

Dans cette connexion, on va créer un channel sur notre Exchange et une Queue (Cf dépôt Github pour le code de cette partie).

Ce qui nous intéresse ici, c'est de voir comment on consomme les messages.

      channel.consume(
        q.queue,
        msg => {
          if (msg.content) {
            let data = JSON.parse(msg.content.toString());

            console.log("New message for client : " + data.clientId);
            let clientRes = connections[data.clientId];

            if (clientRes) {
              clientRes.write(`event: notif\n`);
              clientRes.write(`data: ${data.content}\n\n`);
            }
          }
        },
        {
          noAck: true
        }
      );
Enter fullscreen mode Exit fullscreen mode

On va parser le message pour en récupérer le contenu, grâce au clientId du message, on récupère notre objet response qui correspond.
Si elle existe, on vient écrire une première ligne avec le type d'événement clientRes.write('event: notif\n'); (pour rappel notre front n'écoute que les messages notif).
Puis une seconde ligne avec le contenu du message clientRes.write('data: ${data.content}\n\n');

Et voilà, nous avons poussé notre message dans notre stream d'événements.
Le front réagi, incrémente le compteur et affiche le message !
Alt Text

J'espère que maintenant l'utilisation des SSE est un peu plus claire et concrète pour vous aussi :)


Le code de la démo complète est disponible sur GitHub :

GitHub logo Avalon-Lab / realtime-webapp-sse-rabbitmq

Demo WebApp temps réel avec SSE et RabbitMQ

💖 💪 🙅 🚩
jefflefoll
Jean-François Le Foll

Posted on March 11, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related