{nowwecode.com}

Envoi de données depuis le serveur vers le client avec les server-sent events

Envoi de données depuis le serveur vers le client avec les server-sent events
Illustration de Céline P.

Server-sent events (SSE) est une technique de server push HTTP. Qu’est ce que ça veut dire ? La plupart du temps, dans le web, le client envoie une requête au serveur qui lui envoie ensuite une réponse. Pour obtenir une information située sur le serveur, le client doit faire une action.
Le server push est un mode de communication où le serveur est capable d'envoyer des informations au client sans que ce dernier ne les ai demandées.

Il existe plusieurs méthodes pour ce faire, mais elles ont des contraintes :

Le polling

Cette technique est souvent utilisée avec Ajax.
Avec le polling, le client va envoyer des requêtes au serveur régulièrement afin de récupérer une éventuelle mise à jour. S’il n’y a pas de nouvelle donnée, le serveur renvoie une réponse vide et le client attend quelques instants avant de renvoyer à nouveau une requête au serveur.

Les inconvénients du polling, c’est qu’il faut attendre avant de recevoir la mise à jour du serveur et que cette technique utilise des ressources (réseau et serveur processing) et moins on veut attendre pour avoir la mise à jour, plus on utilise de ressources.

Le long polling

Le long polling tente de réduire le temps d’attente pour avoir la mise à jour ainsi que l’utilisation des ressources, mais vous allez voir, ça ressemble à un hack.
Un peu comme le polling, le client va envoyer une requête au serveur, mais ce dernier ne va répondre que lorsqu’il aura une donnée à passer au client, ou lorsqu’il aura atteint un timeout. Une fois que le client aura eu sa réponse, il va immédiatement relancer une requête long poll au serveur, et ainsi de suite.

L’avantage est qu’en effet, dès que l’information sera disponible coté serveur, le client sera avertit plus rapidement.
Les inconvénients du long polling sont que premièrement, niveau ressources c’est toujours pas top, on fait toujours beaucoup de requêtes, requêtes qui ont un poids si on regarde bien les headers.
Ensuite, le temps de latence pour avoir l'information sera la plupart du temps quasi nul; seulement le temps que le client reçoive la réponse, ce qui correspond à un transit. Cependant, ce temps de latence peut monter à 3 transits selon le timing; le temps que serveur renvoie la réponse tombée en timeout, que le client relance la requête puis que le serveur réponde enfin avec l'information. Et ça c'est en supposant que tout se passe bien.

Les SSE sont bien plus efficaces, le serveur peut envoyer des informations au client quand il le veut, sans que le client n’ait rien à demander, l'information arrive directement puisque la connection reste ouverte tant qu'on l'a pas explicitement fermée.

WebSockets

Les WebSockets sont un peu la cause pour laquelle les server-sent events sont restés dans l’ombre. Ils permettent de faire de la communication bi-directionnelle quand les SSE ne font que du serveur vers client.
Ce qui est très bien pour les applications de discussion ou les jeux vidéos. Cependant, parfois on veut juste mettre à jour un statut, une liste sur une page et le client n’a besoin d’envoyer aucune info pour ça.
De plus je dis les WebSockets mais on parle plutôt du protocole WebSocket car c’est un protocole différent de HTTP, et si vous voulez l’utiliser, il peut être difficile de l'intégrer à votre projet. En revanche, les SSE utilisent HTTP et vous allez voir comme il sont simples à mettre en place et s’intègrent parfaitement dans la plupart des projets.

Il existe encore d’autres techniques comme les pushlets ou les notifications push mais la première souffre d’un problème de design tandis que l’autre nécessite de la configuration.

Il y a tout de même une chose à savoir avec SSE, quand on ne l’utilise pas avec HTTP/2 le nombre de connexions par navigateur + domaine est limité à 6. Avec HTTP/2 ce nombre serait configurable, et serait par défaut de 100. Je vous met le lient vers la page MDN.

Mais pas de panique ! On peut faire passer plusieurs types d’événements par la même connexion, je vous invite à regarder la section “exemples” de la page MDN.

Pour résumer, les server-sent events sont ultras efficaces comparés aux techniques de polling et bien plus simple à mettre en place que les Web sockets.


Maintenant on code !

Pour l’exemple, mon serveur va tourner en Python avec Flask.
Pour que le serveur envoie la modification, il nous faut un déclencheur. Ce peut être un Observer qui réagirait à un event, un pubsub Redis, personnellement je vais utiliser un watcher etcd.

Etcd est un key/value store tout comme Redis

Installation etcd

Je n’aime pas polluer ma machine alors on va utiliser docker

Vous ne connaissez pas Docker ? C'est par là.

On récupère l'image etcd sur quay.io

docker pull quay.io/coreos/etcd:v3.4.13

On crée un network docker pour que d’autres conteneur du même network aient accès à etcd

docker network create sse --driver bridge

On créé le conteneur etcd

docker run -d --name etcd --network sse -p 2379:2379 -p 2380:2380 quay.io/coreos/etcd:v3.4.13 /usr/local/bin/etcd --advertise-client-urls http://etcd:2379 --listen-client-urls http://0.0.0.0:2379

On attend que le conteneur se crée, ensuite on va ajouter une entrée dans etcd.

On ajoute une entrée avec pour clé '/status' et valeur 'offline'.

docker exec -it etcd etcdctl put /status 'offline'

On regarde si notre entrée a bien été ajoutée

docker exec -it etcd etcdctl get --prefix ''

Le serveur Python

De même, pour ne pas polluer votre machine je vous invite à faire ça dans un virtualenv, pipenv ou dans un conteneur docker.

On installe les dépendances flask et etcd3.

pip install --user flask etcd3

Structure du projet

  • app.py
  • templates
    • index.html
# app.py
import etcd3
from flask import Flask, Response, render_template, request

app = Flask(__name__)
etcd = etcd3.client(host='127.0.0.1', port=2379) # 127.0.0.1 => etcd si vous êtes dans un autre conteneur

# route principale
@app.route('/')
def hello_world():
    return render_template('index.html')

# route à laquelle le client va souscrire
@app.route('/stream')
def stream():
    # les SSE reposent sur l'utilisation du mime type text/event-stream
    return Response(eventStream(), mimetype='text/event-stream')


def eventStream():
    # Le premier yield renvoie le statut récupéré dans etcd
    status, _ = etcd.get('/status')
    yield 'data: {}\n\n'.format(status.decode())

    # A chaque changement sur la clé “/status” on yield la nouvelle valeur
    events_iterator, _ = etcd.watch('/status')
    for event in events_iterator:
        yield 'data: {}\n\n'.format(event.value.decode())

Coté template, on déclare un objet EventSource, on attache un handler qui écoute les messages qui arrivent et on met à jour le contenu de la page.

<!-- index.html -->
<h1>App Status</h1>
<div id="status"></div>
<script>
  var element = document.getElementById("status");

  // on déclare un EventSource
  var eventSource = new EventSource("/stream");

  // on écoute les messages qui arrivent
  eventSource.onmessage = function (e) {
    element.innerHTML = e.data;
  };
</script>

Maintenant, on test:

export FLASK_APP=app.py
flask run

on va sur http://127.0.0.1:5000

Status Offline

On met à jour la valeur dans etcd

ETCDCTL_API=3 etcdctl put /status 'online'

Status Offline

Le client a été notifié automatiquement du changement.

Il reste une chose importante à savoir sur les SSE. Une fois la connexion ouverte, elle ne se ferme jamais (à moins de le faire explicitement), la requête reste en attente perpétuellement. Vous devez donc vous assurer que votre serveur autorise le multithreading. Avec Flask vous ne le verrez pas puisque par défaut le serveur de développement est en mode threaded par défaut.

exécutez flask run --without-threads pour voir le fonctionnement sans multithreading

Mais quand vous configurerez votre WSGI, assurez vous d'activer le mode multithreadé.

Si vous utilisez Gunicorn, ça passe par le paramètre worker_class, il suffit qu’il soit différent de `sync`

Lien vers le projet: https://gitlab.com/now-we-code/blog/server-sent-events