Websocket et PHP : présentation de websocketd

Connecter une application mobile à des web-services via une web-socket

De plus en plus d’application utilisent le web-socket pour communiquer avec des web services. Les avantages sont multiples : plus de connexions/déconnexions intempestives, un transport de données beaucoup plus rapide, une économie de ressources (CPU coté serveur, batterie coté client), et la possibilité pour le serveur de pousser des données vers le clients de manière autonome.

Ces derniers mois, nous avons testé plusieurs approches pour créer le serveur Web-socket avec PHP.
Le premier problème qui se présente est la possibilité de pouvoir gérer de multiples connexions vers un seul port de destination.

Une seule solution pour gérer cela : utiliser la librairie pcntl de PHP, qui autorise l’utilisation d’un fork (comme en C). En combinant cette technique avec des sockets non bloquantes, il est possible de créer un serveur multi-processus, doté d’un processus maître de contrôle : gestion des déconnexions, spawn de nouveaux processus, maintient d’un pool de processus spare, etc. Dans la théorie tout est quasi possible.

Dans la pratique, cela s’avère un mauvais choix : PHP n’est pas du tout conçu pour cela :

  • Effectuer un fork est gourmand en ressource et très lent en PHP : l’intégralité de la mémoire du processus maître est copiée.
  • La communication entre les différents processus fils est complexe (via des signaux POSIX, l’utilisation d’une mémoire partagé via shmop devient vite aussi indispensable que peu pratique)
  • La communication entre un processus fils et le processus maître répond aux même contraintes
  • Le diagnostic des crashs et exceptions est plus difficile à tracer.
  • Des processus qui se zombifient sans arrêt (State Z). La commande « top » ressemble rapidement à un film de Roméro 😉
  • Franchement super lent

Vous l’aurez compris, vraiment pas la bonne solution.

Websocketd : un daemon simple et léger

Websocketd

Nous nous sommes alors tournés vers websocketd. Étant de grands fans de Linux, le concept nous a tout de suite plu : il s’agit d’un petit exécutable (dispo Unix/BSD/Win/OSX, 32 et 64bits)
qui permet d’interfacer n’importe quel binaire, script ou fichier avec une web-socket. Plus de gestion des fork, plus de problème de multi-threading !
Websocketd se charge de toute cette logique, difficile à mettre en place avec des langages comme le PHP. Le gain de temps est énorme, et la fiabilité bien meilleure.

Un petit exemple :

websocketd --port 9000 /var/www/ws-server.php

Cette commande va donc exposer le script ws-server.php via des web services sur le port TCP 9000. L’entrée/sortie se fait via STDIN et STDOUT

ATTENTION AU CHOIX DU PORT : après des heures de recherche, nous avons fini par découvrir que de nombreux opérateurs téléphoniques filtrent les données de manière un peu étrange sur certains ports. Par exemple, Bouygues Telecom semble filtrer les paquets entrants sur le port 8080, mais autorise (o_O ??) les paquets sortants. Si vous avez possibilité, utilisez le port 80 par sécurité, le 9000 que nous utilisons ici n’est -à ma connaissance et à l’heure actuelle- pas filtré.

Votre script PHP va être appelé à tourner de manière permanente : faites attention à libérer au maximum la mémoire et à régler set_time_limit à 0.
Vous devez aussi prendre quelques précautions pour éviter la saturation du service en limitant le nombre de connexions (iptables semble une bonne approche pour commencer)

ws-server.php : un simple service echo

#!/usr/local/bin/php
<?php
set_time_limit(0);

while(true){ // boucle infini, c'est un serveur !
    $f = fgets(STDIN);
    // appel bloquant, l’exécution est suspendue
    // tant qu'il n'y a pas de données

    if($f === false){ // STDIN fermé, on quitte
       break;
    }
    
    if(strlen($f) > 0){ // on a des données
        foreach(explode("\n",$f) as $line){ // une commande par ligne
           echo trim($line)."\n; // renvoi la ligne reçue
        }
    }
   
}

Il suffit de lire STDIN pour récupérer les données entrantes, et un simple echo pour les renvoyer au client (ou utiliser STDOUT). N’oubliez pas de faire un « chmod +x ws-server.php » pour le rendre exécutable en mode CLI

Cette solution à de nombreux avantages :

  • Plus de problème de multi-threading / fork et compagnie
  • Une utilisation simple, un outil = une tâche
  • Gestion propre du SSL/TLS
  • Économe en ressources
  • Vraiment. Beaucoup. Plus. Rapide.
  • Vous utilisez le langage que vous voulez. Vous pouvez même connecter les interfaces /dev à une websocket si ça vous chante !

Plus d’informations ici : http://websocketd.com/

En bonus, un script init pour websocketd, à placer dans /etc/init.d (testé sous Debian Jessie)
/etc/init.d/wsserver

#! /bin/sh
### BEGIN INIT INFO
# Provides: Webservice
# Required-Start: $local_fs $network
# Required-Stop: $local_fs $remote_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Webservices via websocket
# Description: WS via Websocket sur port 9000
### END INIT INFO

PIDF=/var/run/websocket
DAEMON=/bin/websocketd
ARGS="--port 9000 /var/www/ws-server.php"
USERID=1000 # UID de l'utilisateur sous lequel votre serveur tournera

case "$1" in
  start)
    echo "Start ws"
    start-stop-daemon --make-pidfile --start -b --pidfile $PIDF --quiet -c $USERID --exec $DAEMON -- $ARGS
    ;;
  stop)
    echo "Stop ws"
    start-stop-daemon --stop --quiet --oknodo --pidfile $PIDF
    ;;
  restart)
   echo "Restart ws"
    start-stop-daemon --stop --quiet --oknodo --pidfile $PIDF
    start-stop-daemon --make-pidfile --start -b --pidfile $PIDF --quiet -c $USERID --exec $DAEMON -- $ARGS
   ;;
  *)
    echo "Usage: /etc/init.d/wsserver {start|stop|restart}"
    exit 1
    ;;
esac
exit 0
Bookmark and Share

Une réflexion sur “Websocket et PHP : présentation de websocketd

  1. Bonjour, comment gérer les sessions ? Chaque connexion semble indépendante de l’autre. Comment envoyer un même message à tous les clients connectés ? Merci.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *