:: Enseignements :: Master :: M1 :: 2016-2017 :: Programmation d'applications réseaux ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
TCP non bloquant (mieux)
|
Dans ces exercices, nous allons reprendre le premier exercice du TP précédent.
Ces exercices ont plusieurs buts:
- 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.
- Nous allons présenter une architecture flexible qui peut être la base de n'importe
quel serveur.
- Nous verrons aussi comment adapter cette architecture pour permettre de fermer les
connections inactives.
- 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:
-
Un buffer de lecture in et un buffer d'écriture out.
-
Un boolean inputClosed permettant de mémoriser si la connection a été fermée par le client.
-
La référence de la SelectionKey à laquelle le Context est attaché.
-
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.
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....
© Université de Marne-la-Vallée