Dans ce projet, nous réalisons un service qui permet (1) de déposer sur une messagerie des fichiers destinés à un utilisateur, et (2) de récupérer les fichiers que d'autres utilisateurs ont été déposés pour un utilisateur.
Pour réaliser ce service, nous disposons d'un serveur gérant un spool stockant tous les fichiers en attente de téléchargement et d'un client pour communiquer avec ce serveur.
Le client utilise deux requêtes pour échanger avec le serveur :
Les fichiers sont stockés dans des spools, c'est-à-dire des files d'attente qui permettent de conserver les fichiers jusqu'à leur réclamation. Il y a un spool par client (utilisateur) et un spool pour le serveur. Concrètement, il s'agit d'une arborescence de fichiers stockés dans les répertoires data/spool (un sous-répertoire par utilisateur) et date/serverSpool. Ces spools sont structurés pour permettre de stocker des fichiers ayant les mêmes noms sans qu'ils entrent en collision. Tout le code gérant cet aspect du sujet est déjà écrit, et vous n'avez ni à le lire ni à l'étudier. L'énoncé qui suit vous donnera les instructions à écrire toutes les fois qu'il faudra utiliser ces spools.
Voici un petit scénario pour concrétiser le service que nous construisons dans ce sujet :
Vous disposer de 3h pour réaliser ce TP. Vous avez le droit aux documents suivants :
JAVA ≥ 17.
      Pour installer le sujet :
En fin de TP :
./cleanAllClientsSpools.sh et ./cleanServerSpool.sh ;
	Le service de messagerie repose sur une architecture client/serveur classique qui communique avec TCP. Cependant, suivant l'avancement du protocole, les données échangées sont de deux natures différentes :
ProtocolMessage qui servent à indiquer les services demandés
   et rendus ;Pour gérer ces échanges, nous reprenons une solution classique adoptée par des protocoles comme FTP. Autrement dit, nous mettons en œuvre un service « à la » FTP, mais vous n'avez pas besoin de connaissance particulière sur le protocole FTP. Voici les informations nécessaires pour comprendre l'énoncé :
FullDuplexMsgWorker développée et utilisée durant les TPs du module CSC4509 ;SocketChannel et leurs méthodes basiques (read, write et close).Le client utilise deux requêtes : PUT et GET. Chacune déroule son propre protocole.
Dès qu'un client se connecte, le serveur lance un nouveau thread pour le gérer et retourne en attente de la connexion du prochain client.
L'envoi d'un fichier à destination d'un autre utilisateur (requête PUT) est effectué en 8 étapes (cf. figure 1) :
FullDuplexMsgWorker ;ProtocolMessage contenant la requête PUT et les informations sur le fichier à envoyer (destinataire, nom du fichier) ;ServerSocketChannel sur un port aléatoire libre ;ProtocolMessage contenant le « nom » de la requête (SEND) ainsi que le numéro du port qu'il a réservé pour le transfert des données ;ServerSocketChannel du DTP du serveur ;
    La demande des fichiers qui sont destinés à un utilisateur (requête GET) est effectuée en 8 étapes (cf. Figure 2) :
FullDuplexMsgWorker ;ProtocolMessage contenant la requête GET ;ServerSocketChannel sur un port aléatoire libre ;ProtocolMessage contenant le « nom » de la requête (RECEIVE), le numéro du port qu'il a réservé pour le transfert de données, et les informations sur le fichier (expéditeur, nom du fichier) ;ProtocolMessage contenant la requête RECEIVE, le client se connecte au ServerSocketChannel du DTP correspondant du côté du serveur grâce au port indiqué dans le message ;Côté serveur, chaque DTP est pris en charge par un nouveau thread qui gère la communication en mode synchrone.
 Côté client, tous les échanges entre les DTP, mais aussi la réception des ProtocolMessages contenant la requête RECEIVE sont gérés par le thread principal en mode asynchrone avec un Selector.
Notez bien que le client sait qu'il n'a plus aucune requête RECEIVE à recevoir uniquement lorsque le serveur ferme la connexion du PI. Par ailleurs, un DTP récepteur sait que le fichier est totalement reçu uniquement lorsque le DTP émetteur ferme sa connexion.
    Certaines classes sont déjà écrites : soit vous les connaissez déjà, soit le sujet vous donne les instructions pour les utiliser.
    Le sujet offre une classe tsp.csc4509.spool.Spool qui prend en charge la création et l'exploration des fichiers. Vous n'avez pas besoin de lire ou étudier ce code. Toutes les fois qu'il faut utiliser une méthode de cette classe, le sujet donne l'instruction à écrire. Sachez juste que les fichiers stockés par le serveur sont dans le répertoire data/serveurSpool et les fichiers reçus par le client « userId » sont dans le répertoire data/spool/userId.
  
   Le sujet offre la classe tsp.csc4509.tcpnio.FullDuplexMsgWorker écrite et utilisée durant le TP. La classe MessType a été enrichie de la constante PROTOCOLMESSAGETYPE qui correspond au type des messages échangés par les PI des clients et des serveurs.
  
  Le sujet offre la classe tsp.csc4509.tpnote.ProtocolMessage  et l'énumération tsp.csc4509.tpnote.ProtocolMessageType qui décrivent les messages de protocole à échanger entre les PI des clients et du serveur. Le sujet donne les instructions à écrire lorsqu'il faut les utiliser.
  
Le sujet offre deux classes d'application :
AppliClient (classe déjà
        écrite) : cette classe possède une méthode main qui instancie un client (classe Client ci-dessous) ;
    AppliServer (classe déjà
        écrite) : cette classe possède une méthode main qui instancie un serveur (classe Server ci-dessous) ;
    
D'autres classes sont partiellement écrites et doivent être complétées :
SenderReceiver (classe à
            compléter) : elle fournit les méthodes (à écrire) pour envoyer ou recevoir un fichier en mode synchrone ;
        Client à compléter :
      Client (classe à
            compléter) : elle fournit le code de la requête PUT (déjà écrite) et de la requête GET (à écrire) ;
        Server (classe déjà
            écrite) : elle prépare la connexion, accepte tous les clients, et crée et démarre un thread par connexion ;
        ServerProtocolInterpreterRunnable (classe à
            compléter) : c'est un Runnable utilisé par le thread lancé à la connexion de chaque client. Cette classe gère la partie PI
          du serveur recevant les ProtocolMessage PUT et GET et en émettant les ProtocolMessage SEND
          et RECEIVE. C'est aussi cette classe qui crée et démarre les threads qui prennent en charge la partie DTP du server ;
        ServerDataTransfertProcessCallable (classe à
            compléter) : c'est un Callable utilisé par le thread lancé à la réception de chaque requête du client. Cette classe gère la partie DTP
          du serveur en recevant ou en émettant les fichiers à échanger.
        Il y a 7 questions :
SenderReceiver (4 pts) ;
    Client (6 pts) ;
    ServerProtocolInterpreterRunnable (7 pts) ;
    ServerDataTransfertProcessCallable (3 pts).
    Le barème est donné à titre d'indication.
 Les méthodes présentes dans l'interface SenderReceiver (Question 1), sont
  utilisées dans le client et dans le serveur. Elles devraient être écrites en premier.
  
Il est possible d'écrire le client ou le serveur dans l'ordre que vous voulez.
Pour le serveur, la question 4 est utilisée dans les questions 5 et 6, mais ces deux dernières peuvent
  être faites dans l'ordre que vous voulez. La dernière question (la question 7 de la classe ServerDataTransfertProcessCallable)
    suppose d'avoir compris l'énoncé des questions 5 et 6 ; mais, vous pouvez répondre à la question 7 sans avoir terminé ces
    questions 5 et 6.
  
Des tests unitaires sont fournis pour les questions 1, 2 3 et 4. Suivez les consignes qui vous sont données pour chacun des ses questions pour les utiliser.
SenderReceiver
  Cette interface fournit des méthodes communes au serveur et au client. Il s'agit des
  méthodes qui réalisent la réception et l'émission d'un fichier par un SocketChannel en
  mode synchrone.
  
recvFile() 
    La méthode recvFile() possède deux
    paramètres :
  
FileChannel fcout : la référence sur un canal
      qui est déjà relié à un fichier ouvert en écriture pour recevoir les données ;
    SocketChannel rwChan : la référence sur un canal
      déjà connecté avec l'émetteur des données à écrire dans le fichier.
    
   Écrivez la méthode recvFile() qui écrit
   dans le fichier relié à fcout toutes les
   données qui arrivent de la connexion reliée à rwChan.
   Pour cette méthode, vous utilisez des ByteBuffer
   de BUFFSIZE octets.
  
Les tests :
    Une fois votre méthode écrite, vous pouvez la tester. Pour cela,
    retirez l'annotation @Disable devant la
    méthode testRecvFile() de la classe de
    test TestReceiver et exécutez ce test.
  
sendFile()
    La méthode sendFile() possède deux
    paramètres  :
  
FileChannel fcin : la référence sur un canal déjà relié
      au fichier ouvert en lecture contenant les données à envoyer ;
    SocketChannel rwChan : la référence sur un canal
      déjà connecté avec le destinataire des données à lire dans le fichier.
    
   Écrivez la méthode sendFile() qui envoie
   sur la connexion reliée à rwChan toutes les
   données contenues dans le fichier relié à  FileChannel fcin.
   Pour cette méthode, vous utilisez des ByteBuffer
   de BUFFSIZE octets.
  
Les tests :
    Une fois votre méthode écrite, vous pouvez la tester. Pour cela,
    retirez l'annotation @Disable devant la
    méthode testSendFile() de la classe de
    test TestSender et exécutez ce test.
  
Client
  Dans son constructeur (déjà écrit), la classe Client ouvre un FullDuplexMsgWorker connecté
  au serveur. Ensuite, elle offre
  deux services possibles :
  
put() (déjà écrit) pour envoyer un fichier  ;get() (à écrire) pour demander et recevoir tous les fichiers qui
  sont destinés à l'utilisateur. Les fichiers sont stockés dans le spool du serveur.get() du client La méthode get() commence juste après l'étape 1 du protocole pour la requête GET
  (voir la section 2.3 et la figure 2). Elle doit gérer la réception de messages de
  plusieurs connexions : 
 Pour gérer tous ces flux sans bloquer, nous utilisons un Selector et donc nous passons
  toutes les connexions en mode asynchrone.
La structure de cette méthode doit donc ressembler à ce qui suit :
Voici les indications pour la mise en œuvre :
worker.configureNonBlocking() ;Selector, vous devez retrouver son SocketChannel avec la méthode worker.getChannel() ;sendGetMessage() ;receiveMessage, vous devez déclarer et initialiser ce fichier avec l'instruction « FileOutputStream fout = Spool.getOutputStream(receiveMessage) » ;SocketChannel avec l'instruction « SocketChannel rwChan = (SocketChannel) clef.channel() » ;SocketChannel, vous pouvez utiliser sa méthode getChannel() : si rwChan.equals(worker.getChannel()) est vrai, alors rwChan est le canal du worker ;Map pour retrouver le fichier à écrire à partir du canal de connexion qui lui est associé ; Écrivez la méthode get().
Les tests :
    Une fois votre méthode écrite, vous pouvez la tester. Pour cela,
    retirez les annotations @Disable devant les
    méthodes testGet1() à testGet5() de la classe de
    test TestClient.
  
Attention, ces tests lancent de nombreux threads difficiles à arrêter en cas de bugs dans votre code. De plus, certains de ces tests demandent de ré-initialiser le spool du client.
Donc, ne lancez pas ces tests avec votre IDE, les threads pourraient survivre jusqu'à l'arrêt de votre IDE, et le contexte ne serait pas le bon. Il y a 5 scripts présents à la racine du projet pour lancer ces tests un à un : les scripts  Une fois ces tests réalisés, replacez les en @Disabled, sinon le testGet1.sh à  testGet5.sh.
Chacun de ces scripts affiche au début et à la fin une phrase avec la raison du test. Utilisez ces scripts pour faire vos tests unitaires.
mvn install ne fonctionnera plus.
ServerProtocolInterpreterRunnable
La classe ServerProtocolInterpreterRunnable contient les instructions du thread démarré par le serveur après chaque accept pour gérer la requête envoyée par ce nouveau client.
Ce code prévoit donc la gestion de la requête PUT dans la méthode put() (à écrire) et de la requête GET dans la méthode get() (à écrire). Ainsi, la partie réception de la requête est déjà écrite dans la méthode run() et le traitement des requêtes est expliqué dans les questions qui suivent.
createServerSocketChannel() préparant le DTP du serveur Les requêtes PUT et GET mènent à l'échange de fichiers entre des DTP côté  client et des
DTP côté serveur. Pour préparer chaque DTP du serveur, il faut lier un ServerSocketChannel pour accepter
la connexion du DTP client. Le numéro du port de ce ServerSocketChannel n'a pas d'importance,
du moment qu'il est libre. Nous laissons donc le choix de ce numéro au système. Pour cela, il suffit de placer
la valeur du port à 0 dans l'objet InetSocketAddress utilisé pour l'appel à bind(). Ensuite (c'est-à-dire lorsque nous en aurons besoin dans la question suivante), il est possible de retrouver l'adresse d'un ServerSocketChannel lié avec la méthode ServerSocketChannel::getLocalAddress().
 La méthode createServerSocketChannel() prépare et retourne un ServerSocketChannel lié à un port libre choisi par le système.
Écrivez la méthode createServerSocketChannel().
put() du serveur La méthode put() côté serveur commence juste après l'étape 2 du protocole pour la requête PUT
  (voir la section 2.2 et la figure 1). Elle doit préparer le DTP du serveur, envoyer la requête SEND,
  puis créer et démarrer le thread qui met en œuvre le DTP.
  
La partie qui prépare le DTP est déjà présente dans le code qui vous est fourni. Vous devez
    compléter cette méthode en envoyant la requête SEND au client, et en créant et démarrant un thread qui fera tourner le ServerDataTransfertProcessCallable contenant le DTP du serveur. Pour la requête SEND, le résultat du call() n'est pas utilisé, et nous ne vous demandons pas de récupérer le résultat par un appel à get().
  
Écrivez la méthode put().
get() du serveur La méthode get() côté serveur commence juste après l'étape 2 du protocole pour la requête GET
  (voir la section 2.3 et la figure 2). Elle doit préparer le DTP du serveur, envoyer les requêtes RECEIVE,
  puis créer et démarrer les threads qui mettent en œuvre le DTP pour chaque fichier à envoyer.
  
L'instruction déjà présente dans le code (« List ») vous donne
  la liste de tous les fichiers à envoyer au client. Vous devez itérer sur tous les éléments de cette liste pour :
  
ServerSocketChannel lié à un port choisi par le système ;ServerDataTransfertProcessCallable contenant le DTP du serveur pour gérer cette requête ;Voici les instructions pour réaliser les points 1 et 2, en supposant que path est la variable contenant l'élément de la liste sur laquelle vous itérez :
Après avoir créé et démarré tous les threads mettant en œuvre les DTP du serveur, il faut attendre le résultat de chaque call() pour effacer le fichier du spool du serveur après son émission. Si vous mémorisez le résultat de la méthode get() dans une variable nommée path, l'instruction qui détruit le répertoire contenant le fichier et ses informations est « Server.deleteSpoolDir(path); ».
Cette dernière instruction détruit de façon récursive un répertoire. Même si certaines sécurités y ont été placées pour éviter de
    détruire des fichiers en dehors des spools du serveur, nous vous invitons à mettre cette instruction en commentaire dans votre code pour éviter tout accident sur vos fichiers. N'effacez pas votre tp noté, ou tout autre fichier important ! Si vous voulez nettoyez les spools, trois scripts sont à votre disposition :
    
    
    
 Écrivez le code qui complète la méthode get().
ServerDataTransfertProcessCallable Le code de la méthode call() de la classe ServerDataTransfertProcessCallable réalise les instructions du DTP du serveur pour les requêtes PUT et GET. La classe ServerDataTransfertProcessCallable possède trois attributs :
ServerSocketChannel listenChannel : le ServerSocketChannel du DTP sur lequel le client se connecte pour émettre ou recevoir ses fichiers ;Path path : le chemin du fichier à émettre ou recevoir. Pour ouvrir en lecture le fichier à émettre (cas des requêtes GET), vous devez utiliser l'instruction « FileInputStream fin = Spool.getInputFile(path); ». Pour créer et ouvrir en écriture le fichier à recevoir (cas des requêtes PUT), vous devez utiliser l'instruction « FileOutputStream fout = Spool.getOutputFile(path); » ;ProtocolMessage message : la requête émise par le client. Pour savoir si cette requête est un ProtocolMessageType.PUT ou un ProtocolMessageType.GET, vous devez utiliser la méthode message.getMessageType().call()La méthode call() doit accepter la connexion du DTP du client, et ensuite, suivant la requête, elle doit utiliser cette connexion pour soit émettre, soit recevoir le fichier désigné par l'attribut path.
Écrivez le code de la méthode call() en utilisant les méthodes écrites lors des questions 1 et 2.
Si vous avez écrit toutes les méthodes, vous pouvez lancer le script ./IntegrationTP.sh qui réalise le scénario présenté en début de sujet.
Notez que si vous avez suivi le sujet, le spool du serveur contient encore les fichiers destinés à Alice, puisque l'instructions qui sert à les effacer doit être en commentaire dans votre code.
Ce TP noté est terminé et vous avez écrit le code permettant de gérer une architecture avec un client asynchrone qui échange avec un serveur avec de multiples thread.
Ce programme reste très améliorable :