Friday, August 26, 2011

Servlet Asynchrones et Programmation Comet

HTTP est un protocole sans état dont l'initiative de la connexion revient au client. Ses défauts font également ses forces puisqu'ils lui confèrent une capacité de montée en charge sans limite et sa tolérance aux pannes. Evidemment, il a fallu le faire évoluer le protocole (version 1.1) et développer des pis-aller pour répondre aux besoins des applications Internet. C'est ainsi que se sont développés des protocoles de streaming, la gestion des informations d'états dans les serveurs d'applications ou à AJAX. Voilà sans doute pourquoi WebSocket...

Mais avant que HTML5, Websocket et Java EE 7 ne deviennent le standard, les applications web auront déjà replacées la plupart des applications de bureau, jusqu'aux suites bureautiques. L'emploi de bibliothèques clientes comme JQuery répond avec succès et de manière quasi transparente pour le développeur à un nombre très important de cas d'utilisation des applications riches sur le client. Ces biblothèques servent de briques et de ciment aux composants graphiques compatibles Java EE, au premier rand desquels les bibliothèques JSF comme PrimeFaces.

Pourtant, il faut pouvoir utiliser, sur le serveur, des techniques de programmation adaptées à des batteries de composants clients. Par exemple, il faut savoir gérer le cas où le serveur est à l'initiative d'envoie de messages aux clients. Ce type d'utilisation est nécessaire pour une messagerie instantanée, des enchères, des alarmes ou des notifications. Les servlets asynchrones offrent donc une solution standard pour adresser ce besoin jusqu'alors traité de manière spécifique en Java. Cet article présente cette nouvelle fonctionnalité ainsi qu'un exemple de mise en oeuvre simple...

Notifications serveurs sur HTTP

Si on enlève l'approche ou la notification est transportée à l'aide d'une autre requête (aka piggy-back), il existe 2 types de solutions, côté client, pour supporter l'envoi de message du serveur via HTTP. La première et la plus évidente consiste à mettre en oeuvre un "polling par interval régulier". Celui-ci vérifie si un message est à traiter sur le serveur.  Cela fonctionne comme décrit dans le schéma ci-dessous :

L'inconvénient de ce type d'approche est que la latence dépend de l'intervalle de polling et donc du nombre de requêtes. D'autre part, une requête peut coûter cher quand il s'agit de rétablir les informations de session, la sécurité, etc... Les ressources nécessaires sur le serveurs sont donc assez vite importantes et/ou la latence mauvaise.

Le principe de programmation Comet, aussi connu sous les noms de streaming (quand la requête ne s'arrête jamais), de "polling long" ou de reverse-Ajax est radicalement différent ; il consiste à effectuer une requête HTTP longue. Lorsqu'un message doit être envoyé par le serveur, la réponse HTTP est alors utilisée par le serveur pour envoyer son message. Ce principe est présenté sur le schéma ci-dessous :
Les avantages de cette approche par rapport à la précédente sont nombreux et notamment en terme de latence et en terme de ressources utiles pour établir la connexion ou vérifier la sécurité. C'est dû à un nombre de requêtes beaucoup plus faible. Et pourtant... si dans le modèle Java EE, chaque requête est traité par une servlet, ce modèle à 2 inconvénients :
  • D'abord cette requête ne permet de ne traiter que les données serveurs vers clients : c'est la réponse qui est longue ; pas la requête. Il faut donc utiliser au moins un canal de plus pour envoyer les demandes du client.
  • Ensuite, et c'est la clé, si les servlets sont synchrones comme le veut la spécification 2.5, chaque servlet doit être active pour envoyer sa réponse et monopolise donc 1 thread Java et la mémoire correspondante sur le serveur. Si vous avez 200 utilisateurs concurrents, vous avez donc 200 threads qui fonctionnent pour envoyer des messages aux clients auxquels s'ajoutent les threads des servlets utilisées pour répondre aux requêtes clientes.

Ainsi naquit la servlet 3.0 Asynchrone

Evidemment, il y a d'autres cas d'utilisation que celui-ci ; ce n'est pas le propos ! Pour répondre au besoin de ne pas monopoliser des threads, et donc des ressources, sur le serveur alors que celui-ci n'a pas d'opération à effectuer, a été introduit la notion de servlet asynchrone. Le principe, dans la cas de la programmation Comet est dessiné ci-dessous :
Lorsque un client demande l'exécution d'une servlet à l'étape (1), la demande est prise en compte et la servlet s'exécute dans le pool de threads prévu à cet effet comme pour une servlet synchrone. Cependant, alors que le cycle de vie d'une servlet synchrone s'arrête à l'étape (2) par une réponse au client lorsque les opérations sont terminée, une servlet asynchrone peut passer son contexte d'exécution (AsyncContext) à d'autres composants.

Dans notre exemple, nous gardons une référence à ce contexte dans le moteur de servlet mais vous pourriez tout à fait l'intégrer dans un autre composant tant qu'il s'exécute dans le serveur d'applications. Vous pourrez le manipuler depuis un pool de threads que vous auriez conservé à cet effet ou un EJB MDB. Dans les étapes suivantes (3) et (3'), des composants peuvent envoyer des messages au client par l'intermédiaire du contexte d'exécution asynchrone de la servlet auquel ils ont accès. Un evènement final (4) mettra fin à ce dialogue et le contexte d'exécution de la servlet sera terminé (complete) à ce moment (5).

Un programme exemple

L'explication est assez simple mais rien ne vaut un exemple... Voici donc un code qui illustre le cas décrit précédemment. Il est composé de 4 classes, la première MyContextListener ajoute un listener au contexte d'utilisation des servlets afin de stocker une structure vide qui référence l'ensemble des contextes de servlets asynchrones en cours d'utilisation :
package com.arkzoyd.asyncservlet;

import java.util.ArrayList;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import java.util.List;
import javax.servlet.AsyncContext;

@WebListener
public class MyContextListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent sce) {
      List<AsyncContext> registeredServlets = 
              new ArrayList<AsyncContext>();
      
      sce.getServletContext()
              .setAttribute("registeredServlets", registeredServlets);
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
    }
    
}
Ce listener est enregistré automatiquement dans l'application du fait de l'annotation @WebListener.

Le second composant est également un listener ; il s'agit du listener qui gère les évènements associés à la servlet asynchrone. Dans ce listener on gère en particulier le dés-enregistrement et la fin du contexte de la servlet asynchrone en cas de timeout :
package com.arkzoyd.asyncservlet;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import java.util.List;

public class MyServletListener implements AsyncListener {

    @Override
    public void onComplete(AsyncEvent event) throws IOException {
    }

    @Override
    public void onTimeout(AsyncEvent event) throws IOException {
        AsyncContext actx=event.getAsyncContext();
        List<AsyncContext> registeredServlets = 
              (List<AsyncContext>) actx.getRequest()
                      .getServletContext().getAttribute("registeredServlets");
        registeredServlets.remove(actx);
        PrintWriter out = actx.getResponse().getWriter();
        out.println((new Date()).toString());
        out.flush();
        actx.complete();
    }

    @Override
    public void onError(AsyncEvent event) throws IOException {
    }

    @Override
    public void onStartAsync(AsyncEvent event) throws IOException {
    }
    
}
La troisième classe est la servlet asynchrone elle-même nommée Subscribe.java. La déclaration se fait à travers le paramètre asyncSupported=true. Dans un vrai cas on formatera le message de sorte qu'il puisse être géré par un composant actif sur le client :
package com.arkzoyd.asyncservlet;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Date;
import javax.servlet.AsyncContext;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;

@WebServlet(name = "subscribe", urlPatterns = {"/subscribe"}, asyncSupported = true)
public class Subscribe extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        ServletContext ctx = request.getServletContext();    
        AsyncContext actx = request.startAsync();

        actx.addListener(new MyServletListener());
        actx.setTimeout(1*60*1000);
        
        List<asynccontext> registeredServlets = 
                (List<asynccontext>) ctx.getAttribute("registeredServlets");
        
        registeredServlets.add(actx);
        
        response.setContentType("text/plain; charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println((new Date()).toString());
        out.flush();

    }
}
Enfin pour que cet exemple soit complet, vous créez une servlet de "publication" ; en général on préfèrera une méthode différente comme un EJB MDB ou un service Web. Cette servlet (synchrone celle-là) accède à tous les contextes des servlets asynchrones et envoie un message sur les réponses HTTP correspondantes. Cette servlet s'appelle Publish.java :
package com.arkzoyd.asyncservlet;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import javax.servlet.AsyncContext;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(name = "publish", urlPatterns = {"/publish"})
public class Publish extends HttpServlet {


    @Override
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response)
            throws ServletException, IOException {
        
        ServletContext ctx=request.getServletContext();
        List<AsyncContext> lactx = (List<AsyncContext>) 
                ctx.getAttribute("registeredServlets");
        long client=0;
        for (AsyncContext i: lactx) {
            PrintWriter out = i.getResponse().getWriter();
            out.println("Push!!!");
            out.flush();
            client++;
        }
        
        response.setContentType("text/plain;charset=UTF-8");
        PrintWriter out = response.getWriter();
        try {
            out.println("The message has been published to  " + 
                    (new Long(client)).toString() + " clients");
        } finally {            
            out.close();
        }
    }
}

Tester l'exemple

Il ne reste plus qu'à tester cet exemple ; lancer quelques clients dans différents navigateurs en exécutant la servlet asynchrone subscribe comme ci-dessous :
Une fois le client lancé, déclenchez des évènements sur le serveur à l'aide de l'autre servlet publish comme ci-dessous :

Evidemment, c'est très rudimentaire et vous voudrez mettre en oeuvre des cas plus avancés... Ces quelques lignes illustrent toutefois assez bien ce qu'il est possible de faire avec cette fonctionnalité. Et surtout, vous comprendrez l'intérêt en faisant un test de charge !
Pour en savoir plus :
Il existe de nombreux articles intéressants sur le sujet des servlets asynchrones et la programmation Comet :
  • Cet article intitulé Asynchronous processing support in Servlet 3.0 décrit en détail les motivations relatives à l'implémentation des servlets asynchrones et donne des exemples plus complets, notamment en délégant la gestion de ces servlets à un pools de threads (Executor)
  • Le projet CometD de la fondation Dojo implémente un protocole de niveau supérieur dont l'implémentation s'appuie sur le principe de programmation Comet qui s'appelle de Bayeux. Apache Tomcat embarque notamment cette solution pour permettre au serveur de notifier des clients ; Weblogic implémente quant à lui la spécification de Bayeux dans son serveur de Publish-Subscribe.
  • Vous n'avez pas besoin d'attendre Java EE 7 pour développer avec Glassfish et WebSocket, cet article sur le blog de l'Aquarium vous donnera quelques idées à propos du support de Websocket dans Glassfish et Grizzly, lequel offre également une implémentation de Bayeux pour information.
  • Enfin, dernier mais pas le moindre, cet exemple et l'exemple suivant de construction de handlers HTTP asynchrones à l'aide de Grizzly le listener HTTP de Glassfish vous donnera une idée assez précise de comment ça peut se passer quand on regarde en dessous...

No comments:

Post a Comment