Ce projet informatique consiste en la création d'un programme de transfert de fichier par réseau. Le but était de réaliser le transfert par UDP pour pouvoir se rendre compte des problèmes inhérents à ce prototocole : perte de paquets, désordre des paquets à l'arrivée... Le cahier des charges est le suivant :
Un certain nombre de choix ont été faits lors de l'élaboration du cahier des charges :
Au démarrage de serveur, celui-ci crée un socket serveur TCP (TCPListener) et un socket serveur UDP (UDPListener). Le flux TCP gère le déroulement du transfert, c'est pourquoi, lorsqu'un client veut envoyer un fichier au serveur, il se connecte sur le serveur TCP. Quand le serveur reçoit une nouvelle connexion TCP, il initialise un nouveau gestionnaire de transfert (TransfertHandler) qui gérera ce nouveau transfert.
Ce gestionnaire de transfert échange ainsi avec le client, par l'intermédiaire du flux TCP, les caractéristiques du fichier à envoyer : son nom, sa taille et sa somme de contrôle. Si l'initialisation se déroule correctement, alors le serveur attribue au client un identificateur de transfert unique qui permettra ensuite au serveur de trier les paquets entrants. En effet, le transfert de données est effectué par UDP et tous les clients envoient leurs paquets vers le même socket UDP. Le serveur a donc besoin d'un identificateur pour ensuite trier les données entrantes et les transmettre au gestionnaire de transfert concerné.
Chaque gestionnaire de transfert stocke toutes les données entrantes le concernant. Cependant, le gestionnaire de fichier (FileOutput), en charge d'écrire correctement les données reçues dans un fichier, écrit ces données séquentiellement : il attend que les données nécessaires soient reçues si tel n'est pas le cas après un certain temps, alors les données sont redemandées au client. Si celui-ci ne répond pas, alors au bout d'un certain temps d'attente, le transfert est annulé.
À la fin du transfert, quand le gestionnaire de fichier du serveur atteint la fin du fichier, la somme de contrôle du fichier reçue est calculée, et le transfert finalisé.
Toutes les classes de ce projet sont des threads et héritent indirectement de StoppableThread. Cette classe met à disposition des méthodes pour arrêter correctement les threads et libérer les ressources associées (sachant que les sockets et les flux ne sont pas automatiquement libérés par le garbage-collector). La méthode quit() permet de demander au thread de s'arrêter mais retourne immédiatement pour éviter les problèmes de "dead-lock". Les appels à cette méthode peuvent ainsi être imbriqués et circulaires. Seuls les descendants de ThreadStopper attendent la fermeture des autres threads par l'intermédiaire de la méthode quitThreadAndWait() : les seuls descendants sont les ordonnanceurs et le gestionnaire de transfert du serveur.
Le client et le serveur possèdent une structure principale commune : la classe principale gèrant l'interface et un ordonnanceur gérant le ou les transferts.
La classe principale (qui dérive de TransfertMaster) analyse les paramètres passés par ligne de commandes puis, s'il n'y a pas eu d'erreur, initialise l'interface. Si l'interface graphique n'est pas activée, alors l'ordonnanceur est démarré et le transfert ou l'attente de transfert débute.
Les paquets UDP possèdent une taille fixe de 1409 octets : la taille a été prise arbitrairement inférieure à la taille classique du MTU sur les réseaux ethernet classiques (valeur MTU de 1500) pour éviter la fragmentation. Le paquet contient trois informations différentes :
byte) : 1 octet.long) : 8 octets.byte[]) : 1400 octets (arbitraire).Le serveur UDP accepte tous les paquets entrants et les trie en fonction de l'identificateur du paquet : l'ordonnanceur serveur possède une table des transferts actifs (transfertIDs). Si le paquet possède une taille invalide ou est adressé à un transfert inexistant, il est détruit.
Comme l'indique le diagramme UML, l'ordonnanceur serveur (ServerScheduler) ne démarre que les sockets UDP et TCP. Le serveur TCP attend ainsi la connexion d'un client et c'est celui-ci qui instantie un gestionnaire de transfert. Lorsque le gestionnaire de transfert (TransfertHandler) est instantié, celui-ci démarre un gestionnaire de connexion TCP (TCPConnection) et un gestionnaire de fichier (FileOutput).
Le gestionnaire de connexion TCP gère la connexion au client : il transmet au gestionnaire de fichier les caractéristiques du fichier à recevoir puis il gère, si nécessaire, les commandes du client (typiquement une annulation de transfert).
Le gestionnaire de fichier gère l'écriture séquentielle du fichier reçu. Lorsque l'écriture du fichier est terminée, il prévient le gestionnaire de transfert de la fin de celui-ci. Le gestionnaire de fichier gère aussi l'optimisation du temps d'intervalle (grâce à TimeAdjustment) entre l'envoi de deux paquets successifs par le client.
Le client ouvre une connexion TCP avec le serveur puis envoie au travers de cette connexion les caractéristiques du fichier ; il reçoit alors un identificateur de transfert qu'il utilisera pour marquer ses paquets UDP. L'ordonnanceur instantie un gestionnaire de fichier, le gestionnaire de la connexion TCP et un gestionnaire d'envoi de paquets UDP.
Le gestionnaire de fichier (FileInput) peut lire de façon "aléatoire" le fichier pour pouvoir relire des données concernant des paquets qui auraient été perdus. En effet, le gestionnaire de fichier possède une file d'attente des offsets à lire : à chaque itération, les données de l'offset sont lues et placées en file d'attente pour envoi dans la file du gestionnaire concerné (UDPClient). Le gestionnaire de fichier possède une deuxième pile du même type, mais prioritaire, qui contient les offsets de paquets perdus : cela permet de réduire le temps de renvoi d'un paquet perdu.
Le gestionnaire d'envoi possède donc une file d'attente et envoie régulièrement les paquets présents dans la file. Le temps d'attente entre l'envoi de paquet est ajusté par le serveur (par l'intermédiaire de la connexion TCP) afin de réduire le temps d'attente du côté du serveur.
Enfin, le gestionnaire de connexion TCP gère les demandes du serveur : le plus souvent, une demande de renvoi d'un paquet perdu.
Lorsque le serveur UDP reçoit un paquet, il stocke celui-ci dans la table de données du transfert concerné. Pendant ce temps, le gestionnaire de fichier teste régulièrement si l'offset désiré est présent. Dès que c'est le cas, il récupère celui-ci de la table de données, l'écrit dans le fichier puis passe à l'offset suivant. À chaque réception d'un paquet, le gestionnaire de fichier demande à l'adaptateur de temps de prendre une décision concernant le temps d'attente entre l'envoi de données.
Au bout d'un certain temps, si l'offset désiré n'est toujours pas présent dans la table de données, alors le gestionnaire de fichier peut décider de demander le renvoi du paquet au client ou d'annuler le transfert.
Lorsque le transfert démarre, l'ordonnanceur ajoute tous les offsets du fichier à la file d'attente du gestionnaire de fichier. Ceci est le processus normal d'envoi de l'intégralité des données. Ainsi, à chaque itération, le gestionnaire de fichier récupère le premier offset de la pile (celle prioritaire ou, à défaut, celle normale) puis lit les données concernées dans le fichier. Le gestionnaire prépare alors les données du paquet et le place dans la file d'attente du gestionnaire d'envoi. Celui-ci traitera ensuite le paquet quand il arrivera en première position dans la file d'attente.
Lorsque le flux TCP reçoit une demande de renvoi d'un paquet, il place l'offset concerné en file d'attente prioritaire du gestionnaire de fichier pour un traitement rapide. Ensuite, de la même manière que le fonctionnement normal, les données concernées sont lues (après déplacement du pointeur dans le fichier) et placées en file d'attente pour envoi.
Le gestionnaire de fichier attend que les données de l'offset courant soient présentes dans la table de données. Ce temps d'attente permet de décider si un paquet doit être considéré perdu ou pas. Deux temsp d'attentes sont stockés : le temps d'attente total pour l'offset courant, et le temps d'attente entre les demandes au client. L'algorithme de décision est assez simpliste : si le temps d'attente dépasse une certaine valeur (TIME_MAX_REQUEST_PACKET, typiquement 1 seconde) alors le paquet est considéré perdu et est demandé une nouvelle fois au client par l'intermédiaire de la connexion TCP. Le temps d'attente entre demandes au client est alors remis zéro : cela permet de demander plusieurs fois le paquet au client si nécessaire. Cependant, si le temps d'attente total dépasse la valeur TIME_MAX_MISS_PACKET alors le programme considère que le client ne répond plus et annule le transfert.
Pour optimiser le taux de transfert, il faut que le temps d'attente défini ci-dessus soit minimal. Pour rendre ce temps minimal, il faut que l'intervalle d'envoi des paquets par le client soit en adéquation avec la qualité de la connexion. En effet, un intervalle trop long n'utilise pas de façon optimale la bande passante, et un temps d'attente trop court fait que la plupart des paquets se perdent et dégrade fortement la connexion ce qui peut mener à l'annulation du transfert par le serveur.
Le gestionnaire de transfert enregistre un certain nombre de données sur le transfert :
L'adapdateur de temps (TimeAdjustment) réalise ses statistiques et prend ses décisions tout les 30 (par exemple) paquets reçus pour que les calculs soient suffisament significatifs. Les statistiques calculées sont le taux de paquets perdus (calculé sur tous les paquets) et le temps d'attente moyen par paquet (calculé sur les derniers paquets).
On peut remarquer qu'un intervalle d'envoi trop élevé donne un temps d'attente élevé et un taux de paquets perdus faible. Un intervalle d'envoi trop faible fait augmenter significativement le taux de perte et augmente le taux d'attente. De plus, laisser trop longtemps un intervalle d'envoi trop faible peut faire annuler le transfert car tous les paquets seront considérés perdus ce qui augmentra le traffic TCP et donc dégradera encore plus la qualité de la connexion.
L'algorithme est testé sur deux cas principaux : transfert sur la boucle locale et transfert par Internet assymétrique. Les intervalles d'attente optimaux respectifs sont 0 et 24 ms. Le temps d'attente par défaut est de 50 ms, ensuite l'algoritme tente d'optimiser ce temps.
Sachant que l'intervalle par défaut est très élevé, l'adaptateur baisse le l'intervalle d'attente et enregistre le temps moyen d'attente. Tant que le temps d'attente diminue quand l'intervalle diminue, l'intervalle est encore diminué. Si le temps d'attente commence a augmenter au lieu de diminuer, alors l'algorithme considère que l'on a atteint un intervalle optimal et enregistre alors cet intervalle.
Le problème est qu'il y a aussi des perturbations alétoires à prendre en compte. En effet, si par exemple à 50 ms, un paquet est perdu alors le temps d'attente augmentra alors que l'on a pas atteint un optimum. C'est pourquoi, lorsqu'un "optimum" est atteint, celui-ci est mémorisé et l'intervalle de temps réaugmenté afin de revérifier que c'était bien un optimum. Ceci est réalisé un certain nombre de fois, puis une moyenne des intervalles d'attente optimaux seront calculés (en enlevant le plus élevé et le plus faible). Enfin, cet intervalle optimal est envoyé au client et l'adaptateur s'arrête.
Il faut aussi prendre garde que lorsqu'un paquet est perdu, les paquets suivant sont stockés ainsi lorque le gestionnaire de fichier les demandera, le temps d'attente sera quasi nul. C'est pourquoi sur quelques itérations, les statistiques ne sont pas enregistrées pour ignorer ces fluctuations non désirées.
Cependant, même en calculant les moyennes, la perte aléatoire de paquets gène l'optimisation. C'est pourquoi l'algorithme peut aléatoirement ignorer un optimum. Il considère en effet que si le taux de perte est encore faible, il y a de grandes chances que l'optimum ne soit pas valide. Il ignore donc, en pondérant avec le taux de perte, certains optimums.
Ce programme nécessite la configuration suivante :
Versions compilées ainsi que les sources du programme de transfert :
Pour démarrer le client, on peut soit exécuter directement le jar, soit en ligne de commande :
java -jar client.jar [args]
| Commande | Paramètre | Action | Valeur par défaut |
|---|---|---|---|
| -help | Aucun | Affiche l'aide et quitte. | Non applicable |
| -console | Aucun | Force le mode console. Si un mode graphique est disponible, l'application démarre en mode graphique sauf si ce paramètre est spécifié. | Désactivé, si le système permet les applications graphiques |
| -execute | Aucun | Désactive le mode intéractif. Si l'interface graphique est désactivée ou non disponible, alors le programme s'exécutera seul sans intéraction possible avec l'utilisateur par la console. Permet de quitter automatiquement à la fin d'un transfert (utile dans un script par exemple). | Désactivé |
| -verbosity | Verbosité | Définit la verbosité du programme définis dans l'ordre croissant : DEBUG, NORMAL, NOTICE, CRITICAL. Définir une verbosité à DEBUG peut ralentir considérablement le programme, et causer des dysfonctionnement dans la connexion à l'hôte distant. | NOTICE |
| -tcp | Port TCP | Définit le port TCP sur lequel se connecter. | 9856 |
| -udp | Port UDP | Définit le port UDP sur lequel envoyer des données. | 9857 |
| -host | Adresse IP ou nom DNS | Définit l'adresse du serveur sur lequel se connecter. | localhost |
| -file | Nom de fichier | Définit le fichier à envoyer. | Aucun, doit être défini si l'interface graphique est désactivée |
Pour démarrer le serveur, on peut soit exécuter directement le jar, soit en ligne de commande :
java -jar serveur.jar [args]
| Commande | Paramètre | Action | Valeur par défaut |
|---|---|---|---|
| -help | Aucun | Affiche l'aide et quitte. | Non applicable |
| -console | Aucun | Force le mode console. Si un mode graphique est disponible, l'application démarre en mode graphique sauf si ce paramètre est spécifié. | Désactivé, si le système permet les applications graphiques |
| -verbosity | Verbosité | Définit la verbosité du programme définis dans l'ordre croissant : DEBUG, NORMAL, NOTICE, CRITICAL. Définir une verbosité à DEBUG peut ralentir considérablement le programme, et causer des dysfonctionnement dans la connexion à l'hôte distant. | NOTICE |
| -tcp | Port TCP | Définit le port TCP sur lequel démarrer le serveur (si le port TCP est inférieur à 1024, le programme nécessite alors les droits administrateur). | 9856 |
| -udp | Port UDP | Définit le port UDP sur lequel démarrer le serveur (si le port UDP est inférieur à 1024, le programme nécessite alors les droits administrateur). | 9857 |
| -incoming | Nom de dossier | Définit le dossier de réception des fichier : tous les fichiers reçus seront stockés dans ce dossier ou dans le dossier courant par défaut. Le dossier spécifié doit exister et être accessible en écriture. | Dossier courant (.) |
| -adjuster | Nom de classe de l'adaptateur | Définit l'algorithme à utiliser pour optimiser l'intervalle d'envoi. | Adapdateur par défaut (RandomLost) |
Pour de plus amples informations, il est conseillé de consulter la Javadoc du programme.