Que sont les WebSockets ?
Un WebSocket est une connexion persistante et full-duplex entre un client et un serveur. Après un handshake HTTP initial, les deux parties peuvent envoyer des messages à tout moment via la même connexion TCP, avec un overhead très faible par message. Le serveur peut ainsi pousser des données dès qu’un changement survient, au lieu d’attendre que le client en fasse la demande.
Cela fait des WebSockets l’outil idéal pour le chat, la gestion de présence, les tableaux de bord en temps réel, l’édition collaborative, les jeux multijoueurs et tout autre cas où les mises à jour doivent arriver instantanément. Pour des mises à jour simples et unidirectionnelles, les Server-Sent Events sont souvent un choix plus simple.
Le handshake de mise à niveau
Une connexion WebSocket commence sa vie sous la forme d’une requête HTTP.
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Si le serveur accepte, il répond :
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Après 101 Switching Protocols, la connexion n’est plus du HTTP. Elle devient un WebSocket, et les données circulent sous forme de frames. Comme elle débute en HTTP, elle fonctionne via les mêmes ports (80 et 443), traverse la plupart des proxys et pare-feu, et passe en TLS avec wss://.
Frames
Les messages WebSocket sont divisés en frames (trames) accompagnées d’un petit en-tête. On trouve des frames de texte, des frames binaires et des frames de contrôle pour le ping, le pong et la fermeture (close). Le framing est minimal, c’est pourquoi les messages WebSocket sont bien moins coûteux que des requêtes HTTP répétées.
Le protocole définit également des codes de fermeture, tels que 1000 pour une fermeture normale et 1001 pour un départ (going away), ce qui permet aux deux parties de comprendre pourquoi une connexion a été interrompue.
L’API du navigateur
L’API client est légère et pilotée par les événements.
// client.js
const socket = new WebSocket("wss://api.example.com/chat");
socket.addEventListener("open", () => {
socket.send(JSON.stringify({ type: "join", room: "general" }));
});
socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
render(message);
});
socket.addEventListener("close", (event) => {
console.log("closed", event.code, event.reason);
});
socket.addEventListener("error", () => {
console.error("connection error");
});
Vous envoyez des données avec socket.send() et recevez des événements message. La propriété readyState vous indique si le socket est en cours de connexion, ouvert, en cours de fermeture ou fermé.
Un serveur Node.js
La bibliothèque ws est la méthode standard pour exécuter un serveur WebSocket sous Node.js.
// server.js
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (socket, request) => {
socket.on("message", (data, isBinary) => {
for (const client of wss.clients) {
if (client !== socket && client.readyState === client.OPEN) {
client.send(data, { binary: isBinary });
}
}
});
socket.on("close", () => console.log("client left"));
});
Le serveur suit chaque connexion et peut envoyer des messages à un seul client, à un sous-ensemble (une room) ou à l’ensemble d’entre eux. Socket.IO est une alternative de plus haut niveau qui ajoute des rooms, des accusés de réception, la reconnexion automatique et des solutions de repli (fallbacks), au prix d’un protocole et d’un client personnalisés.
Salons et présence
Les applications réelles diffusent rarement des messages à tout le monde. Vous devez suivre quelle connexion appartient à quel utilisateur ou à quel salon.
// rooms.js
const rooms = new Map();
function join(socket, room) {
if (!rooms.has(room)) rooms.set(room, new Set());
rooms.get(room).add(socket);
}
function broadcast(room, payload) {
for (const socket of rooms.get(room) ?? []) {
if (socket.readyState === socket.OPEN) socket.send(payload);
}
}
Les salons, la présence (qui est en ligne) et les indicateurs de saisie sont tous basés sur ce modèle, auxquels s’ajoutent des heartbeats pour détecter les déconnexions.
Heartbeats et reconnexion
Une connexion peut être interrompue sans que ni l’un ni l’autre des côtés ne s’en aperçoive, particulièrement sur les réseaux mobiles ou derrière des proxys. Deux mécanismes permettent de maintenir la connexion active :
- Les Heartbeats. Envoyez un ping toutes les 30 secondes et terminez la connexion si aucun pong n’est reçu. De nombreux proxys ferment également les connexions inactives ; le flux de données permet donc de les maintenir ouvertes.
- La reconnexion. Le client doit tenter de se reconnecter avec un backoff exponentiel et rétablir ses abonnements. C’est là qu’une bibliothèque comme Socket.IO permet de gagner un temps précieux.
// heartbeat.js
const interval = setInterval(() => {
for (const socket of wss.clients) {
if (socket.isAlive === false) return socket.terminate();
socket.isAlive = false;
socket.ping();
}
}, 30_000);
wss.on("close", () => clearInterval(interval));
Mise à l’échelle (Scaling)
Une connexion WebSocket réside sur une seule instance de serveur ; pour diffuser des messages entre plusieurs instances, un canal partagé est donc nécessaire.
- Pub/sub : publiez les messages vers Redis, NATS ou un autre broker, et faites en sorte que chaque instance les délivre à ses clients locaux.
- Sticky sessions : si votre infrastructure l’exige, maintenez un client sur la même instance pendant toute la durée de la connexion.
- État dans un store partagé : conservez la présence et l’appartenance aux salons dans Redis afin que n’importe quelle instance puisse répondre.
- Backpressure et limites : plafonnez le nombre de connexions par instance et surveillez la mémoire, car chaque connexion occupe un socket et conserve un certain état.
Sécurité
- Utilisez toujours
wss://en production afin que le trafic soit chiffré. - Authentifiez les clients lors du handshake et rejetez les connexions non autorisées.
- Validez le header
Originpour prévenir le détournement de WebSocket cross-site (CSWSH). - Autorisez chaque message ; ne présumez jamais qu’un client connecté est autorisé à effectuer une action.
- Limitez la taille et la fréquence des messages pour éviter les abus.
- Configurez des timeouts pour éviter que les connexions abandonnées ne provoquent des fuites de ressources.
Bonnes pratiques
- Utilisez les WebSockets pour les mises à jour bidirectionnelles à faible latence et SSE pour le push unidirectionnel.
- Implémentez un heartbeat et un mécanisme de reconnexion ; ne partez jamais du principe qu’une connexion est active.
- Authentifiez l’utilisateur lors du handshake et autorisez chaque message.
- Gardez l’état par connexion minimal et gérez les rooms dans un store partagé.
- Utilisez le pub/sub pour diffuser des messages entre plusieurs instances.
- Définissez des limites de taille de message et des quotas de fréquence (rate limits).
- Fermez les connexions inactives et surveillez le nombre de connexions.
Erreurs courantes
- Utiliser les WebSockets pour de simples requêtes/réponses alors que HTTP suffirait.
- Oublier les heartbeats et laisser des connexions mortes s’accumuler.
- Diffuser un message à tous les clients alors qu’une seule room devrait le recevoir.
- Stocker l’état faisant foi uniquement en mémoire et le perdre lors d’un redémarrage.
- Négliger les vérifications d’origine et l’autorisation des messages.
- Supposer que le load balancer préservera la connexion sans configuration préalable.
Et après ?
Les WebSockets constituent la couche de connexion persistante au-dessus de TCP. Pour mieux les comprendre, appuyez-vous sur le guide HTTP et sur TCP/IP & Sockets, exécutez-les sur Node.js, et comparez le modèle de push avec les GraphQL subscriptions. Ensuite, créez un petit salon de discussion avec des salles et des heartbeats pour mettre ces concepts en pratique.