Exercice : mini-chat en temps réel avec les WebSockets
💬 Présentation
Un petit chat en temps réel entre plusieurs navigateurs, en local, avec PHP côté serveur et les WebSockets — une technologie différente de tout ce qui a été vu jusqu'ici dans le protocole HTTP classique.
Objectifs pédagogiques
- Comprendre la différence entre le modèle requête/réponse de HTTP (une page PHP classique) et une connexion permanente maintenue ouverte
- Découvrir le principe de diffusion (broadcast) : un message envoyé par un client est renvoyé à tous les autres
- Installer et utiliser une bibliothèque externe via Composer — première rencontre avec un gestionnaire de dépendances PHP
- Retrouver, côté JavaScript cette fois, le réflexe déjà vu côté PHP : ne jamais faire confiance à une entrée utilisateur
Exercice local uniquement
Ce mini-chat n'est pas destiné à être déployé sur le site public papinou.org : il nécessite un processus PHP qui tourne en permanence en arrière-plan (php server.php), ce qu'un hébergement web mutualisé classique ne permet pas. C'est un exercice à réaliser en local, en salle, sur les postes des élèves.
🗂️ Fichiers à créer
| Fichier | Rôle |
|---|---|
composer.json |
Déclare la dépendance vers la bibliothèque Ratchet |
server.php |
Le serveur WebSocket : accepte les connexions et diffuse les messages |
chat.html |
La page cliente : formulaire de pseudo, zone de discussion |
⚙️ Prérequis
Environnement nécessaire
- PHP 7.4+ avec l'extension
socketsactivée - Composer installé (
composer --versionpour vérifier) - Un navigateur récent (Chrome, Firefox…) — aucune extension nécessaire côté client, l'API WebSocket est native en JavaScript
🚀 Installation pas à pas
1. Installer la bibliothèque Ratchet
Dans le dossier contenant composer.json :
Cette commande télécharge Ratchet (et ses propres dépendances) dans un dossier vendor/ créé automatiquement.
2. Démarrer le serveur
Une fenêtre de terminal dédiée
Contrairement à index.php dans les exercices précédents, ce script ne se termine jamais tout seul : il tourne en boucle, en écoutant le port 8080. Gardez ce terminal ouvert pendant toute la durée du chat ; Ctrl+C pour l'arrêter.
3. Ouvrir le chat
chat.html doit être ouvert via Apache — pas en double-cliquant dessus — car son adresse WebSocket s'ajuste automatiquement à celle utilisée pour charger la page (voir plus bas, section HTTP classique vs WebSocket).
- Pour tester seul, sur une machine : ouvrir
http://localhost/minichat/chat.htmldans deux ou trois onglets différents, avec un pseudo différent à chaque fois. - Pour tester en classe, sur plusieurs postes du réseau local : depuis chaque poste, ouvrir
http://<IP-du-serveur>/minichat/chat.html(l'adresse IP de la machine qui fait tourner Apache etphp server.php, trouvable avechostname -Isous Linux) — pashttp://localhost/..., qui pointerait vers le mauvais serveur sur chaque poste.
Dans les deux cas, les messages tapés d'un côté doivent apparaître instantanément partout ailleurs.
🔌 HTTP classique vs WebSocket
Page PHP classique (index.php) |
Mini-chat (WebSocket) | |
|---|---|---|
| Connexion | Une nouvelle connexion à chaque clic/rechargement | Une seule connexion, ouverte et maintenue |
| Qui parle en premier | Toujours le navigateur (requête) | Le serveur peut envoyer sans qu'on lui demande |
| Le serveur tourne | Sous Apache, à la demande | En permanence, en ligne de commande |
| Analogie | Envoyer une lettre et attendre la réponse | Un appel téléphonique resté décroché |
Ce qui se passe techniquement à la connexion
Le navigateur commence par une requête HTTP presque normale, avec un en-tête spécial (Upgrade: websocket). Le serveur répond en acceptant de « faire évoluer » cette connexion HTTP en connexion WebSocket. Une fois cette poignée de main (handshake) terminée, les deux côtés peuvent s'envoyer des messages à tout moment, dans les deux sens, sans jamais rouvrir de connexion. Ratchet se charge de tout ce mécanisme : c'est ce que fait WsServer dans server.php.
🧩 Comment le message circule
Client A tape "Salut" et clique sur Envoyer
↓
socket.send(JSON.stringify({pseudo, message})) (chat.html)
↓
onMessage() reçoit le message côté serveur (server.php)
↓
Le serveur reconstruit un paquet propre (pseudo, message, heure)
↓
foreach ($this->clients as $client) { $client->send($paquet); }
↓
Tous les clients (A, B, C…) reçoivent l'événement "message"
↓
afficherMessage() l'ajoute à la page (chat.html)
🔐 Sécurité
- Le serveur ne fait jamais confiance aux données reçues :
onMessage()vérifie quepseudoetmessageexistent et ne sont pas vides avant de les traiter - Longueurs limitées côté serveur (
mb_substr) : ne pas se fier uniquement aumaxlengthdu champ HTML, qui ne protège que l'interface, pas ce qu'un client mal intentionné pourrait envoyer directement au serveur - Le serveur reconstruit lui-même le paquet diffusé plutôt que de retransmettre tel quel ce qu'il a reçu — un réflexe déjà vu avec les requêtes préparées PDO : ne jamais renvoyer une donnée brute non contrôlée
textContentplutôt queinnerHTMLcôté JavaScript pour afficher les messages : c'est l'équivalent exact duhtmlspecialchars()utilisé côté PHP dans les exercices précédents. Sans cela, un pseudo comme<script>...</script>s'exécuterait chez tous les autres participants
À montrer en classe
Remplacer temporairement contenu.textContent = donnees.message par contenu.innerHTML = donnees.message dans chat.html, puis envoyer un message contenant une balise <img src=x onerror="alert(1)">. Un bon moyen de rendre concret, après l'avoir vu côté PHP, ce que signifie une faille XSS — à annuler juste après la démonstration !
🎓 Concepts pédagogiques abordés
- Connexion persistante vs requête/réponse HTTP
- Diffusion (broadcast) à plusieurs clients simultanés
SplObjectStorage: une structure PHP adaptée pour stocker un ensemble d'objets (ici, les connexions) sans doublons- Composer : premier contact avec un gestionnaire de dépendances et l'autoload (
vendor/autoload.php) - JSON comme format d'échange entre client et serveur, déjà familier si vos élèves ont manipulé des API
- XSS côté JavaScript, symétrique de ce qui a été vu côté PHP avec
htmlspecialchars()
🐛 Dépannage
« Erreur de connexion » dans le statut du chat
Vérifier que php server.php tourne toujours dans son terminal — s'il a été fermé ou a planté, aucun onglet ne peut se connecter.
composer install échoue
Vérifier que Composer est bien installé (composer --version) et que le fichier composer.json est présent dans le dossier courant.
Le port 8080 est déjà utilisé
Un autre exercice (façon tableur, servi par php -S localhost:8080) utilise le même port. Arrêter l'autre serveur, ou changer le port dans server.php (le 8080 dans IoServer::factory(..., 8080)) et dans chat.html (ws:// + window.location.hostname + :8080).
Ça fonctionne entre plusieurs onglets, mais pas entre plusieurs machines du réseau local
chat.html utilise window.location.hostname pour deviner l'adresse du serveur : il faut donc que les élèves ouvrent la page via son adresse réseau (http://192.168.x.x/minichat/chat.html), pas via http://localhost/..., sans quoi chaque machine cherche son propre serveur local. Vérifier aussi que le pare-feu de la machine qui fait tourner php server.php autorise les connexions entrantes sur le port 8080 depuis le réseau local — sous Linux, par exemple : sudo ufw allow 8080/tcp.
🔧 Pistes d'extension
- Afficher la liste des pseudos actuellement connectés
- Ajouter une notification « X a rejoint le chat » / « X a quitté »
- Limiter le nombre de connexions simultanées
- Enregistrer l'historique des messages dans une table MariaDB (retour au PDO des exercices précédents !)
- (Plus avancé) Créer des salons (rooms) séparés
💻 Code source complet
Copiez chacun des blocs ci-dessous dans un fichier du même nom, dans le même dossier.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 | |