Transfert de fichier

L'objectif de ce TD est de programmer un serveur de transfert de fichier TFTP. Cette opération doit être à 100 % fiable (aucune perte de donnée autorisée). Il existe deux protocoles classiques pour le transfert de données sur Internet TCP permettant un échange fiable et UDP ne comportant aucun mécanisme qui assure la fiabilité de la communication. Comme l'objectif est de construire le protocole "manuellement", nous allons utiliser les sockets java datagrammes qui implémentent le protocole UDP. Le protocole FTP est un protocole de niveau "application" utilisant les service de transfert de données TCP et ne sera donc pas considéré dans ce TD. Nous nous intéresserons plutôt au protocole TFTP (Trivial File Transfer Protocol) qui nous permettra de découvrir la mise en oeuvre des mécanismes de transfert de fichiers en utilisant un protocole de détection et de correction d'erreur simpliste (Stop and wait). Les paquets sont envoyés un par un. Après l'envoi d'un paquet, un accusé de réception doit être nécessairement reçu avant de procéder à l'envoi du paquet de donnée suivant.

Documentation :

Exercice 1. Serveur TFTP

Réaliser un serveur de transfert de fichier qui accepte uniquement l'opération de lecture de fichiers, i.e. l'envoi d'un fichier demandé par un client. En lançant votre serveur sur une machine truite en écoute sur le port UDP 6969 dans un répertoire contenant un fichier fichier.txt, vous devriez pouvoir récupérer ce fichier avec le client tftp de la distribution debian qui se trouve en /users/profs/info/viennot/tftp (pour que ça marche, il vous reste à programmer le serveur :

truite$ /users/profs/info/viennot/tftp
tftp> connect truite 6969
tftp> binary
tftp> get fichier.txt
tftp> quit

1.1 Description de TFTP

Le client envoi en UDP au port d'écoute du serveur un paquet GET de format suivant :

+----------+-------------+---+----------------+---+
| code = 1 | nom_fichier | 0 | mode_transfert | 0 |
+----------+-------------+---+----------------+---+

(Un paquet similaire PUT (de format similaire avec le code 2) sert à indiquer l'envoi d'un fichier au serveur mais ne sera pas utilisé ici.)

Le serveur ouvre un nouveau socket datagram pour discuter avec le client (avec un port différent du port d'écoute) et répond par l'envoi du premier paquet de données du fichier. Le fichier est découpé en blocs de 512 octets, et chaque bloc est envoyé dans un paquet DATA de données au format suivant :

  +----------+----------+-------+
  | code = 3 | num_bloc | data  |
  +----------+----------+-------+

num_bloc indique le numéro de bloc codé sur deux octets (le premier bloc a numéro 1). data contient 512 octets de données sauf pour le dernier bloc qui est plus court (et contient entre 0 et 511 octets de données). Notons que l'en-tête UDP contient la longueur du paquet UDP (voir DatagramPacket.getLength()) ce qui permet au client de déduire la longueur de data. Le serveur attend l'acquittement du paquet avant d'envoyer le bloc suivant. Le client envoie pour cela pour chaque paquet DATA reçu un paquet ACK au format suivant :

  +----------+----------+
  | code = 4 | num_bloc |
  +----------+----------+

num_bloc indique le numéro du bloc qui a été reçu.

(Il existe de plus un paquet ERROR de code 5 pour indiquer une erreur qu'on n'utilisera pas ici.)

1.2 Serveur TFTP minimal

Écrire un serveur TFTP minimal qui attend l'arrivée d'un paquet GET sur le port 6969, ouvre un nouveau socket de datagrammes pour envoyer le fichier demandé dans des paquets DATA, et termine.

On pourra recevoir un paquet UDP et lire son contenu avec un ByteBuffer par une construction du type :

        DatagramSocket sock = new DatagramSocket (6969) ;
        DatagramPacket pkt = new DatagramPacket (new byte [516], 516) ;
        sock.receive (pkt) ;
	ByteBuffer buf = ByteBuffer.wrap (pkt.getData(), pkt.getOffset(), pkt.getLength()) ;
        short code = buf.getShort () ;
        ...

Pour lire une chaîne terminée par un 0, on utilisera :

    static String getNom (ByteBuffer buf) {
	StringBuilder n = new StringBuilder () ;
        byte c = buf.get () ;
	while (c != 0) {
	    n.append ((char) c) ;
	    c = buf.get () ;
	}
	return n.toString () ;
    }

Pour écrire et envoyer un paquet UDP, on utilisera une construction du type :

        DatagramPacket pkt = new DatagramPacket (new byte [516], 516) ;
	ByteBuffer buf = ByteBuffer.wrap (pkt.getData()) ;

	    // Peut-^etre r'ep'et'e plusieurs fois :
            buf.clear () ;
            buf.putShort ((short)3) ;
            ...
            pkt.setLength (buf.position()) ;
            sock.send (pkt) ;

Pour lire dans un fichier len octets et les copier dans un ByteBuffer, on utilisera la fonction suivante :

    // Tente de lire len octets dans f, renvoie le nombre d'octets
    //  lus (< len seulement si la fin de f est atteinte). 
    static int read (FileInputStream f, 
		      ByteBuffer buf, int len) throws IOException {
        byte[] b = buf.array () ;
        int pos = buf.position () ;
	int i = 0, tot = 0 ;
	do {
	    i = f.read (b, pos, len) ;
	    if (i == -1) break ; // fin de fichier
	    pos += i ;
	    len -= i ;
	    tot += i ;
	} while (len > 0) ;
        buf.position (pos) ;
	return tot ; // nombre d'octets lus
    }

1.3 Réglages du socket

Pour éviter que d'autres clients ne puissent venir interférer dans la connexion, on utilisera DatagramSocket.connect() pour restreindre les paquets acceptés par le socket de discussion avec le client.

Si un paquet ACK n'est pas reçu au bout d'un certain temps, le serveur doit renvoyer à nouveau le dernier paquet DATA. Pour ne pas rester bloqué dans l'attente d'un paquet, on utilisera DatagramSocket.setSoTimeout(). On pourra par exemple utiliser un timeout de 2 secondes.

1.4 Test

Testez par exemple votre programme avec test4blocs.txt qui a une taille de 2048 octets, soit 4 blocs. Attention de ne pas lancer le client dans le même répertoire que le fichier transféré.

Une solution.

Exercice 2. Tests avec pertes et répétitions

Utiliser TftpGet.java qui fournit un client plus erratique. Pour l'utiliser :

javac TftpGet.java
java TftpGet truite 6969 fichier.txt 0.2 0.6 0.2 1000

Les arguments numériques rendent les transmissions aléatoires, ils sont respectivement probErrRecep, probErrEnvoi, probPaqDupl, delaiEnvoiMax. (Le timeout utilisé par le client est de 200 milli-secondes.)

Vérifier le bon fonctionnement de votre serveur dans l'envoi de test4blocs.txt.

Afficher par le serveur le débit obtenu pour des fichiers de plusieurs mégaoctets. Comment le débit varie-t-il avec le taux de perte (avec probErrRecep = probErrEnvoi, et probPaqDupl = 0, delaiEnvoiMax = 0).

Exercice 3. Contrôle de flux

Dans l'exercice 1, le protocole de contrôle de flux implémenté par le serveur est le "stop and wait". Réaliser un serveur qui gère l'envoi d'une "fenêtre" de paquets avant l'arrêt en attente d'un accusé de réception. Tester le fonctionnement avec le client tftp standard. Quel débit moyen maximal obtenez vous ? Pour quelle taille de fenêtre ?

Utiliser le client de l'exercice 2. Si ça ne fonctionne pas ou si les performances sont médiocres, implémenter un serveur (puis un client) capable de gérer la mise en tampon mémoire des paquets de données envoyés pour pouvoir renvoyer les paquets perdus. Quel débit obtenez vous en fonction du taux d'erreur ?


Last modified: Thu Mar 1 14:49:26 CET 2007