Diffuser les mises à jour avec des événements envoyés par le serveur

Les événements envoyés par le serveur (SSE) envoient des mises à jour automatiques à un client à partir d'un serveur, via une connexion HTTP. Une fois la connexion établie, les serveurs peuvent lancer la transmission de données.

Vous pouvez utiliser des SSE pour envoyer des notifications push depuis votre application Web. Les SSE envoient des informations dans un seul sens. Vous ne recevrez donc pas de mises à jour du client.

Le concept d'instructions SSE vous est peut-être familier. Une application Web "s'abonne" à un flux de mises à jour généré par un serveur. Chaque fois qu'un nouvel événement se produit, une notification est envoyée au client. Toutefois, pour bien comprendre les événements envoyés par le serveur, nous devons comprendre les limites de ses prédécesseurs AJAX. Par exemple :

  • Interrogation: l'application interroge à plusieurs reprises un serveur pour obtenir des données. Cette technique est utilisée par la majorité des applications AJAX. Avec le protocole HTTP, la récupération de données repose sur un format de requête et de réponse. Le client envoie une requête et attend que le serveur réponde avec des données. Si aucun n'est disponible, une réponse vide est renvoyée. Les requêtes d'interrogation supplémentaires créent une surcharge HTTP plus importante.

  • Long polling (GET en attente / COMET): si le serveur ne dispose pas de données, il maintient la requête ouverte jusqu'à ce que de nouvelles données soient disponibles. C'est pourquoi cette technique est souvent appelée "GET suspendu". Lorsque les informations sont disponibles, le serveur répond, ferme la connexion et le processus est répété. Ainsi, le serveur répond constamment avec de nouvelles données. Pour ce faire, les développeurs utilisent généralement des hacks tels que l'ajout de balises de script à une iframe "infinie".

Les événements envoyés par le serveur ont été conçus dès le départ pour être efficaces. Lorsqu'il communique avec des SSE, un serveur peut transmettre des données à votre application quand il le souhaite, sans avoir à effectuer de requête initiale. En d'autres termes, les mises à jour peuvent être diffusées du serveur au client au fur et à mesure. Les SSE ouvrent un seul canal unidirectionnel entre le serveur et le client.

La principale différence entre les événements envoyés par le serveur et le long-polling est que les SSE sont gérés directement par le navigateur et que l'utilisateur n'a qu'à écouter les messages.

Événements envoyés par le serveur par rapport aux WebSockets

Pourquoi choisir des événements envoyés par le serveur plutôt que des WebSockets ? Bonne question !

WebSockets dispose d'un protocole riche avec une communication bidirectionnelle en duplex intégral. Un canal bidirectionnel est préférable pour les jeux, les applications de messagerie et tout cas d'utilisation nécessitant des mises à jour en temps quasi réel dans les deux sens.

Toutefois, il arrive parfois que vous n'ayez besoin que d'une communication à sens unique à partir d'un serveur. Par exemple, lorsqu'un ami met à jour son état, des cours boursiers, des flux d'actualités ou d'autres mécanismes de transfert de données automatisés. En d'autres termes, une mise à jour d'une base de données Web SQL côté client ou d'un espace de stockage d'objets IndexedDB. Si vous devez envoyer des données à un serveur, XMLHttpRequest est toujours un allié.

Les SSE sont envoyées via HTTP. Aucune implémentation de protocole ou de serveur spécifique n'est requise. Les WebSockets nécessitent des connexions en duplex intégral et de nouveaux serveurs WebSocket pour gérer le protocole.

De plus, les événements envoyés par le serveur disposent de diverses fonctionnalités que les WebSockets ne possèdent pas par conception, y compris la reconnexion automatique, les ID d'événement et la possibilité d'envoyer des événements arbitraires.

Créer un EventSource avec JavaScript

Pour vous abonner à un flux d'événements, créez un objet EventSource et transmettez-lui l'URL de votre flux:

const source = new EventSource('stream.php');

Ensuite, configurez un gestionnaire pour l'événement message. Vous pouvez éventuellement écouter open et error:

source.addEventListener('message', (e) => {
  console.log(e.data);
});

source.addEventListener('open', (e) => {
  // Connection was opened.
});

source.addEventListener('error', (e) => {
  if (e.readyState == EventSource.CLOSED) {
    // Connection was closed.
  }
});

Lorsque des mises à jour sont transmises depuis le serveur, le gestionnaire onmessage se déclenche et de nouvelles données sont disponibles dans sa propriété e.data. La partie magique est que chaque fois que la connexion est fermée, le navigateur se reconnecte automatiquement à la source au bout d'environ trois secondes. L'implémentation de votre serveur peut même contrôler ce délai avant expiration de la reconnexion.

Et voilà ! Votre client peut désormais traiter les événements provenant de stream.php.

Format du flux d'événements

Pour envoyer un flux d'événements à partir de la source, il suffit de créer une réponse en texte brut, diffusée avec un type de contenu text/event-stream, qui suit le format SSE. Dans sa forme de base, la réponse doit contenir une ligne data:, suivie de votre message, puis de deux caractères "\n" pour mettre fin au flux:

data: My message\n\n

Données multilignes

Si votre message est plus long, vous pouvez le diviser en utilisant plusieurs lignes data:. Deux lignes consécutives ou plus commençant par data: sont traitées comme une seule donnée, ce qui signifie qu'un seul événement message est déclenché.

Chaque ligne doit se terminer par un seul "\n" (sauf la dernière, qui doit se terminer par deux). Le résultat transmis à votre gestionnaire message est une seule chaîne concaténée par des caractères de nouvelle ligne. Exemple :

data: first line\n
data: second line\n\n</pre>

Cela produit "première ligne\ndeuxième ligne" dans e.data. Vous pouvez ensuite utiliser e.data.split('\n').join('') pour reconstruire le message sans caractères "\n".

Envoyer des données JSON

L'utilisation de plusieurs lignes vous permet d'envoyer du code JSON sans enfreindre la syntaxe:

data: {\n
data: "msg": "hello world",\n
data: "id": 12345\n
data: }\n\n

Code côté client possible pour gérer ce flux:

source.addEventListener('message', (e) => {
  const data = JSON.parse(e.data);
  console.log(data.id, data.msg);
});

Associer un ID à un événement

Vous pouvez envoyer un ID unique avec un événement de flux en incluant une ligne commençant par id::

id: 12345\n
data: GOOG\n
data: 556\n\n

Définir un ID permet au navigateur de suivre le dernier événement déclenché afin que, si la connexion au serveur est interrompue, un en-tête HTTP spécial (Last-Event-ID) soit défini avec la nouvelle requête. Cela permet au navigateur de déterminer quel événement est approprié à déclencher. L'événement message contient une propriété e.lastEventId.

Contrôler le délai avant expiration de la reconnexion

Le navigateur tente de se reconnecter à la source environ trois secondes après la fermeture de chaque connexion. Vous pouvez modifier ce délai d'expiration en incluant une ligne commençant par retry:, suivie du nombre de millisecondes à attendre avant d'essayer de se reconnecter.

L'exemple suivant tente une reconnexion au bout de 10 secondes:

retry: 10000\n
data: hello world\n\n

Spécifier un nom d'événement

Une seule source d'événements peut générer différents types d'événements en incluant un nom d'événement. Si une ligne commençant par event: est présente, suivie d'un nom unique pour l'événement, l'événement est associé à ce nom. Sur le client, un écouteur d'événements peut être configuré pour écouter cet événement particulier.

Par exemple, la sortie du serveur suivante envoie trois types d'événements : un événement générique "message", un événement "userlogon" et un événement "update" :

data: {"msg": "First message"}\n\n
event: userlogon\n
data: {"username": "John123"}\n\n
event: update\n
data: {"username": "John123", "emotion": "happy"}\n\n

Avec des écouteurs d'événements configurés sur le client:

source.addEventListener('message', (e) => {
  const data = JSON.parse(e.data);
  console.log(data.msg);
});

source.addEventListener('userlogon', (e) => {
  const data = JSON.parse(e.data);
  console.log(`User login: ${data.username}`);
});

source.addEventListener('update', (e) => {
  const data = JSON.parse(e.data);
  console.log(`${data.username} is now ${data.emotion}`);
};

Exemples de serveurs

Voici une implémentation de serveur de base en PHP:

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache'); // recommended to prevent caching of event data.

/**
* Constructs the SSE data format and flushes that data to the client.
*
* @param string $id Timestamp/id of this connection.
* @param string $msg Line of text that should be transmitted.
**/

function sendMsg($id, $msg) {
  echo "id: $id" . PHP_EOL;
  echo "data: $msg" . PHP_EOL;
  echo PHP_EOL;
  ob_flush();
  flush();
}

$serverTime = time();

sendMsg($serverTime, 'server time: ' . date("h:i:s", time()));
?>

Voici une implémentation similaire sur Node.js à l'aide d'un gestionnaire Express:

app.get('/events', (req, res) => {
    // Send the SSE header.
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    });

    // Sends an event to the client where the data is the current date,
    // then schedules the event to happen again after 5 seconds.
    const sendEvent = () => {
        const data = (new Date()).toLocaleTimeString();
        res.write("data: " + data + '\n\n');
        setTimeout(sendEvent, 5000);
    };

    // Send the initial event immediately.
    sendEvent();
});

sse-node.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <script>
    const source = new EventSource('/events');
    source. => {
        const content = document.createElement('div');
        content.textContent = e.data;
        document.body.append(content);
    };
    </script>
  </body>
</html>

Annuler un flux d'événements

Normalement, le navigateur se reconnecte automatiquement à la source d'événements lorsque la connexion est fermée, mais ce comportement peut être annulé à partir du client ou du serveur.

Pour annuler un flux à partir du client, appelez:

source.close();

Pour annuler un flux à partir du serveur, répondez avec un Content-Type autre que text/event-stream ou renvoyez un état HTTP autre que 200 OK (par exemple, 404 Not Found).

Les deux méthodes empêchent le navigateur de rétablir la connexion.

Quelques mots sur la sécurité

Les requêtes générées par EventSource sont soumises aux règles de même origine que les autres API réseau telles que fetch. Si vous devez rendre le point de terminaison SSE de votre serveur accessible à partir de différentes origines, découvrez comment l'activer avec le partage de ressources entre origines multiples (CORS).