Après Ajax, le « Reverse Ajax »…et le Grizzly !

le 04/09/2008 par Olivier Mallassi
Tags: Software Engineering

- " Encore plus fort ! ce n'est plus ton browser qui demande de l'information. C'est ton serveur qui lui remonte, lui push de l'information et le tout en technologie web ! " - " oulà, oulà...tu ne serai pas en train de me prendre pour un gars sorti de sa campagne toi ?! "

En fait, si un peu mais pas tant que ca non plus ;-)

Le besoin est réel et tend à se démocratiser sur le web : informer l'utilisateur de la modification d'information au niveau du serveur, en (quasi) temps réel. Les cas d'utilisations sont tout autant réels. Les exemples classiques sont Gmail et Gtalk. L'iphone 3G en a fait un argument publicitaire. Des applications d'alarming ou de monitoring en web peuvent en avoir l'utilité, les plateformes de " e-trading " ou certaines applications de call-center également.

Il existe 3 solutions pour " remonter " de l'information au client

Le poll. Dans ce cas, la remontée d'information du serveur vers le client est simulée. En effet, dans le cas du poll ou pull, le client exécute une requête vers son serveur à intervalle de temps régulier, espérant ainsi être mis au courant d'une mise à jour. Cette approche montre les limitations suivantes : <br><br>- Certains évènements peuvent être manqués si l'intervalle entre deux requêtes est trop long<br>- A l'inverse, il est tout à fait possible de réaliser un appel " à vide " si aucun évènement n'est disponible<br>- Un intervalle de temps trop court entre deux requêtes aura tendance à (sur)charger le serveur
Le Streaming http. A l'exact opposé du poll, le streaming http repose sur l'utilisation de connexion http persistantes, ie. qui ne sont jamais relâchés. Les informations de réponse sont écrites au fur et à mesure de leurs arrivées. Très usitée par les applications multimedia (vidéo, audio...), la mise en oeuvre de cette solution demande néanmoins une infrastructure adaptée aux connexions permanentes et il n'est pas évident que tous les proxys (en entreprise surtout) l'accepte.
Le Reverse Ajax ou Comet. A mi chemin entre les précédentes solutions, les termes " Comet " et son synonyme " Reverse Ajax " ont été introduit Alex Russel et Joe Walker et décrivent un mode de fonctionnement où le serveur " remonte " de l'information au client sans que ce dernier ne l'ait expressément demandée. Ce mode de fonctionnement est en fait basé sur le principe du poll sauf que la requête http peut rester en attente, d'un évènement ou du timeout, auquel cas, cette dernière se " clôt " classiquement. Ce mécanisme de long-polling repose - et cela de manière à assurer la scalabilité - sur le découplage des notions de requêtes http et des notions de Threads : il faut être capable de mettre en attente certains clients http, tout en exécutant des services - donc des threads - sans pour autant avoir autant de threads que de clients http potentiels...bref, vu d'avion ca n'a déjà pas l'air simple...

L'élément différentiant entre ces solutions reste la fréquence de mise à jour de la donnée et l'importance qu'à la fraîcheur de cette donnée pour le client.

Comet ou le "long polling"

In web development, Comet is a neologism to describe a web application model in which a long-held HTTP request allows a web server to push data to a browser, without the browser explicitly requesting it

Source: wikipedia

Contrairement à ce que peut laisser penser la première partie, cette problématique de " long-polling " impacte à la fois le client et le serveur :

  • Le client car il faut gérer l'ouverture et surtout la réouverture des connexions http - lors de la fermeture normale ou du timeout -.
  • Le serveur car il faut gérer ces histoires de long polling et de découplage requête/thread sans avoir à trop mettre les mains dans le cambouis...

Il existe aujourd'hui plusieurs solutions open source implémentant les concepts de Comet. La plupart de ces solutions adressent " uniquement " - en même temps c'est déjà beaucoup... - la partie serveur, et ce de manière plus ou moins light en terme d'APIs proposées :

  • Tomcat 6 propose les NIO - advanced IO. Côté code, Tomcat propose uniquement une interface CometProcessor que devra implémenter votre servlet. A vous de jouer avec les threads pour écrire les messages sur les HttpServletResponse souhaitées. La configuration de ces Advanced IO, sans être insurmontable n'est pas immédiate : il est nécessaire de modifier la configuration du connecteur http au niveau du fichier server.xml et de rajouter une dll dans la variable java.library.path
  • Jetty 6 propose une API un peu plus haut niveau que celle de Tomcat. Jetty introduit ainsi le concept de continuations, un objet appelable depuis une servlet et qui permet de suspendre la requête http en cours et de libérer le thread courant.
  • DOJO et le protocole de Bayeux. DOJO est l'initiateur du protocole de Bayeux - j'avoue ne pas avoir trouver de relation avec la ville de Bayeux et ces tapisseries, en même temps il n'y a certainement aucune relation...Bref, Dojo propose une API cliente JavaScript permettant d'interagir avec un serveur implémentant ce fameux protocole de Bayeux. Glassfish - via Grizzly - étant un des serveurs implémentant ce protocole, il est tout à fait possible d'imaginer un client utilisant DOJO et s'interfaçant avec Glassfish.
  • Grizzly/Glassfish. Glassfish dans sa version 3 embarque le moteur http Grizzly et propose une solution " out of the box ". Ce moteur propose un support de comet soit via
    • Une implémentation du protocole de Bayeux.
    • Une API masquant les mécanismes " Asynchronous Request Processing " disponible dans Grizzly.La mise en oeuvre de comet dans glassfish est simplissime et consiste à modifier le fichier domain.xml.

Certaines solutions de développement - a contrario des solutions portées par le serveur d'application ou le conteneur de servlet présentés précédemment - intègrent cette problématique :

  • Côté GWT, on peut noter l'initiative Rocket/GWT qui encapsule ces problématiques dans des APIs java client et serveur
  • Côté Flex, Adobe BlazeDS (et son équivalent payant) propose des mécanismes de " push serveur ". Le principe réside dans la définition de " channel " auxquels s'abonnent et publient les clients
  • Côté JSF, IceFaces annonce une solution de " push server " alors que jBoss RichFaces ne fournit que le poll

Le protocole de Bayeux : une tentative de standardisation des échanges type " push "

Le protocole de Bayeux, poussé par DOJO, vise une normalisation permettant de délivrer des évènements, au format JSON, entre des clients et un serveur, des clients et des clients (via le serveur..), sur la base du modèle publish/subscribe.

Ce protocole repose " principalement " sur :

  • La notion de " channel ", une destination déterminée par un nom logique (ou plus exactement une uri) sur laquelle seront poussés des messages, eux-mêmes consommés par les clients ayant souscris à ce channel. Formuler autrement, ce " channel " peut être vu comme le sujet de conversation que les clients vont partager. Ce protocole définit de plus des " channels " techniques pour gérer l'abonnement et le désabonnement des clients...
  • Les messages encodés en JSON et proposant quelques propriétés intéressantes comme le channel, un identifiant unique du client...

Ainsi dans ce protocole, un exemple de message est :

/*[{"channel":"/cometd/myChannel","successful":true,"clientId":"2d714396f339f90d","id":"6"},
{"channel":"/cometd/myChannel","data":{"test":"one 
message"},"id":"6","clientId":"2d714396f339f90d"}]*/

Ce protocole ne définit pas d'API cliente ou serveur mais décrit seulement les mécanismes d'échanges entre serveur et clients dans le cadre d'échange de type " push ".

Mise en oeuvre...

...avec les APIs Grizzly/glassfish

Loin de moi l'envie de vous refaire un tutorial complet sur Grizzly. Reste que ces APIs sont assez simples et proposent les principaux concepts suivants :

  • CometEngine. C'est par la que tout commence et ce moteur permet de gérer des CometContext
  • CometContext. Cet objet CometContext est associé à une URL - en fait l'url où seront faits les GET et les POST... - et gère un ensemble de CometHandler
  • CometHandler. On peut voir cet objet comme un " client ". Une nouvelle instance du CometHandler est associé au context lors de la méthode doGet() de votre servlet et tout ou partie de ces handlers sont notifiés sur le doPost()...

En terme de code, les APIs sont utilisées dans une servlet comme suit :

  • Le CometContext est initialisé dans le méthode init() de votre servlet
public void init(ServletConfig config) throws ServletException {... 
CometEngine engine = CometEngine.getEngine(); 
CometContext cometContext = engine.register(contextPath);
  • Un nouvel handler est instancié dans la méthode doGet(), donc pour chaque requête, et associé au CometContext. Chaque requête est ainsi mise en attente (regardez avec FireBugs ;-) )
protected void doGet(HttpServletRequest req, HttpServletResponse resp)   throws ServletException, IOException { 
    SampleCometHandler handler = new SampleCometHandler(); 
    handler.attach(resp); 
    CometContext context = CometEngine.getEngine().getCometContext(contextPath);   
    context.addCometHandler(handler); 
}
  • Lors d'un POST, le paramètre company est récupérer de la requête, l'ensemble des handlers associés au CometContext est notifié. A noter qu'il est possible dans ce cas de ne notifier qu'un seul handler (et donc un seul client)
protected void doPost(HttpServletRequest req, HttpServletResponse resp)  throws ServletException, IOException { 
    CometEngine engine = CometEngine.getEngine(); 
    CometContext context = engine.getCometContext(contextPath);
    context.notify(req.getParameter("company"));  
...
  • L'implémentation du handler peut se faire de la manière suivante. Lors de la notification, chaque handler écrit le CometEvent (ie. le message) sur la HttpServeltResponse qui lui est associée.
private class SampleCometHandler implements   CometHandler {

  public void onEvent(com.sun.grizzly.comet.CometEvent event)    throws IOException {   
    if (event.getType() != CometEvent.NOTIFY) {    
    // le client a fait le post    
    this.response.getWriter().write("message: " + event.attachment()); 
    this.response.getWriter().flush();
    // supprimer cette ligne pour faire du streaming
    event.getCometContext().resumeCometHandler(this);
}
}

Reste maintenant à implémenter un " petit " client, simplement en JavaScript et de la manière suivante :

  • Une requête client réalise un GET et attend
function doGet(){
  new Ajax.Request('/CometSample/CometSampleServlet', {
   method:'get',
   onSuccess: function(transport){
     $('result').update(transport.responseText);
    }
  });
 }
  • Un autre client poste son message
function doPost(){
  new Ajax.Request('/CometSample/CometSampleServlet', {
  method:'post',
   parameters: {company:'octo' },
  onSuccess: function(transport){...}
 });
}

...de l'implémentation Grizzly du protocole de bayeux

Grizzly fournit une implémentation du protocole de Bayeux via une servlet " générique " qu'il est simplement nécessaire de configurer au niveau du fichier web.xml

<servlet>
        </servlet><servlet -name>Grizzly Cometd Servlet</servlet>
        <servlet -class>com.sun.grizzly.cometd.servlet.CometdServlet</servlet>
        <init -param>
            <param -name>expirationDelay</param>
            <param -value>60000</param>
        </init>
        <load -on-startup>1</load>
    
    <servlet -mapping>
        </servlet><servlet -name>Grizzly Cometd Servlet</servlet>
        <url -pattern>/cometd/*</url>

Côté client, le framework DOJO fournit une extension JavaScript permettant de s'abonner à un channel et de publier des messages.

  • Concernant la souscription
<script type="text/javascript">
    	dojo.require("dojox.cometd");
    	dojox.cometd.init("http://localhost:8080/CometSample/cometd" );
    	dojox.cometd.subscribe("/cometd/myChannel", function(msg)
    	{
    		$('result').update('message received : ' + msg.data.test);
    	});
</script>

<ul>
	<li>Concernant la publication</li>
</ul>
var data = $F('text2Send');
dojox.cometd.publish('/cometd/myChannel',{test: data });

Les limitations de Comet

La principale limitation à la mise en oeuvre de Comet vient en fait des browsers. Ces derniers ne permettent en général de n'ouvrir que deux connexions http par domaine. En fait, c'est même conseillé dans la norme HTTP/1.1. Cet article de 2006 donne pas mal de détail sur cette limitation. J'ai pu tester cette limitation (sur Firefox 3.0.1) sur mes exemples : lorsque deux de mes browsers ouvraient des connexions de type GET sur le domaine localhost :8080, la seconde connexion GET était systématiquement mise en attente...en remplaçant simplement localhost par mon IP (et donc en changeant de domaine), le problème se résout. Au-delà de l'anecdote, cette limite de deux connexions pose un problème avec le long-polling qui monopolise une des deux connections mais il y a fort à parier que les constructeurs vont résoudre le problème, IE8 devrait déjà porter le nombre de connections à 8...

Pour conclure...

L'API Grizzly me parait très naturelle à utiliser et masque vraiment les problématiques de manipulation de thread ; elle gagnerait à fournir une petite librairie JavaScript permettant d'automatiser l'ouverture de la connexion ainsi que la reprise en cas de timeout... On peut de plus regretter la forte imbrication Grizzly/Glassfish : il semble délicat - pour avoir essayé, rapidement certes... - d'imaginer utiliser le moteur http Grizzly dans Tomcat... A l'inverse, DOJO fournit un client JavaScript bien pensé - au travers de son extension dojox.cometd - et l'implémentation du protocole de Bayeux par Grizzly a cela de génial qu'elle ne demande - pour les cas nominaux d'utilisation - pas de développement supplémentaire : juste la configuration d'une servlet. Reste que ces solutions sont encore artisanales - au sens étymologique, donc positif. Les APIs aussi bien côté serveur que client ne sont pas encore définies mais il faut noter que les mécanismes de " push server " seront standardisés dans la version 3.0 de Servlet - donc les containeurs de servlet JEE6 -. Reste enfin que les cas d'application dans l'informatique d'entreprise ne sont pas forcément des plus répandus et aujourd'hui plutôt implémentés par des solutions RDA, mais demain qui sait ? ;-)