Multicast local

L'objectif de ce TD est se familiariser avec la transmission sur réseau local. Certaines topologies de réseaux locaux (telle que Ethernet) supportent la diffusion (ou broadcast), c'est à dire l'envoi d'un message à toutes les machines connectées au réseau en une seule opération d'envoi.

Pour cela, nous vous proposons de programmer une application de « chat » à plusieurs (ou discussion multipoint). Votre programme devra envoyer sur le réseau Ethernet de la salle toutes les lignes lues sur l'entrée standard et afficher sur la sortie standard tous les messages reçus sur le réseau.

Documentation java utilisée dans le sujet :

1. Envoyer et recevoir

Java permet d'envoyer et de recevoir des paquets sur l'internet. En schématisant, un paquet est principalement constitué de :

1.1 Le tuyau

Un objet système permet d'envoyer et recevoir des paquets : le « socket ». On peut en avoir l'image d'un tuyau par lequel on peut envoyer ou recevoir des paquets. Le numéro de port identifie en fait le socket. Les paquets envoyés par le socket ont ce numéro comme port source. Parmi les paquets reçus par la machine, seul ceux à destination de ce numéro seront reçu via le socket. On peut dire que le socket « écoute » ce port.

Nous allons utiliser ici la classe MulticastSocket qui permet des communications de groupe.

Écrire une classe Socket pour l'envoi et la reception de paquets sur le chat. Utiliser les importations suivantes :

import java.net.* ; // MulticastSocket, InetAddress, DatagramPacket,...
import java.io.* ; // exceptions comme UnknownHostException, SocketException,...
import java.nio.* ; // pour ByteBuffer
import java.util.* ; // pour Scanner

Votre classe contiendra les champs :

    int port ; // port d''ecoute du socket
    InetAddress multicastAddress ; /* L'adresse IP utilis'ee pour la
                                      communication de groupe. */
    MulticastSocket socket ; // Le tuyau identifié par son port

Écrire un constructeur qui :

Inclure tous ces appels dans un try { ... } catch (IOException e) { throw new Error (e) ; } pour transformer en erreur les exceptions qui pourraient se produire.

1.2 Les paquets

Le but ici est de manier les méthodes pour envoyer et recevoir des paquets, il vous faudra chercher un peu dans la documentation de java...

Pour manier le contenu des paquets nous utiliserons la classe ByteBuffer. Un ByteBuffer b manipule de manière interne un tableau d'octets (ou byte en java) que l'on obtient par b.array(). On peut obtenir sa capacité par b.capacity(). On peut de plus lire et écrire à une position courante (obtenue par b.position()) par des méthodes de type get et put. Les lectures se font jusqu'à une position limite donnée par b.limit().

Écrire une méthode send() qui prend en argument un ByteBuffer et envoie son contenu sur le socket par la méthode send de MulticastSocket. Il faudra pour cela construire un DatagramPacket dont on fixera le contenu par le constructeur. Seuls les octets entre le début du ByteBuffer et sa position courante doivent être envoyés. On fixera l'addresse et le port destinations par les méthodes setAddress() et setPort(). (L'adresse destination est à nouveau 231.1.2.3, et le port 7654 puisque c'est le même socket qui va nous servir à envoyer et à recevoir.)

Écrire une méthode receive() qui prend en argument un ByteBuffer, rempli son contenu par celui du premier paquet qui vient et renvoie l'adresse et le port de l'envoyeur sous forme de SocketAddress. Utiliser pour cela la méthode receive() de MulticastSocket pour recevoir un paquet. Lui fournir un DatagramPacket qu'on aura initialisé pour partager le même tableau de bytes que le ByteBuffer. Elle écrira alors le contenu du premier paquet reçu dans le ByteBuffer. Mettre ensuite la position du ByteBuffer à 0 et sa limite à la longueur du DatagramPacket (qui est obtenue par getLength()). Renvoyer l'addresse et le port de l'envoyeur du paquet obtenus par getSocketAddress() (qui renvoie en un seul objet le résultat de getAddress() et getPort()).

Tester dans un premier temps par le code ci-dessous qui envoi un message lu dans les arguments du programme et affiche dans une boucle infinie tous les paquets reçus. (La méthode getString sert à lire une String de longueur connue dans un ByteBuffer, on remarquera aussi la conversion de String vers byte[] par getBytes()).

    static String getString (ByteBuffer b, int len) {
        byte[] t = new byte[len] ;
        b.get (t) ;
        return new String (t, 0, len) ;
    }

    public static void main (String[] args) {
        Socket s = new Socket (7654) ;

        ByteBuffer b = ByteBuffer.allocate(1400) ;
        String msg = "envoi" ;
        for (String a : args) msg += " " + a ;
        b.put (msg.getBytes()) ;
        s.send (b) ;

        while (true) {
            System.out.println ("reception de " + s.receive (b) + " : "
                                + getString (b, b.limit())) ;
        }
    }

Question philosophique : Les méthodes getAddress() et getPort() de DatagramPacket renvoient l'adresse IP de l'envoyeur ainsi que le port du socket avec lequel il a envoyé le paquet. Pourquoi n'y a-t-il pas de méthode pour lire l'adresse et le port destination du paquet (ce que l'envoyeur a écrit dans son paquet par setAddress() et setPort()) ?

2. Codage des messages

Pour pouvoir discuter les uns avec les autres de manière plus robuste, utilisons tous le même format de message :

+---+---+
|  ID   |
+---+---+
| sender|
| name  |
|       |
+---+---+
|  seq  |
+---+---+
| length|
+---+---+
|message|
|  ...  |

Nous utiliserons l'ordre big endian qui consiste à envoyer les nombre en mettant en premier les octets de poids fort. C'est ce que fait la méthode putShort() de ByteBuffer, et ce que suppose la methode getShort().

Écrire finalement le programme de chat avec un thread pour la réception des messages, le thread principal étant dédié à l'envoi. (Voir la documentation de Thread au besoin.) Pour lire l'entrée standard, on pourra utiliser Scanner :

        Scanner inp = new Scanner (System.in) ;
        while (inp.hasNextLine ()) {
            String ligne = inp.nextLine () ;
        }

Une solution.

3. Gérer les pertes de paquets

Dans une application réelle de chat sur Internet, des paquets peuvent être perdus, notamment en cas de congestion dans le réseau. Émulez cela en provoquant des pertes aléatoires dans votre fonction Socket.receive(). Corriger votre programme pour demander la réémission des paquets perdus. (On augmentera le protocole d'un nouveau type de message final static NACK = 2 comportant le surnom de l'émetteur dont il manque un paquet, suivi du numéro du paquet manquant.) De l'autre côté, il faut aussi retenir les dernières lignes envoyées et les ré-émettre au besoin en cas de réception d'un NACK.


Last modified: Thu Jan 25 23:14:21 CET 2007