Contactez-nous

Server-Sent Events (SSE)

Implémentez des mises à jour en temps réel de serveur vers client efficacement avec Server-Sent Events (SSE) et Spring WebFlux, en exploitant la puissance des flux réactifs.

Introduction aux Server-Sent Events (SSE)

Dans de nombreuses applications web modernes, il est nécessaire de pousser des informations du serveur vers le client en temps réel, sans que le client n'ait à demander explicitement (polluer) les mises à jour. Pensez aux notifications, aux flux d'activité, aux mises à jour de statut, aux tableaux de bord en direct, etc. Le polling fréquent par le client est inefficace et gaspille des ressources serveur et réseau.

Server-Sent Events (SSE) est une technologie standardisée par le W3C qui offre une solution élégante à ce problème pour la communication unidirectionnelle serveur vers client. Elle établit une connexion HTTP persistante unique sur laquelle le serveur peut envoyer des données au client dès qu'elles sont disponibles. C'est une alternative plus simple aux WebSockets lorsque seule la communication du serveur vers le client est nécessaire.

SSE est basé sur une simple spécification textuelle et s'intègre parfaitement avec l'API EventSource côté client (navigateur), qui gère automatiquement la connexion, la réception des messages et même la reconnexion en cas de coupure. Spring WebFlux, avec son modèle réactif basé sur Project Reactor, offre un support natif et très efficace pour implémenter des endpoints SSE.

Le protocole SSE : simple et textuel

La communication SSE se fait sur une connexion HTTP standard, mais avec un type de contenu spécifique : text/event-stream. Le serveur envoie un flux continu de messages texte, chaque message étant séparé par une double nouvelle ligne (\n\n).

Chaque message peut être composé de plusieurs champs, les plus courants étant :

  • data: Contient les données réelles du message. Peut apparaître plusieurs fois pour envoyer des données multilignes.
  • event: Définit un type pour l'événement. Le client peut écouter spécifiquement ce type d'événement. Si absent, l'événement est de type "message".
  • id: Attribue un identifiant unique à l'événement. Le navigateur peut utiliser cet ID pour suivre le dernier événement reçu et le renvoyer au serveur (via l'en-tête Last-Event-ID) en cas de reconnexion, permettant au serveur de reprendre l'envoi là où il s'était arrêté.
  • retry: Spécifie le délai (en millisecondes) que le client doit attendre avant de tenter une reconnexion après une coupure.
  • : (Une ligne commençant par deux-points) Utilisé pour les commentaires ou comme signal de maintien de la connexion (keep-alive) pour éviter les timeouts des proxys.

Exemple de flux brut envoyé par le serveur :

: Ceci est un commentaire

retry: 10000

id: 1
data: Premier message

event: update
id: 2
data: {"user": "Alice", "status": "online"}
data: Ceci est la deuxième ligne de données JSON (note: pas valide seul)

Implémentation de SSE avec Spring WebFlux

Grâce à Project Reactor, implémenter un endpoint SSE dans WebFlux est très naturel. Il suffit de retourner un Flux depuis une méthode de contrôleur annotée avec @GetMapping (ou via un Handler fonctionnel). WebFlux détecte le type de retour réactif et configure automatiquement la réponse avec le content type text/event-stream et maintient la connexion ouverte pour streamer les éléments du Flux.

Vous pouvez retourner un Flux de différents types :

  • Flux : Le plus simple. Chaque String émise par le Flux sera envoyée comme un événement SSE avec uniquement le champ data:.
  • Flux : Si vous retournez un Flux d'objets (POJOs, Records), Spring (via Jackson par défaut) les sérialisera en JSON et les enverra dans le champ data:.
  • Flux> : Pour un contrôle total sur le format de chaque événement (définir l'ID, le type d'événement, les commentaires, les données), retournez un Flux de ServerSentEvent. Vous construisez chaque ServerSentEvent à l'aide de son builder.

Exemple de contrôleur WebFlux retournant un flux SSE :

import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.util.function.Tuples;

import java.time.Duration;
import java.time.LocalTime;
import java.util.concurrent.atomic.AtomicLong;

record Notification(String id, String message, LocalTime timestamp) {}

@RestController
@RequestMapping("/sse")
public class SseController {

    // 1. Exemple simple retournant des Strings
    @GetMapping(path = "/time-stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux streamTime() {
        // Emet l'heure actuelle toutes les secondes
        return Flux.interval(Duration.ofSeconds(1))
                   .map(sequence -> "Heure actuelle: " + LocalTime.now().toString());
    }

    // 2. Exemple retournant des objets (sérialisés en JSON)
    @GetMapping(path = "/notifications", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux streamNotifications() {
        return Flux.interval(Duration.ofSeconds(5))
                   .map(sequence -> new Notification(
                           "id-" + sequence,
                           "Nouvelle notification #" + sequence,
                           LocalTime.now()));
    }

    // 3. Exemple avec contrôle complet via ServerSentEvent
    @GetMapping(path = "/custom-events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux> streamCustomEvents() {
        AtomicLong counter = new AtomicLong();
        return Flux.interval(Duration.ofSeconds(2))
                   .map(sequence -> Tuples.of(sequence, "Evénement personnalisé"))
                   .map(data -> ServerSentEvent.builder()
                           .id(String.valueOf(counter.incrementAndGet())) // Définit l'ID de l'événement
                           .event("custom-update") // Définit le type d'événement
                           .data("Séquence: " + data.getT1() + " - Message: " + data.getT2()) // Les données
                           .comment("Envoyé à " + LocalTime.now()) // Ajoute un commentaire
                           .retry(Duration.ofSeconds(10)) // Spécifie le délai de reconnexion client
                           .build());
    }
}

Notez l'utilisation de produces = MediaType.TEXT_EVENT_STREAM_VALUE sur les mappings pour déclarer explicitement le type de contenu, bien que WebFlux puisse souvent l'inférer.

Consommation côté client avec EventSource

Côté client (navigateur), la consommation d'un flux SSE est standardisée via l'interface JavaScript EventSource.

// Se connecter au flux SSE du serveur
const eventSource = new EventSource("/sse/custom-events"); // Utiliser l'URL de l'endpoint

// Ecouter les événements par défaut (ceux sans champ 'event:')
eventSource.onmessage = function(event) {
    console.log("Message reçu (type par défaut):", event.data);
    // event.lastEventId contient l'ID du dernier événement reçu
};

// Ecouter les événements avec un type spécifique ('custom-update' dans notre exemple)
eventSource.addEventListener('custom-update', function(event) {
    console.log("Evénement 'custom-update' reçu:", event.data);
    console.log("ID de l'événement:", event.lastEventId);
});

// Gérer les erreurs de connexion
eventSource.onerror = function(err) {
    console.error("Erreur EventSource:", err);
    // Le navigateur tentera automatiquement de se reconnecter en utilisant la valeur 'retry'
    // On peut choisir de fermer la connexion ici si nécessaire: eventSource.close();
};

// Optionnel: Fermer la connexion manuellement
// window.onbeforeunload = function() {
//     eventSource.close();
// };

L'API EventSource gère automatiquement la connexion, le parsing des messages selon le protocole SSE, et les tentatives de reconnexion en cas de perte de connexion, en respectant le délai spécifié par le champ retry: envoyé par le serveur.

Cas d'usage et considérations

Server-Sent Events est particulièrement adapté pour :

  • Les notifications push (nouveaux messages, alertes).
  • Les mises à jour de données en temps réel (scores sportifs, cours de bourse, statuts de progression).
  • Les tableaux de bord dynamiques.
  • Les flux d'activité.

Il est important de garder à l'esprit certaines considérations :

  • Unidirectionnel : SSE ne permet que la communication du serveur vers le client. Si une communication bidirectionnelle est nécessaire, les WebSockets sont une meilleure option.
  • Limites de connexion : Les navigateurs ont une limite sur le nombre de connexions HTTP ouvertes simultanément par domaine (généralement autour de 6). Chaque flux SSE compte comme une connexion.
  • Proxys et Firewalls : Certains proxys intermédiaires peuvent bufferiser les réponses ou fermer les connexions inactives. L'envoi périodique de commentaires (lignes :) peut servir de mécanisme de keep-alive.
  • Gestion d'état côté serveur : Le serveur doit maintenir la connexion ouverte pour chaque client connecté, ce qui consomme des ressources (bien moins qu'avec un modèle bloquant thread-par-requête grâce à l'approche non-bloquante de WebFlux).
  • Gestion des erreurs sur le Flux : Si le Flux côté serveur se termine par une erreur, la connexion SSE sera fermée. Le client tentera de se reconnecter. Il faut gérer les erreurs dans le flux pour éventuellement envoyer un message d'erreur spécifique via SSE avant de terminer.

En résumé, SSE est une technologie standard, simple et efficace pour les mises à jour serveur-client en temps réel, et Spring WebFlux fournit un excellent support pour son implémentation côté serveur en tirant parti de la programmation réactive.