En continuant à naviguer sur mon site, vous consentez à ce que j'utilise des cookies pour collecter les statistiques de visites. En savoir plus

#Informatique Websockets en PHP : plus simple qu'il n'y paraît

Les Websockets peuvent avoir des applications multiples pour un Web toujours plus dynamique et aux échanges les plus légers possibles : notifications, chats, streams, édition synchronisée d'une même ressource, etc. Là où, avant, on faisait du polling en JS pour demander des informations à jour sur une application partagée, on utilise aujourd'hui une architecture beaucoup plus bas-niveau basée sur des échanges à double sens : ce n'est plus seulement le client qui interroge le serveur, puisque le serveur peut tout aussi bien envoyer des messages au client tant que le canal de communication est ouvert.

Je me suis souvent battu sur mon serveur pour trouver une solution viable pour mettre en place des systèmes avec des Websockets. Une évidence pour ceux qui y sont confrontés au quotidien, mais qui peut vite devenir un casse-tête quand on découvre cette techno. Pourtant, dernièrement, en quelques étapes très rapide, j'ai pu mettre en place cette architecture pour une petite application en un tournemain. Avant de savoir que c'était faisable en PHP, j'utilisais NodeJS que j'aime beaucoup, mais ça m'embêtait d'utiliser 2 technologies différentes pour une même application…

Prérequis

  • Il vous faut la version 5.4.2 de PHP au minimum. Autant vous dire que cette version datant de mai 2012, vous devriez être en version 7.2 depuis longtemps.
  • Ensuite, il vous faut composer, le "package manager" de PHP. Si vous ne l'avez pas, commencez par suivre ces instructions. En quelques mots, considérez cet outil comme un bidule qui va vous télécharger plein de bouts de codes et librairies PHP, à la demande et créer un fichier "composer.json" ainsi qu'un dossier "vendor" dans votre projet. En vérité, l'outil possède une super documentation et ça vaut le coup de se pencher sur le sujet si vous ne l'avez pas encore fait.

Création du serveur en PHP

Installation des pré-requis

La première étape consiste à installer la librairie "Ratchet" depuis votre terminal :

composer require cboden/ratchet

Si votre projet est vierge de tout contexte "Composer", l'outil va vous demander diverses informations pour créer votre fichier "composer.json". Si tel est le cas, exécutez ensuite la commande suivante :

composer install

Mise en place du serveur

Attention - Notez que le fichier PHP que l'on va créer ici n'est pas destiné à être appelé via un navigateur tiers mais à être exécuté directement depuis votre serveur (via un service par exemple).

Honnêtement, je ne fais que reprendre la documentation de Ratchet ici (en la traduisant et en la retouchant un peu). Le code minimum de votre serveur doit ressembler à ça :

<?php require __DIR__.'/vendor/autoload.php';

use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;

define('APP_PORT', 8080);

class ServerImpl implements MessageComponentInterface {
    protected $clients;

    public function __construct() {
        $this->clients = new \SplObjectStorage;
    }

    public function onOpen(ConnectionInterface $conn) {
        $this->clients->attach($conn);
        echo "New connection! ({$conn->resourceId}).\n";
    }

    public function onMessage(ConnectionInterface $conn, $msg) {
        echo sprintf("New message from '%s': %s\n\n\n", $conn->resourceId, $msg);
        foreach ($this->clients as $client) { // BROADCAST
            $message = json_decode($msg, true);
            if ($conn !== $client) {
                $client->send($msg);
            }
        }
    }

    public function onClose(ConnectionInterface $conn) {
        $this->clients->detach($conn);
        echo "Connection {$conn->resourceId} is gone.\n";
    }

    public function onError(ConnectionInterface $conn, \Exception $e) {
        echo "An error occured on connection {$conn->resourceId}: {$e->getMessage()}\n\n\n";
        $conn->close();
    }
}

$server = IoServer::factory(
    new HttpServer(
        new WsServer(
            new ServerImpl()
        )
    ),
    APP_PORT
);
echo "Server created on port " . APP_PORT . "\n\n";
$server->run();

Là, si vous n'êtes pas familiers avec le langage PHP, je ne peux que vous renvoyer aux bases. Surtout que @Grafikart est en train de proposer une super formation pas à pas en PHP.

Nous avons donc une classe (que vous pouvez déclarer dans un fichier à part) qui prend en charge :

  • Les nouvelles connexions (dans la méthode "onOpen")
  • Les messages envoyés par les clients (dans la méthode "onMessage")
  • Les connexions fermées (dans la méthode "onClose")
  • Les erreurs (dans la méthode "onError")

La construction est finalement assez similaire à une appli en NodeJS, puisque quand un nouveau client arrive, on le "stocke" dans notre serveur. Cela nous permet de pouvoir envoyer des messages en "broadcast" (à tout le monde) quand c'est nécessaire (sauf à la personne qui a envoyé le message en question).

Enfin, notons que le serveur est créé et enveloppé dans un serveur "Websocket", lui-même dans un serveur HTTP, les 3 classes étant proposées par Ratchet. Je n'ai pas encore exploré les possibilités de Ratchet, mais ça a l'air assez puissant et complet, notamment parce que leur tuto le plus basique propose une communication via Telnet !

Démarrage du serveur

Pour démarrer votre serveur, c'est simple. Sur votre serveur, effectuez la commande suivante :

php server.php

Évidemment, il faut être placé dans le dossier où vous venez de créer votre petit script PHP, et qu'il s'appelle "server.php". Sinon adaptez la commande.

La prochaine étape, pour moi, est de transformer cet appel en un service pour qu'il soit exécutable automatiquement au démarrage de mon serveur et qu'il tourne dans son coin. Je n'ai pas encore la procédure exacte pour le faire, mais je l'ajouterai dans les commentaires ou à la fin de l'article si j'ai un truc simple et sympa à proposer. Le but serait d'avoir quelque chose comme ça :

sudo service mon-appli start

Il faudrait aussi avoir un fichier de conf à part dans lequel on mettrait au moins le numéro du port à utiliser. C'est la moindre des choses.

Accès depuis l'extérieur

Vous l'aurez remarqué, dans le script ci-dessus, on utilise le port 8080 (un port arbitraire). Or, si votre serveur est un poil sécurisé, vous n'avez pas ouvert le port 8080 et vous n'avez pas spécialement envie de le faire. C'est d'ailleurs une bonne chose aussi de ne pas passer par ce port, puisque sur des réseaux bridés (WiFi public, 4G, …) l'accès à des ports autres que les traditionnels 80 (HTTP) et 443 (HTTPS) peuvent être bloqués. Il faut donc mettre en place un proxy.

Pour ma part, j'utilise Apache, et j'ai juste à ajouter ces quelques lignes dans la configuration de mon VirtualHost :

<VirtualHost *:443>

...

  <Location "/ws/monappli">
    ProxyPass ws://localhost:8080
    ProxyPassReverse ws://localhost:8080
  </Location>

...

</VirtualHost>

Notez que j'ai ajouté ces quelques lignes dans un VirtualHost sur le port 443, en HTTPS donc. J'ai bien mis en place mes certificats HTTPS. Et comme je ne l'ai jamais dit ici : merci Let's Encrypt de nous permettre de mettre en place un Web plus sécurisé gratuitement.

Relancez votre serveur et voilà ! Votre site possède une nouvelle URL qui est branchée sur un canal en Websockets : wss://monsite.com/ws/monappli ! En plus, cet accès est en "WSS", ce qui signifie que je suis bien dans un contexte d'échange sécurisé. Apache fait donc office de relais entre l'extérieur et notre micro-serveur de Websockets accessible uniquement depuis le serveur.

Implémentation en Javascript

On arrive au bout, il ne reste plus qu'à se faire un petit script JS pour que notre serveur… serve à quelque chose.

Voici un bref exemple que j'ai pris également sur le site de Ratchet et que j'ai un peu adapté :

var conn = new WebSocket('wss://monsite.com/ws/monappli');
conn.onopen = function(e) {
    console.log("Connection established!");
    conn.send('Hello World');
};

conn.onmessage = function(e) {
    console.log(e.data);
};

Ce tout petit bout de JS va :

  1. Créer et ouvrir le canal d'échange en Websockets.
  2. Déclarer une fonction qui sera appelée quand la connexion sera établie. Cette fonction va inscrire dans la console "Connection established!" une fois la connexion créée et envoyer un message au serveur "Hello World".
  3. Déclarer une fonction qui sera appelée quand un message proviendra du serveur.

Ainsi quand on ouvre un onglet avec ce petit script JS, on observera ce message dans la console :

> Connection established!

Et si on ouvre un second onglet sur la même page, la console de la première page ressemblera à peu près à ça :

> Connection established!
> {… data: 'Hello World' …}

Ce qui signifie bien que les connexions ont été établies l'une après l'autre et que chaque onglet a bien envoyé un message qui a été reçu par tous les autres clients connectés (le premier message a bien été envoyé, il est visible dans la console du serveur, mais évidemment puisque personne n'était branché sur le serveur, le message a été envoyé à personne d'autre).

Conclusion

Si vous maîtrisez les notions de PHP, Composer, Apache et du JS abordées dans ce tuto, il vous aura fallu 10 minutes maxi pour mettre ce système en place.

À vous les serveurs de notifications fait maison, les chats réactifs, les éditeurs de contenus synchronisés et toutes sortes d'échanges sans avoir besoin de se soucier des en-têtes HTTP !

Rédigé le .

Commentaires

comments powered by Disqus