:: Enseignements :: ESIPE :: E4INFO :: 2015-2016 :: Programmation d'applications réseaux ::
[LOGO]

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:
  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 naive 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.

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:
  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, 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.
  • 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 - 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:
  1. 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.
  2. 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:
  • ProcessStatus process() La méthode process scanne le buffer et éventuellement le consomme. Elle renvoit une valeur pour indiquer l'état de sa lecture. Cette valeur est donnée par l'énumération:
    public static enum ProcessStatus {DONE,REFILL,ERROR};
    
    • REFILL si le buffer ne contient pas un objet complet et qu'il faut rajouter d'autres octets.
    • ERROR si les octets présents dans le buffer ne respectent pas le format attendu.
    • DONE si le buffer contient un objet entier.
  • Object get(). Renvoie l'objet Java approprié et retire les octets correspondant du buffer.
  • void reset(). Remet l'état du reader à zéro pour pouvoir le réutiliser. Le buffer n'est pas modifié.

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.

Vous pouvez tester votre serveur avec le client suivant ClientChat.java.