18 - WebSocket : full-duplex, frames et cas d'usage
Ce que tu vas apprendre
- Le handshake HTTP qui ouvre une connexion WebSocket
- Les types de frames (text, binary, ping, pong, close)
- La différence entre WebSocket, SSE et long polling
- Socket.io et ce qu'il ajoute au-dessus du protocole
- Quand utiliser (et ne pas utiliser) WebSocket
Prerequisites
- 17 - HTTP/3 et QUIC
- Savoir ce qu'est une connexion TCP
HTTP est un protocole requête-réponse. Le client demande, le serveur répond. Point. Si le serveur a quelque chose a te dire, il doit attendre que tu lui poses la question. C'est comme un prof qui ne peut parler que quand un eleve leve la main. Pas ideal pour du temps réel.
WebSocket casse ce modèle. Une fois la connexion etablie, le client et le serveur peuvent s'envoyer des messages a tout moment, dans les deux sens, sans attendre. C'est du full-duplex sur une seule connexion TCP.
Le handshake : de HTTP a WebSocket
WebSocket démarré par une requête HTTP classique. C'est un upgrade de protocole :
httpGET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Le serveur répond :
httpHTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Le code 101 signifie "je change de protocole". A partir de ce moment, la connexion TCP n'est plus du HTTP. C'est du WebSocket.
Le Sec-WebSocket-Key et le Sec-WebSocket-Accept ne sont pas de la sécurité. C'est un mecanisme pour vérifier que le serveur comprend bien WebSocket et que la réponse n'est pas un cache proxy qui renvoie du HTTP. Le serveur concatene la clé avec un GUID fixe, fait un SHA-1 et renvoie le résultat en base64.
Les frames WebSocket
Une fois le handshake fait, les donnees circulent sous forme de frames :
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+-------------------------------+
| Masking-key (if MASK set) |
+-------------------------------+-------------------------------+
| Payload Data |
+---------------------------------------------------------------+
Les opcodes definissent le type de frame :
- 0x1 - Text : du texte UTF-8 (JSON la plupart du temps)
- 0x2 - Binary : des donnees binaires brutes (images, audio, protobuf)
- 0x8 - Close : fermeture propre de la connexion
- 0x9 - Ping : "tu es toujours la ?"
- 0xA - Pong : "oui je suis la"
Les frames ping/pong servent de heartbeat. Si un cote n'envoie rien pendant un moment, l'autre envoie un ping. Si pas de pong en retour, la connexion est considérée comme morte. C'est vital pour détecter les deconnexions sur mobile ou derrière des proxies qui coupent les connexions inactives.
Le bit MASK est toujours a 1 pour les messages client vers serveur. Le masquage est la pour éviter que des caches HTTP intermediaires interpretent les donnees WebSocket comme du HTTP cache. Encore une protection contre les middleboxes.
Exemple en pratique
Cote serveur (Node.js avec la lib ws) :
javascriptconst WebSocket = require("ws");
const wss = new WebSocket.Server({ port: 8080 });
wss.on("connection", (ws) => {
console.log("Client connecte");
ws.on("message", (data) => {
// Broadcast a tous les clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(data.toString());
}
});
});
ws.on("close", () => {
console.log("Client deconnecte");
});
});
Cote client :
javascriptconst ws = new WebSocket("wss://example.com/chat");
ws.onopen = () => {
ws.send(JSON.stringify({ type: "message", text: "Salut" }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("Recu:", data);
};
ws.onclose = () => {
console.log("Connexion fermee");
// Reconnexion automatique ?
};
WebSocket vs SSE vs long polling
Le choix depend de ton cas d'usage :
Long polling : le client envoie une requête, le serveur garde la connexion ouverte jusqu'a avoir quelque chose a dire, puis répond. Le client renvoie immédiatement une nouvelle requête. Simple mais inefficace. Chaque "message" coûte un cycle HTTP complet.
Server-Sent Events (SSE) : une connexion HTTP standard, unidirectionnelle (serveur vers client). Le serveur envoie des événements en texte. Le navigateur les recoit via l'API EventSource. Reconnexion automatique. Fonctionne avec HTTP/2.
WebSocket : full-duplex, bidirectionnel, binaire ou texte. Plus complexe a déployer (proxies, load balancers doivent supporter le upgrade).
| Critère | Long Polling | SSE | WebSocket |
|---|---|---|---|
| Direction | Requete-réponse | Serveur vers client | Bidirectionnel |
| Protocole | HTTP | HTTP | WebSocket |
| Reconnexion auto | Non | Oui (natif) | Non (a coder) |
| Binaire | Non | Non | Oui |
| Support HTTP/2 | Oui | Oui | Non (HTTP/1.1) |
| Complexite déploiement | Faible | Faible | Moyenne |
Mon avis : si tu n'as besoin que de notifications serveur vers client (actualisation de dashboard, notifications), SSE suffit et c'est beaucoup plus simple. WebSocket vaut le coup quand tu as du trafic bidirectionnel frequent : chat, jeux, collaboration en temps réel, trading.
Sur paltemps.fr, SSE aurait suffi pour les mises à jour meteo. Mais j'ai utilise WebSocket parce que je voulais experimenter. En retrospective, SSE aurait ete le bon choix.
Socket.io : l'abstraction qui simplifie (et complique)
Socket.io est une librairie qui ajoute des fonctionnalités au-dessus de WebSocket :
- Reconnexion automatique avec backoff exponentiel
- Fallback vers long polling si WebSocket ne passe pas
- Rooms et namespaces pour organiser les connexions
- Acknowledgements (confirmation de reception de message)
- Broadcast facilite
Le piège : Socket.io n'est pas du WebSocket standard. Un client Socket.io ne peut pas se connecter a un serveur WebSocket brut, et inversement. Le protocole ajoute des enveloppes autour des messages. Si tu utilises Socket.io, tu es verrouille dans l'ecosysteme.
Pour un nouveau projet, je recommande de commencer avec du WebSocket natif (ou SSE). Si tu as besoin de reconnexion et de rooms, regarde Socket.io. Mais sache que tu ajoutes une couche de complexité.
Les pièges classiques
Proxies et load balancers : le header Upgrade doit passer. Beaucoup de reverse proxies le supportent mais pas par défaut. En Nginx :
nginxlocation /ws/ {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
Scaling horizontal : avec plusieurs instances de ton serveur, un client WebSocket est connecte a une seule instance. Si un message doit aller a un client sur une autre instance, tu as besoin d'un pub/sub (Redis, NATS) entre tes instances.
Timeouts : les connexions WebSocket restent ouvertes longtemps. Les proxies, load balancers et firewalls ont des timeouts d'inactivite. Les ping/pong sont indispensables.
Résumé
- WebSocket utilise un handshake HTTP (101 Switching Protocols) puis passe en mode binaire
- Full-duplex : client et serveur envoient des messages a tout moment
- Les frames text, binary, ping, pong et close gerent la communication
- SSE est plus simple pour du serveur vers client unidirectionnel
- WebSocket brille pour le bidirectionnel : chat, jeux, collaboration
- Socket.io ajoute de la valeur mais n'est pas du WebSocket standard
Article précédent : 17 - HTTP/3 et QUIC Article suivant : 19 - Security headers