samedi, octobre 21, 2006

Prochaine étape pour AJAX : un middleware orienté message

Avertissement : ce billet contient des fragments explicites de CODE ; le CODE a été reconnu comme substance à risque dans de très nombreuses études de cas ; consulter votre urbaniste des systèmes d'information avant toute consommation de CODE et pas d'usage prolongé sans avis médical.




Le succès d'AJAX auprès des programmeurs d'applications Web est fulgurant. Il faut l'attribuer d'une part à la demande croissante d'interactivité dans le navigateur Web, conséquence du succès de sa diffusion comme frontal aux applications d'entreprise et aux services grand public, et, d'autre part, à la simplicité de l'implémentation. Toutes les versions modernes des navigateurs offrent en effet un interpréteur JavaScript, permettant d'exécuter des traitements programmés directement dans la page Web sur le navigateur du poste client, et, bien sûr, la fonction d'appel asynchrone de pages Web, écrites en général en XML - l'objet XMLHttpRequest.

Le principe de fonctionnement est assez simple. Lorsqu'un navigateur affiche une page Web, il construit une représentation interne de la page en mémoire sous forme d'un arbre dont les « feuilles » représentent les objets de base à afficher (texte, image, etc.) avec leurs attributs graphiques. Cet arbre en mémoire est formellement défini par une spécification du W3C, le DOM (Document Object Model), à laquelle se conforment les navigateurs Web. Cette spécification définit également une interface de programmation complète qui permet de modifier au vol cet arbre, et donc le document, à partir d'un langage de programmation, en particulier de Java et de JavaScript. (D'autres bibliothèques ont été depuis développées qui permettent également de travailler sur l'arbre DOM depuis d'autres langages comme C, C++, PHP, Python, etc.) Dans le cadre d'AJAX, l'API JavaScript est particulièrement intéressante dans la mesure où le navigateur lui-même contient un interpréteur ce qui autorise le développeur à mettre dans la même page Web le contenu et les programmes les modifiant en réponse aux actions des utilisateurs. D'un simple document la page Web est devenue un objet complexe, contenant programmes et données.

Voilà pour l'interface graphique interactive. Pour ce qui est de la communication avec les serveurs, le sésame s'appelle XMLHttpRequest. Il transforme la page Web en client dans une architecture client-serveur classique. Au travers d'un requête XMLHttpRequest qui est envoyée à un serveur, le script JavaScript en cours d'exécution depuis une page Web peut recevoir des données supplémentaires du serveur. Cette communication est asynchrone : le script est « réveillé » par l'arrivée des données lorsqu'elles ont été envoyées par le serveur ; la communication ne bloque donc pas l'utilisateur qui peut poursuivre les interactions pendant l'attente des résultats de la requête (ou d'un message d'erreur du serveur). La requête est transportée par le protocole HTTP, elle bénéficie donc des mêmes avantages et souffre des mêmes inconvénient que le protocole lui-même. En particulier, cette implémentation du mode de communication n'offre pas de notion de session : chaque requête est indépendante des autres.

En théorie, la réponse du serveur à une requête asynchrone XMLHttpRequest peut prendre des formats divers. Le texte complet de la réponse est renvoyé dans un champ particulier "responseText". Cependant, et dans l'esprit d'interopérabilité des services Web prôné dans l'architecture orientée service (SOA), il est habituel que la réponse à une telle requête soit un document XML. Pour en simplifier le traitement, un autre champ du résultat, "responseXML", contient les données directement sous forme d'arbre DOM qui peut alors être manipulé par la même API en JavaScript que nous avons mentionné plus haut.

Mais ce n'est pas, loin de là, la seule possibilité. On trouve par exemple de plus en plus des réponses constituées de données au format JSON. JSON est un format léger de transport de données sous forme d'une chaîne de caractères empruntant directement à la syntaxe JavaScript. Ainsi ce format n'a formellement pas à être « interprété » par un script qui le reçoit mais simplement « évalué ». Les données au format JSON occupent moins de place qu'un document XML dans le transport via HTTP et leur exploitation est immédiate du côté navigateur, qui n'a pas dans ce cas à utiliser d'interpréteur XML.

Ajax ne pourra atteindre un nouveau degré de maturité que lorsque la question de la communication avec les programmes et processus se déroulant sur les serveurs aura trouvé une réponse qui s'affranchisse des contraintes imposées par le mode « sans mémoire » de HTTP. Plus précisément, l'épineuse difficulté réside dans le moyen à trouver pour qu'un processus serveur, déclenché ou non par une requête XMLHttpRequest, puisse, à son initiative, communiquer des données au navigateur du poste client. Le protocole HTTP ayant été conçu pour transporter des requêtes et des données à l'initiative du client, il s'agit de réfléchir à une forme d'inversion du modèle de communication. C'est ce à quoi s'attellent aujourd'hui différents groupes de travail dans la communauté des développeurs Ajax.

En l'état, il existe des solutions techniques. D'abord signalons que le protocole HTTP prévoit déjà la possibilité de provoquer le rechargement périodique d'une page depuis le serveur. En utilisant une balise:

META HTTP-EQUIV="Refresh" CONTENT="n"

on indique au navigateur de recharger et réafficher toute la page en question toutes les « n » secondes. Le rafraîchissement périodique de toute la page ne fait cependant pas forcément l'affaire de scripts Ajax du côté client, qui peuvent n'être intéressés que par une partie des données qui y figurent, qui doivent conserver un état de l'interaction avec l'utilisateur à un instant donné (état qui serait perdu au rafraîchissement suivant), voire pour lesquels la périodicité d'un rafraîchissement est inutile ou incohérente.

D'autres solutions ont donc été recherchées pour inverser l'initiative de la communication. Une première repose sur l'utilisation des protocoles des serveurs de messagerie instantanée (IM). Dans ces serveurs, la gestion de la présence des interlocuteurs est à l'initiative du serveur lorsqu'un nouvel utilisateur se déclare prêt à recevoir les messages. Nous avons tous l'expérience des messageries instantanées MSN dans lesquelles les interlocuteurs disponibles apparaissent dans nos « buddy lists » dès qu'ils se connectent. Dans l'esprit Ajax, et XML en particulier, le projet ouvert Jabber est particulièrement intéressant. La Jabber Software Foundation a mis au point un protocole basé sur XML pour le développement de services de messagerie instantanée (au sens le plus large). Cette spécification, XMPP pour Extensible Messaging and Presence Protocol, vise à décrire les bases d'un protocole de communication en temps réel qui emploie des messages au format XML. Il y également une communauté active de développeurs travaillant à des extensions du protocole pour des applications spécifiques. Beaucoup de clients Jabber sont aujourd'hui disponibles, en versions Open Source et commerciales, implémentés dans différents langages de programmation, y compris JavaScript (par exemple, JSJaC). Il y a également des implémentations facilement accessibles de serveurs Jabber comme Jabberd, ejabberd et WildFire. L'intégration d'une librairie cliente Jabber en JavaScript à un programme Ajax permet, en liaison avec un serveur XMPP, de mettre en place, au prix certes d'un environnement un peu plus lourd et complexe du côté serveur, un véritable canal de communication bidirectionnel entre une page Web et des services Web. Dans cette architecture, on détourne la gestion de la présence fournie par le serveur Jabber pour envoyer, à l'initiative du service Web, des données à la page affichée par le navigateur. Ces données, enveloppées dans un message XMPP issu du serveur vers le navigateur, appellent au vol un script JavaScript dans la page qui récupère ainsi résultats et données pour affichage et présentation à l'utilisateur.

On peut également se passer de cette couche supplémentaire IM et simuler cette méthode de communication avec JavaScript et XMLHttpRequest seuls. Une première idée consiste à implémenter le service de façon à ce qu'il maintienne la connexion HTTP ouverte tant qu'il n'a pas fini de communiquer ses données à la page Web qui l'héberge. Chaque nouvelle donnée disponible sur le serveur est glissée dans un script JavaScript, qui, exécuté dans le navigateur client, va rafraîchir la page avec la nouvelle valeur et celle-là seulement. Le fragment de PHP suivant :

<div id="news"></div>
<?PHP
while( $n > 0 ){
?>
<script language="javascript">
$("#news").html( '<? echo getLatest(); ?>' );
</script>
<?PHP
ob_end_flush();
sleep( 1 );
}
?>

rajoute n copies du même script JavaScript, à la valeur de la donnée près (fournie par le résultat de l'appel getLatest(), dans l'exemple). Ces scripts modifient simplement le contenu de la balise « div » qui les précède. Vu de l'utilisateur, le contenu de la page change une fois par seconde, plus ou moins la latence, avec la succession des n valeurs distinctes. Il n'y a qu'un aller-retour HTTP et pas de rechargement de page. Cette forme de « page streaming » présente le double inconvénient de maintenir le navigateur en mode lecture tant que le service n'est pas terminé (la barre d'attente dans le bandeau inférieur du navigateur progresse donc lentement pendant le défilement des n valeurs des données) et la page Web elle-même s'allonge au fur et à mesure de l'accumulation des copies du même script successivement appelés avec des valeurs différentes. Si l'on songe à des pages Ajax même moyennement complexes qui contiennent une grande quantité de données variables, ces inconvénients peuvent rapidement tourner au blocage complet du client et du service.

Pour pallier ces difficultés, on peut songer à charger la page une fois pour toute en affichant la première valeur de la donnée appelée à varier par la suite. Ensuite, la page Web émet une requête XMLHttpRequest vers le serveur pour récupérer les nouvelles données. Cet appel est asynchrone et le script reprend la main juste après l'émission de la requête, pour lancer, via un « timer » une interrogation périodique de l'état de la requête. Une « astuce » dans l'implémentation de l'objet XMLHttpRequest permet de récupérer le contenu de la réponse avant qu'il ne soit complet. Le fameux champ reponseText est accessible et à jour dans l'intervalle de temps séparant l'émission de la requête de la réception de la réponse complète. Il suffit alors d'en extraire uniquement la dernière donnée pour reconstituer sur le poste client la file d'attente des données construite en parallèle sur le serveur. Illustrons :

1) du côté du serveur, notre service émet à intervalle régulier une balise XML « message » contenant la donnée réactualisée, en PHP :

$n = 50;

function getXMLData(){
global $n;
return '<message>'.$n .'</message>';
}

while( $n > 0 ){
sleep( 1 );
echo getXMLData();
ob_end_flush();
$n = $n - 1;
}


qui envoie la suite décroissante de nombres depuis 50.

2) du côté du client, le script charge la page et affiche la première valeur (50), émet une requête XMLHttpRequest qui se terminera lorsque notre code PHP aura construit ses 50 messages XML, puis immédiatement se met à demander périodiquement l'état de la réponse, en JavaScript :

var xhReq = createXMLHttpRequest();
window.onload = function() {
setInterval(pollLatestResponse, 250);
xhReq.open("GET", "service.php", true);
xhReq.onreadystatechange = function() {
if (xhReq.readyState==4)
{ /*On ignore la notification de fin d'exécution*/ }
}
xhReq.send(null);
}

dans lequel la fonction pollLatestResponse, appelées toutes les 250 ms, extrait la dernière balise « message » du champ responseText de la requête.

Vu de l'utilisateur, dans cette forme de « service streaming », la page affiche successivement la suite de valeurs de 50 à 1 comme si le serveur lui envoyait la nouvelle valeur au fur et à mesure de son exécution. En fait, dans un exemple réel il est fort probable que la connexion soit rompue d'autorité par le protocole HTTP après un timeout, puisque la page a été chargée d'emblée et en une fois au début. La synchronisation du timer dans les scripts Ajax et de l'exécution des services sur le serveur pose alors de vrais problèmes.

Ces solutions présentent toutes mérites et inconvénients. Aujourd'hui le temps est à la réflexion et au développement d'une authentique architecture de middleware par échange de messages pour Ajax. La Dojo Foundation, célèbre pour son « Ajax Toolkit » utilisé dans nombre d'applications Web 2.0, a lancé le projet cometd qui vise à fournir une librairie de communication à base de messages entre applications Web sous Ajax. Sur ce sujet, signalons encore la spécification Bayeux - un hommage à la tapisserie mais un curieux anachronisme qui verrait le héros, rempart des Achéens, fils de Telamon voisiner avec Guillaume le Conquérant, mais enfin, passons - dans laquelle les messages envoyés pour synchroniser client et serveur utilisent JSON. La spécification est en cours d'élaboration mais vise ambitieusement à donner à Ajax une base de communication par messages comparables aux produits comme MQSeries et Tibco. Une API de messagerie permettrait aux applications Ajax de passer les portes de l'informatique d'entreprise et donnerait probablement naissance à une nouvelle génération de services et d'applications Web interactives.

ShareThis