Quand WebSocket est trop, ou pourquoi SSE est de nouveau à la mode
\n\nImaginez : vous devez envoyer des données du serveur au client en temps réel. Taux de change, notifications de nouveaux messages, logs serveur, progression du traitement d'un fichier. La première chose qui vient à l'esprit est WebSocket. Mais que faire si vous n'avez pas besoin d'un canal bidirectionnel ? Que faire si le client écoute seulement et le serveur parle seulement ?
\n\nC'est là qu'interviennent les Server-Sent Events (SSE) — un protocole qui permet au serveur d'envoyer des données au client via une connexion HTTP ordinaire. Sans handshakes superflus, sans protocoles complexes, sans maux de tête avec les proxies et les équilibreurs de charge. Et oui, avec Rust, cela fonctionne tout simplement divinement.
\n\nDans cet article, nous allons voir comment mettre en place SSE en Rust à partir de zéro, en utilisant les frameworks populaires. Vous verrez des exemples fonctionnels, découvrirez les pièges et comprendrez quand SSE est votre meilleur choix.
\n\nQue sont les Server-Sent Events et pourquoi Rust ?
\n\nSSE est un standard W3C (EventSource API) qui permet au serveur d'envoyer des données en streaming au client via HTTP. Le client se connecte à un endpoint et le serveur commence à envoyer des messages au format text/event-stream. Tout fonctionne sur HTTP/1.1 ou HTTP/2 standard, ne nécessite pas de proxies spéciaux et est supporté par tous les navigateurs modernes.
Pourquoi Rust ? Parce que Rust nous offre :
\n- \n
- De l'asynchrone sans surprises — Tokio et async-std permettent de gérer des milliers de connexions SSE sur un seul thread \n
- La sécurité mémoire — pas de fuites, pas de race conditions dans le streaming multithread \n
- Une consommation minimale de ressources — une seule connexion SSE consomme quelques kilo-octets \n
- Un écosystème — Actix Web, Rocket, Axum — tous supportent SSE nativement ou avec des extensions minimales \n
Comment fonctionne SSE : décortiquons le protocole
\n\nLe client crée un EventSource dans le navigateur, en spécifiant une URL. Le serveur répond avec l'en-tête Content-Type: text/event-stream et commence à envoyer des lignes au format :
data: Bonjour le monde !\\n\\n\n\nChaque message est un bloc où data: contient la charge utile. Les messages sont séparés par deux caractères de nouvelle ligne. On peut transmettre du JSON, des nombres, des chaînes — n'importe quoi. Il existe également les champs id, event et retry.
Voici un exemple minimal de message SSE :
\n\nid: 1\nevent: update\ndata: {"status": "processing", "progress": 42}\n\n\n\nLe client en JavaScript reçoit cela comme un événement update avec le champ data contenant du JSON.
Écrivons un serveur SSE en Rust avec Actix Web
\n\nCommençons par le framework le plus populaire — Actix Web. Installons les dépendances :
\n\n[dependencies]\nactix-web = "4"\nactix-rt = "2"\ntokio = { version = "1", features = ["full"] }\nserde = { version = "1", features = ["derive"] }\nserde_json = "1"\n\nÉcrivons maintenant un endpoint simple qui enverra l'heure actuelle toutes les 2 secondes :
\n\nuse actix_web::{get, web, App, HttpRequest, HttpResponse, HttpServer, middleware};\nuse actix_web::rt::time;\nuse futures::stream::StreamExt;\nuse std::time::Duration;\n\n#[get("/events")]\nasync fn stream_events(req: HttpRequest) -> HttpResponse {\n let mut rx = tokio::sync::broadcast::channel::<String>(100).1;\n\n let stream = async_stream::stream! {\n loop {\n let message = format!("data: {}\\n\\n", chrono::Local::now());\n yield Ok::<_, actix_web::Error>(\n web::Bytes::from(message)\n );\n time::sleep(Duration::from_secs(2)).await;\n }\n };\n\n HttpResponse::Ok()\n .insert_header(("Content-Type", "text/event-stream"))\n .insert_header(("Cache-Control", "no-cache"))\n .insert_header(("Connection"