Comment fonctionnent les WebSockets : Le guide complet des connexions en temps réel

Aux débuts du web, le navigateur était un simple lecteur de documents. Vous demandiez une page, le serveur la générait et la connexion se fermait. Ce cycle requête-réponse est le cœur du protocole HTTP (Hypertext Transfer Protocol).

Cependant, à mesure que les applications web ont évolué vers des expériences riches et interactives — telles que le chat en temps réel, les flux financiers en direct, l’édition collaborative et les jeux multijoueurs —, le modèle HTTP traditionnel a commencé à montrer ses limites.

Pour obtenir des mises à jour en direct, les développeurs s’appuyaient initialement sur des solutions de contournement :

  • Short Polling (Sondage court) : Le navigateur envoie de manière répétée des requêtes HTTP au serveur toutes les quelques secondes pour demander de nouvelles données. Cela crée une surcharge massive d’en-têtes et gaspille les ressources du serveur.
  • Long Polling (Sondage long / Comet) : Le navigateur envoie une requête et le serveur la maintient ouverte jusqu’à ce que de nouvelles données soient disponibles. Une fois les données envoyées, la connexion se ferme et le navigateur ouvre immédiatement une nouvelle requête. Cette approche est complexe à gérer et génère toujours une surcharge importante lors de l’établissement des connexions.

Les WebSockets ont résolu ces limites en introduisant un protocole standardisé pour une communication persistante, bidirectionnelle et full-duplex sur une seule connexion TCP.


Qu’est-ce qu’un WebSocket ?

Les WebSockets (définis dans la RFC 6455) fonctionnent parallèlement à HTTP. Alors que HTTP est un protocole sans état où seul le client peut initier des requêtes, une connexion WebSocket reste ouverte indéfiniment, permettant au client et au serveur de s’envoyer des données à tout moment avec une latence minimale.

Voici la règle fondamentale des WebSockets :

Une fois établie, chaque partie peut envoyer des messages à tout moment sans initier de nouvelle requête de connexion.


Guide étape par étape : Le cycle de vie de la connexion

Une connexion WebSocket passe par trois phases distinctes : le Handshake (poignée de main), le Transfert de données et la Fermeture.

WebSocket Connection Lifecycle Diagram

1. Le HTTP Handshake (Mise à niveau du protocole)

Puisque les pare-feu et les routeurs sont configurés pour autoriser le trafic web standard sur les ports 80 (HTTP) et 443 (HTTPS), les WebSockets commencent leur parcours sous la forme d’une requête HTTP/1.1 standard. C’est ce qu’on appelle l’Upgrade Handshake.

La requête du client

Le client envoie une requête HTTP GET avec des en-têtes spécifiques demandant un changement de protocole :

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
  • Upgrade: websocket et Connection: Upgrade : Indiquent au serveur que le client souhaite changer de protocole.
  • Sec-WebSocket-Key : Une valeur aléatoire de 16 octets encodée en Base64. Elle sert à prouver que le serveur a reçu la poignée de main et comprend le protocole WebSocket.
  • Sec-WebSocket-Version : Spécifie la version du protocole WebSocket (généralement 13).
  • Origin : Utilisé par le serveur pour décider s’il autorise la connexion (contrôle de sécurité contre les sites non autorisés).

La réponse du serveur

Si le serveur prend en charge les WebSockets, il valide la requête et répond avec un code d’état HTTP 101 Switching Protocols :

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  • Comment le serveur calcule Sec-WebSocket-Accept :
    1. Le serveur récupère le Sec-WebSocket-Key du client (dGhlIHNhbXBsZSBub25jZQ==).
    2. Il le concatène avec un GUID magique standard : "258EAFA5-E914-47DA-95CA-C5AB0DC85B11".
    3. Il calcule le hash SHA-1 de la chaîne combinée.
    4. Il encode le hash résultant en Base64.
    5. Si le client vérifie que cette valeur correspond à ses attentes, la poignée de main réussit, la connexion HTTP bascule vers un socket TCP brut et les deux parties passent au protocole WebSocket.

2. Structuration et transfert des données

Contrairement à HTTP, qui envoie des en-têtes en texte clair suivis d’un corps, les WebSockets transmettent les données dans des paquets binaires structurés appelés trames (frames).

Une trame WebSocket possède un en-tête très léger (allant de 2 à 14 octets) suivi de la charge utile (payload). Cet en-tête contient :

  • Bit FIN (1 bit) : Indique s’il s’agit de la dernière trame d’un message.
  • Opcode (4 bits) : Définit le type de trame :
    • 0x1 : Trame texte (encodée en UTF-8)
    • 0x2 : Trame binaire
    • 0x8 : Requête de fermeture de connexion
    • 0x9 : Ping
    • 0xA : Pong
  • Bit de masque (1 bit) : Spécifie si les données de charge utile sont masquées.
  • Longueur de la charge utile : La taille des données.
  • Clé de masquage (4 octets) : Exigence de sécurité cruciale : Toutes les trames envoyées du client au serveur doivent être masquées (obfusquées par XOR) à l’aide d’une clé aléatoire de 4 octets. Cela empêche les caches proxy de lire le trafic ou d’exécuter des attaques d’empoisonnement du cache. Les trames serveur-vers-client ne doivent pas être masquées.

Heartbeats (Ping/Pong)

Pour éviter que les routeurs et les répartiteurs de charge ne ferment les connexions inactives, chaque partie peut envoyer une trame Ping. La partie réceptrice doit répondre immédiatement par une trame Pong contenant la même charge utile.

3. Fermeture de la connexion

Pour fermer proprement une connexion :

  1. Un pair envoie une trame Close contenant un code d’état (par exemple, 1000 pour une fermeture normale, 1006 pour une fermeture anormale) et un motif textuel facultatif.
  2. L’autre pair répond avec sa propre trame Close.
  3. Le socket TCP sous-jacent est fermé.

Exemple de code : Implémentation WebSocket en Node.js

Pour voir les WebSockets en action, écrivons une application Node.js simple. Nous allons créer un serveur WebSocket local qui renvoie tout message qu’il reçoit, ainsi qu’un script client pour s’y connecter.

Le serveur WebSocket (server.js)

const { WebSocketServer } = require('ws');
const http = require('http');

// 1. Créer un serveur HTTP standard
const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('HTTP Server running. Use WebSocket to connect.\n');
});

// 2. Attacher un serveur WebSocket au serveur HTTP
const wss = new WebSocketServer({ server });

wss.on('connection', (ws, req) => {
    const clientIp = req.socket.remoteAddress;
    console.log(`[Serveur] Nouveau client connecté depuis ${clientIp}`);

    // Envoyer un message de bienvenue au client
    ws.send(JSON.stringify({ type: 'welcome', message: 'Connecté au serveur WebSocket Ghaznix !' }));

    // Écouter les messages entrants de ce client
    ws.on('message', (message) => {
        console.log(`[Serveur] Reçu : ${message}`);
        
        try {
            const data = JSON.parse(message);
            ws.send(JSON.stringify({
                type: 'echo',
                message: `Écho du serveur : ${data.text.toUpperCase()}`,
                timestamp: new Date().toISOString()
            }));
        } catch (e) {
            ws.send(JSON.stringify({ type: 'error', message: 'Format JSON invalide' }));
        }
    });

    // Gérer la déconnexion du client
    ws.on('close', (code, reason) => {
        console.log(`[Serveur] Client déconnecté (Code : ${code}, Raison : ${reason.toString() || 'Aucune'})`);
    });

    ws.on('error', (error) => {
        console.error(`[Serveur] Erreur de socket : ${error.message}`);
    });
});

server.listen(8080, () => {
    console.log('Serveur WebSocket à l\'écoute sur ws://localhost:8080');
});

Le client navigateur (JavaScript côté client)

Vous pouvez exécuter ce client directement dans la console de votre navigateur :

// 1. Établir la connexion avec le serveur
const socket = new WebSocket('ws://localhost:8080');

// 2. Gestionnaire de connexion ouverte
socket.addEventListener('open', (event) => {
    console.log('[Client] Connecté au serveur.');
    
    const payload = JSON.stringify({ text: 'bonjour, serveur !' });
    socket.send(payload);
    console.log(`[Client] Envoyé : ${payload}`);
});

// 3. Écouter les messages du serveur
socket.addEventListener('message', (event) => {
    const response = JSON.parse(event.data);
    console.log('[Client] Message reçu du serveur :', response);
});

// 4. Écouter la fermeture de la connexion
socket.addEventListener('close', (event) => {
    console.log(`[Client] Connexion fermée (Code : ${event.code})`);
});

// 5. Écouter les erreurs
socket.addEventListener('error', (error) => {
    console.error('[Client] Erreur WebSocket :', error);
});

HTTP vs. WebSockets : Comparaison détaillée

Caractéristique HTTP/1.1 WebSockets
Communication Unidirectionnelle (initiée par le client) Bidirectionnelle (client ou serveur)
Modèle de connexion Requête-Réponse (courte durée) Persistante (longue durée)
Surcharge Élevée (en-têtes envoyés avec chaque requête) Très faible (surcharge de trame minimale)
État Sans état Avec état (le contexte de connexion est maintenu)
Protocole http:// ou https:// ws:// ou wss://
Idéal pour Récupération de documents, API REST Chats en temps réel, tableaux de bord, flux en direct

Considérations de sécurité pour les WebSockets

Parce que les WebSockets contournent le routage HTTP standard après la poignée de main, ils introduisent des vecteurs de sécurité uniques :

  1. Utilisez WebSocket Secure (wss://) : Exécutez toujours les WebSockets sur TLS/SSL (port 443). WSS chiffre la charge utile de la trame, empêchant les écoutes clandestines et les falsifications par des intermédiaires.
  2. Validation de l’origine : Les WebSockets ne sont pas limités par la Same-Origin Policy (SOP). Validez toujours l’en-tête Origin sur le serveur lors de la poignée de main afin d’empêcher les accès non autorisés.
  3. Authentification lors du Handshake : Authentifiez les utilisateurs avant que la connexion ne soit établie. Cela se fait généralement en passant un jeton (comme un JWT) dans les paramètres de requête, ou en vérifiant les cookies de session.
  4. Assainissement des entrées : Traitez chaque message reçu via WebSockets comme une entrée non fiable. Validez et assainissez les charges utiles pour empêcher le Cross-Site Scripting (XSS).

Résumé

Les WebSockets ont transformé les applications web en temps réel en éliminant la surcharge du polling HTTP traditionnel. En maintenant une seule connexion TCP persistante, ils permettent une messagerie bidirectionnelle instantanée, alimentant les tableaux de bord en direct, les jeux multijoueurs et les applications de chat d’aujourd’hui. Comprendre la mise à niveau HTTP, l’architecture des trames et les pratiques de sécurité cruciales vous assure de créer des services en temps réel rapides et sécurisés.


Explorez plus de tutoriels et de guides pour les développeurs sur le blog Ghaznix →