12. Rooms
Apprenez à organiser vos clients Socket.IO en groupes avec les rooms
Les rooms (salles) sont une fonctionnalité puissante de Socket.IO qui permet de regrouper des clients pour envoyer des messages uniquement à un sous-ensemble de connexions. C'est très utile pour créer des salons de discussion, des parties de jeux multijoueurs, ou des canaux thématiques.
Contrairement à la communication globale où tous les clients reçoivent les messages, les rooms permettent de cibler précisément les destinataires.
Nous verrons comment :
- Créer et gérer des rooms (salons de discussion)
- Permettre aux utilisateurs de rejoindre et quitter des rooms
- Envoyer des messages ciblés à une room spécifique
- Afficher la liste des utilisateurs par room
Concept des rooms
Une room est simplement un canal arbitraire dans lequel les sockets peuvent rejoindre et quitter. Chaque socket peut être dans plusieurs rooms à la fois.
// Un client rejoint une room
socket.join('room-name')
// Un client quitte une room
socket.leave('room-name')
// Envoyer un message à tous les clients d'une room
io.to('room-name').emit('message', 'Hello!')
// Envoyer à une room sauf à l'émetteur
socket.to('room-name').emit('message', 'Hello!')
Remarque : Chaque socket rejoint automatiquement une room portant son propre ID (
socket.id). Cela permet d'envoyer des messages à un client spécifique.
Méthodes principales
| Méthode | Description |
|---|---|
socket.join(room) | Fait rejoindre une room au client |
socket.leave(room) | Fait quitter une room au client |
io.to(room).emit() | Envoie à tous les clients de la room |
socket.to(room).emit() | Envoie à tous les clients de la room sauf l'émetteur |
io.in(room).emit() | Alias de to() |
socket.rooms | Set contenant toutes les rooms du socket |
Différence entre to() et in()
Les méthodes to() et in() sont des alias - elles font exactement la même chose. Utilisez celle qui rend votre code
plus lisible :
// Ces deux lignes sont équivalentes
io.to('room-name').emit('message', 'Hello')
io.in('room-name').emit('message', 'Hello')
Adapation de ChatServer
Nous allons reprendre la classe ChatServer du cours précédent et y ajouter la gestion des rooms.
import {Server as HTTPServer} from 'http'
import {Server, Socket} from 'socket.io'
import jwt from 'jsonwebtoken'
// Types simplifiés pour les événements avec rooms
interface ClientToServerEvents {
user: (username: string) => void
message: (username: string, message: string) => void
'join-room': (room: string) => void
'leave-room': (room: string) => void
'room-message': (room: string, message: string) => void
}
interface ServerToClientEvents {
welcome: (message: string) => void
'user-joined': (message: string) => void
message: (data: { username: string, message: string }) => void
'room-joined': (data: { room: string, users: string[] }) => void
'room-user-joined': (username: string) => void
'room-user-left': (username: string) => void
'room-message': (data: { username: string, message: string }) => void
}
// Données stockées après authentification (correspond au JWT du cours 11)
interface UserData {
userId: number
email: string
}
type TypedSocket = Socket<ClientToServerEvents, ServerToClientEvents>
type TypedServer = Server<ClientToServerEvents, ServerToClientEvents>
export class ChatServer {
private io: TypedServer
private rooms: Map<string, Set<string>> // roomName -> Set of socketIds
constructor(httpServer: HTTPServer) {
this.io = new Server<ClientToServerEvents, ServerToClientEvents>(httpServer, {
cors: {origin: '*'},
})
// Initialiser les rooms par défaut
this.rooms = new Map([
['general', new Set()],
['nodejs', new Set()],
])
this.setupAuthMiddleware()
this.initializeSocket()
}
// Middleware d'authentification (même que le cours précédent)
private setupAuthMiddleware() {
this.io.use((socket, next) => {
const token = socket.handshake.auth.token
if (!token) {
return next(new Error('Token manquant'))
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as UserData
socket.data = decoded
next()
} catch (error) {
next(new Error('Token invalide ou expiré'))
}
})
}
private initializeSocket() {
this.io.on('connection', (socket) => {
const userData = socket.data as UserData
console.log('Nouvelle connexion:', socket.id, `(${userData.email})`)
socket.emit('welcome', `Bienvenue ${userData.email}!`)
socket.on('user', (username) => this.handleUser(socket, userData))
socket.on('message', (username, message) => this.handleMessage(socket, userData, message))
socket.on('join-room', (room) => this.handleJoinRoom(socket, userData, room))
socket.on('leave-room', (room) => this.handleLeaveRoom(socket, userData, room))
socket.on('room-message', (room, message) => this.handleRoomMessage(socket, userData, room, message))
socket.on('disconnect', () => this.handleDisconnect(socket, userData))
})
}
private handleUser(socket: TypedSocket, userData: UserData) {
console.log('Utilisateur connecté:', userData.email)
socket.broadcast.emit('user-joined', `${userData.email} s'est connecté`)
}
private handleMessage(socket: TypedSocket, userData: UserData, message: string) {
console.log(`${userData.email}: ${message}`)
this.io.emit('message', {username: userData.email, message})
}
private handleJoinRoom(socket: TypedSocket, userData: UserData, room: string) {
if (!this.rooms.has(room)) {
return socket.emit('error', "Cette room n'existe pas")
}
socket.join(room)
this.rooms.get(room)!.add(socket.id)
const users = this.getRoomUsers(room)
socket.emit('room-joined', {room, users})
socket.to(room).emit('room-user-joined', userData.email)
console.log(`${userData.email} a rejoint la room ${room}`)
}
private handleLeaveRoom(socket: TypedSocket, userData: UserData, room: string) {
const roomSet = this.rooms.get(room)
if (roomSet && roomSet.has(socket.id)) {
roomSet.delete(socket.id)
socket.leave(room)
socket.to(room).emit('room-user-left', userData.email)
console.log(`${userData.email} a quitté la room ${room}`)
}
}
private handleRoomMessage(socket: TypedSocket, userData: UserData, room: string, message: string) {
const roomSet = this.rooms.get(room)
if (roomSet && roomSet.has(socket.id)) {
this.io.to(room).emit('room-message', {username: userData.email, message})
}
}
private handleDisconnect(socket: TypedSocket, userData: UserData) {
this.rooms.forEach((roomSet, roomName) => {
if (roomSet.has(socket.id)) {
roomSet.delete(socket.id)
socket.to(roomName).emit('room-user-left', userData.email)
}
})
}
private getRoomUsers(room: string): string[] {
const roomSet = this.rooms.get(room)
if (!roomSet) return []
const users: string[] = []
roomSet.forEach(socketId => {
const socket = this.io.sockets.sockets.get(socketId)
if (socket) {
users.push((socket.data as UserData).email)
}
})
return users
}
}
Modifications apportées
Par rapport au cours précédent, nous avons ajouté :
-
Nouveaux événements :
join-room: Pour rejoindre une roomleave-room: Pour quitter une roomroom-message: Pour envoyer un message dans une roomroom-joined,room-user-joined,room-user-left: Notifications côté client
-
Structure de données
rooms: UnMapqui associe chaque room à unSetde socket IDs pour un tracking efficace -
Nouveaux handlers :
handleJoinRoom(): Gère l'entrée dans une roomhandleLeaveRoom(): Gère la sortie d'une roomhandleRoomMessage(): Gère l'envoi de messages dans une roomhandleDisconnect(): Nettoie les rooms quand un utilisateur se déconnecte
-
Méthode utilitaire :
getRoomUsers(): Récupère la liste des utilisateurs dans une room
Client HTML
Créez public/chat-rooms.html :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Chat avec Rooms</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
h1 {
margin-bottom: 20px;
}
.hidden {
display: none;
}
#loginForm input {
display: block;
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
}
#roomButtons {
margin: 10px 0;
}
#roomButtons button {
padding: 10px 15px;
margin: 5px;
background: #f0f0f0;
border: 1px solid #ddd;
cursor: pointer;
}
#roomButtons button.active {
background: #28a745;
color: white;
border-color: #28a745;
}
#messages {
border: 1px solid #ddd;
height: 400px;
overflow-y: auto;
padding: 10px;
margin: 10px 0;
background: #f9f9f9;
}
#messages div {
margin: 5px 0;
padding: 5px;
}
.system {
color: #666;
font-style: italic;
}
.error {
color: red;
}
#form {
display: flex;
gap: 10px;
}
input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
}
button {
padding: 10px 20px;
background: #007bff;
color: white;
border: none;
cursor: pointer;
}
button:hover {
background: #0056b3;
}
</style>
</head>
<body>
<h1>Chat avec Rooms</h1>
<div id="loginForm">
<input id="email" placeholder="Email" type="email"/>
<input id="password" placeholder="Password" type="password"/>
<button id="loginButton">Se connecter</button>
<p class="error" id="loginError"></p>
<p><small>Comptes : alice@example.com/password123 ou bob@example.com/password123</small></p>
</div>
<div class="hidden" id="chatContainer">
<p>Connecté : <strong id="currentUser"></strong> | Room : <strong id="currentRoom">-</strong></p>
<div id="roomButtons">
<button data-room="general">General</button>
<button data-room="nodejs">Node.js</button>
</div>
<p id="usersList" style="font-size: 0.9em; color: #666;"></p>
<div id="messages"></div>
<form id="form">
<input autocomplete="off" id="input" placeholder="Votre message..."/>
<button>Envoyer</button>
</form>
</div>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script>
let socket, userEmail, currentRoom = null
document.getElementById('loginButton').addEventListener('click', async () => {
const email = document.getElementById('email').value.trim()
const password = document.getElementById('password').value.trim()
const loginError = document.getElementById('loginError')
loginError.textContent = ''
try {
const response = await fetch('/auth/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email, password})
})
if (!response.ok) {
const error = await response.json()
loginError.textContent = error.error
return
}
const {token} = await response.json()
userEmail = email
document.getElementById('loginForm').classList.add('hidden')
document.getElementById('chatContainer').classList.remove('hidden')
document.getElementById('currentUser').textContent = userEmail
connectToChat(token)
} catch (error) {
loginError.textContent = 'Erreur réseau'
}
})
function connectToChat(token) {
socket = io('http://localhost:3000', {auth: {token}})
const messages = document.getElementById('messages')
const form = document.getElementById('form')
const input = document.getElementById('input')
socket.emit('user', userEmail)
function addMessage(text, isSystem = false) {
const div = document.createElement('div')
div.className = isSystem ? 'system' : ''
div.textContent = text
messages.appendChild(div)
messages.scrollTop = messages.scrollHeight
}
socket.on('welcome', (msg) => addMessage(msg, true))
socket.on('user-joined', (msg) => addMessage(msg, true))
socket.on('message', (data) => addMessage(`${data.username}: ${data.message}`))
socket.on('room-joined', (data) => {
currentRoom = data.room
document.getElementById('currentRoom').textContent = data.room
document.getElementById('usersList').textContent = `Utilisateurs: ${data.users.join(', ')}`
addMessage(`Vous avez rejoint #${data.room}`, true)
updateActiveButton(data.room)
})
socket.on('room-user-joined', (username) => {
addMessage(`${username} a rejoint`, true)
})
socket.on('room-user-left', (username) => {
addMessage(`${username} a quitté`, true)
})
socket.on('room-message', (data) => {
addMessage(`${data.username}: ${data.message}`)
})
document.querySelectorAll('#roomButtons button').forEach(btn => {
btn.addEventListener('click', () => {
const room = btn.dataset.room
if (currentRoom) socket.emit('leave-room', currentRoom)
socket.emit('join-room', room)
})
})
form.addEventListener('submit', (e) => {
e.preventDefault()
if (input.value && currentRoom) {
socket.emit('room-message', currentRoom, input.value)
input.value = ''
}
})
function updateActiveButton(room) {
document.querySelectorAll('#roomButtons button').forEach(btn => {
btn.classList.toggle('active', btn.dataset.room === room)
})
}
}
</script>
</body>
</html>
Points importants
Le client HTML ajoute les fonctionnalités de rooms :
- Boutons de rooms : permettent de rejoindre les rooms "general" ou "nodejs"
- Affichage dynamique : affiche la room courante et la liste des utilisateurs
- Gestion des rooms :
- Quitte automatiquement la room actuelle avant d'en rejoindre une nouvelle
- Écoute les événements
room-joined,room-user-joined,room-user-left - Envoie les messages avec
room-messageau lieu demessageglobal