Les WebSockets sont une technologie de communication bidirectionnelle en temps réel qui permet d'établir une connexion continue entre un client (navigateur web, application mobile) et un serveur. Contrairement au modèle classique de communication HTTP, où le client doit envoyer une requête pour obtenir une réponse du serveur, les WebSockets permettent au serveur d'envoyer des données au client à tout moment, sans attendre une requête.
Principe des WebSockets
Le fonctionnement des WebSockets repose sur les étapes suivantes :
- Ouverture de la connexion : Lorsqu'un client souhaite communiquer avec le serveur, il initie une connexion WebSocket. Cette connexion commence par une requête HTTP spéciale, appelée « handshake », qui passe ensuite à un protocole de communication WebSocket une fois établie.
- Communication bidirectionnelle : Une fois la connexion établie, les deux parties peuvent envoyer et recevoir des messages en temps réel. Contrairement aux requêtes HTTP classiques, il n'est pas nécessaire de rétablir la connexion pour chaque message, ce qui rend les WebSockets beaucoup plus performants pour les communications fréquentes.
- Fermeture de la connexion : Lorsque le client ou le serveur décide de terminer la communication, la connexion WebSocket est fermée proprement, libérant les ressources associées.
Fonctionnalités possibles avec les WebSockets
Les WebSockets offrent un large éventail de possibilités pour des applications interactives et en temps réel.
- Messagerie instantanée et chat en temps réel : Les WebSockets sont parfaits pour les applications de chat où les messages doivent être transmis et affichés instantanément aux utilisateurs.
- Notifications en temps réel : Les notifications push peuvent être envoyées par le serveur aux utilisateurs pour les informer d'événements importants (mises à jour, alertes, nouveaux messages, etc.).
- Jeux en ligne multijoueurs : Les jeux nécessitant une synchronisation rapide entre les actions des joueurs peuvent utiliser les WebSockets pour une communication efficace en temps réel.
- Tableaux de bord et monitoring : Les WebSockets permettent d'afficher en direct les données d'un serveur, comme les métriques système ou les statistiques d'utilisation, avec des mises à jour en continu.
- Collaborations en temps réel : Pour les applications de collaboration comme les éditeurs de texte partagés ou les outils de dessin en ligne, les WebSockets facilitent la synchronisation des modifications entre plusieurs utilisateurs.
- Streaming de données : Les WebSockets sont idéaux pour les flux de données en temps réel, tels que les cours de la bourse, les résultats sportifs, ou les données de capteurs IoT.
Avantages
- Communication efficace : Comme les connexions WebSocket restent ouvertes, il y a moins de surcharge par rapport aux requêtes HTTP répétées.
- Réactivité : Les messages peuvent être envoyés et reçus instantanément.
- Économie de bande passante : Les données sont envoyées dans un format JSON.
Exemples
- Un chat en direct sans utiliser de mécanismes comme Ajax. (Surcharge des requêtes + latence)
- Notifications push.
Streaming audio/vidéo avec WebSockets
Il est techniquement possible d'utiliser les WebSockets pour envoyer des flux, mais ce n'est pas optimisé. Les WebSockets n'offrent pas de mécanismes intégrés pour la compression ou l'encodage, d'où des temps de latence.
- WebRTC : solution la plus populaire pour le streaming audio et vidéo en temps réel. Il est conçu pour les communications peer-to-peer et offre une faible latence.
- HTTP Live Streaming (HLS) : protocole de streaming développé par Apple.
- MPEG-DASH : divise le contenu en segments et permet la diffusion adaptative.
- RTMP (Real-Time Messaging Protocol) : protocole traditionnellement utilisé pour le streaming en direct, notamment dans les plateformes de streaming comme YouTube ou Twitch.
Mise en œuvre : les composants
Les trois composants d'une implémentation WebSocket standard :
APACHE
Un seul domaine peut héberger plusieurs points de terminaison WebSocket distincts, par exemple un par salon de discussion, chacun étant associé à un port local différent.
Les échanges sont chiffrés via wss (WebSocket Secure, TLS), ce qui garantit la confidentialité des messages en transit.
Apache agit ici en proxy inverse : il reçoit les connexions WebSocket sur le port 443 et les route vers le processus PHP qui écoute en local. Le site HTTP/HTML n'est pas modifié ; seule la couche proxy est ajoutée.
PHP
Pour le serveur, on s'appuie sur la bibliothèque Ratchet, dont le rôle est de gérer les connexions et les messages en temps réel.
Notre serveur reçoit un paquet de données JSON envoyé par un utilisateur, puis le redistribue à tous les autres utilisateurs connectés. Ce JSON contient non seulement les messages des utilisateurs, mais aussi des données techniques comme des identifiants, des informations sur le canal de discussion, etc.
Le fichier PHP chat1.php n'est pas servi par le web : il s'exécute en ligne de commande, directement depuis le terminal ou via une tâche planifiée (cron).
JS + HTML
Le code JavaScript présenté implémente un client WebSocket qui permet à l'utilisateur de se connecter à un serveur de chat en temps réel, d'envoyer et de recevoir des messages.
Le script JavaScript agit comme un client de chat en ligne. Il se connecte à un serveur WebSocket (via une URL sécurisée wss://), envoie des données et reçoit des messages en temps réel. Les WebSockets permettent une communication bidirectionnelle et continue entre le client et le serveur, ce qui signifie que le serveur peut envoyer des messages au client sans que celui-ci ait besoin d'envoyer une requête d'abord.
APACHE VirtualHost
Avant tout, on active les modules Apache nécessaires au proxy WebSocket. Sans proxy_wstunnel, le tunnel ws:// ne sera pas établi :
sudo a2enmod proxy proxy_http proxy_wstunnel ssl
sudo systemctl restart apache2
<VirtualHost *:80>
ServerAdmin moi@mon_orange.fr
ServerName monsupersite.fr
ServerAlias www.monsupersite.fr
Redirect permanent / https://monsupersite.fr/
DocumentRoot /var/www/vhosts/monsupersite.fr/public
</VirtualHost>
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName monsupersite.fr
ServerAlias www.monsupersite.fr
DocumentRoot /var/www/vhosts/monsupersite.fr/public
# Proxy WebSocket (nécessite le module proxy_wstunnel, voir ci-dessous)
ProxyRequests Off
ProxyPass "/chat1" "ws://localhost:8443/"
ProxyPassReverse "/chat1" "ws://localhost:8443/"
ProxyPass "/chat2" "ws://localhost:8453/"
ProxyPassReverse "/chat2" "ws://localhost:8453/"
ProxyPass "/chat3" "ws://localhost:8463/"
ProxyPassReverse "/chat3" "ws://localhost:8463/"
# Configuration du répertoire
<Directory "/var/www/vhosts/monsupersite.fr">
Options -Indexes +FollowSymLinks +ExecCGI
AllowOverride All
Require all granted
</Directory>
# Configuration SSL
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/monsupersite.fr/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/monsupersite.fr/privkey.pem
# SSL : désactiver les protocoles obsolètes et sécuriser les suites de chiffrement
SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite HIGH:!aNULL:!MD5:!3DES
SSLHonorCipherOrder on
SSLCompression off
SSLOptions +StrictRequire
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
</IfModule>
PHP : chat1.php
Pour démarrer le fichier, on se place dans le répertoire où il se trouve. Le script chat1.php est conçu pour fonctionner en continu. Une fois lancé depuis la console avec la commande :
php chat1.php
le script restera en cours d'exécution tant que la console ou le serveur est en marche. Il agit comme un serveur en arrière-plan, écoutant les connexions WebSocket et traitant les messages en temps réel. Le script ne s'arrête pas automatiquement, il doit donc être arrêté manuellement ou redémarré en cas de besoin.
Si le script est lancé automatiquement via une tâche cron, il ne sera pas aussi simple de l'arrêter que Ctrl + C. Vous devez d'abord trouver l'ID du processus (PID) :
ps aux | grep chat1.php
kill PID
# en dernier recours seulement :
kill -9 PID
Utiliser un fichier de verrouillage (lock file) : l'idée est d'ajouter à votre script une vérification d'un fichier de verrouillage pour savoir s'il doit continuer à s'exécuter ou s'arrêter. Par exemple, le script pourrait vérifier l'existence d'un fichier comme /tmp/chat1.lock, et s'il est supprimé, le script s'arrêterait.
rm /tmp/chat1.lock
# via cron pour arrêter le script :
* * * * * pkill -f chat1.php
Ressources :
http://socketo.me/
https://packagist.org/packages/cboden/ratchet
composer require cboden/ratchet
websocket.php :
<?php
require dirname( __FILE__ ) . '/vendor/autoload.php';
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class MySocket implements MessageComponentInterface {
protected $sockets;
public function __construct() {
$this->sockets = new \SplObjectStorage;
date_default_timezone_set('Europe/Paris');
}
public function onOpen(ConnectionInterface $conn) {
$this->sockets->attach($conn);
error_log("Connection opened: {$conn->resourceId}");
}
public function onMessage(ConnectionInterface $conn, $msg) {
$decodedMsg = json_decode($msg, true);
if ($decodedMsg === null) {
error_log("Invalid JSON message received from {$conn->resourceId}");
return;
}
foreach ($this->sockets as $client) {
if ($client !== $conn) { // Éviter d'envoyer le message à l'expéditeur
$client->send(json_encode($decodedMsg));
}
}
error_log("Message from {$conn->resourceId}: " . json_encode($decodedMsg));
}
public function onClose(ConnectionInterface $conn) {
$this->sockets->detach($conn);
error_log("Connection closed: {$conn->resourceId}");
}
public function onError(ConnectionInterface $conn, \Exception $e) {
error_log("An error occurred with connection {$conn->resourceId}: {$e->getMessage()}");
$conn->close();
}
public static function startServer() {
error_log("Starting WebSocket server...");
$server = IoServer::factory(
new HttpServer(
new WsServer(
new MySocket()
)
),
8443
);
$server->run();
}
}
MySocket::startServer();
JS & HTML
chat.js :
const MySocket = (function(){
"use strict";
let socket = null;
let msg = {
header: {
host: window.location.host,
pathname: window.location.pathname,
user: 'nomuser'
},
chat: {
channel: "all",
message: null
}
};
const init = () => {
try {
socket = new WebSocket(`wss://monsupersite.fr/chat1?token=${conf.session.compte}`);
if (socket) {
socket.onopen = (e) => {
msg.object = 'onopen';
socket.send(JSON.stringify(msg));
socket.onmessage = (e) => {
const result = JSON.parse(e.data);
console.log(result)
}
};
}
socket.addEventListener("error", (event) => {
event.preventDefault();
});
} catch (error) {
}
}
const sendChat = (message) => {
msg.chat.message = message;
socket.send(JSON.stringify(msg));
}
return {
init: init,
};
})();
document.addEventListener('DOMContentLoaded', () => MySocket.init());