TCP non bloquant (1/3)

Mini additionneur TCP non bloquant

On souhaite écrire un serveur qui traite de multiples clients avec un seul thread en non-bloquant et qui permet d'effectuer des additions de deux entiers pour chaque client.

Protocole SumOneShot
Le client envoie deux int (4 octets) en Big Endian, puis ferme la connexion en écriture. Le serveur renvoie un int en Big Endian, valant la somme des deux entiers reçus, puis il ferme la connexion.
Par exemple, si le client envoie 7 et 10, le serveur renverra 17.

En partant de la trame ServerSumOneShot.java, écrire le code du serveur ServerSumOneShot en non bloquant, pour le protocole SumOneShot. Vous aurez également besoin de la classe Helpers.java
Il n'est pas nécessaire de vérifier si le client a bien fermé la connexion après l'envoi des deux int : après envoi de la réponse, le serveur ferme la connexion.

  1. Pour cet exercice, vous stockerez dans le champ attachement de la SelectionKey un ByteBuffer de taille 2 * Integer.BYTES (alloué lors de l'acceptation du client).
  2. Rappelez-vous que la méthode socketChannel.write (comme la méthode socketChannel.read) est non-bloquante et qu'il n'y a donc aucune garantie que toute la zone de travail du ByteBuffer soit écrite. Regardez dans votre code ce qu'il se produirait si, au moment d'écrire la somme vers un client, le serveur ne pouvait écrire que 2 des 4 octets lors d'une tentative d'écriture non bloquante. Même s'il est improbable dans notre exemple, prévoyez ce cas dans votre code.
  3. Dans la méthode processSelectedKeys, comment faut-il traiter les exceptions levées dans les méthodes doRead, doWrite et doAccept ?

Pour tester votre code, utilisez le jar ClientSumOneShot.jar qui demande 2 entiers au clavier, les envoie au serveur et affiche la réponse du serveur.

% java fr.upem.net.tcp.nonblocking.ServerSumOneShot 7777
% java -jar ClientSumOneShot.jar localhost 7777
Connected to server.
Enter two integers:
1
2
Waiting for server's reply ...
Server replied : 3

En passant l'option -bug à ClientSumOneShot.jar, on simule un client qui ferme la connexion en écriture sans avoir envoyé les deux entiers. Dans ce cas, votre serveur doit fermer la connexion car le client ne respecte pas le protocole.

% java fr.upem.net.tcp.nonblocking.ServerSumOneShot 7777
% java -jar ClientSumOneShot.jar localhost 7777 -bug
Connected to server.
Enter two integers:
1
2
Closing the write-connection without sending the second INT.
Waiting for server's reply ...
Server closed the connection before sending the answer (This is the normal behavior).

On change maintenant légèrement le protocole pour autoriser un client à effectuer plusieurs sommes sur la même connexion.

Protocole Sum
Le client envoie deux int (4 octets) en Big Endian. Le serveur répond un int en Big Endian, valant la somme des deux entiers reçus. Ces échanges continuent jusqu'à ce que le client ferme la connexion en écriture.

Adaptez votre code en un serveur ServerSum pour qu'il puisse traiter une succession de sommes.

Vous pourrez tester votre serveur avec le jar ClientSum.jar.
% java fr.upem.net.tcp.nonblocking.ServerSum 7777
% java -jar ClientSum.jar localhost 7777
    

Si tout est correct, vous devriez voir un affichage du type :
Client 0 connected to server.
Client 1 connected to server.
Client 2 connected to server.
Client 3 connected to server.
Client 4 connected to server.
Client 1 finished sending its requests.
Client 2 finished sending its requests.
Client 3 finished sending its requests.
Client 0 finished sending its requests.
Client 4 finished sending its requests.
Client 1 finished receiving the answers.
Client 0 finished receiving the answers.
Client 4 finished receiving the answers.
Client 3 finished receiving the answers.
Client 2 finished receiving the answers.    
 
Attention, le serveur que nous avons codé n'est pas très efficace car il lit sur la SocketChannel 8 octets par 8 octets. Nous verrons dans le TP suivant comment coder un serveur plus performant qui effectue des lectures et des écritures en plus grande quantité si cela est possible.

Serveur Echo

Le but de cet exercice est d'écrire un serveur pour le protocole Echo. Ce sera l'occasion de commencer à mieux structurer nos serveurs non-bloquants en utilisant des Context.

Protocole Echo
Dans le protocole Echo, le serveur renvoie au client tous les octets envoyés par le client et ce jusqu'à ce que le client ferme la connexion en écriture.

On veut que notre serveur puisse être à la fois en OP_READ et en OP_WRITE. Pour cela, on va attacher à chaque SelectionKey correspondant à une SocketChannel d'un client un objet de la classe Context que vous allez coder.

static private class Context {
  private final SelectionKey key;
  private final SocketChannel sc;
  private final ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
  private boolean closed = false;

  private Context(SelectionKey key){
      this.key = key;
      this.sc = (SocketChannel) key.channel();
  }

  private void updateInterestOps() {
      // TODO
  }

  private void doRead() throws IOException {
      // TODO
  }

  private void doWrite() throws IOException {
      // TODO
  }
}

Le principe de la classe Context est de contenir toutes les informations relatives à la SocketChannel, i.e., au client. Elle contiendra toujours la SocketChannel et la SelectionKey. Dans le cas d'un serveur pour le protocole Echo, elle contiendra un ByteBuffer et un boolean qui servira à mémoriser si le client a fermé la connexion. De plus, la classe Context contient les méthodes doRead, doWrite et une méthode updateInterestOps. La méthode updateInterestOps sera appelée dans doRead et dans doWrite et est en charge de mettre à jour les interestOps de la SelectionKey. La mise à jour se fera uniquement à partir de l'état du ByteBuffer et du boolean.

Convention : Pour les méthodes doRead, doWrite et updateInterestOps on supposera que le ByteBuffer est en mode écriture avant et après l'appel de ces méthodes.

En partant de la trame ServerEcho.java, écrivez un serveur pour le protocole Echo en mode en non-bloquant.

Une fois que votre serveur semble fonctionner, testez le avec le jar ClientEchoSlow.jar, qui envoie en boucle l'octet 1 et, dans un autre thread, affiche le numéro des octets reçus. Le client ne s'arrête jamais.

L'intérêt de ce client est qu'il lit "lentement" et donc, à terme, ralentit le rythme d'écriture du serveur (à cause du contrôle de flot effectué par TCP). Ainsi, en consultant (par la commande top sous linux) les ressources CPU utilisées par votre serveur, vous devrez constater qu'elles sont très limitées. Si le serveur consomme beaucoup de CPU, c'est par exemple parce qu'il tente d'écrire alors que ce n'est pas possible (il fait alors une forme d'attente active) : le principe des entrées sorties non bloquantes est précisément d'éviter ces comportements.

% java fr.upem.net.tcp.nonblocking.ServerEcho 7777
% java -jar ClientEchoSlow.jar localhost 7777