Skip to main content

10. Socket.IO

Découvrez les WebSockets avec Socket.IO pour créer des applications temps réel

Introduction

Socket.IO est une bibliothèque qui permet de mettre en place une communication bidirectionnelle en temps réel entre un client et un serveur. Contrairement aux requêtes HTTP classiques où le client doit toujours initier la communication, les WebSockets permettent au serveur d'envoyer des données au client à tout moment.

Socket.IO est particulièrement utile pour :

  • Les applications de chat en temps réel
  • Les notifications en direct
  • Les jeux multijoueurs
  • Les tableaux de bord avec des données qui se mettent à jour automatiquement
  • La collaboration en temps réel (comme Google Docs)

Remarque: Socket.IO utilise WebSocket comme protocole principal, mais peut automatiquement basculer vers d'autres méthodes (long polling) si les WebSockets ne sont pas disponibles.

Dans ce chapitre, nous allons organiser notre code Socket.IO de manière modulaire, en suivant l'architecture présentée au chapitre 13 sur la documentation. Nous verrons aussi deux approches : programmation fonctionnelle et orientée objet.

Installation

Pour utiliser Socket.IO, nous devons installer deux packages : un pour le serveur et un pour le client.

npm install socket.io
npm install --save-dev @types/socket.io

Si vous souhaitez utiliser le client dans un projet Node.js (par exemple pour des tests), vous pouvez aussi installer :

npm install socket.io-client

Serveur de base

Commençons par créer un serveur Socket.IO simple. Socket.IO fonctionne en s'attachant à un serveur HTTP, ce qui signifie qu'il peut coexister avec Express.

import 'dotenv/config'
import express from 'express'
import {Server} from 'socket.io'
import {userRouter} from "@/routes/user.route";
import {authRouter} from "@/routes/auth.route";
import swaggerUi from 'swagger-ui-express'
import {swaggerDocument} from './docs'
import * as http from "node:http";

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

const port = 3000

// Middleware pour parser le JSON
app.use(express.json())

// Documentation Swagger UI
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, {
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: "API Documentation"
}))


// Route Express classique
app.get('/', (_req, res) => {
res.send('Serveur Socket.IO actif')
})

// Autres middlewares et routes Express
app.use('/users', userRouter)
app.use('/auth', authRouter)


// Écoute des connexions Socket.IO
io.on('connection', (socket) => {
console.log("Un client s'est connecté:", socket.id)
socket.on('disconnect', () => {
console.log("Un client s'est déconnecté:", socket.id)
})
})

server.listen(port, () => {
console.log(`Serveur démarré sur http://localhost:${port}`)
})

Note : Nous utilisons http.createServer pour créer un serveur HTTP séparé, puis nous passons ce serveur à Socket.IO. Cela permet de mieux organiser le code et de faciliter l'intégration avec Express.

L'option cors est configurée pour permettre les connexions depuis n'importe quelle origine. En production, vous devriez restreindre cela à vos domaines spécifiques.

Événements

Le principe de Socket.IO repose sur l'émission et la réception d'événements. Un événement a un nom et peut transporter des données.

Les méthodes principales pour émettre et envoyer des événements :

  • socket.on() : Écoute un événement émis par le client
  • socket.emit() : Envoie à ce client uniquement
  • io.emit() : Envoie à tous les clients connectés
  • socket.broadcast.emit() : Envoie à tous les clients sauf celui qui émet
io.on('connection', (socket) => {
console.log('Nouvelle connexion:', socket.id)
// Envoyer un événement uniquement à ce client
socket.emit('welcome', 'Bienvenue sur le serveur!')

socket.on('user', (username) => {
console.log('Utilisateur connecté:', username)
socket.broadcast.emit('user-joined', "Un nouvel utilisateur s'est connecté")
})

// Écoute d'un événement avec plusieurs paramètres
socket.on('message', (username, message) => {
console.log(`${username}: ${message}`)
// Renvoyer le message à tous les clients (y compris l'émetteur)
io.emit('message', {username, message})
})
})

Client HTML

Créez public/chat.html :

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Chat Socket.IO</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;
}

#messages {
border: 1px solid #ddd;
height: 400px;
overflow-y: auto;
padding: 10px;
margin-bottom: 10px;
background: #f9f9f9;
}

#messages div {
margin: 5px 0;
padding: 5px;
}

.system {
color: #666;
font-style: italic;
}

#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 Socket.IO</h1>
<div id="messages"></div>
<form id="form">
<input autocomplete="off" id="input" placeholder="Votre message..."/>
<button>Envoyer</button>
</form>

<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<script>

const socket = io('http://localhost:3000')
const messages = document.getElementById('messages')
const form = document.getElementById('form')
const input = document.getElementById('input')

// Récupérer le nom d'utilisateur
const username = prompt('Entrez votre pseudo:') || 'Anonyme'
socket.emit('user', username)

// Fonction pour ajouter un message
function addMessage(text, isSystem = false) {
const div = document.createElement('div')
div.className = isSystem ? 'system' : ''
div.textContent = text
messages.appendChild(div)
messages.scrollTop = messages.scrollHeight
}

// Écouter les événements du serveur
socket.on('welcome', (msg) => addMessage(msg, true))
socket.on('user-joined', (msg) => addMessage(msg, true))
socket.on('message', (data) => addMessage(`${data.username}: ${data.message}`))

// Envoyer un message
form.addEventListener('submit', (e) => {
e.preventDefault()
if (input.value) {
socket.emit('message', username, input.value)
input.value = ''
}
})
</script>
</body>
</html>

Note : Nous utilisons le CDN de Socket.IO pour le client. En production, vous installeriez le package et le bundleriez avec votre application.

Classe ChatServer

Jusqu'à présent, nous avons utilisé une approche fonctionnelle. TypeScript supporte aussi la programmation orientée objet avec des classes. Voici comment implémenter notre chat avec cette approche.

Créez src/socket/ChatServer.ts :

import {Server as HTTPServer} from 'http'
import {Server, Socket} from 'socket.io'

// Types simplifiés pour les événements
interface ClientToServerEvents {
user: (username: string) => void
message: (username: string, message: string) => void
}

interface ServerToClientEvents {
welcome: (message: string) => void
'user-joined': (message: string) => void
message: (data: { username: string, message: string }) => void
}

type TypedSocket = Socket<ClientToServerEvents, ServerToClientEvents>
type TypedServer = Server<ClientToServerEvents, ServerToClientEvents>

export class ChatServer {
private io: TypedServer

constructor(httpServer: HTTPServer) {
this.io = new Server<ClientToServerEvents, ServerToClientEvents>(httpServer, {
cors: {origin: '*'},
})
this.initializeSocket()
}

private initializeSocket() {
this.io.on('connection', (socket) => {
console.log('Nouvelle connexion:', socket.id)

// Envoyer un événement uniquement à ce client
socket.emit('welcome', 'Bienvenue sur le serveur!')

socket.on('user', (username) => this.handleUser(socket, username))
socket.on('message', (username, message) => this.handleMessage(socket, username, message))
})
}

private handleUser(socket: TypedSocket, username: string) {
console.log('Utilisateur connecté:', username)
socket.broadcast.emit('user-joined', "Un nouvel utilisateur s'est connecté")
}

private handleMessage(_socket: TypedSocket, username: string, message: string) {
console.log(`${username}: ${message}`)
// Renvoyer le message à tous les clients (y compris l'émetteur)
this.io.emit('message', {username, message})
}
}

Utilisation de la classe

Modifiez src/index.ts pour utiliser la classe :

import 'dotenv/config'
import express from 'express'
import {userRouter} from "@/routes/user.route";
import {authRouter} from "@/routes/auth.route";
import swaggerUi from 'swagger-ui-express'
import {swaggerDocument} from './docs'
import * as http from "node:http";
import {ChatServer} from "@/socket/Chat";

const app = express()
const server = http.createServer(app);

// Initialiser le serveur de chat avec la classe
new ChatServer(server)

const port = 3000

// Middleware pour parser le JSON
app.use(express.json())

// Documentation Swagger UI
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, {
customCss: '.swagger-ui .topbar { display: none }',
customSiteTitle: "API Documentation"
}))


// Route Express classique
app.get('/', (_req, res) => {
res.send('Serveur Socket.IO actif')
})

// Autres middlewares et routes Express
app.use('/users', userRouter)
app.use('/auth', authRouter)

server.listen(port, () => {
console.log(`Serveur démarré sur http://localhost:${port}`)
})

Pourquoi utiliser des classes ?

Les classes permettent de :

  • Encapsuler les données et les méthodes liées
  • Organiser le code de manière plus structurée
  • Faciliter la réutilisation et la maintenance
  • Utiliser l'héritage et le polymorphisme