Introduction : pourquoi Rust pour WebSocket ?
Les applications temps réel modernes — chats, terminaux boursiers, jeux en ligne, systèmes de surveillance — exigent des performances élevées, des latences minimales et une fiabilité. Rust, avec son système de possession, ses abstractions à coût zéro et son runtime asynchrone Tokio, devient le choix idéal pour construire des serveurs WebSocket évolutifs.
Dans cet article, nous dépasserons le simple serveur d'écho et examinerons des techniques avancées : gestion de milliers de connexions simultanées, pools de threads, frameworks pour construire une logique complexe, ainsi que des modèles pour la mise à l'échelle horizontale. Le contenu est destiné aux développeurs déjà familiers avec les bases de Rust et la programmation asynchrone.
Architecture d'un serveur WebSocket à forte charge
1. Base asynchrone : Tokio et warp
Pour un niveau production, nous utilisons la combinaison Tokio (runtime asynchrone) et warp (framework web basé sur des filtres). Contrairement aux solutions simples, warp offre une composition flexible des routes et un support intégré de WebSocket.
use warp::Filter;use futures_util::{SinkExt, StreamExt};use tokio::sync::broadcast;
#[tokio::main]async fn main() { let (tx, _rx) = broadcast::channel::<String>(100); let ws_route = warp::path("ws") .and(warp::ws()) .and(with_sender(tx.clone())) .map(|ws: warp::ws::Ws, tx| { ws.on_upgrade(move |socket| handle_connection(socket, tx)) });
let health_route = warp::path("health") .map(|| warp::reply::json(&serde_json::json!({"status": "ok"})));
let routes = ws_route.or(health_route); warp::serve(routes).run(([0, 0, 0, 0], 8080)).await;}
fn with_sender(tx: broadcast::Sender<String>) -> impl Filter<Extract = (broadcast::Sender<String>,), Error = std::convert::Infallible> + Clone { warp::any().map(move || tx.clone())}Points clés :
- Nous utilisons
broadcast::channelpour diffuser des messages à tous les clients connectés (modèle "pub/sub"). - Un canal avec un tampon de 100 messages empêche les blocages en cas de pic soudain de trafic.
- Le filtre
with_senderinjecte l'expéditeur dans le gestionnaire via une fermeture.
2. Gestion de l'état des connexions
Au niveau avancé, il ne suffit pas d'envoyer des messages — il faut gérer le cycle de vie de chaque connexion, suivre les salles, l'authentification et les délais d'attente. Nous utilisons HashMap avec Arc<Mutex<>> pour un accès sécurisé depuis plusieurs tâches.
use std::collections::HashMap;use std::sync::Arc;use tokio::sync::Mutex;use warp::ws::Message;
type Clients = Arc<Mutex<HashMap<String, Vec<tokio::sync::mpsc::UnboundedSender<Message>>>>>;
async fn handle_connection(ws: warp::ws::WebSocket, clients: Clients, room_id: String) { let (mut ws_tx, mut ws_rx) = ws.split(); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<Message>(); // Enregistrer le client dans la salle { let mut map = clients.lock().await; map.entry(room_id.clone()).or_insert_with(Vec::new).push(tx); }
// Tâche d'envoi de messages au client let send_task = tokio::spawn(async move { while let Some(msg) = rx.recv().await { if ws_tx.send(msg).await.is_err() { break; } } });
// Tâche de réception de messages du client let receive_task = tokio::spawn(async move { while let Some(Ok(msg)) = ws_rx.next().await { // Ici, on peut traiter le message entrant // Par exemple, analyser le JSON, vérifier les droits println!("Reçu : {:?}", msg); } });
// Attendre la fin de l'une des