TP Noté 2022 : téléchargements multiserveurs de fichiers par bloc.

1. Contexte et consignes

1.1 Introduction

Dans ce projet, nous réalisons un service qui permet à un programme de télécharger un fichier en l'« important » par morceaux (blocs) de plusieurs sources différentes.

Pour réaliser ce service, nous disposons d'un ensemble de serveurs ayant tous une version complète des fichiers que l'on souhaite télécharger. Le programme client souhaitant télécharger un fichier se connecte à un ou plusieurs serveurs (éventuellement plusieurs fois à un même serveur). Pour chaque connexion, il émet au serveur une requête demandant de lui envoyer les données correspondant à une section choisie du fichier. Pour la suite de ce sujet, nous nommons cette section un « bloc ». Bloc par bloc, le client télécharge ainsi l'intégralité du fichier.

1.2 Consignes

Vous disposer de 3h pour réaliser ce TP. Vous avez le droit aux documents suivants :

Pour installer le sujet :

En fin de TP :

  1. si ce n'est pas déjà fait, ajouter au fichier readme.txt les informations à destination des correcteurs ;
  2. vérifiez la conformité de vos fichiers aux exigences de SpotBugs ;
  3. supprimez tous les fichiers générés avec un « mvn clean » ;
  4. supprimez les fichiers présents dans les répertoires data/copy, data/info et data/save ;
  5. exportez votre projet Eclipse sous la forme d'une archive :
  6. déposez le fichier sur moodle comme devoir ;
  7. vérifiez avec l'enseignant présent dans la salle que le fichier téléchargé est correct.

2. Présentation du sujet

2.1 Architecture

Pour télécharger plus vite le fichier, le programme client établit plusieurs (Client.NBCONNECTIONS) connexions simultanées avec les serveurs. Toutes les fois qu'un bloc est complètement téléchargé, le client ferme la connexion concernée et en ouvre une nouvelle pour envoyer la requête d'un nouveau bloc. Il procède ainsi jusqu'à avoir demandé et reçu tous les blocs du fichier.

Pour réussir à gérer tous ces échanges simultanés, les connexions du client changent de mode suivant l'avancement du protocole. Chaque connexion de client suit le cycle de vie suivant :

  1. ouverture de la connexion TCP vers un des serveurs (cf Fig1) ;
  2. envoie d'une requête (objet de la classe Request) au serveur en mode synchrone (cf Fig1) ;
  3. bascule de la connexion en mode asynchrone et gestion des échanges avec un Selector ;
  4. réception de toutes les données du bloc en restant en mode asynchrone pour pouvoir gérer toutes les connexions en même temps (cf Fig2) .

De son côté, le serveur fonctionne en deux parties :

  1. Le threads principal, crée en avance un pool de plusieurs (Server.NBTHREADS) threads pour pouvoir gérer plusieurs clients en simultané. Ce thread principal ne gére pas et ne reçoit même pas les requêtes des clients. Il accepte la connexion des clients, place immédiatement le canal (SocketChannel) dans une file d'attente, et oublie l’existence de ce client (cf Fig1) ;
  2. Dès qu'un thread du pool se retrouve sans tâche, il extrait un canal de la file d'attente, lit alors la requête postée par le client, puis la traite (cf Fig2) .

Ainsi, la gestion multiclient du serveur se fait grâce à plusieurs threads, et donc tous ses échanges restent en mode synchrone.

Fig 1 : Les clients établissent plusieurs connexions avec différents serveurs en mode synchrone.
connexions des clients
                                                aux serveurs
Fig 2 : Les clients reçoivent, en mode asynchrone, les données envoyées par les threads des serveurs.
réceptions par les
                                               clients des données
                                               émises par les threads
                                               des serveurs

2.2 Descriptions des classes

Deux classes servent à décrire et gérer les fichiers divisés en blocs :

  1. BlockFile : (classe déjà écrite) elle décrit et gère les fichiers divisés en blocs. Les méthodes que vous devez connaître sont :
  2. Block : (classe déjà écrite) elle décrit et gère un bloc du fichier à importer. Les méthodes que vous devez connaître sont :

Deux classes servent à décrire et gérer les clients et les serveurs :

  1. Client: (classe à compléter) elle décrit et gère les clients. Ses attributs sont :
  2. Server : (classe à compléter) elle décrit et gère les serveurs. Son seul attribut est :

Deux classes d'application :

  1. AppliClient: (classe déjà écrite) elle met en œuvre le client ;
  2. AppliServer: (classe déjà écrite) elle met en œuvre le serveur ;

Trois classes et une énumération déjà écrites pour gérer une partie des échanges sur le réseau :

  1. TcpSocketSynchrone et TcpServeurSynchrone: (classes déjà écrites) il s'agit des classes TcpSocket et TcpServeur écrites pour le devoir maison. Elles servent à envoyer les objets de la classe Request entre un client et un serveur ;
  2. Request: (classe déjà écrite) classe qui contient la requête pour demander la transmission d'un bloc à un serveur ;
  3. ServerInfo: (énumération déjà écrite) elle contient l'annuaire des serveurs utilisables par un client (énumération à ne pas modifier pour que les tests fonctionnent).

3. Les questions du sujet

3.1 Introduction

Il y a 7 questions :

Le barème est donné à titre d'indication.

Il est possible d'écrire le client ou le serveur dans l'ordre que vous voulez.

Pour le client, les trois questions doivent être faites dans l'ordre. Si vous bloquez sur une des deux premières, une procédure de contournement vous est donnée en fin de question.

Pour le serveur, il faut commencer par les questions 4 et 5, et ensuite les questions 6 et 7 sont imbriquées. Si vous bloquez sur les questions 4 ou 5, une procédure de contournement vous est donnée en fin de question.

Des tests unitaires sont fournis pour les questions 1, 2, 4 et 5. Dans l'archive qui vous est donnée, ils sont désactivés par l'annotation @Ignore. Après l'écriture d'une méthode, pour la tester il faut :

Des tests d'intégration vous sont fournis sous la forme de shell-scripts. Il y a un test pour la fin de la question 3, la fin des questions 6 plus 7, et la fin du TP.

3.2 Écriture du client

3.2.1 Introduction

La gestion d'un bloc par la classe Client fonctionne en deux étapes :

Les méthodes connectAndSendRequest() et manageNextBlock() gèrent la phase synchrone, et la méthode importFile() gère la phase asynchrone.

3.2.2 Question 1 : écriture de la méthode connectAndSendRequest()

La méthode connectAndSendRequest() possède deux paramètres  :

  1. ServerInfo serverInfo : la référence sur un objet qui offre la méthode String getHostname() pour obtenir le nom du serveur, et la méthode short getPort() pour obtenir le port du serveur ;
  2. Request request : la référence sur la requête à envoyer au serveur.

Écrivez la méthode connectAndSendRequest() qui doit :

Utilisez la classe TcpSocketSynchrone (écrite durant le devoir maison) pour ces instructions.

Les tests :

Une fois vôtre méthode écrite, vous pouvez la tester. Pour cela, retirez l'annotation @Ignore devant la méthode testConnectAndSendRequest() de la classe de test TestClient et lancez ce test.

Si vous n'arrivez pas à faire cette question, mais que vous souhaitez faire les suivantes, voici la procédure à suivre:

La méthode héritée de classe d'aide va prendre la relais de la vôtre. Si vous voulez revenir plus tard sur votre méthode, il suffit de retirer les commentaires, et votre méthode reprendra le dessus sur la méthode héritée.

3.2.3 Question 2 : écriture de la méthode manageNextBlock()

Cette méthode réalise la prise en charge d'un nouveau bloc, jusqu'au passage en mode asynchrone. Cette méthode doit gérer l'absence éventuelle de serveurs listés dans l'annuaire (l'énumération ServerInfo), mais on fait l'hypothèse qu'au moins un des serveurs est fonctionnel.

Cette méthode doit  :

Les tests :

Une fois votre méthode écrite, vous pouvez la tester. Pour cela, commencez par retirer l'annotation @Ignore devant la méthode testManageNextBlock() de la classe de test TestClient et lancez ce test. Une fois que ce premier test réussit, retirez l'annotation @Ignore devant la méthode testRobustesteManageNextBlock() de la classe de test TestClient et lancez ce test. Si ce second test échoue, vous pouvez malgré cela continuer le TP avec votre code. Il fonctionne tant que tous les serveurs de l'annuaire sont en route.

Si le premier test (testManageNextBlock()) échoue, mais que vous souhaitez malgré cela passer à la question suivante, voici la procédure à suivre:

Si vous voulez revenir plus tard sur votre méthode, il suffit de retirer la ligne ajoutée, et de reprendre votre code.

3.2.4 Question 3 : écriture de la méthode importFile()

Cette méthode réalise tout le téléchargement du fichier. Pour cela elle doit :

Les tests :

Votre client est terminé. Vous pouvez le tester avec les tests d'intégration. Ces tests font l'hypothèse que l'énumération ServerInfo n'a pas été modifiée, et qu'elle propose les serveurs localhost:4545 et localhost:4546.

Un premier test permet de vérifier votre client face à des serveurs fiables. Lancez le script « IntegrationClient.sh » (pensez à lui donner les droits d'exécution si ce n'est pas déjà fait).

Si le test réussit, il doit se terminer par l'affichage « Test OK », et s'il échoue par l'affichage « Test KO ».

Un second test permet de vérifier que votre client peut fonctionner même face à des serveurs défaillants, tant qu'il y en a au moins un qui fonctionne correctement. Lancez le script « IntegrationClientRobuteste.sh » (pensez à lui donner les droits d'exécution si ce n'est pas déjà fait).

Si le test réussit, il doit se terminer par l'affichage « Test OK », et s'il échoue par l'affichage « Test KO ».

En cas d'échec de ces tests, si vous ne trouvez pas rapidement la correction, vous pouvez continuer le TP, et revenir le corriger plus tard si vous avez le temps. La suite du sujet n'utilise pas le code de votre client, sauf pour le test d'intégration du TP en toute fin du sujet.

3.3 Écriture du serveur

3.3.1 Introduction

Le serveur utilise deux classes pour fonctionner :

Nous avons vu dans le devoir maison que JAVA propose des interfaces et des classes pour gérer les pools de threads. Mais pour ce TP noté, nous réalisons nous-même ce service.

Pour transmettre les canaux reliés aux clients au pool de threads, la classe Server utilise une file d'attente implémentant l'interface BlockingQueue<E>. Ces files d'attente gèrent elles-mêmes les sections critiques grâce à des Lock, et vous n'avez pas à vous occuper de les synchroniser.

Pour créer une file d'attente, il faut choisir une classe d'implémentation, et nous vous suggérons d'utiliser la classe LinkedBlockingQueue<E>.

Pour ce serveur, vous devez comprendre l'usage de deux méthodes pour utiliser ces files :

3.3.2 Question 4 : écriture du constructeur de la classe Server

Écrivez le constructeur de la classe Server. Il doit préparer le serveur à pouvoir faire des accept() sur le port passé en paramètre.

Les tests :

Une fois votre constructeur écrit, vous pouvez le tester. Pour cela, retirez l'annotation @Ignore devant la méthode testServer() de la classe de test TestServer et lancez ce test.

Si vous n'arrivez pas à faire cette question, mais que vous souhaitez faire les suivantes, voici la procédure à suivre :

Si vous voulez revenir plus tard sur votre méthode, il suffit de retirer la ligne ajoutée et de reprendre votre code.

3.3.3 Question 5 : écriture de la méthode Server::accept()

Écrivez la méthode accept() de la classe Server. Elle doit accepter la connexion d'un client, et retourner le canal relié à ce client.

Les tests :

Une fois votre méthode écrite, vous pouvez la tester. Pour cela, retirez l'annotation @Ignore devant la méthode testAccept() de la classe de test TestServer et lancez ce test.

Si vous n'arrivez pas à faire cette question, mais que vous souhaitez faire les suivantes, voici la procédure à suivre :

Si vous voulez revenir plus tard sur votre méthode, il suffit de retirer la ligne ajoutée et de reprendre votre code.

3.3.4 Questions 6 et 7 : écriture de la méthode Server::service() et de la classe ServerRunnable

Question 6 : écrivez la méthode service() de la classe Server. Cette méthode doit :

Question 7 : écrivez la classe ServerRunnable utilisée par le pool de threads. Ces threads doivent :

Le serveur conserve les exemplaires des fichiers sources à envoyer dans le répertoire BlockFile.SOURCE

Pour lire sur le fichier source la section de données correspondant au bloc demandé, il faut utiliser une classe JAVA qui permet un accès directe à cette section. Nous vous conseillons d'utiliser la classe RandomAccessFile.

Avec cette classe il faut utiliser la méthode seek() pour positionner le curseur de lecture à l'endroit où on veut lire les données :

Les tests :

Votre serveur est terminé. Vous pouvez le tester avec le test d'intégration en lançant le script « IntegrationServeur.sh » (pensez à lui donner les droits d'exécution si ce n'est pas déjà fait). Ce script fait l'hypothèse que l'énumération ServerInfo n'a pas été modifiée, et qu'elle propose le serveur localhost:4545.

Si le test réussit, il doit se terminer par l'affichage « Test OK », et s'il échoue par l'affichage « Test KO »

Une fois les tests d'intégrations du client et du serveur réussis, vous pouvez tester l'intégration des deux réunis en lançant le script « IntegrationTP.sh » (pensez à lui donner les droits d'exécution si ce n'est pas déjà fait). Ce script fait l'hypothèse que l'énumération ServerInfo n'a pas été modifiée, et qu'elle propose les serveurs localhost:4545 et localhost:4546.

Si le test réussit, il doit se terminer par l'affichage « Test OK », et s'il échoue par l'affichage « Test KO ». Attention, ce test n'utilise que vos classes. Si vous n'avez activé aucun Log dans celles-ci, il sera entièrement muet mis à part le verdict final.

4. Conclusions

Ce TP noté est terminé, et vous avez écrit le code permettant de gérer une architecture multiclient, multiserveur avec de l'asynchrone et un pool de threads.

Ce programme reste très améliorable :