Prise en main de WebSocket
Une connexion WebSocket permet au navigateur et au serveur d'échanger des messages dans les deux sens, sur une connexion qui reste ouverte. Le serveur peut envoyer une information dès qu'elle est disponible, sans attendre une nouvelle requête du navigateur.
Cette prise en main permet d'observer trois propriétés : le handshake, les échanges full duplex et la persistance de la connexion. Nous utilisons le WebSocket natif du navigateur et la bibliothèque ws côté Node.js.
Un serveur WebSocket
Dans un nouveau dossier, initialisez un projet Node.js et installez ws :
npm init -y
npm install ws
Créez un fichier server.mjs. L'extension .mjs permet d'utiliser les imports ES modules dans Node.js.
import { WebSocketServer } from 'ws';
const server = new WebSocketServer({ port: 8888 });
server.on('connection', client => {
console.log('Client connected');
client.on('message', message => {
console.log('Received:', message.toString());
client.send('Message received!');
});
const timer = setInterval(() => {
client.send(`Server time: ${new Date().toISOString()}`);
}, 3000);
client.on('close', () => {
clearInterval(timer);
console.log('Client disconnected');
});
client.on('error', error => console.error(error));
});
Lancez le serveur et laissez le terminal ouvert :
node server.mjs
L'événement connection fournit la connexion d'un client. On peut y écouter ses messages avec message et lui en envoyer avec send. Ici, chaque client reçoit une confirmation lorsqu'il envoie un message, ainsi que l'heure du serveur toutes les trois secondes, même s'il n'envoie rien.
Se connecter depuis le navigateur
Ouvrez une page de votre serveur de développement local en HTTP, puis les outils de développement du navigateur. Dans l'onglet Réseau, activez le filtre WS avant d'exécuter le code suivant dans la console :
const ws = new WebSocket('ws://localhost:8888');
ws.addEventListener('open', () => {
console.log('Connection opened');
ws.send('Hello from the browser!');
});
ws.addEventListener('message', event => {
console.log('Received:', event.data);
});
ws.addEventListener('close', event => {
console.log('Connection closed', event.code);
});
ws.addEventListener('error', () => {
console.error('WebSocket connection error');
});
Le constructeur démarre la connexion de manière asynchrone. Il faut attendre l'événement open avant d'envoyer un message. Côté navigateur, le contenu reçu est disponible dans event.data.
L'adresse localhost suppose que Node.js tourne sur la même machine que le navigateur. Le schéma ws:// correspond à une connexion non chiffrée ; wss:// est son équivalent chiffré avec TLS, à utiliser avec une page HTTPS et un serveur configuré pour cela.
Observer le handshake
La connexion est toujours initiée par le client, ici le navigateur : le serveur attend une demande, il ne peut pas ouvrir spontanément une connexion vers une page. L'appel à new WebSocket(...) déclenche le handshake d'ouverture en HTTP. Dans cette configuration HTTP/1.1, le navigateur envoie une requête demandant un changement de protocole.
Dans l'onglet Réseau, sélectionnez la connexion WebSocket et retrouvez ces en-têtes dans la requête :
Connection: Upgrade
Upgrade: websocket
Le serveur accepte avec le statut 101 Switching Protocols. Cet échange initial est le handshake : une fois terminé, la connexion sert à transporter des messages WebSocket. Le navigateur et ws gèrent cet échange pour nous.
HTTP/1.1 reste souvent utilisé pour ouvrir les connexions WebSocket en production, même lorsque les pages du site sont chargées en HTTP/2 ou HTTP/3. WebSocket peut aussi utiliser ces versions, mais avec un CONNECT étendu à la place du mécanisme Upgrade / 101 présenté ici. Leur prise en charge dépend du navigateur et de l'infrastructure. Références : WebSocket sur HTTP/2 et sur HTTP/3.
Dans l'onglet Messages ou Frames de cette connexion, retrouvez le message envoyé par le navigateur, la confirmation du serveur et les heures reçues périodiquement.
Origine et connexions entre sites
WebSocket permet les connexions vers une autre origine : par exemple, une page sur http://localhost:8080 peut se connecter au serveur sur ws://localhost:8888. Contrairement à fetch, le mécanisme CORS ne s'applique pas au handshake WebSocket : le navigateur ne fait pas de prérequête CORS et le serveur n'a pas besoin de renvoyer Access-Control-Allow-Origin pour autoriser la connexion. Une fois établie, les messages WebSocket ne sont pas non plus soumis à CORS.
Le navigateur transmet cependant l'en-tête HTTP Origin pendant le handshake. Il indique l'origine de la page, par exemple http://localhost:8080. Pour limiter les sites autorisés à se connecter, le serveur peut vérifier cet en-tête et refuser le handshake si l'origine n'est pas autorisée. Notre serveur minimal ne fait pas ce filtrage.
Un script exécuté dans une page ne peut pas choisir librement cet en-tête : cette vérification apporte donc une protection contre les connexions venant d'autres sites dans un navigateur classique. En revanche, un client hors navigateur peut falsifier Origin : ce contrôle ne remplace pas une authentification. D'autres règles du navigateur, comme le contenu mixte et la politique CSP de la page, peuvent aussi bloquer une connexion.
Référence : le handshake WebSocket et l'en-tête Origin.
Des échanges full duplex
Laissez le client ouvert sans rien envoyer : les heures continuent d'arriver. Leur envoi n'est pas déclenché par une requête du navigateur.
Pendant ce temps, envoyez un autre message depuis la console :
ws.send('I can send a message too!');
Le serveur le reçoit et envoie sa confirmation, tandis que les heures continuent d'arriver. Chaque côté peut envoyer des messages indépendamment de l'autre : la communication est full duplex. Les messages ne sont pas limités à une alternance requête/réponse.
Une connexion persistante
Répétez l'appel à ws.send plusieurs fois. Tous ces échanges apparaissent dans la même connexion de l'onglet Réseau : on ne refait pas un handshake pour chaque message. Il n'y a pas non plus de requête HTTP périodique pour récupérer l'heure.
La connexion reste ouverte entre les messages, jusqu'à sa fermeture ou une interruption. Vous pouvez consulter son état avec ws.readyState :
| Constante | Valeur | État |
|---|---|---|
WebSocket.CONNECTING |
0 |
Ouverture en cours |
WebSocket.OPEN |
1 |
Connexion ouverte |
WebSocket.CLOSING |
2 |
Fermeture en cours |
WebSocket.CLOSED |
3 |
Connexion fermée |
Fermez maintenant la connexion depuis la console :
ws.close(1000, 'End of the introduction');
Le premier argument, 1000, est le code WebSocket de fermeture normale : il signale que la connexion se termine volontairement, sans erreur. Ce n'est ni une durée ni un statut HTTP. Le second argument est une courte raison textuelle envoyée à l'autre côté. Le code de fermeture reçu est accessible dans event.code, et la raison dans event.reason. Voir WebSocket.close().
Observez l'événement close dans le navigateur et le message de déconnexion dans le terminal Node.js. Le serveur arrête le timer associé à ce client et les heures ne sont plus envoyées. Une connexion fermée ne peut pas être réutilisée : il faut créer un nouvel objet WebSocket pour se reconnecter.
En production : WSS, ports et reverse proxy
Le port 8888 convient pour notre expérience locale. En production, un port inhabituel peut être bloqué par certains pare-feu ou proxys. Les ports standards sont 80 pour HTTP et WS, et 443 pour HTTPS et WSS. Avec une page HTTPS, utilisez une connexion WebSocket chiffrée en wss://. Voir la remarque MDN sur les ports WebSocket.
HTTPS et WSS peuvent partager le port public 443, mais deux services indépendants ne peuvent généralement pas écouter simultanément sur la même adresse IP et le même port TCP. Une solution courante consiste à placer un reverse proxy, comme Nginx ou HAProxy, devant les services : lui seul écoute sur le port public, gère le certificat TLS et transmet les demandes au service approprié.
| URL publique | Destination derrière le reverse proxy |
|---|---|
https://example.com/ |
Serveur du site, par exemple sur 127.0.0.1:8080 |
wss://example.com/ws |
Serveur WebSocket, par exemple sur 127.0.0.1:8888 |
Le chemin /ws est un choix de routage dans cet exemple, pas une obligation. Le reverse proxy peut aussi reconnaître la demande de handshake HTTP/1.1 grâce aux en-têtes Upgrade: websocket et Connection: Upgrade, puis la diriger vers le service WebSocket. HTTPS et WSS peuvent alors utiliser le même domaine, le même port et le même chemin.
Le proxy doit transmettre les en-têtes nécessaires au handshake HTTP/1.1, puis relayer les échanges dans les deux sens. La connexion navigateur-proxy est chiffrée ; entre le proxy et Node.js sur la même machine, le serveur peut conserver une connexion locale non chiffrée. Référence : WebSocket avec Nginx.
Une autre possibilité est d'attacher ws à un serveur HTTP ou HTTPS Node.js existant : un seul service accepte alors les requêtes du site et les ouvertures WebSocket sur le même port.
WsMini
Nous avons échangé de simples textes. WebSocket transporte des messages, mais ne définit pas leur signification pour notre application : une confirmation n'est pas automatiquement associée à une demande particulière.
La bibliothèque WsMini, construite sur ws côté serveur et sur le WebSocket natif côté navigateur, fournit plusieurs mécanismes pour structurer ces échanges :
- RPC : appeler une fonction du serveur avec
await ws.rpc(...), recevoir son résultat et gérer les erreurs avectry/catch. - Commandes serveur : envoyer une information à un client ou la diffuser à tous avec
broadcastCmd, et la recevoir aveconCmd. - PubSub : publier sur des canaux et s'abonner à ceux qui nous intéressent avec
pubetsub. Les messages sont transmis aux abonnés connectés, sans file de messages persistante. Voir WSServerPubSub. - Rooms : regrouper les participants dans des espaces distincts, par exemple une room par partie, avec leurs messages, leurs commandes et leur cycle de vie. Voir WSServerRoomManager.
- Jeux : WSServerGameRoom ajoute une boucle de simulation à pas fixe côté serveur.
WsMini permet également de s'attacher à un serveur HTTP ou HTTPS Node.js existant en passant ce serveur à start({ server: httpServer }). Les requêtes du site et les connexions WebSocket peuvent ainsi partager le même port. Voir la documentation de WSServer.start(options).
D'autres bibliothèques pour le temps réel
- Socket.IO propose des échanges par événements, des accusés de réception, des rooms et la reconnexion automatique. Ce n'est pas une simple implémentation de WebSocket : elle utilise son propre protocole au-dessus de transports comme WebSocket, le HTTP long-polling ou WebTransport. Il faut un client compatible Socket.IO ; un client WebSocket natif ne peut pas communiquer directement avec un serveur Socket.IO.
- Colyseus est un framework Node.js destiné aux jeux multijoueurs. Il fournit des rooms, du matchmaking et une synchronisation automatique de l'état défini côté serveur, avec envoi des modifications sous forme de patches différentiels. Il apporte ainsi des outils propres à la gestion des parties et de leur état, au-delà du transport des messages.