Aller au contenu principal
GiwiSoft
Ws01

WebSockets avec Socket.IO : chat, notifications, temps réel

Giwi 9 min de lecture Web, JavaScript

Le HTTP classique suit un modèle requête-réponse : le client demande, le serveur répond. Mais pour du temps réel - notifications, chat, mise à jour de tableaux de bord - ce modèle montre ses limites. Le client devrait interroger le serveur en permanence pour savoir s’il y a du nouveau.

C’est le problème du polling : le client demande “as-tu du neuf?” à intervalles réguliers, sans savoir si la réponse a changé. C’est inefficace (des requêtes pour rien), coûteux et induit une latence inévitable (jusqu’à l’intervalle suivant). Le temps réel demande une autre approche.

Les WebSocket établissent une connexion bidirectionnelle persistante. Et Socket.IO simplifie tout ça.

WebSocket vs HTTP

HTTP

sequenceDiagram participant Client participant Serveur Client->>Serveur: requête (HTTP) Serveur-->>Client: réponse (HTTP) Note over Client,Serveur: connexion fermée

WebSocket

sequenceDiagram participant Client participant Serveur Client->>Serveur: connexion WebSocket Note over Client,Serveur: connexion persistante, bidirectionnelle Serveur->>Client: message Client->>Serveur: message Client->>Serveur: message Serveur->>Client: message Note over Client,Serveur: Les deux peuvent envoyer<br>des messages à tout moment

La différence essentielle est visible dans les diagrammes : en HTTP, chaque échange ouvre et referme une connexion, et seul le client peut initier. En WebSocket, une seule connexion reste ouverte, et les deux parties peuvent envoyer des messages quand elles le souhaitent. Le serveur peut donc pousser de l’information dès qu’elle est disponible, sans attendre une requête.

Pourquoi Socket.IO plutôt que les WebSocket natives ?

Socket.IO ajoute plusieurs fonctionnalités cruciales :

  • Reconnexion automatique : si la connexion tombe, elle se rétablit
  • Fallback : si WebSocket n’est pas supporté, il utilise du long-polling HTTP
  • Rooms : groupes de sockets pour broadcaster
  • Namespaces : séparation logique des canaux
  • Événements : on travaille avec des événements nommés, pas des messages bruts

La WebSocket native est un protocole de bas niveau : elle n’offre que l’envoi et la réception de messages bruts, sans reconnexion, sans notion de groupe ni d’événement nommé. Socket.IO se superpose à ce protocole (ou à un fallback HTTP) pour apporter la couche applicative : vous travaillez avec des événements nommés (message:new, user:online), des groupes logiques (les rooms), et vous bénéficiez d’une reconnexion transparente quand le réseau se coupe. Pour la quasi-totalité des applications, c’est un gain de productivité énorme.

Mise en place du serveur

npm install socket.io
import { Server as HttpServer } from "http";
import { Server } from "socket.io";
import express from "express";

const app = express();
const httpServer = new HttpServer(app);
const io = new Server(httpServer, {
cors: {
origin: ["http://localhost:3000"],
methods: ["GET", "POST"],
},
pingTimeout: 60000,
pingInterval: 25000,
});

httpServer.listen(4000, () => {
console.log("Socket.IO server ready on port 4000");
});

Notez que Socket.IO s’appuie sur un serveur HTTP existant (ici Express). On ne remplace pas HTTP : on le complète, en servant à la fois l’API REST et les connexions temps réel sur le même port. C’est pratique pour un déploiement, un seul endpoint à exposer. La configuration cors est importante : elle précise quelles origines web sont autorisées à se connecter.

pingTimeout/pingInterval : ces paramètres permettent de détecter les connexions mortes. Le serveur envoie un ping toutes les 25s, et si pas de réponse en 60s, il ferme la connexion.

Ce mécanisme de heartbeat est ce qui permet au serveur de se débarrasser des connexions fantômes (client qui ferme son laptop, réseau qui tombe sans message de fin). Sans lui, les connexions mortes s’accumuleraient et finiraient par épuiser les ressources.

Gestion des connexions

io.on("connection", (socket) => {
console.log(`Client connecté : ${socket.id}`);

socket.on("disconnect", (reason) => {
console.log(`Client déconnecté ${socket.id} : ${reason}`);
});

socket.on("error", (err) => {
console.error(`Erreur sur ${socket.id} :`, err.message);
});
});

Le callback principal de Socket.IO, io.on("connection"), reçoit un objet socket qui représente la connexion d’un client. C’est sur cet objet qu’on enregistre les gestionnaires d’événements entrants, et c’est lui qu’on utilise pour envoyer des messages à ce client précis. Le disconnect renseigne la raison (déconnexion volontaire, timeout, erreur réseau), ce qui est précieux pour le diagnostic.

Chat en temps réel

Le cas d’usage le plus classique. Côté serveur :

io.on("connection", (socket) => {
// Rejoindre un salon de discussion
socket.on("join:room", (room: string) => {
socket.join(room);
socket.to(room).emit("notification", {
type: "join",
username: socket.data.username,
});
});

// Quitter un salon
socket.on("leave:room", (room: string) => {
socket.leave(room);
});

// Message dans un salon
socket.on("message:send", ({ room, content }) => {
const message = {
id: crypto.randomUUID(),
username: socket.data.username,
content,
timestamp: new Date().toISOString(),
};

// Broadcast à tout le salon (sauf l'émetteur)
socket.to(room).emit("message:new", message);
// Confirmation à l'émetteur
socket.emit("message:sent", message);
});
});

Le cœur du chat repose sur les rooms. socket.join(room) fait entrer le socket dans le salon ; socket.to(room).emit(...) envoie un message à tous les sockets du salon sauf l’émetteur. On distingue ainsi nettement trois cas : socket.emit (à moi seul), socket.to(room).emit (aux autres du salon), io.to(room).emit (à tous, moi compris). C’est cette granularité qui rend le chat simple à écrire.

Côté client (navigateur) :

import { io, Socket } from "socket.io-client";

const socket: Socket = io("http://localhost:4000", {
auth: { token: "jwt-token" },
});

socket.on("connect", () => {
console.log("Connecté au serveur");
});

// Écouter les nouveaux messages
socket.on("message:new", (message) => {
displayMessage(message);
});

// Envoyer un message
document.getElementById("send-btn")?.addEventListener("click", () => {
const input = document.getElementById("msg-input") as HTMLInputElement;
socket.emit("message:send", {
room: "general",
content: input.value,
});
input.value = "";
});

Le client suit le même modèle événementiel que le serveur : on écoute avec socket.on et on émet avec socket.emit. L’option auth permet de transmettre un token dès la poignée de main initiale, que le serveur pourra vérifier dans un middleware. Remarquez l’émotion du temps réel : le message part, et la confirmation revient quasi instantanément, sans rechargement de page.

Notifications en temps réel

Un cas que j’utilise pour alerter sur l’état de mon infrastructure :

// Service de monitoring qui notifie via Socket.IO
class NotificationService {
constructor(private io: Server) {}

async sendAlert(alert: Alert) {
// Envoyer aux admins connectés
this.io.to("admins").emit("alert:critical", {
id: alert.id,
severity: alert.severity,
message: alert.message,
timestamp: new Date(),
});

// Notification moins urgente pour tout le monde
if (alert.severity === "info") {
this.io.emit("notification", {
type: "info",
message: alert.message,
});
}
}

async sendMetricUpdate(metric: Metric) {
// Envoyer uniquement aux clients qui regardent ce dashboard
this.io.to(`dashboard:${metric.dashboardId}`).emit("metric:update", metric);
}
}

// Dans le monitoring
monitor.on("cpu:high", (data) => {
notifier.sendAlert({
severity: "warning",
message: `CPU à ${data.percent}% sur ${data.host}`,
});
});

L’intérêt ici est le routage ciblé : io.to("admins") n’atteint que les administrateurs, io.to(\dashboard:${id}`)` seulement ceux qui regardent ce dashboard précis. On évite ainsi de noyer tous les clients connectés dans des messages qui ne les concernent pas. C’est une bonne pratique : ne diffusez à chacun que ce qui le concerne, c’est moins de trafic et moins de bruit côté UI.

Tableau de bord temps réel

Pour un dashboard qui se met à jour automatiquement :

// Serveur : push périodique des métriques
setInterval(() => {
const stats = {
users: await getActiveUsers(),
requests: await getRequestCount(),
cpu: await getCpuUsage(),
memory: await getMemoryUsage(),
};

io.to("dashboard:main").emit("stats:update", stats);
}, 5000);
// Client : abonnement et mise à jour de l'UI
const socket = io("http://localhost:4000");

socket.emit("subscribe:dashboard", "main");

socket.on("stats:update", (stats) => {
document.getElementById("cpu-bar")!.style.width = `${stats.cpu}%`;
document.getElementById("mem-value")!.textContent = `${stats.memory}%`;
});

Le serveur pousse les métriques toutes les 5 secondes, sans que le client ait rien à demander. Le client, lui, ne fait que mettre à jour les éléments du DOM quand un événement arrive. On a ainsi un dashboard qui se rafraîchit tout seul, avec une latence bornée par l’intervalle de push, et sans aucun polling du côté client.

Namespaces : séparer les préoccupations

Les namespaces permettent de cloisonner les communications :

// Namespace public : chat et notifications
const chatNamespace = io.of("/chat");
chatNamespace.on("connection", (socket) => {
// logique du chat uniquement
});

// Namespace privé : monitoring
const adminNamespace = io.of("/admin");
adminNamespace.use((socket, next) => {
// Middleware d'authentification
const token = socket.handshake.auth.token;
if (!verifyToken(token)) {
return next(new Error("Non autorisé"));
}
next();
});

adminNamespace.on("connection", (socket) => {
socket.join("admins");
// logique d'administration
});

Les namespaces (io.of("/chat"), io.of("/admin")) sont des canaux indépendants sur le même serveur, chacun avec sa propre logique de connexion et ses propres middlewares. C’est un bon découpage : on isole les responsabilités (le chat n’a pas besoin de l’authentification stricte du monitoring), et on peut appliquer des middlewares spécifiques par namespace.

Middlewares d’authentification

Sécuriser les connexions Socket.IO :

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

if (!token) {
return next(new Error("Token manquant"));
}

try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
socket.data.userId = payload.sub;
socket.data.username = payload.username;
socket.data.roles = payload.roles;
next();
} catch {
next(new Error("Token invalide"));
}
});

Le middleware s’exécute avant la connexion complète : si next() est appelé avec une erreur, le client est rejeté. En cas de succès, on stocke l’identité dans socket.data, où elle restera disponible pour tous les gestionnaires ultérieurs de cette connexion. C’est le moyen propre d’authentifier chaque socket et de conserver le contexte de l’utilisateur à portée de main.

Gestion des rooms et broadcast

// Envoyer à un utilisateur spécifique
io.to(socketId).emit("private:message", data);

// Envoyer à tous sauf l'émetteur
socket.broadcast.emit("user:online", username);

// Envoyer à tous
io.emit("system:maintenance", { at: "2026-07-01T02:00:00Z" });

// Compter les connexions dans une room
const count = io.sockets.adapter.rooms.get("general")?.size || 0;

Ce petit récapitulatif vaut la peine d’être mémorisé, car il résume toutes les granularités d’envoi disponibles : un client précis, tous sauf l’émetteur, tout le monde, ou le contenu d’une room. Avoir ces distinctions en tête évite des bugs de diffusion (un message privé envoyé à trop de monde, par exemple).

Monitoring et statistiques

Socket.IO expose des métriques utiles :

setInterval(() => {
const stats = {
connected: io.engine.clientsCount,
rooms: io.sockets.adapter.rooms.size,
namespaces: io._nsps.size,
};
console.table(stats);
}, 30000);

io.engine.clientsCount donne le nombre de clients connectés, io.sockets.adapter.rooms l’ensemble des rooms actives. C’est suffisant pour surveiller la santé d’un serveur temps réel : un pic soudain de connexions, une room qui ne se vide jamais, voilà les signaux qui méritent votre attention.

Conclusion

Socket.IO est la solution la plus mature pour ajouter du temps réel à vos applications Node.js. Avec sa gestion automatique de la reconnexion, ses rooms, ses namespaces, et son fallback HTTP, il résout tous les problèmes des WebSocket natives.

Ce qui fait la différence par rapport aux WebSocket brutes, c’est le niveau d’abstraction : vous pensez en événements nommés et en groupes logiques, pas en flux de bytes. La reconnexion et le fallback HTTP gèrent les aléas du réseau sans que vous ayez à écrire une ligne pour ça.

J’utilise Socket.IO pour le chat de mon outil de collaboration, les notifications d’infrastructure, et les mises à jour de dashboards. C’est un outil fiable qui tourne depuis des mois sans intervention.

Et vous, quel cas d’usage temps réel comptez-vous implémenter ?