Nous verrons dans cet article les limites du principe de Server Push lorsqu’il est implanté avec une API I/O standard (non multiplexée). Pour mieux comprendre, rappelons brièvement le cycle de vie d’une servlet (source).

Cycle de vie d’une Servlet
Le cycle de vie d’une servlet est assuré par le conteneur de servlet (Tomcat, Jetty, Glassfish, etc..). Ainsi afin d’être à même de fournir la requête à la servlet, récupérer la réponse ou bien tout simplement démarrer/arrêter la servlet, il implémente l’interface (servlet) permettant de réaliser :
- Le serveur crée un pool de threads auxquels il va pouvoir affecter chaque requête (ce pool est limité dans la configuration du serveur, pas plus de x threads exécutant la servlet)
- La servlet est chargée au démarrage du serveur ou lors de la première requête
- La servlet est instanciée par le serveur (1 fois)
- La méthode init() est invoquée par le conteneur
- Lors de la première requête, le conteneur crée les objets Request et Response spécifiques à la requête
- La méthode service() est appelée à chaque requête dans une nouvelle thread. Les objets Request et Response lui sont passés en paramètre. Une servlet est donc accessible par plusieurs threads en même temps.
- Grâce à l’objet Request, la méthode service() va pouvoir analyser les informations en provenance du client
- Grâce à l’objet Response, la méthode service() va fournir une réponse au client (puis ferme la connexion)
- La méthode destroy() est appelée lors du déchargement de la servlet, c’est-à-dire lorsqu’elle n’est plus requise par le serveur. La servlet est alors signalée au garbage collector (communément francisé en ramasse-miette)
Geler une requête
Dans le but de « geler » la réponse du coté serveur, on interrompt le traitement de la réponse en « bloquant » le thread (à l’aide d’un verrou en attente sur une ressource - autre que la servlet sinon toutes les requêtes seront bloquées). Autre remarque, les spécifications servlets interdisent d’utiliser un Thread.sleep() dans une servlet. En effet, une servlet est un objet partagé par plusieurs clients (donc plusieurs Threads).
Dans l’exemple ci-dessous, on désire toujours récupérer la 200éme ligne. La servlet se met en attente – wait() – s’il n’y a pas cette dernière.
try{ synchronized ( chatMessages ) { while ( chatMessages.size() < 200 ) try { // on gèle la connexion tant qu’il n’y a pas eu d’événement chatMessages.wait(); } repondre(chatMessages.get(200)); } }catch ( InterruptedException e ) { repondre("Server interrupted. Chat Session Closed."); }
Lorsqu’on client envoie un message, il ajoute ce message à la ligne des messages. Puis, on réveille alors tous les threads endormis (en attente d’un message). Lorsqu’il se réveille, il exécute donc l’instruction juste après le wait()), c’est à dire return(chatMessages.get(200)); qui retourne au client la 200éme ligne, libère le thread et ferme la connexion.
Ci-dessous les instructions de traitement lorsqu’un client envoie un message.
synchronized ( chatMessages ) { chatMessages.add("je suis la 200eme ligne..."); chatMessages.notifyAll(); }
Problèmes
Malheureusement, cela signifie qu’un thread est alloué pour chaque requête gelée (type “server push”). Généralement les serveurs web (Tomcat, Jetty, etc..) ont un nombre limité de threads autorisés (sinon, il serait très facile de faire crasher un serveur). Une fois ce cap atteint, il refuse d’accepter une nouvelle connexion, donc de lancer un nouveau thread. Toute nouvelle requête est simplement ignorée. Le serveur pense qu’il est sous une charge importante, alors qu’il ne l’est pas du tout.
Il est toujours possible de modifier le nombre de threads disponibles pour une servlet, mais ce n’est pas une bonne solution. Aujourd’hui, il existe quelque chose de beaucoup plus robuste et qui supporte très bien la charge. Ça s’appelle Comet. Il est implanté dans Jetty sous le nom de « continuations » ou « CometProcessor » sous Tomcat.
L’idée derrière la continuation est bien sûr de geler les connexions (comme on l’a vu précédemment). Sauf que cette fois-ci on va employer une API de bas niveau pour gérer les entrées/sorties dans le but de n’avoir qu’un seul thread qui gère les requêtes « server push » (au lieu du schéma “1 requête = 1 thread”).
Jusqu’à l’API 3.0 Servlet, le principe de continuation n’existait pas. Chaque conteneur de Servlet le faisait à sa sauce.
On peut citer les deux plus connus :
- Jetty Continuations
- Tomcat Advanced I/O Support (CometProcessor)
ps: l’API Servlet 3.0 est encore en bêta.
Romain LAFOND
