:: Enseignements :: Master :: Master TTT :: 2012-2013 :: Programmation réseau en Java ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) | TCP et concurrence |
Ce TD est une introduction à la programmation de serveurs TCP concurrents en Java. Le premier exercice est une adaptation d'un serveur d'un modèle itératif vers un modèle concurrent. Le deuxième exercice nous invite à réaliser un serveur servant de médiateur entre des clients producteurs et consommateurs d'éléments ; il nous incite à utiliser les mécanismes d'attente passive et de signalisation. Le troisième exercice consiste à implanter un proxy TCP : il s'agit de supporter un scénario de communication asynchrone et ainsi d'utiliser une thread pour chaque direction de transfert.
Exercice 1 - Tortureur de lignes concurrent
- On adapte le serveur de torture de lignes réalisé précédemment afin de pouvoir communiquer avec plusieurs clients simultanément. À cet effet, on créé une nouvelle thread pour chaque nouvelle socket acceptée.
- Réaliser les modifications nécessaires afin d'éteindre proprement le serveur lorsqu'une commande SHUTDOWN est reçue de la part d'un client (il faut fermer les connexions avec tous les clients puis fermer la socket serveur).
Exercice 2 - Un serveur médiateur
On souhaite transmettre des objets de producteurs vers des consommateurs. Une possibilité est de créer un serveur consommateur recevant des éléments de clients producteurs. Mais comment faire si nous avons plusieurs consommateurs qui se partagent les objets produits ? Nous proposons de créer un serveur médiateur se chargeant de relayer les éléments des producteurs vers les consommateurs. Son fonctionnement peut ainsi être décrit :
- Un producteur se connecte en tant que client et peut laisser un élément avec la commande "PUT element" (l'élément est une chaîne de caractères).
- Un consommateur se connecte également en tant que client et peut récupérer l'élément laissé avec "TAKE" (le serveur retourne une ligne avec l'élément).
Dans un premier temps, le serveur ne peut stocker qu'un élément en cours de transit (dans un champ String par exemple).
Malheureusement le rythme de production et de consommation peut être disparate. Pour gérer cette situation, on décide que les commandes PUT et TAKE sont bloquantes :
- PUT bloque tant que le champ de transit n'est pas vide.
- TAKE bloque tant que le champ de transit est vide.
Plutôt que de réaliser des attentes actives consommatrices de ressources processeur, on opte pour l'utilisation d'un verrou ou d'un moniteur ainsi que des mécanismes d'attente passive et de signalisation.
Implanter un tel serveur médiateur contactable concurremment par plusieurs producteurs et consommateurs. Quel est l'inconvénient ici d'utiliser un moniteur avec wait()/notify()/notifyAll() plutôt qu'un verrou et des conditions ?
On peut généraliser le concept d'unique élément de transit par une file d'attente de taille N : les producteurs placent des éléments à la fin d'une file ; les consommateurs les récupèrent en début de file. L'ajout bloque lorsque la file possède N éléments, la récupération bloque tant que la file est vide. L'API du JDK propose des files d'attente bloquantes avec l'interface java.util.concurrent.BlockingQueue (par exemple LinkedBlockingQueue en utilisant une liste chaînée) avec des méthodes bloquantes void put(E e) et E take().
Exercice 3 - Bonus: un proxy TCP
Un proxy a pour mission de jouer le rôle d'intermédiaire entre deux sockets : pour chaque sens de communication, il reçoit les données de l'expéditeur et les transmet à leur destinataire final. Nous allons implanter un proxy utilisant le protocole TCP qui se montre neutre vis-a-vis des données transportées (elles ne sont pas modifiées) mais cela ne l'empêchera pas de conserver quelques statistiques au passage (volume des données échangées). Au lieu de dialoguer directement avec le serveur, le client communiquera avec le proxy qui sera chargé de relayer les données vers/depuis le serveur.
- Créer une classe fr.upemlv.javares.TCPProxy qui contiendra l'implantation du proxy. Elle comporte notamment un constructeur TCPProxy(int port, InetAddress serverAddress, int serverPort) pour indiquer le port d'écoute du serveur ainsi que l'adresse et le port du serveur à contacter.
- Écrire la méthode launch() qui initialise la socket du serveur proxy (ServerSocket), accepte une connexion d'un client (Socket) et créé une connexion vers le serveur final (Socket).
- On remarque que le travail à réaliser pour relayer les données du client vers le serveur et du serveur vers le client est en fait le même : il s'agit de transférer des données de l'InputStream d'une socket vers l'OutputStream d'une autre socket. Créer une méthode créant une thread chargée de transférer les données d'une socket vers une autre dans un sens de communication.
- Terminer l'implantation du proxy.
- Tester le proxy pour contacter un serveur distant (on pourra essayer par exemple towel.blinkenlights.nl sur le port 23 ou 666, telehack.com sur le port 23 en utilisant un client telnet) ou un serveur local (un serveur netcat TCP en écoute).
- Que se passe-t-il lorsque le débit sur un premier segment (socket 1 -> proxy) est plus élevé que le débit sur le deuxième segment (proxy -> socket 2) ? Comment peut-on s'adapter à cette situation ?
© Université de Marne-la-Vallée