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 :