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, avec une connexion HTTP. Une fois la connexion établie, les serveurs peuvent lancer la transmission des données.

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

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

  • Sondage: l'application interroge un serveur de façon répétée afin d'obtenir des données. Cette technique est utilisée par la majorité des applications AJAX. Avec le protocole HTTP, l'extraction de données s'articule autour d'un format de requête et de réponse. Le client effectue une requête et attend que le serveur réponde avec les données. Si aucun n'est disponible, une réponse vide est renvoyée. L'interrogation supplémentaire augmente la surcharge HTTP.

  • Sondage long (Hanging GET / COMET): si le serveur ne dispose d'aucune donnée, il maintient la requête ouverte jusqu'à ce que de nouvelles données soient disponibles. C'est pourquoi cette technique est souvent appelée "Hanging GET". Lorsque des informations deviennent disponibles, le serveur répond, ferme la connexion et le processus est répété. Ainsi, le serveur répond en permanence avec de nouvelles données. Pour configurer cela, les développeurs ont généralement recours à des astuces, telles que l'ajout de balises de script dans un iFrame "infini".

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

La principale différence entre les événements envoyés par le serveur et l'interrogation longue 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 et WebSockets

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

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

Cependant, il arrive que vous n'ayez besoin que d'une communication à sens unique depuis un serveur. Par exemple, lorsqu'un ami met à jour son statut, les valeurs boursières, les flux d'actualités ou d'autres mécanismes de transmission de données automatisés. En d'autres termes, il s'agit d'une mise à jour d'une base de données Web SQL ou d'un magasin d'objets IndexedDB côté client. Si vous devez envoyer des données à un serveur, XMLHttpRequest sera toujours un ami.

Les SSE sont envoyées via HTTP. Il n'y a pas de protocole ou d'implémentation de serveur spécial pour commencer à fonctionner. Les WebSockets nécessitent des connexions full-duplex et de nouveaux serveurs WebSocket pour gérer le protocole.

En outre, les événements envoyés par le serveur présentent diverses fonctionnalités dont WebSockets ne disposent pas par conception, telles que la reconnexion automatique, les ID d'événement et la possibilité d'envoyer des événements arbitraires.

Créer une 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');

Configurez ensuite 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 à partir du serveur, le gestionnaire onmessage se déclenche et de nouvelles données sont disponibles dans sa propriété e.data. Par ailleurs, chaque fois que la connexion est fermée, le navigateur se reconnecte automatiquement à la source après environ trois secondes. La mise en œuvre de votre serveur peut même contrôler le délai avant expiration de la reconnexion.

C'est tout. Votre client peut désormais traiter les événements 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 respecte le format SSE. Dans sa forme de base, la réponse doit contenir une ligne data:, suivie de votre message, suivi de deux caractères "\n" pour mettre fin au flux:

data: My message\n\n

Données multilignes

S'il est plus long, vous pouvez le diviser en utilisant plusieurs lignes data:. Au moins deux lignes consécutives 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" (à l'exception de la dernière, qui doit se terminer par deux). Le résultat transmis à votre gestionnaire message est une chaîne unique concaténée par des caractères de retour à la ligne. Exemple :

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

Cela produit une "première ligne\ndeuxième ligne" en e.data. On pourrait ensuite utiliser e.data.split('\n').join('') pour reconstruire le message sans les caractères "\n".

Envoyer des données JSON

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

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

Possibilité de code côté client 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 identifiant 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

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

Contrôler le délai d'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 en incluant une ligne commençant par retry:, suivie du nombre de millisecondes d'attente avant d'essayer de se reconnecter.

L'exemple suivant tente de se reconnecter après 10 secondes:

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

Spécifier un nom d'événement

Une source d'événement unique 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, il est associé à ce nom. Sur le client, vous pouvez configurer un écouteur d'événements pour écouter cet événement particulier.

Par exemple, la sortie serveur suivante envoie trois types d'événements, un événement "message" générique, "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

Si les écouteurs d'événements sont 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 base d'un serveur 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 avec 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.onmessage = (e) => {
        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 de l'événement lorsque la connexion est fermée, mais ce comportement peut être annulé depuis le client ou le serveur.

Pour annuler un flux à partir du client, appelez:

source.close();

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

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

Un mot sur la sécurité

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