Une introduction en douceur aux WebSockets
HTTP fonctionne en requête-réponse : le client demande, le serveur répond, et la connexion a fini son travail. Ce modèle s'effondre dès que le serveur doit dire quelque chose au client sans qu'on le lui ait demandé : un message de chat qui arrive, un cours de bourse qui bouge, un build qui se termine. Les WebSockets existent précisément pour corriger cela : une unique connexion de longue durée où chaque côté peut envoyer un message à l'autre, à tout moment, sans le surcoût par requête de HTTP.
Pourquoi ne pas simplement faire du polling ?
Avant les WebSockets, le contournement classique était le polling : le client demande « du nouveau ? » toutes les quelques secondes.
setInterval(async () => {
const response = await fetch('/api/messages/latest');
const messages = await response.json();
renderNewMessages(messages);
}, 3000);
Cela fonctionne, mais c'est un mauvais compromis dans les deux sens. Si l'on interroge trop lentement, les mises à jour semblent en retard ; trop vite, on fait surtout des requêtes HTTP inutiles (chacune avec le surcoût de sa poignée de main TCP, ou la complexité de la réutilisation de connexion, ses en-têtes et son traitement côté serveur) pour s'entendre répondre, la plupart du temps, « rien n'a changé ». Une connexion WebSocket est ouverte une seule fois, puis n'envoie que les données réelles, dans les deux sens, à l'instant où elles existent.
La poignée de main : encore du HTTP, mais brièvement
Une connexion WebSocket commence comme une requête HTTP normale avec un en-tête Upgrade. Le serveur accepte de changer de protocole, et à partir de là, la même connexion TCP transporte le format de trame WebSocket au lieu de HTTP.
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Vous ne construisez jamais cela à la main : l'API WebSocket du navigateur et les bibliothèques côté serveur s'en chargent. Mais il est utile de savoir que cela existe : c'est pourquoi une URL WebSocket utilise ws:// ou wss:// (la version sécurisée, sur TLS) plutôt que http://, et pourquoi une connexion WebSocket peut être bloquée par les mêmes choses qu'une requête HTTP : vérifications d'origine voisines de CORS, proxys qui ne gèrent pas Upgrade, pare-feu d'entreprise.
Côté client
const socket = new WebSocket('wss://example.com/chat');
socket.addEventListener('open', () => {
console.log('connected');
socket.send(JSON.stringify({ type: 'join', room: 'general' }));
});
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
console.log('received:', data);
});
socket.addEventListener('close', (event) => {
console.log(`connection closed: ${event.code} ${event.reason}`);
});
socket.addEventListener('error', (event) => {
console.error('socket error', event);
});
Quatre événements couvrent tout le cycle de vie : open une fois la poignée de main terminée, message pour chaque trame entrante, close quand la connexion se termine (par l'un ou l'autre côté) et error pour les défaillances au niveau du transport. Tout ce que vous envoyez et recevez est du texte ou du binaire : il n'y a pas d'appariement requête/réponse intégré, donc pour relier une réponse à un message précis que vous avez envoyé, c'est à votre protocole applicatif de s'en charger (typiquement : inclure un identifiant dans le contenu).
Un serveur minimal avec la bibliothèque ws de Node
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({ port: 8080 });
const clients = new Set();
wss.on('connection', (socket) => {
clients.add(socket);
console.log(`client connected, ${clients.size} total`);
socket.on('message', (raw) => {
const message = JSON.parse(raw.toString());
// broadcast to everyone except the sender
for (const client of clients) {
if (client !== socket && client.readyState === client.OPEN) {
client.send(JSON.stringify(message));
}
}
});
socket.on('close', () => {
clients.delete(socket);
console.log(`client disconnected, ${clients.size} remaining`);
});
});
C'est un serveur de chat à diffusion complet : chaque client connecté reçoit tous les messages envoyés par n'importe qui d'autre. Le Set de sockets ouverts constitue toute la notion « qui écoute » du serveur : pas de polling, pas de comparaison d'horodatages, juste du push.
La reconnexion, c'est votre travail, pas celui du navigateur
Si la connexion tombe (redémarrage du serveur, coupure réseau, ordinateur portable mis en veille), l'objet WebSocket ne se reconnecte pas tout seul. Il faut le gérer explicitement, généralement avec un backoff exponentiel pour qu'une panne de serveur ne soit pas martelée par tous les clients qui se reconnectent en même temps à l'instant où il revient.
function connectWithRetry(url, { onMessage }) {
let attempt = 0;
function connect() {
const socket = new WebSocket(url);
socket.addEventListener('open', () => {
attempt = 0; // reset backoff once we're actually connected
});
socket.addEventListener('message', onMessage);
socket.addEventListener('close', () => {
const delay = Math.min(1000 * 2 ** attempt, 30000);
attempt++;
setTimeout(connect, delay);
});
}
connect();
}
Les délais doublent à chaque tentative (1 s, 2 s, 4 s, 8 s…) jusqu'à un plafond de 30 secondes, et reviennent à zéro dès qu'une connexion réussit vraiment. Les systèmes de production ajoutent généralement une gigue aléatoire pour que de nombreux clients qui se reconnectent après la même panne ne réessaient pas tous exactement au même moment.
Quand les WebSockets sont le mauvais outil
Ce n'est pas une amélioration universelle par rapport à HTTP. Si le client n'a besoin que de mises à jour occasionnelles à l'initiative du serveur et tolère quelques secondes de latence, les Server-Sent Events (SSE) sont plus simples : unidirectionnels (du serveur vers le client uniquement), avec reconnexion automatique intégrée, et une simple réponse HTTP que le navigateur sait garder ouverte. Si la communication est vraiment du simple requête-réponse, le HTTP ordinaire avec fetch est encore plus simple et vous offre mise en cache, nouvelles tentatives et sémantique HTTP standard gratuitement. Choisissez un WebSocket précisément quand vous avez besoin d'une communication bidirectionnelle à faible latence : chat, état de jeu multijoueur, édition collaborative, tableaux de bord en direct où le serveur pousse aussi souvent que le client demande.