JAVA et réseau

1. Adressage IP

   

Comme nous l'avons dit, un hôte sur le réseau peut être nommé soit par son adresse internet soit son nom DNS; leur manipulation se fait par la classe java.net.InetAddress.

 
public final class InetAddress extends Object implements Serializable {
public boolean isMulticastAddress()
public String getHostName()
public byte[] getAddress()
public String getHostAddress()
public int hashCode()
public boolean equals(Object obj)
public String toString()
public static InetAddress getByName(String host) throws UnknownHostException
public static InetAddress[] getAllByName(String host) throws UnknownHostException
public static InetAddress getLocalHost() throws UnknownHostException
}

Cette classe ne possède pas de constructeur. En général, les instances de InetAddress s'obtiennent comme résultat des méthodes static: getByName, getAllByName et getLocalHost.

 public static InetAddress getByName (String host) throws UnknownHostException

Retourne un objet de la classe InetAddress codant l'adresse IP d'un hôte. L'argument host est le nom DNS de l'hôte ou son adresse IP.

 public static InetAddress[] getAllByName (String host) throws UnknownHostException

Retourne un tableau d'objets de la classe InetAddress codant toutes les adresses IP d'un hôte. L'argument host est le nom DNS de l'hôte ou son adresse IP.

 public static InetAddress getLocalHost () throws UnknownHostException

Retourne un objet de la classe InetAddress codant l'adresse IP du hôte local.

 public String getHostName ()

Retourne une chaîne de caractères contenant le nom d'hôte de l'objet InetAddress ou son adresse IP (si le nom n'existe pas).

 public byte[] getAddress ()

Retourne un tableau d'octet contenant les 4 octets de l'adresse IP de l'objet InetAddress.

 public String getHostAddress ()

Retourne une chaîne de caractères contenant les 4 octets de l'adresse IP de l'objet InetAddress; cette chaîne est de la forme "%d.%d.%d.%d".
 

   import java.net.*;

   public class Adresses {
      public static void main(String[] args) {
         for(int i=0 ; i< args.length; i++) {
            try {
               InetAddress[] inetadrs = InetAddress.getAllByName(args[i]);
               System.out.println(args[i] + ":");
               for(int j=0 ; j<inetadrs.length ; j++) System.out.println(inetadrs[j]);
            }
               catch (UnknownHostException e) { System.out.println(args[i] + " inconnu !!! "); }
         }
      }
   }




2. Les sockets

 

TCP/IP offre, en particulier, une interface dite sockets pour l'écriture d'applications communicantes. Les sockets a été introduit avec le système d'exploitation UNIX de Berkeley. L'idée des sockets est de faire en sorte que deux programmes sur des hôtes différents puissent communiquer l'un avec l'autre à travers sans se soucier des détails de bas niveau de la communication réseau.

Les primitives TCP pour les sockets permettent :

Les sockets ne sont pas des objets spécifiquement réseaux, et encore moins spécifiquement TCP/IP. Ce sont des objets génériques qui doivent être paramétrés lors de leur création selon l'utilisation prévue :

Dans la communication par sockets, il existe toujours une machine qui joue le rôle du serveur et l'autre (ou les autres) qui joue le rôle de client.

Le serveur est une application qui tourne sur un hôte et qui est à l'écoute des requêtes d'un ou de plusieurs clients sur un port particulier.

Le client est une application qui tourne un hôte (pas forcément différent de celui sur lequel tourne le serveur). Il doit connaître l'hôte et le port sur lequel le serveur est à l'écoute; le client tente alors une connexion au serveur sur l'hôte et le port approprié.

Lorsque le serveur est conçu pour communiquer avec plusieurs clients, quand tout se passe bien,

Le client et le serveur peuvent alors communiquer l'un avec l'autre en écrivant et en lisant sur les sockets.

Les programmes serveurs doivent toujours être actifs lorsque des clients initient une connexion. Il existe deux façons d'assurer que ces serveurs soient activés :

Activation "manuelle"

Le lancement manuel signifie que le programme serveur s'exécute en permanence, même s'il n'est pas appelé par des programmes clients. Compte tenu du grand nombre de services réseaux disponibles sur une machine donnée, de nombreux processus, oisifs en attente mais consommant du temps CPU et des ressources, existent sur le système. Cette solution est peu recommandée.

Utilisation de INETD sous UNIX

La deuxième solution est l'utilisation d'un serveur particulier, le démon inetd, qui réalise les attentes pour le compte d'autres programmes serveurs. De plus, l'utilisation du réseau par un démon lancé par inetd est cachée : le programmeur utilise les descripteurs de fichiers correspondant à l'entrée et à la sortie standard du programme. La redirection de ces deux descripteurs vers un socket est réalisée de façon implicite et systématique par le processus inetd lui-même.

Le fichier /etc/inetd.conf

Le démon inetd, lors de son activation, consulte un fichier de configuration décrivant les services à assurer sur le système. Le fichier s'appelle /etc/inetd.conf, et contient un ensemble de lignes au format suivant :

 
service       stream  tcp      nowait  root    /usr/etc/progd  progd

Une telle ligne signifie que service est implanté sur cette machine, que les sockets utilisés sont de type stream, que le protocole utilisé est tcp, que le programme à activer lors d'une demande de service est /usr/etc/progd, que le nom à donner au programme sera progd, que le programme doit s'exécuter avec comme propriétaire root.

Le paramètre nowait indique que plusieurs instances de ce programme peuvent être activées sans attendre la terminaison des instances précédantes.

Le fichier /etc/services

Les ports à utiliser pour les sockets (ici les sockets TCP) sont définis dans un fichier particulier, /etc/services, qui contiendra la ligne suivante :

 
 service       78/tcp   alias_service

Cette ligne indique que pour le service défini dans le fichier inetd.conf, il faut utiliser le port 78 du protocole tcp. Le reste de la ligne est un alias possible pour le nom du service. Une fois ces informations récupérées, le serveur inetd va créer un socket, lui associer le numéro de port, et se mettre en attente d'une indication d'établissement de connexion à ce port. Comme de nombreux services existent, le superserveur inetd crée de nombreux sockets, assigne les numéros de port adéquats, se met en attente, etc..

Cas de services UDP

Dans le cas de service utilisant udp, le fichier inetd.conf contient des informations légèrement différentes :

 
service       dgram   udp     wait     root    /usr/etc/progd         progd

Le paramètre dgram identifie un socket de type datagramme, udp indique que le protocole utilisé est UDP, wait indique qu'il faut attendre la terminaison d'une instance du service avant de pouvoir en lancer la suivante. Notons que si les programmes serveurs sont conçus pour s'exécuter en parallèle (voir chapitre 5), on peut trouver la ligne suivante dans le fichier /etc/inetd.conf :

 
service       dgram   udp     nowait   root    /usr/etc/progd         progd

Dans les deux cas, on devra trouver la ligne correspondante dans le fichier /etc/services :

 
service       78/udp  alias_service


2.1. Connexion TCP et sockets

Java fournit deux classes pour la gestion des sockets:


2.1.1. Le client TCP

 
   public class Socket {
      protected Socket()
      protected Socket(SocketImpl impl) throws SocketException
      public Socket(String host, int port) throws UnknownHostException, IOException
      public Socket(InetAddress address, int port) throws IOException
      public Socket(String host, int port, InetAddress localAddr, int localPort) throws IOException
      public Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException
      public Socket(String host, int port, boolean stream) throws IOException
      public Socket(InetAddress host, int port, boolean stream) throws IOException
      public InetAddress getInetAddress()
      public InetAddress getLocalAddress()
      public int getPort()
      public int getLocalPort()
      public InputStream getInputStream() throws IOException
      public OutputStream getOutputStream() throws IOException
      public void setTcpNoDelay(boolean on) throws SocketException
      public boolean getTcpNoDelay() throws SocketException
      public void setSoLinger(boolean on, int val) throws SocketException
      public int getSoLinger() throws SocketException
      public void setSoTimeout(int timeout) throws SocketException
      public int getSoTimeout() throws SocketException
      public void close() throws IOException
      public String toString()
      public static void setSocketImplFactory(SocketImplFactory fac) throws IOException
   }

Une application cliente doit ouvrir une connexion sur en créant un socket.

 
   try {
      socket sock = new Socket("www.univ-nc.nc", 2000);
   }
   catch (UnknoxnHostException e) { System.out.println("Hôte inconnu"); System.exit(-1); }
   catch (IOException e) { System.out.println("Erreur lors de connexion"); System.exit(-1);}

La connexion établie, les canaux d'entrée/sortie s'obtiennent grâce aux méthode getInputStream et getOutputStream.

Pour tester le client, mous allons utiliser une application serveur qui existe sur tous les systèmes UNIX et qui est à l'écoute du port 7: il s'agit du serveur echo qui se contente de renvoyer au client la chaîne que ce dernier lui envoie.

 

   import java.io.*;
   import java.net.*;
   public class Client {
      public static void main(String[] args) throws Exception, NumberFormatException {
         Socket serveur = new Socket("www.univ-nc.nc", Integer.parseInt(args[0]));
         PrintWriter Sout = new PrintWriter(serveur.getOutputStream());
         BufferedReader Sin = new BufferedReader(new InputStreamReader(serveur.getInputStream()));
         BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
         String jEnvoie, jeReçois;
         do  {
            jEnvoie = in.readLine();
            Sout.println(jEnvoie); 
            Sout.flush();
            jeReçois = Sin.readLine();
            System.out.println(jeReçois);
         }
         while (! jEnvoie.equals("fin"));
         Sin.close();   
         Sout.close();
         serveur.close();
      }
   }


2.1.2. Le serveur TCP

 
   public class ServerSocket {
      public ServerSocket(int port) throws IOException
      public ServerSocket(int port, int backlog) throws IOException
      public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException
      public InetAddress getInetAddress()
      public int getLocalPort()
      public Socket accept() throws IOException
      protected final void implAccept(Socket s) throws IOException
      public void close() throws IOException
      public void setSoTimeout(int timeout) throws SocketException
      public int getSoTimeout() throws IOException
      public String toString()
      public static void setSocketFactory(SocketImplFactory fac) throws IOException
   }

Une application serveur doit créer un instance ServerSocket:

 
   try {
      ServerSocket sock = new ServerSocket(2000);
   }
   catch (IOException e) {
      System.out.println("Erreur de création du serveur sur le port 2000");
      System.exit(-1);
   }

Une fois le serveur crée, il va se mettre en attente d'une connexion cliente:

 
   Socket sockClient;
   try {
      sockClient= serverSocket.accept();
   }
   catch (IOException e) {
      System.out.println("Echec de la mise en attente sur le port 2000");
      System.exit(-1);
   }

La méthode accept permet au serveur d'être à l'écoute du port 2000 et d'attendre une future connexion cliente. Lorsqu'une telle connexion s'établit, la varibale sockClient permet de récupérer le canal de communication établit avec le client grâce aux méthodes getInputStream et getOutputStream.

Pour illustrer le fonctionnement du serveur, programmons le fameux serveur echo.

 

   import java.io.*;
   import java.net.*;

   public class TestEcho {
      public static void main(String[] args) throws Exception {
         ServerSocket serveur = new ServerSocket(2000);
         while (true) {
            Socket client = serveur.accept();
            PrintWriter Cout = new PrintWriter(client.getOutputStream());
            BufferedReader Cin = new BufferedReader(new InputStreamReader(client.getInputStream()));
            String jeReçois;
            do  {
               jeReçois = Cin.readLine();
               Cout.println("Vous avez dit " + jeReçois); 
               Cout.flush();
            }
            while (! jeReçois.equals("fin"));
            Cin.close();   
            Cout.close();
            client.close();
         }
      }
   }


2.1.3. Connexions multiples

Le serveur TestEcho est bien trop simpliciste. En effet, ce serveur n'accepte qu'un client à la fois. Que se passe-t-il si plusieurs clients essayent de se connecter en même temps ? Le premier qui se connecte monopolise la connexion et les autres clients doivent attendre la fin de communication du premier pour pouvoir dialoguer avec le serveur à tour de rôle.

Cette solution est très restrictive. Nous allons modifier le serveur pour qu'il puisse répondre à plusieurs clients à la fois. Pour ce faire, le serveur devra créer un thread par client connecté de manière à pouvoir se remettre en attente d'un éventuel futur client.

Le squelette d'un programme qui réalise ceci est de la forme:

 
while (true) {
   attente d'un connexion
   créer un thread pour dialoguer avec le client qui s'est connecté
}




 

   import java.io.*;
   import java.net.*;

   public class TestEchoMultiple {
      public static void main(String[] args) throws Exception {
         ServerSocket serveur = new ServerSocket(2000);
         while (true) {
            Socket client = serveur.accept(); System.out.println("C'est par");
            new EncoreUnClient(client).start();
         }
      }
   }

   class EncoreUnClient extends Thread {
      Socket client;
      public EncoreUnClient(Socket client) {
         super("Clients");
         this.client = client;
      }
   
      public void run()  {
         PrintWriter Cout = null;
         BufferedReader Cin = null;
         try {
         System.out.println("C'est parti");
            Cout = new PrintWriter(client.getOutputStream());
            Cin = new BufferedReader(new InputStreamReader(client.getInputStream()));
            String jeReçois;
            do  {
               jeReçois = Cin.readLine();
               Cout.println("Vous avez dit " + jeReçois); 
               Cout.flush();
            }
            while (! jeReçois.equals("fin"));
            
            if (Cin != null) Cin.close();   
            if (Cout != null) Cout.close();
            client.close();
         }
            catch (IOException e) { e.printStackTrace(); }
      }
   }



2.2. Datgrammes UDP et sockets

On dispose de deux classes pour la communication par UDP:

Ainsi, l'envoi de données par UDP consite à les confier à un DatagramPacket en vue de leur emballage sous forme de paquets et à envoyer ce DatagramPacket à un DatagramSocket pour leur expédition. Un DatagramSocket se charge de la récéption; les paquets reçus doivent être mis dans un DatagramPacket en vue de leur déballage.



2.2.1. La classe DatagramPacket

 
   public final class DatagramPacket {
      public DatagramPacket(byte ibuf[], int ilength)
      public DatagramPacket(byte ibuf[], int ilength, InetAddress iaddr, int iport)
      public synchronized InetAddress getAddress()
      public synchronized int getPort()
      public synchronized byte[] getData()
      public synchronized int getLength()
      public synchronized void setAddress(InetAddress iaddr)
      public synchronized void setPort(int iport)
      public synchronized void setData(byte ibuf[])
      public synchronized void setLength(int ilength)
   }

La classe DatagramPacket dispose de deux constructeurs

 
public DatagramPacket(byte ibuf[], int ilength)
public DatagramPacket(byte ibuf[], int ilength, InetAddress iaddr, int iport)

Le premier constructeur est utiliser pour recevoir les paquets et le deuxième pour en envoyer.

Les méthodes getAddress, getPort, getLength et getData permettent de récupérer respectivement l'adresse de destination, le port de destination, la taille du paquet et les données contenues dans le paquet.

Les méthodes setAddress, setPort, setLength et setData assigne respectivement l'adresse de destination, le port de destination, la taille du paquet et les données contenues dans le paquet.



2.2.2. La classe DatagramSocket

 
   Class java.net.DatagramSocket {
      public DatagramSocket() throws SocketException
      public DatagramSocket(int port) throws SocketException
      public DatagramSocket(int port, InetAddress laddr) throws SocketException
      public void send(DatagramPacket p) throws IOException
      public synchronized void receive(DatagramPacket p) throws IOException
      public InetAddress getLocalAddress()
      public int getLocalPort()
      public synchronized void setSoTimeout(int timeout) throws SocketException
      public synchronized int getSoTimeout() throws SocketException
      public void close()
   }

La classe DatagramSocket possède trois constructeurs:

 
public DatagramSocket() throws SocketException

Les méthodes send et receive de cette classe permettent d'envoyer et de recevoir les packets UDP.

La méthode send lève une exception lorsque le SecurityManager refuse l'envoi d'un paquet vers l'hôte spécifié. C'est le cas ou une erreur peut se produire.

La méthode receive (tout comme la méthode accept de la classe SocketServer) se met en attente et lève également une exception lorsqu'il y une erreur dans la reception du paquet UDP. La méthode receive doit disposer d'un tampon assez grand pour le packet à recevoir. Dans le cas contraire, le paquet est tronqué à la taille du tampon.



2.2.3. Le client UDP

Une application cliente doit ouvrir une connexion sur en créant un socket.

 
   try {
      socket sock = new Socket("www.univ-nc.nc", 2000);
   }
   catch (UnknoxnHostException e) { System.out.println("Hôte inconnu"); System.exit(-1); }
   catch (IOException e) { System.out.println("Erreur lors de connexion"); System.exit(-1);}

La connexion établie, les canaux d'entrée/sortie s'obtiennent grâce aux méthode getInputStream et getOutputStream.

Pour tester le client, mous allons utiliser une application serveur qui existe sur tous les systèmes UNIX et qui est à l'écoute du port 7: il s'agit du serveur echo qui se contente de renvoyer au client la chaîne que ce dernier lui envoie.

 

   import java.io.*;
   import java.net.*;
   public class Client {
      public static void main(String[] args) throws Exception, NumberFormatException {
         Socket serveur = new Socket("www.univ-nc.nc", Integer.parseInt(args[0]));
         PrintWriter Sout = new PrintWriter(serveur.getOutputStream());
         BufferedReader Sin = new BufferedReader(new InputStreamReader(serveur.getInputStream()));
         BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
         String jEnvoie, jeReçois;
         do  {
            jEnvoie = in.readLine();
            Sout.println(jEnvoie); 
            Sout.flush();
            jeReçois = Sin.readLine();
            System.out.println(jeReçois);
         }
         while (! jEnvoie.equals("fin"));
         Sin.close();   
         Sout.close();
         serveur.close();
      }
   }



2.2.4. Le serveur UDP

Et voici à présent un serveur UDP à l'image du serveur TCP que nous vu. Il faut donc :

 
paquet = new DatagramPacket(buf, buf.length, address, port);
socket.send(paquet);



Et voici le programme complet.

 

   import java.io.*;
   import java.net.*;

   public class UdpTestEcho {
      public static void main(String[] args) throws Exception {
      
         String jeReçois;
         DatagramSocket socket = new DatagramSocket(2001);
         while (true) {
         
            byte[] buf = new byte[56];
            DatagramPacket paquet  = new DatagramPacket(buf, buf.length);
            socket.receive(paquet);
            jeReçois = new String(paquet.getData(), 0, paquet.getLength());
            System.out.println("j'ai recu : " + jeReçois + " " + jeReçois.length());
         
            InetAddress address = paquet.getAddress();
            int port = paquet.getPort();
            paquet = new DatagramPacket(buf, buf.length, address, port);
            socket.send(paquet);
         }
      }
   }