Comprendre le protocole WebSocket de zéro et construire un système de chat complet : connexion, utilisateurs en mémoire, messages privés et groupes — sans base de données.
Une connexion WebSocket est anonyme par défaut. Le serveur sait qu'un client est connecté, pas qui il est. Pour un chat, il faut associer chaque socket à un username et maintenir ce registre en mémoire tant que le serveur tourne.
Un Map JavaScript fait exactement ce dont on a besoin : clé = username, valeur = socket.
const { WebSocketServer, WebSocket } = require('ws')
const wss = new WebSocketServer({ port: 8080 })
// username → socket
const users = new Map()
wss.on('connection', (socket) => {
// username stocké directement sur le socket pour y accéder partout
socket.username = null
socket.on('message', (data) => {
let msg
try {
msg = JSON.parse(data.toString())
} catch {
return
}
handleMessage(socket, msg)
})
socket.on('close', () => {
if (socket.username) {
users.delete(socket.username)
broadcast({ type: 'system', content: `${socket.username} a quitté le chat` })
console.log(`${socket.username} déconnecté`)
}
})
})
function handleMessage(socket, msg) {
switch (msg.type) {
case 'register':
registerUser(socket, msg)
break
default:
socket.send(JSON.stringify({ type: 'error', content: 'Identifiez-vous d\'abord' }))
}
}function registerUser(socket, msg) {
const username = (msg.username || '').trim()
if (!username) {
socket.send(JSON.stringify({ type: 'error', content: 'Username vide' }))
return
}
if (users.has(username)) {
socket.send(JSON.stringify({ type: 'error', content: 'Username déjà pris' }))
return
}
// Associer le username à ce socket
socket.username = username
users.set(username, socket)
socket.send(JSON.stringify({
type: 'registered',
content: `Bienvenue ${username} !`,
users: [...users.keys()],
}))
broadcast(
{ type: 'system', content: `${username} a rejoint le chat` },
socket // exclure l'expéditeur
)
console.log(`${username} connecté. Utilisateurs en ligne : ${users.size}`)
}function broadcast(msg, exclude = null) {
const data = JSON.stringify(msg)
wss.clients.forEach((client) => {
if (client !== exclude && client.readyState === WebSocket.OPEN) {
client.send(data)
}
})
}exclude permet d'éviter d'envoyer un message à l'émetteur lui-même — utile pour les notifications système comme "X a rejoint".
// Ajouter dans handleMessage, case 'list':
case 'list':
socket.send(JSON.stringify({
type: 'list',
users: [...users.keys()],
}))
break// Client envoie :
ws.send(JSON.stringify({ type: 'list' }))
// Serveur répond avec :
// { type: 'list', users: ['alice', 'bob', 'carol'] }Avant de traiter tout autre type de message, vérifier que l'utilisateur est inscrit :
function handleMessage(socket, msg) {
if (msg.type === 'register') {
registerUser(socket, msg)
return
}
// Tout le reste nécessite d'être identifié
if (!socket.username) {
socket.send(JSON.stringify({ type: 'error', content: 'Non identifié' }))
return
}
switch (msg.type) {
case 'list':
socket.send(JSON.stringify({ type: 'list', users: [...users.keys()] }))
break
default:
socket.send(JSON.stringify({ type: 'error', content: 'Type de message inconnu' }))
}
}Ouvrir client.html dans deux onglets du navigateur. Dans la console de chaque onglet :
Onglet 1 :
ws.send(JSON.stringify({ type: 'register', username: 'alice' }))Onglet 2 :
ws.send(JSON.stringify({ type: 'register', username: 'bob' }))L'onglet 1 reçoit une notification système "bob a rejoint le chat". Le serveur affiche dans son terminal les connexions au fil des inscriptions.
Map users:
'alice' → WebSocket { username: 'alice', readyState: 1 }
'bob' → WebSocket { username: 'bob', readyState: 1 }Chaque entrée de la Map pointe vers le socket live du client. Pour envoyer un message à Alice, il suffit de faire users.get('alice').send(...).
Le registre est en place. Le chapitre suivant ajoute le chat global — diffusion de messages à tous les utilisateurs connectés.