jeudi 31 mars 2011

Une messagerie instantannée ... en JEE 6

Durant ma formation universitaire il m'a été donné plusieurs fois l'occasion de programmer des applications orientées réseau.
Tout d'abord en C afin d'apprendre l'utilisation des Sockets et d'illustrer les cours de réseaux, puis par la suite en Java pour mettre en oeuvre la programmation multi-Thread avec une interface graphique basique.
Le thème récurrent de tous ces exercices est le cas d'une messagerie instantanée permettant l'échange de messages entre différents interlocuteurs.
On ne change pas une technique qui gagne, je remet une pièce dans la machine pour illustrer cette fois une implémentation en ... XHTML/JavaScript reposant sur un serveur Java et une Servlet asynchrone.

La théorie


En théorie rien de bien compliqué:

Le client

Une partie de l'application responsable de l'envoi du commentaire de chaque utilisateur à destination du serveur et en même temps à l'écoute des commentaires en provenance du serveur

Le serveur

L'autre partie de l'application recevant les commentaires de chaque client et renvoyant ces commentaires à l'ensemble des clients connectés


La pratique


En C puis en Java, la technique est assez similaire et consiste à ouvrir une socket client->serveur permettant l'envoi des commentaires ainsi qu'une autre socket serveur->client permettant la restitution des commentaires à chaque client. On pouvait envisagé également l'utilisation des IP de broadcast pour simplifier l'architecture. Mais en JEE, comment implémenter tout cela ?

Le serveur


Une solution de base est trouvée tout simplement avec l'apparition des traitements asynchrones des requêtes proposé par les Servlets Java à partir de la spécification 3.0. Cette Servlet traitera donc de manière asynchrone les requêtes de type GET pour mettre les clients en attente de commentaires, ces derniers étant transmis à la même Servlet par des requête de type POST.

La réception de message


Pour recevoir les messages, la Servlet propose une implémentation de la méthode doPost(...), cette dernière recevant le commentaire d'un client le transmet aussitôt à l'ensemble des clients en écoute via une méthode fireMessage(...) décrite plus bas.

/**
* On répond au POST en transmettant les informations à tous les clients en attente.
*/
@Override
protected void doPost( HttpServletRequest req, HttpServletResponse resp )
throws ServletException, IOException
{
String nickname = req.getParameter( "nickname" );
String message = req.getParameter( "message" );
fireMessage( new PostData( nickname, message ) );
}

Des clients en écoute ?


Et oui, ces derniers on quand à eux fait un appel GET et doivent être en attente, c'est donc la méthode doGet(...) qui fait cela en appelant la méthode startAsync sur l'objet request transmis à la Servlet. Cet appel retourne une instance de AsyncContext que nous allons conserver précieusement dans une file d'attente.

/**
* Un client se tient informé des nouveaux messages, on le place en file d'attente.
*/
@Override
protected void doGet( HttpServletRequest req, HttpServletResponse resp )
throws ServletException, IOException
{
if ( req.isAsyncSupported() )
{
AsyncContext context = req.startAsync();
context.setTimeout( 1 * MINUTE );
contexts.add( context );
}
else
{
log( "Les requêtes asynchrones ne sont pas supportées, dommage!" );
}
}

La notification des clients


Il est temps maintenant de prévenir les clients en attente du dernier message reçu:

/*
* Envoi les message aux clients en attente.
*/
private void fireMessage( PostData postData )
throws IOException
{
while ( !contexts.isEmpty() )
{
AsyncContext context = contexts.poll();
ServletResponse response = context.getResponse();
assert response != null;
response.setContentType( "text/json" );
Writer out = response.getWriter();
String json = postData.toJSON();
out.write( json );
out.flush();
context.complete();
}
}

Dans cette méthode comme dans la méthode doGet(...), on fait appel à un membre nommé contexts. J'ai choisit ici de mettre les clients dans une file d'attente protégée contre les accès concurrents.

/* Ensemble des requétes asynchrones en attente de réponse. */
private Queue<AsyncContext> contexts = new ConcurrentLinkedQueue<AsyncContext>();

Le client


Maintenant que notre serveur est prêt, il convient d'implémenter un client. Pour respecter l'emploi des fondamentaux de cet article, c'est donc une page Web en XHTML/JavaScript qui fera office de client au moyen du navigateur. Cette page devra être capable d'envoyer les commentaires de l'utilisateur et en même temps d'afficher les commentaires reçus.
C'est une technique connu sous le nom d'AJAX qui permettra de gérer l'envoi / reception de messages. J'ai choisit d'utiliser jQuery afin de simplifier son utilisation mais il aurait été tout à fait possible de coder tout cela avec une belle XmlHTTPRequest.

L'envoi d'un commentaire


Pour saisir le commentaire, rien de mieux qu'un petit formulaire.

<div>
<label for="nickname">Pseudo</label><input id="nickname" type="text" value="" tabindex="1" />
<input id="send" type="button" value="Envoyer" tabindex="3" />
</div>
<textarea id="message" rows="5" cols="40" tabindex="2"></textarea>

Puis un peu de JavaScript avec jQuery.

$(document).ready(function() {//onload de la page
$("#send").click(function() {//onclick sur le bouton send
sendMessage($("#nickname").val(), $("#message").val());
});
});
function sendMessage(nickname, message) {
$.post("chat", {
nickname : nickname,
message : message
}).success(function() {
$("#message").val('');
}).error(function() {
alert('NOGO : failure sendind data');
});
}

La réception d'un commentaire


La réception d'un commentaire commence dès le chargement de la page.

$(document).ready(function() {//onload de la page
...
tryToPull();
});
function tryToPull() {
$.ajax({//long polling
url : "chat"
}).success(function(data) {
addMessage(data);
}).error(function() {
alert('Error!');
});
}
function addMessage(data) {
try {
if (data.nickname && data.message) {
$("#messages").html("
" + data.nickname + "
" + data.message + "
");
}
tryToPull();
} catch (e) {
alert(e.message);
}
}

Les données reçues sont au format JSON comme indiqué par le code de la Servlet. jQuery reçoit donc un flux JSON qui devient un objet JavaScript, le fameux data de la méthode addMessage(...)

Conclusion


Une fois l'application chargée dans Tomcat 7 : TADA ! Le chat a encore frappé, voici une illustration de l'utilisation des Servlet 3.0 pour le traitement asynchrone des requêtes.
Remarque n°1: Il faut utiliser 2 navigateurs différents pour être sûr d'avoir 2 clients.
Remarque n°2: Je n'ai pas écrit de fichier web.xml car c'est devenu optionnel avec JEE 6.