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.
Vous disposer de 3h pour réaliser ce TP. Vous avez le droit aux documents suivants :
JAVA ≥ 11.
      Pour installer le sujet :
En fin de TP :
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 :
Request) au serveur en mode synchrone (cf Fig1) ;
    Selector ;
    De son côté, le serveur fonctionne en deux parties :
SocketChannel) dans une file d'attente, et
    oublie l’existence de ce client (cf Fig1) ;
    Ainsi, la gestion multiclient du serveur se fait grâce à plusieurs threads, et donc tous ses échanges restent en mode synchrone.
     
    
     
    Deux classes servent à décrire et gérer les fichiers divisés en blocs :
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 :
      BlockFile(final String fileName), qui demande le nom du
	fichier en paramètre ;Block getABlock() qui retourne un des blocs qui restent à importer. Retoune null s'il ne reste plus de bloc à gérer ;boolean hasRemaining() qui retourne vrai tant qu'il reste des blocs non traités ;void addBlock(Block block) qui permet de replacer un bloc dans l'ensemble des blocs
	à traiter, pour récupérer une éventuelle situation d'erreur ;void close() pour fermer les ressources lorsque le fichier est entièrement importé.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 :
      int fill(final ReadableByteChannel fcin) qui ajoute des données dans le bloc 
	à importer. Son seul paramètre est le canal qui est relié à la connexion TCP vers le serveur. La méthode 
	retourne le nombre d'octets reçus, et donc -1 en cas de déconnexion. Cette méthode est écrite
	pour fonctionner en mode asynchrone, et il faut éventuellement plusieurs appels pour 
	importer le bloc ;boolean hasRemaining() qui retourne vrai tant que le bloc n'est pas totalement importé ;void close() qui libère les ressources et assure que les données importées sont
	bien sauvées sur le fichier. Il faut l'appeler une fois le bloc totalement importé ou qu'il est 
	abandonné suite à une défaillance.Deux classes servent à décrire et gérer les clients et les serveurs :
Client: (classe à
      compléter) elle décrit et gère les clients. Ses attributs
      sont :
      BlockFile blockFile : le fichier divisé en
          blocs à importer ;
        Map<SocketChannel,Block> serverMap :
          un dictionnaire associant les canaux et les blocs en
          cours de téléchargement ;
        Selector selector : le sélecteur gérant la
          partie asynchrone du client.
        Server : (classe à
        compléter) elle décrit et gère les serveurs.  Son seul
      attribut est :
      ServerSocketChannel listenChannel : canal
          gérant les accept() du serveur.
        Deux classes d'application :
AppliClient: (classe déjà
        écrite) elle met en œuvre le client ;
    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 :
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 ;
    Request: (classe déjà
        écrite) classe qui contient la requête pour demander la
      transmission d'un bloc à un serveur ;
    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).
    Il y a 7 questions :
Client (9 pts) ;
    Serveur (5 pts) ;
    ServerRunnable  (6 pts).
    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 :
  
@Ignore du test ;
    JUnit Test » au sein de votre IDE, ou
      lancer la commande
      « mvn clean install » ;
      les tests réactivés sont exécutés.
    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.
    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.
  
connectAndSendRequest() 
    La méthode connectAndSendRequest() possède deux
    paramètres  :
  
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 ;
    Request request : la référence sur la requête
      à envoyer au serveur.
    
   Écrivez la méthode connectAndSendRequest() qui doit :
  
serverInfo ;
    request à ce serveur ;
    
    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:
Client hérite de la
      classe tsp.csc4509.tpnote.aide.ClientAide
      
    connectAndSendRequest() (y compris la ligne
      d'entête).
    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.
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 :
blockFile géré par le client
      
       ;
    Request utilise deux paramètres  :
      BlockFile blockFile : le fichier à
          importer ;
        Block block : le bloc demandé par cette
          requête ;
        serverMap.
    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:
  
Client hérite de la
      classe tsp.csc4509.tpnote.aide.ClientAide
      
    manageNextBlock() la ligne
      suivante :
      
    Si vous voulez revenir plus tard sur votre méthode, il suffit de retirer la ligne ajoutée, et de reprendre votre code.
importFile()Cette méthode réalise tout le téléchargement du fichier. Pour cela elle doit :
NBCONNECTIONS premiers
      blocs ;
    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.
Le serveur utilise deux classes pour fonctionner :
Server, qui contient les instructions du
      thread principal. Ce thread ne gère que
      les accept() des clients, et confie la gestion du
      nouveau client à un pool de threads en plaçant
      dans une file d'attente le canal relié au client ;
    ServerRunnable (à créer de toutes
      pièces) qui contient les instructions qui envoie les
      données du bloc au client.
    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 :
void put(E e) : elle
      ajoute l'élément e en queue de la file
      d'attente ;
    E take() : elle retire
      l'élément en tête de la file d'attente, en bloquant si la file
      est vide.
    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 :
Server hérite de la
      classe tsp.csc4509.tpnote.aide.ServerAide
      
    Server() la ligne
      suivante :
      
  Si vous voulez revenir plus tard sur votre méthode, il suffit de retirer la ligne ajoutée et de reprendre votre code.
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 :
Server hérite de la
      classe tsp.csc4509.tpnote.aide.ServerAide
      
    accept()
      la ligne suivante :
      
    Si vous voulez revenir plus tard sur votre méthode, il suffit de retirer la ligne ajoutée et de reprendre votre code.
Server::service() et de la
  classe ServerRunnable
    Question 6 : écrivez la méthode service() de la
    classe Server. Cette méthode doit :
  
NBTHREADS threads
      utilisant la classe ServerRunnable (à
      écrire) ;
    
    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.
   
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 :