Passer au contenu principal

🔴 WebSocket vs SSE vs Long Polling

📖 Définition

La communication en temps réel est une technologie qui permet l'échange instantané de données entre le serveur et le client. WebSocket fournit une communication bidirectionnelle en temps réel, SSE (Server-Sent Events) envoie des données uniquement du serveur au client, et Long Polling est une méthode de communication en temps réel utilisant HTTP. Chacune a des avantages et des inconvénients, le choix doit donc être fait selon le scénario d'utilisation.

🎯 Comprendre par analogies

Téléphone vs Radio vs Messagerie

HTTP normal = Courrier postal
Vous : "Bonjour !" (envoi de lettre)
↓ (après plusieurs jours)
Ami : "Salut !" (réponse)
↓ (après plusieurs jours)
Vous : "Ça va ?" (autre lettre)

- Lent
- Nouvelle connexion à chaque fois
- Pas en temps réel

Long Polling = Téléphone (en attente d'appel)
Vous : "Préviens-moi s'il y a quelque chose !" (appeler et attendre)
↓ (attendre longtemps...)
Ami : "J'ai quelque chose à dire maintenant !" (réponse)
↓ (appel terminé)
Vous : "Préviens-moi encore s'il y a quelque chose !" (rappeler)

- Basé sur HTTP
- Maintenir connexion → réponse → reconnecter
- Inefficace

SSE = Émission radio
Ami : "Bonjour à tous !" (début d'émission)
"La météo d'aujourd'hui est..." (diffusion continue)
"La prochaine nouvelle est..." (diffusion continue)
Vous : (juste écouter)

- Serveur → Client (unidirectionnel)
- Connexion maintenue
- Simple

WebSocket = Appel vidéo
Vous : "Salut !" (envoi instantané)
Ami : "Content de te voir !" (réponse instantanée)
Vous : "Que fais-tu ?" (envoi instantané)
Ami : "Je programme !" (réponse instantanée)

- Bidirectionnel en temps réel
- Connexion maintenue
- Rapide et efficace

Commande au restaurant

HTTP normal = Self-service
1. Aller au comptoir et commander de la nourriture
2. Attendre
3. Recevoir la nourriture et retourner à la table
4. Retourner au comptoir et commander une boisson
5. Attendre encore

Long Polling = Appuyer sur la sonnette et attendre
1. Appuyer sur la sonnette et attendre l'employé (maintenir connexion)
2. Employé arrive → "Votre commande"
3. Commander et appuyer sur la sonnette à nouveau
4. Attendre encore...

SSE = Affichage de cuisine
Cuisine : "Client 1, votre nourriture est prête !"
Cuisine : "Client 2, en préparation !"
Cuisine : "Client 3, bientôt prêt !"
Vous : (juste écouter)

WebSocket = Service à table
Vous : "De l'eau, s'il vous plaît"
Employé : "Oui, je vous l'apporte"
Vous : "Du kimchi aussi"
Employé : "Je l'apporte tout de suite"
Employé : "Voici votre nourriture"
Vous : "Merci"

- Conversation libre
- Réponse rapide

⚙️ Principe de fonctionnement

1. HTTP normal vs Communication en temps réel

========== HTTP normal (Requête-Réponse) ==========

Client Serveur
│ │
│ 1. Connexion (Request) │
│────────────────────────>│
│ │
│ │ Traitement...
│ │
│ 2. Réponse (Response) │
│<────────────────────────│
│ │
│ Connexion fermée │
╳ ╳

│ 3. Reconnecter │
│────────────────────────>│
│ │

Caractéristiques :
- Répond uniquement quand le client demande
- Nouvelle connexion à chaque fois
- Le serveur ne peut pas envoyer en premier
- Pas en temps réel

========== Communication en temps réel ==========

Client Serveur
│ │
│ Connexion │
│<───────────────────────>│
│ │
│ Communication bidirectionnelle maintenue │
│<───────────────────────>│
│ │
│ Échange de données │
│<───────────────────────>│
│ │

Caractéristiques :
- Connexion maintenue
- Le serveur peut envoyer en premier
- Temps réel possible

2. Long Polling

Client                              Serveur
│ │
│ 1. Requête (Nouvelles données ?) │
│──────────────────────────────────>│
│ │
│ Connexion maintenue (en attente...)│
│ │
│ │ Pas de données...
│ │ Continuer d'attendre...
│ │
│ │ Nouvelles données !
│ │
│ 2. Réponse (Voici les données) │
│<──────────────────────────────────│
│ │
│ Connexion fermée │
╳ ╳
│ │
│ 3. Reconnecter immédiatement │
│──────────────────────────────────>│
│ │
│ Attendre à nouveau... │

Processus :
1. Client → Serveur : "Nouvelles données ?"
2. Serveur : Attendre jusqu'à ce que des données soient disponibles
3. Données disponibles → Réponse
4. Connexion fermée
5. Reconnecter immédiatement (répéter)

Avantages :
✅ Basé sur HTTP (utiliser l'infrastructure existante)
✅ Pas de problèmes de pare-feu
✅ Implémentation simple

Inconvénients :
❌ Inefficace (reconnexion continue)
❌ Charge serveur élevée
❌ Surcharge d'en-têtes

3. Server-Sent Events (SSE)

Client                              Serveur
│ │
│ 1. Demande de connexion │
│──────────────────────────────────>│
│ │
│ 2. Maintenir connexion (début de stream) │
│<══════════════════════════════════│
│ │
│ 3. Push de données │
│<──────────────────────────────────│
│ │
│ 4. Autre push de données │
│<──────────────────────────────────│
│ │
│ Connexion maintenue... │
│<══════════════════════════════════│

Caractéristiques :
- Serveur → Client (unidirectionnel)
- Connexion maintenue
- Basé sur HTTP
- Reconnexion automatique

Avantages :
✅ Simple (intégré au navigateur)
✅ Reconnexion automatique
✅ Peut reprendre avec ID d'événement
✅ Efficace avec HTTP/2

Inconvénients :
❌ Unidirectionnel (Serveur → Client)
❌ Pas de données binaires
❌ Pas de support IE

4. WebSocket

Client                              Serveur
│ │
│ 1. Requête HTTP Upgrade │
│──────────────────────────────────>│
│ │
│ 2. Approbation d'Upgrade │
│<──────────────────────────────────│
│ │
│ Basculer vers protocole WebSocket│
│<══════════════════════════════════>│
│ │
│ 3. Envoi de données │
│──────────────────────────────────>│
│ │
│ 4. Réception de données │
│<──────────────────────────────────│
│ │
│ 5. Envoi de données │
│──────────────────────────────────>│
│ │
│ Communication bidirectionnelle continue... │
│<══════════════════════════════════>│

Caractéristiques :
- Communication bidirectionnelle en temps réel
- Protocole séparé (ws://, wss://)
- Connexion maintenue
- Faible latence

Avantages :
✅ Vrai temps réel
✅ Communication bidirectionnelle
✅ Faible surcharge
✅ Support binaire

Inconvénients :
❌ Complexe
❌ Problèmes possibles avec proxy/pare-feu
❌ Charge serveur (maintenir connexion)

💡 Exemples pratiques

Exemple de Long Polling

// ========== Serveur (Express.js) ==========
const express = require('express');
const app = express();

let messages = [];
let waitingClients = [];

// Ajouter un message (appelé d'ailleurs)
function addMessage(message) {
messages.push(message);

// Répondre immédiatement aux clients en attente
waitingClients.forEach(client => {
client.json({ messages });
});
waitingClients = [];
}

// Point de terminaison Long Polling
app.get('/messages', (req, res) => {
const lastId = parseInt(req.query.lastId) || 0;

// Répondre immédiatement s'il y a de nouveaux messages
if (messages.length > lastId) {
return res.json({ messages: messages.slice(lastId) });
}

// Sinon ajouter à la liste d'attente (max 30 secondes)
waitingClients.push(res);

// Timeout de 30 secondes
req.setTimeout(30000, () => {
const index = waitingClients.indexOf(res);
if (index > -1) {
waitingClients.splice(index, 1);
res.json({ messages: [] }); // Réponse vide
}
});
});

app.listen(3000);

// ========== Client ==========
let lastMessageId = 0;

async function longPolling() {
while (true) {
try {
const response = await fetch(`/messages?lastId=${lastMessageId}`);
const data = await response.json();

// Traiter les nouveaux messages
if (data.messages.length > 0) {
data.messages.forEach(msg => {
console.log('Nouveau message:', msg);
displayMessage(msg);
});
lastMessageId += data.messages.length;
}

// Reconnecter immédiatement
await longPolling();
} catch (error) {
console.error('Erreur:', error);
// Réessayer après 3 secondes
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
}

// Démarrer
longPolling();

// ========== Problèmes ==========
/*
1. Reconnexion continue (inefficace)
2. Utilisation réseau élevée
3. Charge serveur élevée
4. Consommation batterie (mobile)
*/

Exemple de Server-Sent Events (SSE)

// ========== Serveur (Express.js) ==========
const express = require('express');
const app = express();

app.use(express.static('public'));

// Point de terminaison SSE
app.get('/events', (req, res) => {
// Définir les en-têtes SSE
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');

// CORS (si nécessaire)
res.setHeader('Access-Control-Allow-Origin', '*');

// Message de confirmation de connexion
res.write('data: Connected\n\n');

// ID du client
const clientId = Date.now();
console.log(`Client ${clientId} connecté`);

// Envoyer l'heure toutes les 5 secondes
const intervalId = setInterval(() => {
const data = {
time: new Date().toLocaleTimeString(),
message: 'Bonjour !'
};

// Envoyer au format SSE
res.write(`data: ${JSON.stringify(data)}\n\n`);
}, 5000);

// Gérer la déconnexion du client
req.on('close', () => {
console.log(`Client ${clientId} déconnecté`);
clearInterval(intervalId);
res.end();
});
});

// API pour publier des événements
const clients = [];

app.get('/events/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');

// Enregistrer le client
clients.push(res);

req.on('close', () => {
const index = clients.indexOf(res);
clients.splice(index, 1);
});
});

// Envoyer un message à tous les clients
app.post('/broadcast', express.json(), (req, res) => {
const { message } = req.body;

clients.forEach(client => {
client.write(`data: ${JSON.stringify({ message })}\n\n`);
});

res.json({ success: true });
});

app.listen(3000);

// ========== Client (HTML) ==========
/*
<!DOCTYPE html>
<html>
<body>
<div id="messages"></div>

<script>
// Connexion SSE
const eventSource = new EventSource('/events');

// Recevoir des messages
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Données reçues:', data);

const div = document.getElementById('messages');
div.innerHTML += `<p>${data.time}: ${data.message}</p>`;
};

// Connexion ouverte
eventSource.onopen = () => {
console.log('SSE connecté');
};

// Gestion des erreurs
eventSource.onerror = (error) => {
console.error('Erreur SSE:', error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log('Connexion SSE fermée');
}
};

// Fermer la connexion (en quittant la page)
window.addEventListener('beforeunload', () => {
eventSource.close();
});
</script>
</body>
</html>
*/

// ========== Fonctionnalités avancées ==========

// 1. Spécifier le type d'événement
app.get('/events/typed', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');

// Plusieurs types d'événements
setInterval(() => {
// Message général
res.write(`event: message\ndata: Hello\n\n`);

// Notification
res.write(`event: notification\ndata: New notification!\n\n`);

// Mise à jour
res.write(`event: update\ndata: {"count": 10}\n\n`);
}, 5000);
});

// Traitement par type d'événement dans le client
/*
eventSource.addEventListener('message', (e) => {
console.log('Message:', e.data);
});

eventSource.addEventListener('notification', (e) => {
console.log('Notification:', e.data);
});

eventSource.addEventListener('update', (e) => {
const data = JSON.parse(e.data);
console.log('Mise à jour:', data);
});
*/

// 2. ID d'événement (reprendre lors de la reconnexion)
let eventId = 0;

app.get('/events/resumable', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');

const lastEventId = parseInt(req.headers['last-event-id']) || 0;
console.log('Dernier ID d\'événement:', lastEventId);

// Envoyer uniquement les événements après lastEventId
setInterval(() => {
eventId++;
res.write(`id: ${eventId}\ndata: Event ${eventId}\n\n`);
}, 1000);
});

// 3. Définir le temps de réessai
app.get('/events/retry', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');

// Reconnecter après 5 secondes
res.write('retry: 5000\n');
res.write('data: Connected\n\n');
});

Exemple de WebSocket (Socket.io)

// ========== Serveur (Socket.io) ==========
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: {
origin: '*'
}
});

app.use(express.static('public'));

// Gestion des utilisateurs connectés
const users = new Map();

// Connexion WebSocket
io.on('connection', (socket) => {
console.log('Nouvel utilisateur connecté:', socket.id);

// Enregistrer les informations utilisateur
socket.on('register', (username) => {
users.set(socket.id, { username, socket });
console.log(`${username} enregistré`);

// Notifier tous les utilisateurs
io.emit('user-joined', {
username,
totalUsers: users.size
});
});

// Recevoir un message
socket.on('chat-message', (message) => {
const user = users.get(socket.id);
console.log(`${user.username}: ${message}`);

// Diffuser à tous les utilisateurs
io.emit('chat-message', {
username: user.username,
message,
timestamp: new Date().toISOString()
});
});

// Indicateur de saisie
socket.on('typing', () => {
const user = users.get(socket.id);
socket.broadcast.emit('user-typing', user.username);
});

socket.on('stop-typing', () => {
const user = users.get(socket.id);
socket.broadcast.emit('user-stop-typing', user.username);
});

// Message privé
socket.on('private-message', ({ to, message }) => {
const targetSocket = Array.from(users.values())
.find(u => u.username === to)?.socket;

if (targetSocket) {
const sender = users.get(socket.id);
targetSocket.emit('private-message', {
from: sender.username,
message
});
}
});

// Déconnexion
socket.on('disconnect', () => {
const user = users.get(socket.id);
if (user) {
console.log(`${user.username} déconnecté`);
users.delete(socket.id);

// Notifier tous les utilisateurs
io.emit('user-left', {
username: user.username,
totalUsers: users.size
});
}
});
});

server.listen(3000, () => {
console.log('Serveur en cours d\'exécution: http://localhost:3000');
});

// ========== Client (HTML + Socket.io) ==========
/*
<!DOCTYPE html>
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<div id="chat">
<div id="messages"></div>
<input id="username" placeholder="Nom" />
<input id="message" placeholder="Message" />
<button onclick="sendMessage()">Envoyer</button>
</div>

<script>
// Connexion Socket.io
const socket = io('http://localhost:3000');

// Connexion réussie
socket.on('connect', () => {
console.log('Connecté:', socket.id);
});

// Enregistrer l'utilisateur
function register() {
const username = document.getElementById('username').value;
socket.emit('register', username);
}

// Envoyer un message
function sendMessage() {
const message = document.getElementById('message').value;
socket.emit('chat-message', message);
document.getElementById('message').value = '';
}

// Recevoir un message
socket.on('chat-message', (data) => {
const div = document.getElementById('messages');
div.innerHTML += `
<p><strong>${data.username}:</strong> ${data.message}</p>
`;
});

// Utilisateur rejoint
socket.on('user-joined', (data) => {
console.log(`${data.username} a rejoint (total ${data.totalUsers})`);
});

// Utilisateur quitté
socket.on('user-left', (data) => {
console.log(`${data.username} est parti (total ${data.totalUsers})`);
});

// Saisie
let typingTimeout;
document.getElementById('message').addEventListener('input', () => {
socket.emit('typing');

clearTimeout(typingTimeout);
typingTimeout = setTimeout(() => {
socket.emit('stop-typing');
}, 1000);
});

socket.on('user-typing', (username) => {
console.log(`${username} tape...`);
});

// Déconnexion
socket.on('disconnect', () => {
console.log('Déconnecté');
});

// Reconnexion
socket.on('reconnect', () => {
console.log('Reconnecté');
});
</script>
</body>
</html>
*/

API WebSocket native

// ========== Serveur (bibliothèque ws) ==========
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
console.log('Client connecté');

// Recevoir un message
ws.on('message', (message) => {
console.log('Message reçu:', message.toString());

// Diffuser à tous les clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message.toString());
}
});
});

// Déconnexion
ws.on('close', () => {
console.log('Client déconnecté');
});

// Erreur
ws.on('error', (error) => {
console.error('Erreur WebSocket:', error);
});

// Message de bienvenue
ws.send('Connecté au serveur !');
});

console.log('Serveur WebSocket en cours d\'exécution: ws://localhost:8080');

// ========== Client (navigateur) ==========

// Connexion WebSocket
const ws = new WebSocket('ws://localhost:8080');

// Connexion ouverte
ws.addEventListener('open', (event) => {
console.log('WebSocket connecté');
ws.send('Bonjour !');
});

// Recevoir un message
ws.addEventListener('message', (event) => {
console.log('Du serveur:', event.data);
});

// Erreur
ws.addEventListener('error', (error) => {
console.error('Erreur WebSocket:', error);
});

// Déconnexion
ws.addEventListener('close', (event) => {
console.log('WebSocket déconnecté');
if (event.code === 1000) {
console.log('Fermeture normale');
} else {
console.log('Fermeture anormale:', event.code);
}
});

// Envoyer un message
function sendMessage(message) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
} else {
console.error('WebSocket n\'est pas ouvert');
}
}

// Fermer la connexion
function closeConnection() {
ws.close(1000, 'Fermeture normale');
}

// ========== Transmission de données binaires ==========

// Envoi de fichier
async function sendFile(file) {
const arrayBuffer = await file.arrayBuffer();
ws.send(arrayBuffer);
}

// Réception binaire sur le serveur
ws.addEventListener('message', (event) => {
if (event.data instanceof ArrayBuffer) {
console.log('Données binaires reçues:', event.data.byteLength, 'bytes');
} else {
console.log('Données texte:', event.data);
}
});

// ========== Ping/Pong (maintenir la connexion) ==========

// Serveur
wss.on('connection', (ws) => {
ws.isAlive = true;

ws.on('pong', () => {
ws.isAlive = true;
});
});

// Ping toutes les 30 secondes
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate(); // Terminer la connexion si pas de réponse
}

ws.isAlive = false;
ws.ping();
});
}, 30000);

// ========== Reconnexion automatique ==========

let ws;
let reconnectAttempts = 0;
const maxReconnectAttempts = 10;

function connect() {
ws = new WebSocket('ws://localhost:8080');

ws.addEventListener('open', () => {
console.log('Connecté');
reconnectAttempts = 0;
});

ws.addEventListener('close', (event) => {
console.log('Déconnecté');

// Reconnexion automatique
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
console.log(`Tentative de reconnexion dans ${delay}ms...`);
setTimeout(connect, delay);
} else {
console.error('Échec de la reconnexion');
}
});

ws.addEventListener('error', (error) => {
console.error('Erreur:', error);
ws.close();
});
}

connect();

🤔 Questions fréquentes

Q1. Quelle méthode dois-je choisir ?

R :

✅ Choisir Long Polling quand :
├─ Environnement sans support WebSocket/SSE
├─ Système legacy
├─ Notifications simples
└─ Ex : Support navigateurs anciens, polling simple

Exemples d'utilisation :
- Cours boursiers (mise à jour peu fréquente)
- Vérification d'e-mail
- Notifications simples

✅ Choisir SSE quand :
├─ Unidirectionnel Serveur → Client
├─ Mises à jour en temps réel
├─ Désire implémentation simple
├─ Besoin de reconnexion automatique
└─ Ex : Flux d'actualités, notifications, tableau de bord

Exemples d'utilisation :
- Flux d'actualités (mise à jour en temps réel)
- Graphiques boursiers (push depuis le serveur)
- Indicateur de progression
- Streaming de logs
- Surveillance de serveur

✅ Choisir WebSocket quand :
├─ Communication bidirectionnelle en temps réel
├─ Faible latence importante
├─ Échange fréquent de messages
├─ Données binaires
└─ Ex : Chat, jeux, outils collaboratifs

Exemples d'utilisation :
- Application de chat
- Jeux multijoueurs
- Édition collaborative de documents (Google Docs)
- Visioconférence
- Contrôle en temps réel de l'IoT

📊 Tableau comparatif :

Caractéristique | Long Polling | SSE | WebSocket
----------------|-------------|---------------|----------
Direction | Bidirectionnel | Unidirectionnel | Bidirectionnel
Protocole | HTTP | HTTP | WebSocket
Latence | Élevée | Faible | Très faible
Surcharge | Élevée | Moyenne | Faible
Navigateur | Tous | Plupart | Tous
Complexité | Faible | Faible | Élevée
Reconnexion | Manuelle | Automatique | Manuelle
Binaire | Possible | Non | Oui

Recommandation :
- Temps réel simple : SSE
- Chat/jeux : WebSocket
- Support legacy : Long Polling

Q2. Différence de performance ?

R :

// ========== Exemple de benchmark ==========

// 1. Long Polling
// Par requête :
// - En-tête HTTP : ~800 bytes
// - Temps de reconnexion : ~50ms
// - Ressources serveur : 1 thread par connexion

// 100 utilisateurs simultanés :
// - Requêtes par seconde : 100
// - Transfert de données : 80KB/s (en-têtes uniquement)
// - Charge serveur : Élevée

// 2. SSE
// Par connexion :
// - En-tête HTTP : Seulement 1x initial (~800 bytes)
// - Surcharge de message : ~10 bytes
// - Ressources serveur : Maintenir connexion (Keep-Alive)

// 100 utilisateurs simultanés :
// - Connexion initiale : 80KB
// - Par message : 1KB (sans en-têtes)
// - Charge serveur : Moyenne

// 3. WebSocket
// Par connexion :
// - En-tête Upgrade : Seulement 1x (~500 bytes)
// - Surcharge de frame : 2~6 bytes
// - Ressources serveur : Maintenir connexion

// 100 utilisateurs simultanés :
// - Connexion initiale : 50KB
// - Par message : ~0.002KB (frame uniquement)
// - Charge serveur : Faible

// ========== Mesure réelle ==========

// Test : 100 utilisateurs envoyant 1 message par seconde

// Long Polling
const longPollingBenchmark = {
requetesParSeconde: 100,
latenceMoyenne: '200ms',
bandPassante: '8MB/min',
cpuServeur: '70%',
memoire: '500MB'
};

// SSE
const sseBenchmark = {
requetesParSeconde: 0, // Connexion maintenue
latenceMoyenne: '10ms',
bandPassante: '600KB/min',
cpuServeur: '30%',
memoire: '200MB'
};

// WebSocket
const webSocketBenchmark = {
requetesParSeconde: 0, // Connexion maintenue
latenceMoyenne: '2ms',
bandPassante: '100KB/min',
cpuServeur: '15%',
memoire: '100MB'
};

// ========== Optimisation pratique ==========

// 1. Limiter le nombre de connexions
const MAX_CONNECTIONS = 1000;

io.on('connection', (socket) => {
if (io.engine.clientsCount > MAX_CONNECTIONS) {
socket.disconnect();
return;
}
});

// 2. Compression de messages
const io = socketIo(server, {
perMessageDeflate: {
threshold: 1024 // Compresser seulement si > 1KB
}
});

// 3. Traitement par lots
const messageQueue = [];

setInterval(() => {
if (messageQueue.length > 0) {
io.emit('batch', messageQueue);
messageQueue.length = 0;
}
}, 100); // Envoi par lots toutes les 100ms

// 4. Utilisation de salle (Room)
socket.join('room1');
io.to('room1').emit('message', data); // Seulement room1

// 5. Transmission binaire
// WebSocket est efficace avec binaire
const buffer = Buffer.from('Hello');
socket.send(buffer); // Plus rapide que texte

// 6. Optimisation Heartbeat
const HEARTBEAT_INTERVAL = 25000; // 25 secondes
const HEARTBEAT_TIMEOUT = 30000; // 30 secondes

setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, HEARTBEAT_INTERVAL);

Q3. Et sur les applications mobiles ?

R :

// ========== Considérations mobiles ==========

// 1. Consommation de batterie
// Long Polling : ❌ Élevée (reconnexion continue)
// SSE : ⚠️ Moyenne
// WebSocket : ✅ Faible (connexion maintenue)

// 2. Changement de réseau
// Changement WiFi <-> Données mobiles

// Reconnexion automatique
let ws;

function connect() {
ws = new WebSocket('wss://api.example.com');

ws.onclose = () => {
// Backoff exponentiel
const delay = Math.min(1000 * Math.pow(2, attempts), 30000);
setTimeout(connect, delay);
};
}

// Détecter l'état du réseau
window.addEventListener('online', () => {
console.log('En ligne - reconnexion');
connect();
});

window.addEventListener('offline', () => {
console.log('Hors ligne');
ws.close();
});

// 3. Traitement en arrière-plan
// Que se passe-t-il quand l'app passe en arrière-plan ?

// Exemple React Native
import { AppState } from 'react-native';

AppState.addEventListener('change', (nextAppState) => {
if (nextAppState === 'background') {
// Arrière-plan → Maintenir connexion ou fermer
ws.close();
} else if (nextAppState === 'active') {
// Premier plan → Reconnecter
connect();
}
});

// 4. Mode économie de données
// Selon les paramètres utilisateur

const isDataSaverMode = await getDataSaverSetting();

if (isDataSaverMode) {
// Long Polling (moins fréquent)
setInterval(poll, 60000); // Toutes les 1 minute
} else {
// WebSocket (temps réel)
connect();
}

// 5. Intégration des notifications push
// Combinaison WebSocket + FCM/APNs

// App en cours d'exécution : WebSocket
if (appIsActive) {
useWebSocket();
}

// Arrière-plan : Notification push
if (appIsBackground) {
usePushNotification();
}

// ========== Exemple React Native ==========
import React, { useEffect, useState } from 'react';
import { View, Text } from 'react-native';

function ChatScreen() {
const [ws, setWs] = useState(null);
const [messages, setMessages] = useState([]);
const [isConnected, setIsConnected] = useState(false);

useEffect(() => {
const websocket = new WebSocket('wss://api.example.com');

websocket.onopen = () => {
console.log('Connected');
setIsConnected(true);
};

websocket.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};

websocket.onerror = (error) => {
console.error('WebSocket error:', error);
};

websocket.onclose = () => {
console.log('Disconnected');
setIsConnected(false);

// Reconnecter après 3 secondes
setTimeout(() => {
// Logique de reconnexion
}, 3000);
};

setWs(websocket);

// Nettoyage
return () => {
websocket.close();
};
}, []);

const sendMessage = (text) => {
if (ws && isConnected) {
ws.send(JSON.stringify({ text }));
}
};

return (
<View>
<Text>Connected: {isConnected ? 'Yes' : 'No'}</Text>
{messages.map((msg, i) => (
<Text key={i}>{msg.text}</Text>
))}
</View>
);
}

Q4. Comment sécuriser ?

R :

// ========== 1. Utiliser HTTPS/WSS ==========

// ❌ HTTP/WS (transmission en texte clair)
const ws = new WebSocket('ws://api.example.com');

// ✅ HTTPS/WSS (chiffré)
const ws = new WebSocket('wss://api.example.com');

// ========== 2. Authentification ==========

// Méthode 1 : JWT dans l'URL (simple mais non recommandé)
const token = getJWTToken();
const ws = new WebSocket(`wss://api.example.com?token=${token}`);
// ⚠️ Risque d'exposition du token dans l'URL

// Méthode 2 : Authentification après connexion (recommandé)
const ws = new WebSocket('wss://api.example.com');

ws.onopen = () => {
// Envoyer un message d'authentification
ws.send(JSON.stringify({
type: 'auth',
token: getJWTToken()
}));
};

// Serveur
io.use((socket, next) => {
const token = socket.handshake.auth.token;

try {
const decoded = jwt.verify(token, SECRET_KEY);
socket.userId = decoded.userId;
next();
} catch (error) {
next(new Error('Authentification échouée'));
}
});

// Méthode 3 : Cookie (HttpOnly)
// Client
const ws = new WebSocket('wss://api.example.com');
// Le cookie est envoyé automatiquement

// Serveur
io.use((socket, next) => {
const cookies = parseCookies(socket.request.headers.cookie);
const sessionId = cookies.sessionId;

if (isValidSession(sessionId)) {
next();
} else {
next(new Error('Authentification échouée'));
}
});

// ========== 3. Configuration CORS ==========

const io = socketIo(server, {
cors: {
origin: 'https://myapp.com', // Uniquement domaine spécifique
credentials: true
}
});

// Plusieurs domaines
const allowedOrigins = ['https://myapp.com', 'https://admin.myapp.com'];

const io = socketIo(server, {
cors: {
origin: (origin, callback) => {
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS refusé'));
}
},
credentials: true
}
});

// ========== 4. Limitation de débit ==========

const rateLimitMap = new Map();

io.on('connection', (socket) => {
socket.on('message', (data) => {
const userId = socket.userId;
const now = Date.now();

// Suivre le nombre de messages par utilisateur
if (!rateLimitMap.has(userId)) {
rateLimitMap.set(userId, []);
}

const userMessages = rateLimitMap.get(userId);

// Filtrer les messages de la dernière minute
const recentMessages = userMessages.filter(
timestamp => now - timestamp < 60000
);

// Limite de 10 par minute
if (recentMessages.length >= 10) {
socket.emit('error', 'Limite d\'envoi de messages dépassée');
return;
}

recentMessages.push(now);
rateLimitMap.set(userId, recentMessages);

// Traiter le message
handleMessage(data);
});
});

// ========== 5. Validation des entrées ==========

socket.on('message', (data) => {
// Validation de type
if (typeof data !== 'object') {
return socket.emit('error', 'Format de données incorrect');
}

// Validation des champs obligatoires
if (!data.type || !data.content) {
return socket.emit('error', 'Champs obligatoires manquants');
}

// Validation de longueur
if (data.content.length > 1000) {
return socket.emit('error', 'Message trop long');
}

// Prévention XSS
const sanitizedContent = sanitizeHtml(data.content);

// Traiter
broadcastMessage(sanitizedContent);
});

// ========== 6. Namespace et Room ==========

// Isoler avec namespace
const chatNamespace = io.of('/chat');
const adminNamespace = io.of('/admin');

chatNamespace.on('connection', (socket) => {
// Chat général
});

adminNamespace.use(authenticateAdmin);
adminNamespace.on('connection', (socket) => {
// Administrateurs uniquement
});

// Contrôle des permissions avec Room
socket.on('join-room', (roomId) => {
if (canAccessRoom(socket.userId, roomId)) {
socket.join(roomId);
} else {
socket.emit('error', 'Pas de permission d\'accès à la salle');
}
});

// ========== 7. Prévention DDoS ==========

// Limiter le nombre de connexions
const MAX_CONNECTIONS_PER_IP = 5;
const connectionsByIP = new Map();

io.on('connection', (socket) => {
const ip = socket.handshake.address;

const count = connectionsByIP.get(ip) || 0;

if (count >= MAX_CONNECTIONS_PER_IP) {
socket.disconnect();
return;
}

connectionsByIP.set(ip, count + 1);

socket.on('disconnect', () => {
const newCount = connectionsByIP.get(ip) - 1;
if (newCount <= 0) {
connectionsByIP.delete(ip);
} else {
connectionsByIP.set(ip, newCount);
}
});
});

Q5. Gestion des erreurs ?

R :

// ========== Gestion des erreurs du client ==========

class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 10;
this.reconnectDelay = 1000;
}

connect() {
try {
this.ws = new WebSocket(this.url);

this.ws.onopen = () => {
console.log('Connexion réussie');
this.reconnectAttempts = 0;
this.onConnected();
};

this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.onMessage(data);
} catch (error) {
console.error('Erreur d\'analyse du message:', error);
}
};

this.ws.onerror = (error) => {
console.error('Erreur WebSocket:', error);
this.onError(error);
};

this.ws.onclose = (event) => {
console.log('Déconnecté:', event.code, event.reason);

// Fermeture normale (1000)
if (event.code === 1000) {
console.log('Fermeture normale');
return;
}

// Tentative de reconnexion
this.reconnect();
};
} catch (error) {
console.error('Échec de connexion:', error);
this.reconnect();
}
}

reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Abandon de reconnexion');
this.onReconnectFailed();
return;
}

this.reconnectAttempts++;
const delay = Math.min(
this.reconnectDelay * Math.pow(2, this.reconnectAttempts),
30000
);

console.log(`Tentative de reconnexion dans ${delay}ms (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);

setTimeout(() => {
this.connect();
}, delay);
}

send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try {
this.ws.send(JSON.stringify(data));
} catch (error) {
console.error('Erreur d\'envoi:', error);
}
} else {
console.error('WebSocket n\'est pas ouvert');
// Enregistrer dans la file de messages
this.queueMessage(data);
}
}

close() {
if (this.ws) {
this.ws.close(1000, 'Fermeture du client');
}
}

// Gestionnaires d'événements (à remplacer)
onConnected() {}
onMessage(data) {}
onError(error) {}
onReconnectFailed() {}

queueMessage(data) {
// Implémenter la file de messages hors ligne
}
}

// Utilisation
const client = new WebSocketClient('wss://api.example.com');

client.onConnected = () => {
console.log('Connecté !');
};

client.onMessage = (data) => {
console.log('Message:', data);
};

client.onError = (error) => {
console.error('Erreur survenue:', error);
// Afficher l'erreur dans l'UI
showErrorNotification('Erreur de connexion');
};

client.onReconnectFailed = () => {
// Gérer l'échec de reconnexion
showErrorModal('Impossible de se connecter au serveur');
};

client.connect();

// ========== Gestion des erreurs du serveur ==========

io.on('connection', (socket) => {
// Gestionnaire d'erreurs
socket.on('error', (error) => {
console.error('Erreur de socket:', error);
});

// Erreur de traitement de message
socket.on('message', async (data) => {
try {
// Validation d'entrée
if (!isValidMessage(data)) {
throw new Error('Message invalide');
}

// Traiter
await processMessage(data);
} catch (error) {
console.error('Erreur de traitement de message:', error);

// Envoyer l'erreur au client
socket.emit('error', {
code: 'MESSAGE_PROCESSING_ERROR',
message: error.message
});
}
});

// Gérer la déconnexion
socket.on('disconnect', (reason) => {
console.log('Déconnecté:', reason);

// Nettoyer l'utilisateur
cleanupUser(socket.userId);

// Notifier les autres utilisateurs
socket.broadcast.emit('user-left', socket.userId);
});
});

// Gestionnaire d'erreurs global
io.engine.on('connection_error', (error) => {
console.error('Erreur de connexion:', error);
});

// ========== Gestion du timeout ==========

socket.on('message', (data, callback) => {
// Définir le timeout (5 secondes)
const timeout = setTimeout(() => {
callback({
error: 'TIMEOUT',
message: 'Temps de réponse dépassé'
});
}, 5000);

// Traiter
processMessage(data)
.then(result => {
clearTimeout(timeout);
callback({ success: true, data: result });
})
.catch(error => {
clearTimeout(timeout);
callback({ error: error.message });
});
});

🎓 Prochaines étapes

Une fois que vous comprenez la communication en temps réel, vous pouvez apprendre ce qui suit :

  1. HTTP de base (document en préparation) - Comprendre le protocole HTTP
  2. Qu'est-ce qu'une API ? (document en préparation) - Concept de base de l'API
  3. REST API vs GraphQL - Conception d'API

Pratique

# ========== 1. Application de chat avec Socket.io ==========

mkdir chat-app
cd chat-app
npm init -y
npm install express socket.io

# Après avoir écrit server.js
node server.js

# ========== 2. Notifications en temps réel avec SSE ==========

mkdir sse-demo
cd sse-demo
npm install express

# Écrire server.js
# Écrire index.html

node server.js
# Accéder à http://localhost:3000

# ========== 3. Jeu avec WebSocket ==========

mkdir websocket-game
cd websocket-game
npm install ws express

# Implémenter un jeu multijoueur
node server.js

🎬 Conclusion

La communication en temps réel est une technologie centrale dans le web moderne :

  • Long Polling : Support legacy, basé sur HTTP
  • SSE : Push serveur, implémentation simple
  • WebSocket : Bidirectionnel en temps réel, meilleures performances
  • Critères de sélection : Exigences, environnement, complexité

Choisissez la méthode de communication en temps réel appropriée pour votre projet et créez une excellente expérience utilisateur ! 🔴