Projet informatique : transfert de fichier par réseau

I. Présentation

A. Introduction - Cahier des charges

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 :

B. Choix

Un certain nombre de choix ont été faits lors de l'élaboration du cahier des charges :

C. Fonctionnement général

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é.

Dépendances entre paquets
Dépendances entre paquets

II. Architecture

A. Structure commune

Arbre des classes
Arbre des classes

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.

Structure principale du client
Structure principale du client
Structure principale du serveur
Structure principale du serveur

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.

B. Structure des paquets UDP

Structure d'un paquet UDP
Structure d'un paquet UDP

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 :

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.

C. Structure serveur

Structure interne du serveur
Structure interne du serveur

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).

Création d'un nouveau transfert
Création d'un nouveau transfert

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.

D. Structure client

Structure interne du client
Structure interne du 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.

III. Fonctionnement

A. Côté serveur

Réception d'un paquet
Réception d'un paquet

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.

Perte de paquet
Perte de paquet

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.

B. Côté client

Démarrage d'un nouveau transfert
Démarrage d'un nouveau 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.

Perte d'un paquet
Perte d'un paquet

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.

IV. Optimisation et prise de décisions

A. Demande d'un paquet perdu

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.

B. Optimisation de l'intervalle d'envoi

1. Présentation

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.

2. Algorithme

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.

V. Évolutions possibles

L'algorithme présenté ci-dessus ne parvient pas à atteindre correctement l'intervalle optimal, il faudrait donc l'améliorer. De plus, ce programme pose clairement un problème de sécurité puisque aucune authentification n'est réalisée lors de la réception d'un fichier : c'est la porte ouverte à des attaques de type DoS visant par exemple à saturer le disque de la machine cible.

VI. Programme

A. Configuration nécessaire

Ce programme nécessite la configuration suivante :

Information
Ni le client, ni le serveur ne nécessitent un système graphique pour fonctionner. Si le système ne dispose que d'une console texte, alors le paramètre -console est implicite.
Attention
Si le système possède une interface graphique active, démarrer à partir d'une console le programme ne démarre pas le programme en mode console (sauf si le paramètre -console est spécifié).

B. Téléchargement

Versions compilées ainsi que les sources du programme de transfert :

VII. Documentation

A. Démarrage

1. Client

Pour démarrer le client, on peut soit exécuter directement le jar, soit en ligne de commande :

java -jar client.jar [args]
Argument en ligne de commande
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
Information
Si l'interface graphique est activée, définir les paramètres -tcp, -udp, -host et -file permet de changer les valeurs par défaut dans les champs texte. Sinon, en mode console, ces valeurs seront utilisés et le transfert débutera avec ses paramètres.

2. Serveur

Pour démarrer le serveur, on peut soit exécuter directement le jar, soit en ligne de commande :

java -jar serveur.jar [args]
Argument en ligne de commande
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)

B. Javadoc

Pour de plus amples informations, il est conseillé de consulter la Javadoc du programme.