:: Enseignements :: ESIPE :: E4INFO :: 2015-2016 :: Programmation d'applications réseaux ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) |
TCP non bloquant
|
Exercice 1 - Mini aditionneur TCP non-bloquant (Nouvelle version)
Dans cet exercice, nous allons reprendre le premier exercice du TP précédent. L'exercice a 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 naive 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.
Pour rappel, on souhaite écrire un serveur qui accepte de
multiples clients en non-bloquant et permet d'effectuer l'addition
de deux entiers pour chaque client.
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 ...
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,
process.
La méthode
process a pour but de transférer, si possible, les données du buffer
in vers le buffer
out.
La convention pour toutes les méthodes est que les buffers
in et
out sont en mode d'écriture.
Le fichier
ServerSumNew.java contient la base de cette architecture. Par simplicité,
la classe
Context est une sous-classe 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 champs interestOps de la clé en se basant que sur la valeur la valeur des champs de l'objet Context.
La méthode process produit à 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 - Chat de base
Dans cet exercice, on cherche à réaliser un serveur de chat public très simple en implémentation non-bloquante.
Le client envoie des messages au serveur au format suivant:
taille_login(INT) | login UTF8 | taille_txt(INT) | txt UTF8
Un message ne peut pas faire plus de 1024 octets.
A la réception d'un message, le serveur le transmet à tous les clients connectés.
En partant de l'architecture présentée dans l'exercice précédent, écrire un serveur en mode non-bloquant pour ce protocole.
Le protocole présente deux difficultés:
-
Lors qu'un message est reçu, il doit être transmis à tous les clients connectés.
On pourrait imaginer écrire directement dans les buffers out des clients
mais il n'y aura pas nécessairement la place. On rajoute donc dans chaque Context
une queue contenant les messages à envoyer. Attention, lorsqu'on ajoute un message
dans cette queue il faut penser à modifier interestOps.
-
Il faut être capable de lire les messages dans le buffer in.
Pour résoudre la deuxième difficulté, nous vous proposons une architecture qui s'adapte
facilement pour gérer un grand nombre de formats de messages. L'idée est
de créer des lecteurs pour chaque type de donnée. Un lecteur implémentera
l'interface
Reader.java.
Un reader travaille sur un buffer qui lui est donné à la construction.
La convention est que ce buffer est toujours en
write mode.
Les méthodes offertes par un
Reader sont les suivantes:
Pour fixer les idées, nous allons donner une solution possible du
premier exercice (mini aditionneur) en se basant sur l'architecture des readers.
Nous commençons par un reader très simple pour un INT en BigEndian.
En utilisant ce
IntReader, nous pouvons écrire un reader qui cherche deux INT et renvoie leur somme.
Une fois les readers mis en place, le code de la méthode
process du
Context devient:
private static class Context {
private boolean inputClosed = false;
private final ByteBuffer in = ByteBuffer.allocate(BUF_SIZE);
private final ByteBuffer out = ByteBuffer.allocate(BUF_SIZE);
private final SelectionKey key;
private final SocketChannel sc;
private final TwoIntReader reader;
public Context(SelectionKey key) {
this.key = key;
this.sc = (SocketChannel) key.channel();
this.reader = new TwoIntReader(in);
}
public void doRead() throws IOException {
...
}
public void doWrite() throws IOException {
...
}
private void process() {
while(out.remaining()>=Integer.BYTES){
Reader.ProcessStatus status = reader.process();
switch (status){
case REFILL: if (in.remaining()==0) {
silentlyClose(sc);
}
return;
case ERROR: silentlyClose(sc);
return;
case DONE: out.putInt(reader.get());
reader.reset();
break;
}
}
}
private void updateInterestOps(){
...
}
}
Pour le serveur de chat, vous pouvez écrire un
StringReader qui lit une chaîne au format
size (INT) | txt (UTF8)
En utilisant ce reader, vous pouvez écrire un
MessageReader qui lit un message entier.
© Université de Marne-la-Vallée