WebSocket en PHP : de la théorie aux solutions de production
\n\nLe protocole WebSocket a révolutionné le développement web, permettant de créer des applications temps réel complètes fonctionnant sur une seule connexion TCP. Contrairement au HTTP classique où le client initie constamment les requêtes, WebSocket établit un canal de communication bidirectionnel par lequel le serveur peut envoyer des données à tout moment. Cela est crucial pour les chats, les plateformes de trading, les jeux en ligne et les systèmes de surveillance.
\n\nDe nombreux développeurs pensent à tort que PHP n'est pas adapté aux tâches temps réel en raison de sa nature synchrone. Cependant, des outils modernes comme Ratchet et ReactPHP changent complètement cette perception. Dans cet article, nous explorerons les techniques avancées pour créer des applications WebSocket évolutives en PHP.
Architecture d'un serveur WebSocket en PHP
\n\nChoix de la bonne pile technologique
\n\nPour les solutions de production, les bibliothèques simples fonctionnant via socket_create ne conviennent pas. Une approche event-driven est nécessaire. La pile recommandée :
- \n
- ReactPHP — boucle d'événements pour les entrées/sorties asynchrones \n
- Ratchet — composant WebSocket basé sur ReactPHP \n
- Redis — pub/sub pour la communication inter-processus \n
- Supervisor — gestionnaire de processus pour garantir la disponibilité \n
Serveur de base avec Ratchet
\n\nVoyons comment créer un serveur WebSocket complet capable de gérer des milliers de connexions :
\n\nuse Ratchet\\MessageComponentInterface;\nuse Ratchet\\ConnectionInterface;\nuse React\\EventLoop\\Factory;\nuse React\\Socket\\Server;\nuse Ratchet\\Server\\IoServer;\nuse Ratchet\\Http\\HttpServer;\nuse Ratchet\\WebSocket\\WsServer;\n\nclass ChatServer implements MessageComponentInterface {\n protected $clients;\n protected $redis;\n\n public function __construct() {\n $this->clients = new \\SplObjectStorage;\n $this->redis = new \\Predis\\Client([\n 'scheme' => 'tcp',\n 'host' => '127.0.0.1',\n 'port' => 6379,\n ]);\n // Abonnement au canal Redis pour la synchronisation inter-serveur\n $this->redis->subscribe(['chat_messages'], function($redis, $channel, $message) {\n $this->broadcast(json_decode($message, true));\n });\n }\n\n public function onOpen(ConnectionInterface $conn) {\n $this->clients->attach($conn);\n echo "Nouvelle connexion : {$conn->resourceId}\\n";\n }\n\n public function onMessage(ConnectionInterface $from, $msg) {\n $data = json_decode($msg, true);\n \n // Validation du message\n if ($this->validateMessage($data)) {\n // Publication dans Redis pour les autres serveurs\n $this->redis->publish('chat_messages', json_encode($data));\n // Envoi aux clients locaux\n $this->broadcast($data, $from);\n }\n }\n\n public function onClose(ConnectionInterface $conn) {\n $this->clients->detach($conn);\n echo "Connexion fermée : {$conn->resourceId}\\n";\n }\n\n public function onError(ConnectionInterface $conn, \\Exception $e) {\n echo "Erreur : {$e->getMessage()}\\n";\n $conn->close();\n }\n\n protected function broadcast(array $data, ?ConnectionInterface $exclude = null) {\n foreach ($this->clients as $client) {\n if ($exclude === null || $client !== $exclude) {\n $client->send(json_encode($data));\n }\n }\n }\n\n protected function validateMessage(array $data): bool {\n return isset($data['type'], $data['payload']) && \n in_array($data['type'], ['message', 'typing', 'status']);\n }\n}\n\n// Démarrage du serveur\n$loop = Factory::create();\n$webSock = new Server('0.0.0.0:8080', $loop);\n$webServer = n