Quand les éléPHPants regardent passer la Comet de Bayeux.
Par Clochix le mardi 14 avril 2009, 00:46 - Technoweb - Lien permanent
Longtemps après avoir été confronté au problème à l'époque de Couac, je me retrouve à nouveau à réfléchir à l'amélioration de l'interaction entre une application Web et le serveur. Tentative de résumé de mes lectures du (trop court) week-end sur Comet, une technique pour permettre à un serveur de pousser de l'information vers un client web, et sur son utilisation avec PHP. Le but est toujours le même, trouver le meilleur moyen de rafraîchir des éléments de l'interface (widgets ou autres) lorsque de nouvelles informations sont disponibles sur le serveur.
C'est quoi ton problème ?
Depuis que le Web est Web, il fonctionne essentiellement en mode client-serveur : un client envoie une requête, le serveur lui répond. C'est donc toujours le client qui initie la communication, le serveur n'a pas moyen de discuter avec le client de sa propre initiative. Ce qui est un peu limité: comment savoir que de nouvelles informations sont disponibles sur le serveur ? En attendant l'arrivée de HTML 5 qui va introduire de vrais sockets[1], différentes techniques ont été développées pour mettre à jour le client quand l'information change sur le serveur. Des préhistoriques balises META refresh qui demandaient au navigateur de recharger régulièrement la page, à l'utlisation d'AJAX, où le client envoie à intervalles réguliers des requêtes au serveur pour savoir s'il y a du neuf. Toutes ces solutions ne sont guère satisfaisantes, car elles gaspillent de la bande passante et de la charge serveur: en effet, la plupart des requêtes vont simplement répondre qu'il n'y a rien de neuf. On aura donc réalisé une connexion pour rien, et le serveur aura dû mouliner juste pour découvrir qu'il n'avait rien à dire.
Une solution plus élégante serait de permettre au serveur d'avertir le client qu'il a du nouveau pour lui, sans que ce dernier ne lui pose la question toutes les 10 secondes. Différentes techniques, regroupées sous l'appellation Comet, ont été développées pour simuler une communication bi-directionnelle. Les deux principales reposent l'une sur le streaming, l'autre sur le long polling.
Aparté : des connexions persistantes ?
Commençons par un petit rappel sur HTTP. La version 1.1 du protocole HTTP (qui date de 1997, on peut espérer que la plupart des navigateurs l'implémentent) a introduit la notion de connexions persistantes, c'est à dire que le navigateur n'ouvre pas une nouvelle connexion par requête, mais peut utiliser une seule connexion pour plusieurs requêtes[2]. Il est évidemment possible pour le client d'ouvrir plusieurs connexions persistantes simultanées avec le serveur, mais la spécification recommande de ne pas en utiliser plus de deux[3]. Certains clients ou serveur peuvent refuser d'utiliser plus de deux connexions persistantes par client, ce qui pose un problème pour utiliser Comet. Firefox 3 autorise par défaut 8 connexions simultanées par serveur.
Le long polling
C'est une approche qui reste classique: le client envoie des requêtes au
serveur, mais celui-ci ne lui répond que lorsqu'il a de nouvelles
informations. La requête reste suspendue en attente d'une réponse. Une
fois une réponse reçue, le client envoie une nouvelle requête. La méthode la
plus simple est d'utiliser des XMLHttpRequest, auxquelles le
serveur ne répond que lorsqu'il a des informations à envoyer. Une alternative
est d'insérer dynamiquement côté client des balises <script>
appelant un script sur le serveur. Comme pour les XHR, celui-ci ne répond que
lorsque c'est nécessaire. Il envoie alors des commandes JavaScript pour
afficher la réponse, et insérer une nouvelle balise <script>
qui va générer un nouvel appel. Cette seconde méthode a l'avantage de pouvoir
interroger des serveurs situés dans plusieurs domaines, sans être limité par le
same domain policy.
Ces solutions ont l'avantage d'être simples à mettre en œuvre et relativement résistantes, c'est à dire qu'elles devraient passer sans trop de soucis les proxys et être compatibles avec la plupart des logiciels côté client et serveur.
Le streaming
Il n'y a pas à l'état actuel moyen de faire du véritable streaming avec
HTTP, en utilisant par exemple des sockets. Mais plusieurs techniques
permettent de simuler ce fonctionnement. La plus utilisée repose sur
l'utilisation d'une iframe masquée. Cette iframe affiche une page
du serveur dont le chargement ne s'arrête jamais (le serveur y écrit sans
jamais fermer la connexion). A chaque fois que le serveur veut communiquer avec
le client, il ajoute du contenu à cette page. Ce contenu peut soit être du code
JavaScript qui va être interprété et déclencher une action, soit simplement des
données dont l'ajout dans le DOM va déclencher un évènement et être traité par
le client. Un des inconvénients majeurs de cette méthode est qu'il est
difficile de détecter une coupure de la communication.
Une autre solution de streaming est d'utiliser une requête XHR dont la
réponse est envoyée en plusieurs partie: le serveur répond en envoyant des
paquets dont le type MIME est multipart/x-mixed-replace, chacun
déclenchant un appel de onreadystatechange côté client.
Malheureusement, cette technique, développée par Netscape à partir de 1995,
n'est je crois implémentée pour l'instant que dans Gecko. cf la documentation de
XMLHttpRequest sur le MDC.
En passant par Bayeux
Les techniques de long polling comme celles de streaming restent encore un peu de la bidouille. Il n'y a guère de moyen d'être sûr que l'une ou l'autre fonctionnera dans toutes les configurations de navigateur, de réseau, etc. Heureusement, des tentatives de normalisation de Comet sont en cours. L'une des plus intéressantes, à mon humble avis, est honorablement connue sous le nom de Bayeux, comme la pâtisserie.
Le terme Comet a je crois été utilisé pour la première fois pour décrire ces techniques dans un article d'Alex Russell : Comet: Low Latency Data for the Browser. Alex Russell est un développeur du framework JavaScript dojo, et la fondation Dojo a dans la foulée proposé un protocole, Bayeux , pour normaliser ce type d'échanges, et un projet, Cometd, qui propose plusieurs implémentations de ce protocole, utilisant divers langages: côté client, JavaScript et ActionScript (via Flash/Flex) et côté serveur, Java, Python, et Perl (mais pas encore PHP). Une implémentation du client pour jQuery existe également, sous forme d'un plugin. Mais utiliser le protocole de Bayeux nécessite d'avoir un serveur le supportant, donc d'installer un logiciel en Python, Ruby ou Java sur le serveur. Pas très pratique pour s'interfacer avec une application PHP existante.
Le protocole de Bayeux décrit un mécanisme pour l'échange de messages asynchrones entre un client et un serveur. Les messages transitent via des canaux nommés au dessus du protocole HTTP. Les messages circulent entre les différents composants clients et serveurs sur un bus et sont routés par défaut via un mécanisme d'inscription (un client s'inscrit à un canal pour recevoir les messages qui y sont publiés). La communication est bi-directionnelle, et des clients peuvent également discuter entre eux (via le serveur).
Et côté serveur
Si côté client on peut assez simplement créer des connections persistantes, le travail est un peu plus complexe du côté du serveur, et PHP ne semble pas être un langage de premier choix pour développer des applications utilisant Comet. En effet, il soufre de limitations, comme l'absence de support du multi-threading, qui compliquent les choses. S'il existe plusieurs solutions en Python (par exemple avec Twisted), Ruby, Java (avec Jetty)..., je n'ai pas trouvé de framework PHP pour faciliter le développement de ce type d'application. Si vous en connaissez vous seriez gentil de me les signaler en commentaire.
Le principal problème posé côté serveur par les techniques Comet est celui de la montée en charge. Elles impliquent en effet de conserver une connexion ouverte en permanence entre chaque client et le serveur. Si on a des centaines ou des milliers d'utilisateurs connectés (pas forcément en train de travailler simultanément, il suffit que le site soit ouvert en tâche de fond dans un onglet du navigateur), on va peu à peu asphyxier le serveur web, en phagocytant toutes ses connexions disponibles (sans parler de la charge mémoire).
Digression Apache
Apache dispose de 3 modes de fonctionnement: "pre-fork" est la méthode traditionnelle, présente depuis la version 1, où n processus tournent parallèlement en mémoire. C'est une méthode très gourmande en ressources, puisque chaque processus a besoin de ses propres ressources, en particuliers la mémoire. La version 2 du serveur a donc introduit un nouveau fonctionnement utilisant des threads (via module worker). L'avantage des threads est qu'ils partagent leur mémoire virtuelle et certaines ressources, d'où une empreinte mémoire beaucoup plus faible. Chaque thread gère une connection. Ce fonctionnement est donc moins coûteux. Enfin, à titre expérimental, Apache propose également le module event qui utilise une gestion particulière des threads pour permettre à chacun de gérer plusieurs requêtes. C'est le module idéal pour gérer de nombreuses connexions persistantes, mais je ne sais pas s'il est suffisamment stable pour être utilisé en production.
Lorsqu'on utilise des connexions persistantes, chacune monopolise un processus (en mode prefork) ou un thread (en mode worker) du serveur. Malheureusement, certaines extensions de PHP ne supportent pas de fonctionner dans un environnement multi-threadé. Donc si on utilise PHP comme module Apache, il est recommandé de faire fonctionner Apache en mode "pre-fork". Ce qui signifie que chaque connexion persistante va occuper un processus. Il est par conséquent difficilement envisageable d'utiliser Comet avec une installation de PHP utilisant le module Apache.
Heureusement, il existe des alternatives:
- utiliser PHP en mode CGI (ou plutôt avec FastCGI), ce qui permettra d'utiliser les modules worker ou event d'Apache, voire de remplacer Apache par un autre serveur web (nginx, lighthttpd... je n'ai pas encore creusé de ce côté, mais ça semble être la meilleure solution. Tout retour d'expérience sera le bienvenu);
- créer un serveur léger en PHP chargé uniquement de gérer les connexions persistantes. On pourra pour cela utiliser l'excellente bibliothèque phpsocketdaemon de Chris Chabot. Chris l'utilise dans WebChat, un client IRC en PHP + JavaScript.
Sur le serveur, gérer des événements
Au delà de ces problèmes techniques, l'utilisation de Comet implique d'utiliser sur le serveur d'autres paradigmes de programmation. On n'est plus dans le fonctionnement traditionnel où l'application répond à une sollicitation du client. Il faut implémenter une approche plus événementielle, réagir à des événements qui déclencheront une écriture dans le canal de communication avec le client. Par exemple, si on veut afficher la liste des utilisateurs connectés au site, dans le fonctionnement classique le client envoyait une requête, le serveur comptait les utilisateurs connectés et répondait. Avec Comet, à chaque fois qu'un utilisateur se connecte ou se déconnecte, un événement est déclenché qui entraîne la mise à jour des clients que cet événement intéresse.
Pistes
J'avais pratiquement fini d'écrire ce billet lorsque je suis tombé sur une
présentation qui contredit un peu mes derniers paragraphes. Depuis que j'ai
dégagé le player Flash de ma machine (No non-free or contrib packages
installed on xxx ! rms would be proud.
) je n'accédais plus à slideshare[4]. Grâce à un script Greasemonkey j'y
accède enfin à nouveau, et j'ai donc pu consulter une
présentation de Will Sinclair, responsable des développement du Zend
Framework, où il décrit une implémentation du protocole de Bayeux basée sur
Dojo et un serveur en PHP. Cette présentation est vraiment bien faite et je
vous en recommande chaudement la lecture (grrr, si je l'avais lue plus tôt je
n'aurai pas perdu ma journée à écrire ce billet). Malheureusement, elle reste
un peu évasive sur l'implémentation du serveur. Manifestement, ils utilisent
lighthttpd. Il va falloir que je creuse, ça semble très intéressant. J'avais
écarté le protocole de Bayeux, par manque de composant serveur en PHP, mais si
des implémentations existent, ça en fait une solution particulièrement sexy.
Suite au prochain épisode.
Autres approches non-standard
Ayant un peu joué avec les techno Adobe, je me dois de signaler qu'il est également possible de les utiliser comme alternative à Comet. Flash peut se connecter à un serveur via une socket persistante. On peut donc imaginer insérer dans chaque page une animation Flash masquée chargée de communiquer avec un serveur type BlaseDS. Une solution qui s'apparente moins à de la bidouille, mais plus limitée: Flash n'est pas disponible sur de nombreuses plate-forme, notamment sur certains gadgets mobiles propriétaires à la mode.
De même, on pourrait aussi utiliser des applets Java, mais le souvenir douloureux des premières applications de chat en ligne m'incite à ne pas creuser dans cette direction.
C'est n'importe quoi ce billet !
Oui, j'étais parti pour parler de la création d'un serveur en PHP avec des
chaussettes, et j'ai fini à Bayeux, sans avoir le temps d'écrire la moindre
ligne de code. C'est bien le web, on s'y perd mais la route est toujours
passionnante 
Notes
[1] c'est à dire la possibilité de maintenir un canal de communication ouvert en permanence entre le client et le serveur
[2] afficher une page web nécessite de nombreuses requêtes, puisqu'il en faut pour chaque élément externe inclus: feuilles de styles, images, etc. Créer une nouvelle connexion à chaque fois est coûteux
[3] simple politesse, pour ne pas qu'un client monopolise toutes les ressources d'un serveur. Certains navigateurs permettent de régler ce nombre, dans Firefox cela fait partie des options de configuration avancée
[4] quelle aberration que ce site qui oblige à utiliser Flash là où un diaporama basé sur des standards, comme S5, suffirait largement !
Commentaires
Merci pour ton article
C'est très instructif
+++
+1 pour la dernière phrase !
Il n'est pas bien sorcier d'écrire un serveur en php qui gère des threads, ceci est tout à fait envisageable. on retrouve dans php des fonctions assez bas niveau comme select() qui permet de gérer facilement un pool de sockets.
On trouve de même de plus en plus d'exemple de php " daemonisé " et de gestion des threads. J'ai même trouvé une implémentation simple mais fonctionnelle d'une serveur web.
J'y songe moi même pour un futur projet.
Je t'encourage dans tes recherches une communication bi-canal serait un vrai renouveau pour le web.
s/patissserie/tapisserie
Pour apache vs nginx vs lighttpd : lighttpd est vraiment sympa et plus léger qu'apache. J'avais remplacé apache par lighttpd sur mon serveur perso quand j'étais encore en dédié. J'aurais bien utilisé nginx mais pour les applis php à l'époque il fallait utiliser certains binaires de lighttpd pour lancer les process cgi pour PHP. Donc quitte à avoir du lighttpd autant utiliser que lighttpd.
Très instructif sur comet en tous cas que je vois apparaitre régulièrement dans certains flux RSS mais sans prendre le temps de voir ce qu'il se cachait derrière...
@molokoloco: ça l'a avant tout été pour moi
@Seza: je peux me tromper, mais j'avais pas mal creusé le sujet à l'époque où je codais la partie serveur de Couac, et si PHP est très correctement équipé pour la gestion réseau bas niveau, avec les sockets, il n'est malheureusement pas multithread, c'est à dire qu'un démon PHP ne pourra faire qu'une seule chose à la fois. Tout opération qui prend du temps ou bloquante (comme par exemple une connexion à une base de données) pénalisera le démon. Il ne peut pas lancer plusieurs traitements en parallèle dans des threads. Pour cela, il faut adopter une gestion multi-processus à base de fork. C'est faisable mais on se retrouve dans la même situation qu'avec Apache en mode pre-fork: une multiplication de processus, donc une forte consommation de mémoire.
Pour ce qui est de la gestion des sockets, et d'un exemple de serveur web en PHP, je te conseille, si tu ne la connais pas, de jeter un œil à la bibliothèque phpsocketdaemon.