**M2-RSH** **GLAR - TP2 : UDP** = David LE DAIN Vincent ORLANDO ## Partie 1 : protocole TFTP Le protocole TFTP est un protocole unicast basé sur UDP. Son implémentation et son décodage sont simples. ### Exercice 1 : Observation 1. Y a-t-il une demande de connexion initiale ? Effectuons tout d'abord un `get test.html` et vérifions la trace TFTP wireshark sur le port 6969: ![](../images/dMqgddi.png) Ensuite effectuons un `put test.html` et vérifions également la trace wireshark: ![](../images/LC8EAyy.png) L'analyse des captures wireshark nous montre les 5 envois successifs du client vers le serveur. Par conséquent, il n'y a pas de demande de connexion initiale. 2. Capturez une demande de lecture du fichier `/tmp/$nom.txt` et une demande d'écriture de `/tmp/$nom.txt` ou vous remplacerez $nom par votre nom de famille. * Demande de lecture du fichier `/tmp/ledain.txt` ![](../images/OJtqwZU.png) * Demande d'écriture du fichier `/tmp/ledain.txt` ![](../images/7SXBZLf.png) 3. Décrivez les champs de la requête en vous appuyant sur la RFC si nécessaire. Voici la représentation d'un paquet TFTP (Cf. RFC 1350): | 2 bytes | string | 1 byte | string | 1 byte | |:-------:|:--------:|:------:|:------:|:------:| | Opcode | Filename | 0 | Mode | 0 | * Description des champs pour la lecture du fichier : Opcode: 00 01 (2 octets): Read Request Filename (16 octets): 2f 74 ... 78 74 00: /tmp/ledain.txt Mode: 6e 65 ... 69 69 00 (9 octets): netascii * Description des champs pour l'écriture du fichier : Opcode: 00 02 (2 octets): Write Request Filename (16 octets): 2f 74 ... 78 74 00: /tmp/ledain.txt Mode: 6e 65 ... 69 69 00 (9 octets): netascii ### Exercice 2 : Décodage des requêtes Ce programme : * attends les paquets en prevenance d'un client sur le port 6969 * affiche le contenu du paquet au format hexadécimal * décode la requête sans la traiter Le code correspondant peut être décomposé en 4 parties distinctes. 1. la réception du paquet (une taille du buffer de 50 octets suffit pour stocker le nom d'un fichier) ```java // Réception du packet byte[] buffer = new byte[50]; DatagramPacket reception = new DatagramPacket(buffer, buffer.length); socketDuServeur.receive(reception); ``` 2. l'identification du type de paquet d'après la valeur du deuxième octet ```java //On détermine le type de requête String type = ""; switch (buffer[1]) { case 1 : type = "GET"; break; case 2 : type = "PUT"; break; default : System.out.println("Message inconnu"); } ``` 3. la délimitation et la copie des informations utiles ```java //On détermine le nom du fichier for (i=2; buffer[i] != 0; i++); int finNomFic = i; byte[] nomFic = Arrays.copyOfRange(buffer, 2, finNomFic); String nomFichier = new String(nomFic); //On détermine le mode de transmission finNomFic = finNomFic+1; for (i=finNomFic; buffer[i] != 0; i++); int finModeTrans = i; byte[] modeTrans = Arrays.copyOfRange(buffer, finNomFic, finModeTrans); String modeTransmission = new String(modeTrans); ``` 4. l'affichage du paquet et des informations utiles permettant le décodage du paquet ```java //On affiche le contenu du paquet au format hexadécimal finModeTrans = finModeTrans+1; byte[] paquet = Arrays.copyOfRange(buffer, 0, finModeTrans); System.out.println("----------------------------------------------------"); System.out.print("\nPaquet de type "+type+" de "+reception.getLength()+" octets envoyé par le client :"); affiche(paquet); //Décodage du paquet decodeRequest(type,nomFichier,modeTransmission); ``` La plus grosse partie de ce programme utilise donc la méthode `copyOfRange()` qui nous sert à délimiter et à copier les informations utiles contenues dans le buffer. Voici le résultat pour un PUT: ![](../images/UNJP5bx.png) ![](../images/mK2HD2d.png) Voici le résultat pour un GET: ![](../images/vBXOvGr.png) ![](../images/5VmFoo2.png) ### Exercice 3 : Acquitter En vous basant sur ce que vous avez fait précédement, ajoutez une méthode effectuant l'acquittement des paquets sendAck. Les acquittements pour PUT fonctionnent de la façon suivante : * après un premier PUT, le serveur répond ACK 0 * le client envoie ensuite des datagrammes DATA avec un block number (seqNumber) X * le serveur confirme chaque datagramme DATA X par un ACK X Une partie du code précédent traitant le décodage des requêtes a été repris ici. Nous allons donc étudier les parties de code supplémentaires qui permettent de traiter l'acquittement. Tout d'abord, il est important de noter qu'il est nécessaire de prendre une taille de buffer supérieure ou égale à 518 octets. En effet, la taille des paquets `DATA` dans le protocole TFTP est fixée à 512 octets par défaut, auxquels il faut rajouter 6 octets pour le reste des données du paquet (principe d'encapsulation). ```java // Réception du packet byte[] buffer = new byte[518]; DatagramPacket reception = new DatagramPacket(buffer, buffer.length); socketDuServeur.receive(reception); ``` De plus, il est nécessaire de prendre en compte deux types de paquets supplémentaires dans notre `switch` `case`, les paquets de données `DATA` et les paquets d'acquittement `ACK`. ```java case 3 : type = "DATA"; break; case 4 : type = "ACK"; break; ``` Ensuite, on traite l'envoi de l'acquittement en fonction du type de requête déterminé. ```java // Envoi de l'acquittement et gestion de l'envoi de plusieurs fichiers successifs // (RAZ du seqNumber et de la tailleTotale) if (type == "PUT") { seqNumber = 0; tailleTotale = 0; sendAck(socketDuServeur, seqNumber, reception.getSocketAddress()); seqNumber++; } else if (type == "DATA") { sendAck(socketDuServeur, seqNumber, reception.getSocketAddress()); seqNumber++; } ``` L'une des méthodes majeures du programme s'appelle `sendAck()`. Elle se décompose en 3 étapes : * l'instanciation d'un objet ByteBuffer, d'une taille de 4 octets, prenant comme variable le "short" 4 (correspond au paquet `ACK` dans le protocole TFTP) et qui comporte enfin un numéro de séquence permettant l'acquittement des paquets `DATA` envoyés par le client ; * l'affichage du paquet ainsi créé, permettant notamment d'observer le numéro de séquence du paquet `ACK` que l'on envoie au client ; * l'envoi à proprement parler du paquet vers le client. ```java // Méthode pour gérer l'acquittement des paquets reçus public static void sendAck(DatagramSocket server, short seqNumber, SocketAddress dstAddr) throws IOException { System.out.println("\nSend " + seqNumber + " to " + dstAddr); // Construction du paquet à envoyer ByteBuffer byteBuffer = ByteBuffer.allocate(4); byteBuffer.putShort((short) 4); byteBuffer.putShort(seqNumber); byte[] ACK = byteBuffer.array(); // Affichage du tableau de bytes envoyé System.out.print("\nOn envoie le paquet ACK suivant au client :"); byte[] paquetACK = Arrays.copyOfRange(ACK, 0, 4); affiche(paquetACK); // Envoi du paquet à la bonne addresse DatagramPacket paquetEnvoi = new DatagramPacket(ACK, ACK.length,dstAddr); server.send(paquetEnvoi); } ``` Les autres méthodes permettent surtout de valoriser l'affichage en mode console. Il est à noter que le programme prend en compte l'envoi successif de fichiers, mais pas l'envoi simultané. L'utilisation de Threads permettrait de répondre à cette problématique. Vérification du fonctionnement du code (avec un fichier de petite taille): ![](../images/I0QIK8G.png) ![](../images/Wx4O7Tx.png) *Nota Bene :* le calcul de la taille du fichier affichée en fin d'envoi diffère très légérement de la taille réelle du fichier envoyé. En effet, on se base ici sur l'ensemble des données contenues dans un paquet DATA. Mais du fait de l'encapsulation, les données reçues seront toujours plus importantes que les données réellement envoyées. Vérification du fonctionnement du code (avec un fichier plus lourd) ![](../images/RqpYGew.png) ``` java Serveur en attente de connexion... -------------------------------------------------------- Paquet de type PUT de 22 octets envoyé par le client : 00 02 74 65 73 74 32 2e 68 74 6d 6c 00 6e 65 74 61 73 63 69 69 00 Décodage du paquet : Type : PUT, nom du fichier : test2.html, mode de transmission : netascii Send 0 to /127.0.0.1:52110 On envoie le paquet ACK suivant au client : 00 04 00 00 -------------------------------------------------------- Paquet de type DATA de 516 octets envoyé par le client : 00 03 00 01 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 0d 0a 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 62 0d 0a 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 63 0d 0a 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 64 0d 0a 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 65 0d 0a 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 66 0d 0a 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 67 0d 0a 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 0d 0a 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 00 Send 1 to /127.0.0.1:52110 On envoie le paquet ACK suivant au client : 00 04 00 01 -------------------------------------------------------- Paquet de type DATA de 102 octets envoyé par le client : 00 03 00 02 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 69 0d 0a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 6a 0d 0a 00 Send 2 to /127.0.0.1:52110 On envoie le paquet ACK suivant au client : 00 04 00 02 Réception du fichier terminée. Taille du fichier reçu : 618.0 octets ``` On observe bien la séquence suivante entre le client et le serveur : ```mermaid sequenceDiagram participant Client TFTP participant Serveur TFTP Client TFTP->>Serveur TFTP: PUT Serveur TFTP->>Client TFTP: ACK seqNum 00 Client TFTP->>Serveur TFTP: DATA seqNum 01 (516 o) Serveur TFTP->>Client TFTP: ACK seqNum 01 Client TFTP->>Serveur TFTP: DATA seqNum 02 (102 o) Serveur TFTP->>Client TFTP: ACK seqNum 02 ``` C'est-à-dire: * un PUT envoyé par le client au serveur ; * un premier ACK à 0 renvoyé par le serveur ; * un premier envoi de DATA de 516 octets par le client ; * un ACK renvoyé par le serveur pour la réception de ce premier envoi ; * un second envoi de 102 octets par le client ; * un ACK renvoyé par le serveur pour la réception de ce second et dernier paquet. ### Exercice 4 Capturez un échange entre votre serveur et votre client grâce à wireshark. Pour obtenir l'échange recherché entre le client et le serveur nous appliquons le filtre suivant sur notre interface de loopback dans wireshark : **udp.port == 6969** 1. Capture avec un fichier de petite taille : ![](../images/2AZpWSa.png) ![](../images/PppGnw4.png) Nous obtenons bien 4 échanges entre le port 58820 (client) et le port 6969 (serveur) que nous allons détailler. * **No.1:** premier datagramme du client vers le serveur pour établir la connexion et prévenir de l'envoi d'un fichier, grâce à un message PUT caractérisé par le deuxième octet du paquet défini à 2 (visible dans le champ Data de la fenêtre wireshark). ![](../images/rPhdIcE.png) ![](../images/ODRHVe9.png) * **No.2:** le serveur répond par un ACK à 00, avec le deuxième octet défini à 04 visible dans le champ Data. ![](../images/8S1kIuN.png) ![](../images/u3zGBn8.png) * **No.3:** le client envoi le datagramme DATA (contenu du fichier). On peut identifier que le numéro de séquence envoyé est 01. ![](../images/HlDgtom.png) ![](../images/Uk1v37i.png) On voit bien ici le contenu du fichier en clair dans wireshark. De plus, ce paquet est d'une taille inférieure à 516 octets, on en déduit donc que la réception du fichier est terminée. * **No.4:** le serveur renvoi un ACK pour confirmer qu'il a reçu la totalité du datagramme. Il répond au numéro de séquence utilisé par le client, c'est-à-dire 01. ![](../images/F4T6QWJ.png) ![](../images/8BjksNW.png) 2. Capture avec un gros fichier : Ici on va transmettre le fichier test2.html qui va demander 2 envois de paquets différents pour être totalement diffusé. On constate bien sur wireshark le premier acquittement après la réception des 516 premiers octets, puis le second acquittement pour la réception des 102 derniers octets. ![](../images/1Qv1g3G.png) ## Partie 2 : Chat UDP MULTICAST Dans cette partie nous avons fait le choix de réaliser un chat en utilisant l’API Multicast de Java sur les sockets. Ce chat devait respecter les caractéristiques suivantes : * il n’y a pas de serveur * les messages s’échangent sur le canal 225.0.4.1 ```java InetAddress groupeIP = InetAddress.getByName("225.0.4.1"); int port = 9999; ``` * le nom doit être envoyé à chaque envoi de message « bob> message » Ici on va déjà prendre en compte la saisie du message par une lecture au clavier avec une condition de sortie du chat. Ensuite, lorsqu'un message (non vide) est envoyé, il est précédé du nom du rédacteur. ```java // Boucle permettant la saisie des messages while(true) { String message = ""; message = lecture.nextLine(); // Condition de sortie du chat if(message.equalsIgnoreCase("exit")) { finished = true; lecture.close(); socket.leaveGroup(groupeIP); socket.close(); break; } // Si l'utilisateur a écrit quelque chose, on envoie le message // qui commencera par le nom d'utilisateur. if(!message.isEmpty()) { message = nomUser + "> " + message; envoyerMessage(socket,message,groupeIP,port); } } ``` * la classe possédera les méthodes suivantes : * `envoyerMessage(MulticastSocket server, String message, InetAddress groupeIP, int port)` ; Cette méthode crée les paquets (d'une taille maximale de 1024 octets) d'après les données saisies au clavier par l'utilisateur, puis les envoie. ```java // Méthode pour l'envoi des messages public static void envoyerMessage(MulticastSocket server, String message, InetAddress groupeIP, int port) throws IOException { // Création du paquet pour l'envoi, limité comme demandé à 1024 octets byte[] buffer = new byte[1024]; buffer = message.getBytes(); DatagramPacket packet; packet = new DatagramPacket(buffer, buffer.length, groupeIP,port); // On teste si le paquet fait moins de 1024, sinon on avertit l'utilisateur que son // message n'a pas été envoyé. if(buffer.length >= 1024) { System.out.println("Message non envoyé : votre message est trop long (vous êtes trop bavard !)"); } else { // Envoi du paquet en multicast server.send(packet); } } ``` * * `String recevoirMessage(MulticastSocket server, String message, InetAddress groupeIP, int port)` Cette méthode va servir à la réception et l'affichage des messages. Même si cette fonctionnalité n'était pas demandée, nous avons essayé de gérer le Charset (les tests se sont effectués entre un client Windows et Linux). Sans gestion, à l'affichage nous avions des soucis de sauts de lignes supplémentaires, ainsi que des problèmes pour les caractères accentués. Avec l'ajout du Charset dans le code, nous avons toujours des soucis concernant les accents, mais plus pour les sauts de ligne. Par ailleurs, pour faciliter la lecture dans le chat, nous avons choisi de n'afficher que les messages reçus dans la fenêtre du client. Et on indique aussi l'heure de réception. Pour finir, un des points prépondérant du programme et que cette méthode est bloquante tant qu’un message n’est pas reçu. C'est pourquoi on l'utilise dans notre deuxième Thread. ```java // Méthode pour la réception des messages public void recevoirMessage(MulticastSocket server, String message, InetAddress groupeIP, int port) throws IOException { // On crée un buffer d'une taille de 1024 octets byte[] buffer = new byte[1024]; DatagramPacket packet = new DatagramPacket(buffer,buffer.length,groupeIP,port); socket.receive(packet); // Même si non demandé, on inclut la gestion du charset en UTF-8 (les tests ont été // réalisés sur un client Linux et Windows) message = new String(buffer,0,packet.getLength(),"UTF-8"); // On n'affiche que les messages reçus et non ceux émis pour des questions // de lisibilité à l'écran if(!message.startsWith(ChatMulticast.nomUser)) { affHeure(ChatMulticast.horloge="heure"); System.out.println(" "+message); } } ``` * * utilisation d'un Thread pour être capable d’envoyer et de recevoir simultanément ```java // On crée le thread pour la réception et l'affichage des messages Thread t = new Thread(new ReadThread(socket,groupeIP,port)); t.start(); ``` ```java // Interface qui sera exécutée lorsque le Thread est démarré class ReadThread implements Runnable { private MulticastSocket socket; private InetAddress groupeIP; private int port; ReadThread(MulticastSocket socket,InetAddress groupeIP,int port) { this.socket = socket; this.groupeIP = groupeIP; this.port = port; } @Override public void run() { // Boucle pour la réception et l'affichage des messages while(!ChatMulticast.finished) { String message=""; try { recevoirMessage(socket,message,groupeIP,port); } catch(IOException e) { // En cas de fermeture du socket System.out.println("Au revoir..."); } } } ``` **Validation du fonctionnement du chat entre un client Windows et un client Linux (Ubuntu).** * Client Windows : ![](../images/FKPNCO3.png) * Client Linux : ![](../images/dxBk3ED.png) Capture wireshark : ![](../images/Vs5QpW6.png) Test d'envoi d'un message trop long : * Côté Linux ![](../images/LCcjulv.png) * Côté Windows ![](../images/Y7KZL28.png) Sur la capture wireshark, on voit que le message trop long n'est pas envoyé. ![](../images/FsPORQA.png)