Skip to main content

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éthodeDescription
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.roomsSet 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é :

  1. Nouveaux événements :

    • join-room : Pour rejoindre une room
    • leave-room : Pour quitter une room
    • room-message : Pour envoyer un message dans une room
    • room-joined, room-user-joined, room-user-left : Notifications côté client
  2. Structure de données rooms : Un Map qui associe chaque room à un Set de socket IDs pour un tracking efficace

  3. Nouveaux handlers :

    • handleJoinRoom() : Gère l'entrée dans une room
    • handleLeaveRoom() : Gère la sortie d'une room
    • handleRoomMessage() : Gère l'envoi de messages dans une room
    • handleDisconnect() : Nettoie les rooms quand un utilisateur se déconnecte
  4. 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 :

  1. Boutons de rooms : permettent de rejoindre les rooms "general" ou "nodejs"
  2. Affichage dynamique : affiche la room courante et la liste des utilisateurs
  3. 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-message au lieu de message global