:: Enseignements :: Master :: M1 :: 2016-2017 :: Programmation d'applications réseaux ::
[LOGO]

TCP non bloquant (mieux)


Dans ces exercices, nous allons reprendre le premier exercice du TP précédent. Ces exercices ont plusieurs buts:
  1. Contrairement à ce que nous avons fait dans la première version, nous allons écrire un serveur qui peut être à la fois en OP_READ et en OP_WRITE. La première version naïve alternait entre le mode de lecture et le mode d'écriture.
  2. Nous allons présenter une architecture flexible qui peut être la base de n'importe quel serveur.
  3. Nous verrons aussi comment adapter cette architecture pour permettre de fermer les connections inactives.
  4. Enfin, nous verrons comment on peut faire interagir un tel serveur non bloquant avec une autre thread, par exemple pour gérer des commandes via la console.


Exercice 1 - Mini aditionneur TCP non-bloquant (Nouvelle version)

Pour rappel, on souhaite écrire un serveur qui accepte de multiples clients en non-bloquant et qui permet, pour chacun d'entre-eux, d'effectuer successivement l'addition des couples d'entiers transmis par chaque client sur sa connexion et de lui renvoyer les sommes correspondantes.

Plus précisément, le client envoie deux int en Big Endian. Le serveur renvoie un int en Big Endian, valant la somme des deux entiers reçus. Par exemple, si le client envoie 7 et 10, le serveur renverra 17. Et ainsi de suite, jusqu'à ce que le client ferme le flot de communication en écriture vers le serveur.

Le principe de l'architecture est globalement de tout faire dans l'objet qui est attaché à la clé. On définit ainsi une classe Context qui va contenir:
  1. Un buffer de lecture in et un buffer d'écriture out.
  2. Un boolean inputClosed permettant de mémoriser si la connection a été fermée par le client.
  3. La référence de la SelectionKey à laquelle le Context est attaché.
  4. La référence de la SocketChannel correspondant à la clé.
De plus, la classe Context contient les méthodes doRead,doWrite, updateInterestOps et process. La convention pour toutes les méthodes est que les buffers in et out sont en mode d'écriture (write mode).

Le fichier ServerSumNew.java contient la base de cette architecture. Par simplicité, la classe Context est une classe interne de la classe ServerSum mais vous pouvez tout à fait en faire une classe séparée.

Les méthodes doRead et doWrite sont déjà implémentées. Il vous ne vous reste plus qu'à implémenter updateInterestOps et process.
La méthode updateInterestsOps met à jour le champ interestOps de la clé en se basant que sur la valeur la valeur des champs de l'objet Context.
La méthode process produit, si possible, à partir des données de in celles qui doivent être placées dans out.

Vous pourrez tester votre serveur avec le jar ClientTestServerMulti.jar.
java -jar ClientTestServerMulti.jar localhost 7777
    

Si tout est correct, vous devriez voir, après une dizaine de secondes, un affichage du type:
Client 2 : finished receiving
Client 2 : waiting for server to close connection
Client 2 : finished writing
Client 4 : finished receiving
Client 4 : waiting for server to close connection
Client 4 : finished writing
Client 0 : finished receiving
Client 0 : waiting for server to close connection
Client 0 : finished writing
Client 1 : finished receiving
Client 1 : waiting for server to close connection
Client 1 : finished writing
Client 3 : finished receiving
Client 3 : waiting for server to close connection
Client 3 : finished writing
Everything is OK.     
 

Exercice 2 - Tuer les clients inactifs

Lorsque un client a été accepté par le serveur mais qu'il n'est plus actif depuis une durée jugée suffisante (TIMEOUT) on aimerait que le serveur mette fin à cette connexion.

L'idée est de rajouter un compteur dans chaque Context qui compte le temps (en millisecondes) écoulé depuis la dernière fois que la clé a été sélectionnée.
Concrètement, on ajoute deux méthodes:
  • resetInactiveTime qui remet le compteur à zéro.
  • addInactiveTime(long time,long timeout) qui augmente le compteur de time et ferme la connection si on dépasse timeout.

Ces méthodes sont utilisées dans la boucle de sélection de la manière suivante.
  • Dans la méthode processSelectedKeys, on remet les compteurs à zéro pour les clés sélectionnées.
  • A chaque tour de boucle de sélection, on calcule le temps passé dans la boucle puis on l'ajoute à tous les compteurs. Dans le code ci-dessous, ceci est réalisé par la méthode updateInactivityKeys qui va parcourir l'ensemble de toutes les clés selection.keys() et appeler la méthode addInactiveTime.
    long startLoop = System.currentTimeMillis();
    selector.select(TIMEOUT/10);
    processSelectedKeys();
    long endLoop = System.currentTimeMillis();
    long timeSpent = endLoop-startLoop;
    updateInactivityKeys(timeSpent);
    

Exercice 3 - Interagir avec la console

Typiquement, il est fréquent de devoir interagir en mode console avec un serveur qui tourne... Le problème est que lorsqu'on lit ces commandes sur un terminal, il faut le faire dans une thread différente de celle qui rend le service (dans le cas où il n'y en a qu'une seule comme pour notre serveur non bloquant).

Il faut être très vigilant sur le fait que la thread qui invoque le select() a des moyens très limités d'interagir avec les autres thread, comme un listener de commande par exemple. Comment faire, par exemple, pour "réveiller" la thread qui est dans un select() bloquant? La seule manière de le faire proprement est d'invoquer la méthode wakeup() sur le sélecteur depuis la thread listener. Charge alors à la thread réveillée du select de prendre en compte les informations que l'autre thread voulait lui transmettre.

Pour illustrer cette situation, vous allez outiller votre serveur avec une méthode startCommandListener(InputStream in) qui sera appelée avant de lancer le serveur par la méthode launch. Cette méthode startCommandListener() pourra accepter, dans une thread dédiée et depuis l'entrée standard, les commandes suivantes, pour les faire prendre en compte par le serveur:
  • "HALT" : Quitte le serveur le plus vite possible (le serveur, pas la JVM: ne faites pas System.exit() !)
  • "STOP" : Quitte le serveur dès qu'il aura fini de traiter les clients en cours (n'en accepte plus de nouveaux)
  • "FLUSH" : Tue dès que possible tous les clients connectés, mais laisse le serveur en vie pour qu'il en accepte de nouveaux
  • "SHOW" : Donne des informations sur les clients actuellement connectés (combien il y en a par exemple)
On peut imaginer des commandes plus rigolotes, qui tuent uniquement les clients qui proviennent d'une adresse IP donnée ou des numéros de port client impairs....