frontend / javascript
WebSockets côté client
Explication
Ce que vous allez apprendre
- Expliquer pourquoi
fetchne convient pas à une communication temps réel bidirectionnelle - Ouvrir une connexion WebSocket et envoyer/recevoir des messages
- Vérifier le
readyStateavant d'envoyer un message pour éviter une erreur silencieuse - Implémenter une reconnexion automatique avec un backoff exponentiel
- Mettre en place un heartbeat pour détecter une connexion "zombie"
Dans quel contexte ?
Une application de chat en temps réel fonctionne parfaitement en développement, mais en production, certains utilisateurs signalent qu'ils cessent de recevoir des messages après avoir fermé puis rouvert leur ordinateur portable (changement de réseau Wi-Fi). Le navigateur croit encore la connexion WebSocket active alors que le serveur l'a fermée depuis longtemps côté réseau. La solution passe par un mécanisme de heartbeat (un ping régulier) combiné à une reconnexion automatique avec délai croissant, pour détecter et réparer ce genre de connexion "zombie" sans intervention de l'utilisateur.
1. La limite du modèle requête-réponse
Toutes les communications réseau vues jusqu'ici (fetch) suivent un modèle requête-réponse : le navigateur demande, le serveur répond, la connexion se referme. Ce modèle est mal adapté à un chat ou un tableau de bord qui se met à jour en continu.
Prérequis
Cette leçon suppose fetch (leçon 15) et les classes (leçon 6) bien maîtrisées : l'exemple de code encapsule la logique de reconnexion dans une classe dédiée.
2. Ce qu'il manque : que le serveur parle en premier
Dans ces cas, le serveur doit pouvoir envoyer des données à la page SANS que celle-ci ait explicitement redemandé quoi que ce soit. fetch ne permet pas ça.
3. La solution : une connexion qui reste ouverte
Les WebSockets établissent une connexion persistante et bidirectionnelle : une fois ouverte, les deux parties peuvent s'envoyer des messages à tout moment, dans les deux sens, sans jamais la rouvrir.
| Modèle | Qui initie l'échange | Connexion |
|---|---|---|
fetch (HTTP classique) | Toujours le client | Fermée après chaque réponse |
| WebSocket | Client ou serveur, à tout moment | Persistante, ouverte une fois pour toutes |
4. Une connexion réseau reste fragile
Une connexion peut se couper sans prévenir (coupure Wi-Fi, changement de réseau). Il faut donc toujours vérifier le readyState avant d'envoyer un message.
Piège fréquent
Appeler socket.send(message) sans vérifier socket.readyState === WebSocket.OPEN peut lever une exception si la connexion est fermée ou pas encore établie. Toujours vérifier cet état avant d'envoyer, ou mettre le message en file d'attente le temps de la reconnexion.
5. Se reconnecter intelligemment
Une stratégie de reconnexion automatique avec un délai croissant ("backoff exponentiel") évite de marteler inutilement un serveur déjà en difficulté, tout en garantissant que l'application se rétablit d'elle-même.
6. Détecter une connexion "zombie"
Un "heartbeat" — un petit message technique envoyé périodiquement — permet de détecter une connexion que le navigateur croit encore active mais qui, en réalité, ne répond plus.
Cette leçon illustre concrètement, sur un cas réel, plusieurs concepts déjà vus : classes, closures, et gestion robuste des erreurs réseau.
Commandes & code
WebSockets côté client
Une connexion bidirectionnelle persistante, pour du temps réel sans polling.
// --- Connexion de base ---
const socket = new WebSocket("wss://exemple.com/chat");
// readyState : CONNECTING(0), OPEN(1), CLOSING(2), CLOSED(3)
console.log(socket.readyState);
socket.addEventListener("open", () => {
console.log("Connexion etablie");
socket.send(JSON.stringify({ type: "auth", token: "abc123" }));
});
socket.addEventListener("message", (event) => {
const message = JSON.parse(event.data);
console.log("Recu :", message);
});
socket.addEventListener("error", (event) => {
console.error("Erreur WebSocket :", event);
});
socket.addEventListener("close", (event) => {
// code 1000 = fermeture normale, 1006 = anormale (reseau coupe), etc.
console.log(`Connexion fermee : code=${event.code}, raison=${event.reason}`);
});
// --- Envoyer differents types de donnees ---
socket.send("message texte brut");
socket.send(JSON.stringify({ type: "message", contenu: "Bonjour" }));
// Donnees binaires : ArrayBuffer ou Blob
const buffer = new Uint8Array([1, 2, 3, 4]).buffer;
socket.send(buffer);
// --- Toujours verifier l'etat avant d'envoyer (send() sur une socket fermee leve une erreur) ---
function envoyerSecurise(socket, donnees) {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(donnees));
} else {
console.warn("Socket non ouverte, message ignore :", donnees);
}
}
// --- Reconnexion automatique avec backoff exponentiel (pattern de production) ---
class SocketResiliente {
#url;
#socket;
#tentative = 0;
#maxTentatives = 10;
#ecouteurs = { message: [] };
constructor(url) {
this.#url = url;
this.#connecter();
}
#connecter() {
this.#socket = new WebSocket(this.#url);
this.#socket.onopen = () => {
console.log("Connecte");
this.#tentative = 0; // reinitialise le compteur apres un succes
};
this.#socket.onmessage = (event) => {
this.#ecouteurs.message.forEach((cb) => cb(JSON.parse(event.data)));
};
this.#socket.onclose = (event) => {
if (event.code !== 1000 && this.#tentative < this.#maxTentatives) {
const delai = Math.min(1000 * 2 ** this.#tentative, 30_000); // plafonne a 30s
console.log(`Reconnexion dans ${delai}ms (tentative ${this.#tentative + 1})`);
setTimeout(() => this.#connecter(), delai);
this.#tentative++;
}
};
}
on(evenement, callback) {
this.#ecouteurs[evenement].push(callback);
}
envoyer(donnees) {
envoyerSecurise(this.#socket, donnees);
}
fermer() {
this.#tentative = this.#maxTentatives; // empeche une reconnexion apres fermeture volontaire
this.#socket.close(1000, "Fermeture volontaire");
}
}
const connexionChat = new SocketResiliente("wss://exemple.com/chat");
connexionChat.on("message", (msg) => console.log("Chat :", msg));
// --- Heartbeat : detecter une connexion "zombie" que le navigateur croit encore ouverte ---
function demarrerHeartbeat(socket, intervalleMs = 30_000) {
const intervalId = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: "ping" }));
} else {
clearInterval(intervalId);
}
}, intervalleMs);
return () => clearInterval(intervalId);
}
// --- File d'attente : bufferiser les messages envoyes AVANT que la connexion soit ouverte ---
class SocketAvecFile {
#socket;
#file = [];
constructor(url) {
this.#socket = new WebSocket(url);
this.#socket.onopen = () => {
this.#file.forEach((msg) => this.#socket.send(msg));
this.#file = [];
};
}
envoyer(donnees) {
const message = JSON.stringify(donnees);
if (this.#socket.readyState === WebSocket.OPEN) {
this.#socket.send(message);
} else {
this.#file.push(message); // envoye des que la connexion s'ouvre
}
}
}Résumé
readyStatedoit être vérifié (WebSocket.OPEN) avant toutsend(), sous peine d'exception sur une socket fermée.- Le code de fermeture (
event.code) distingue une fermeture normale (1000) d'une coupure anormale, condition pour reconnecter. - Une reconnexion avec backoff exponentiel évite de marteler le serveur après une coupure réseau.
- Un heartbeat applicatif détecte les connexions "zombies" que le navigateur croit encore actives.
Exercices pratiques
Mission : stabiliser une connexion de chat instable
Objectif : Corriger un envoi WebSocket prématuré et distinguer une fermeture volontaire d'une coupure réseau à réparer automatiquement.
Contexte
Un développeur écrit ce code pour un chat en temps réel :
const socket = new WebSocket("wss://exemple.com/chat");
socket.send(JSON.stringify({ type: "auth", token: "abc123" }));Certains utilisateurs signalent aussi que l'application tente de se reconnecter en boucle même quand ils cliquent volontairement sur "Se déconnecter", ce qui déclenche socket.close(1000, "Deconnexion utilisateur").