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 !