6 Cours ENPC, printemps 2001
Java, objets, interfaces et Internet

Internet, adresses et noms

L'Internet étant une interconnexion de réseaux, toute machine raccordée à l'Internet est d'abord sur un réseau. A ce titre, elle est désignée par une adresse IP, qui spécifie le réseau, et à l'intérieur de ce réseau, la machine. Ces adresses sont des objets de la classe InetAddress du paquet java.net. Les méthodes de cette classe peuvent déclencher l'exception UnknownHostException. L'adresse IP de la machine locale s'obtient par la méthode de classe getLocalHost().

    import java.net.*;
    
    try {
      InetAddress adresse = InetAddress.getLocalHost();
    } catch (UnknownHostException e) {
      System.out.println(e);
      System.exit(1);
    }
	
    

Une adresse IP se compose actuellement de 4 octets ; elle se décompose en numéro de réseau et numéro de machine. L'adresse du serveur Web de l'École est 195.221.195.17, c'est la machine 17 dans le réseau 195.221.195. On peut obtenir ces quatre octets sous la forme d'un tableau d'octets retourné par la méthode getAddress. Comme Java ne connait que des entiers signés (pour les octest, compris entre -127 et 127), il faut faire une translation si on veut les afficher comme des entiers positifs, ou utiliser la méthode getHostAddress() :

    System.out.println(adresse.getHostAddress());
    

Plutôt que d'utiliser directement ces adresses IP, il est préférable de désigner les machines par des noms, plus faciles à retenir, et d'organiser ces noms de façon plus signifiante pour l'utilisateur. Par l'exemple, le nom www.enpc.fr indique immédiatement que www est dans le domaine enpc de l'Ecole, qui est dans le domaine français fr. L'association d'une adresse IP à un nom de machine est obtenue grâce au service des noms ou DNS. La machine locale est désignée par le nom localhost. On obtient l'adresse à partir d'un nom de machine ou d'un numéro IP par la méthode de classe getByName(String).

InetAddress adresse1 = InetAddress.getByName("www.enpc.fr");
InetAddress adresse2 = InetAddress.getByName("195.221.193.72");
    

Protocoles et organisation en couches

Un service est mis en oeuvre par un ensemble de programmes partageant un même protocole. Un protocole est un ensemble de règles (format de données, de dialogues, etc) permettant de réaliser un service. Un protocole n'est pas un programme, mais il est implémenté par une bibliothèque de fonctions.

Les protocoles et les services qu'ils offrent sont organisés en couches. L'idée est qu'une couche utilise des services fournis par la couche inférieure et offre des services à la couche supérieure. On distingue, par niveaux décroissants, les services d'application (les seuls qui aient un sens pour l'utilisateur), les services de transport, les services d'interconnexion des réseaux, et les services de liaison de données.

coucheprotocoles
ApplicationTelnet, FTP, NNTP, SMTP, HTTP
TransportTCP, UDP
RéseauIP (ICMP)
LiaisonEthernet

Services d'application

Les services d'application fonctionnent généralement sur le mode client-serveur. Clients et serveurs sont des processus (c'est-à-dire des exécutions de programmes). Un service d'application se compose ainsi de deux processus, chacun sur une machine et communicant par l'intermédiaire d'un protocole.

Le processus client prend l'initiative d'une communication destinée au processus serveur et lui transmet une requête. Ce processus serveur écoute en permanence les requêtes : il peut leur répondre lui-même, ou créer un autre processus serveur qu'il charge de cette réponse. Les serveurs sont souvent des démons (processus qui s'exécutent en tâche de fond) : on trouve par exemple les programmes ftpd, telnetd et httpd, réalisant les services FTP, Telnet et HTTP sous Unix. Pour ces mêmes applications, on peut trouver côté client les programmes ftp, telnet, netscape.

Dans le cas du Web, le protocole est HTTP, «HyperText Transfer Protocol» [RFC 2068], les programmes clients sont des navigateurs, comme Netscape, Explorer ou Lynx ; le programme serveur est généralement httpd (sous Unix).

Ports et adresses de transport

Sur une machine, il peut y avoir plusieurs serveurs coexistants (par exemple un serveur FTP et un serveur HTTP). Il faut donc dire quel serveur doit traiter la requête émise par le client. On utilise pour cela un numéro de port (entier positif 16 bits) attribué par les instances de l'Internet, connu de tous : 23 pour Telnet, 21 pour FTP, 80 pour HTTP, etc. Les ports compris entre 1 et 1023 sont réservées, sous Unix, au super-utilisateur. La donnée d'une machine (adresse IP ou nom) et d'un numéro de port détermine une adresse de transport.

Il peut aussi y avoir plusieurs clients sur une machine, et même plusieurs clients requérant le même service d'application. Il faut donc que le serveur puisse distinguer entre ces différents clients pour leur répondre correctement. On utilise aussi un numéro de port côté client, mais cette fois-ci le numéro de port est généré de manière à assurer l'unicité.

Protocoles et services de transport

Un protocole de transport permet de réaliser un service d'application reliant deux processus, l'un serveur et l'autre client. Un service de transport supporte une communication bidirectionnelle de données entre les processus client et serveur, et est réalisé à l'initiative du client.

Une communication se fait en mode connecté s'il y a accord préalable entre l'émetteur et le récepteur sur l'établissement de la communication, avant que l'émission de données ne commence. Elle est en mode non-connecté sinon. La téléphonie fonctionne en mode connecté, la messagerie électronique en mode non-connecté. Le mode connecté assure une meilleure sécurité à l'acheminement, et permet de négocier la qualité du service (par exemple, pour déterminer un taux de compression). Mais la mise en place d'une connexion demande toujours un temps supplémentaire, ce qui peut être pénalisant si le message transmis est court. Le mode non connecté est plus rapide et moins sûr.

Il y a deux protocoles de transport dans l'Internet :

  • TCP (Transmission Control Protocol) offre un service de transport fiable en mode connecté ; les classes Socket et ServerSocket implémentent un service de transport basé sur TCP
  • UDP (User Datagram Protocol) offre un service de transport non fiable en mode non connecté ; les classes DatagramPacket et DatagramSocket implémentent un service de transport basé sur UDP

Une requête de transport est spécifiée par le protocole (TCP ou UDP) et les deux adresses de transport, du client et du serveur. Ce sera un quintuplet, par exemple (TCP, 192.54.211.35, 6492; 192.54.211.74, 21) dans le cas d'une connexion Telnet.

Sockets, côté client

Java permet la communication entre deux processus par un mécanisme de flot basé sur le protocole TCP, au moyens des sockets. Ce sont des objets de la classe Socket, dont les constructeurs et certaines méthodes peuvent déclencher l'exception IOException. Un socket est construit à partir de l'adresse de transport distante :

  InetAddress a = InetAddress.getByName("www.enpc.fr");
  Socket httpSocket = new Socket(a, 80);
  Socket localDateSocket = new Socket("localhost", 13);

Les méthodes getInetAddress() et getPort() d'une part, getLocalAddress() et getLocalPort(), d'autre part permettent d'obtenir l'adresse de transport distante (spécifiée en argument du constructeur) et l'adresse de transport locale.

La méthode getInputStream() de la classe Socket permet de lire un flot de données à partir d'un socket ; la méthode getOutputStream() permet d'écrire un flot de données à destination d'un socket. Un programme peut se connecter à un serveur distant en créant un socket (client) et en utilisant les flots d'entrée et de sortie de ce socket. La création de ce socket exprime une demande d'un service de transport TCP, qui devra être satisfaite par une connexion mise en place par la machine distante. Le programme suivant se connecte au port 13 de la machine locale à l'aide d'un socket, reçoit sur le flot d'entrée de ce socket une chaîne de caractères, ferme le flot et ferme la connexion :

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

class ClientDate {
  public static void main(String[] args) 
    throws IOException {
    
    Socket localDateSocket = new Socket("localhost", 13);
    BufferedReader reponse = 
      new BufferedReader(
        new InputStreamReader(
	  localDateSocket.getInputStream()));
    String date = reponse.readLine();
    System.out.println("Date : " + date);
    reponse.close();
    localDateSocket.close();
  }
}

Serveurs

Le processus serveur correspondant au client ClientDate doit écouter en permanence sur un port (on doit choisir un port supérieur ou égal à 1024, plutôt que 13, qui est le port du serveur time standard). Cette écoute est réalisée par un objet de la classe ServerSocket. Quand un autre processus demande une connection à ce port, la méthode accept() de l'objet serveur est invoquée ; celle-ci retourne un socket qui réalise la connexion demandée avec le socket client. Contrairement au socket côté client, celui côté serveur n'est pas créé par un constructeur.

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

public class ServeurDate {

 final static int PORTDATE = 1314;    

  public static void main(String[] args) 
    throws IOException {

    ServerSocket serveur = new ServerSocket(PORTDATE);
    Socket connexion;
    PrintWriter p;
    
    while (true) {
      connexion = serveur.accept();
      p = new PrintWriter(connexion.getOutputStream());
      p.println(new java.util.Date());
      p.flush();
      connexion.close();
    }
  }
}

Une autre forme du constructeur permet de spécifier le taille maximum de la file d'attente des demandes de connexions :

ServerSocket serveur = new ServerSocket(PORTDATE, 16);

Serveurs concurrents

Le type du serveur précédent est dit itératif, parce que les demandes de connexion sont satisfaites l'une après l'autre. Si chaque connexion doit être durable (avec un dialogue entre client et serveur, ou bien un temps de calcul du serveur important), il faut plutôt réaliser un serveur concurrent à l'aide de threads. L'exemple suivant implémente un serveur concurrent réalisant un écho ; chaque demande de connexion acceptée par le serveur provoque la création et le lancement d'un nouveau thread qui réalise la connexion :

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

public class ServeurConcurrent {

 final static int PORTDATE = 1314;    

  public static void main(String[] args) 
    throws IOException {

    ServerSocket serveur = new ServerSocket(PORTDATE);
    while (true) {
      Connexion connexion = new Connexion(serveur.accept());
      connexion.start();
    }
  }
}

class Connexion extends Thread {

  Socket connexion;

  Connexion (Socket s) {
    connexion = s;
  }

  public void run() {
    String requete;

    try {
      PrintWriter out =
	new PrintWriter(connexion.getOutputStream());
      BufferedReader in =
	new BufferedReader(
          new InputStreamReader(
            connexion.getInputStream()));
      out.println("Tapez n'importe quoi, ou un \".\" pour terminer!");
      out.flush();
      while (!(requete = in.readLine()).equals(".")) {
	out.println(requete + " depuis " + 
		    connexion.getInetAddress() +
		    ":" + connexion.getPort());
        out.flush();
      }
    connexion.close();
    }
    catch (IOException e) {
      System.err.println(e);
    }
  }
}

Datagrammes UDP

Le protocole UDP est beaucoup plus simple, ne permettant pas de reconstituer l'ordre d'envoi des messages, donc plus efficace, mais moins fiable, puisqu'il n'est pas doté d'accusé de réception. Les données sont placées dans un datagramme UDP, muni d'un en-tête comportant les numéros de port d'origine et de destination, la taille du datagramme et une somme de contrôle ; ce datagramme est lui-même placé dans un datagramme IP (ou paquet IP), muni d'un en-tête comportant entre autre les adresses IP d'émission et de réception. À cause de ces deux en-têtes, la taille des données est limitée à 65 507 octets.

Ces datagrammes sont implémentés en Java par des objets de la classe DatagramPacket ; les données sont contenues dans un tableau d'octets. Voici d'abord la construction d'un datagramme destiné à une machine distante, à partir des données et de l'adresse de transport distante :

String s = "Des données, ...";
byte[] donnees = s.getBytes();
InetAddress adresse = InetAddress.getByName("www.enpc.fr");
final int PORT = 1314;
DatagramPacket paquet =
  new DatagramPacket(donnees, donnees.length, adresse, PORT);
En réception 9côté serveur), il faut construire un datagramme à l'aide d'un tableau d'octets qui recevra les données :
byte[] donnees = new byte[8096];;
DatagramPacket d =
  new DatagramPacket(donnees, donnees.length);

Les méthodes getData, getLength, getAddress, getPort permettent d'obtenir le tableau d'octets des données, la taille des données (en octets) et l'adresse de transport distante. Dans le cas d'un datagramme de réception, ces méthodes ne devront être invoquées qu'après réception.

Sockets UDP

Ces datagrammes sont émis et reçus par l'intermédiaire d'un objet de la classe DatagramSocket. Ses constructeurs peuvent déclencher l'exception SocketException. En émission (côté client), on utilise un port anonyme pour construire le DatagramSocket, puis la méthode send :

String s = "Des données, ...";
byte[] donnees = s.getBytes();
InetAddress adresse = InetAddress.getByName("www.enpc.fr");
final int PORT = 1314;
DatagramPacket paquet =
  new DatagramPacket(donnees, donnees.length, adresse, PORT);
DatagramSocket client = new DatagramSocket();
client.send(paquet);

En réception, le port doit être passé en argument au constructeur DatagramSocket(int), et la méthode receive remplit le tableau d'octets du paquet :

final int PORT = 1314;
DatagramSocket serveur = new DatagramSocket(PORT);
byte[] donnees = new byte[8096];;
DatagramPacket paquet =
  new DatagramPacket(donnees, donnees.length);
serveur.receive(paquet);

Client UDP

Le programme suivant communique avec le port 1315 d'une machine éventuellement spécifiée sur la ligne de commande, à l'aide d'un DatagramSocket. À chaque ligne entrée par l'utilisateur, constituant la requête, il prépare un paquet contenant la requête, l'envoie sur ce DatagramSocket, puis reçoit un paquet contenant la réponse sur ce même DatagramSocket.

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

class ClientEchoUDP {

  public static void main(String[] args) 
    throws UnknownHostException, IOException {
    
    String nomHote = args.length>0 ? args[0] : "localhost";
    InetAddress adresse = InetAddress.getByName(nomHote);
    final int PORT = 1315;
    String requete, reponse;
    DatagramPacket paquetRequete, paquetReponse;
    DatagramSocket client = new DatagramSocket();
    BufferedReader entree = 
      new BufferedReader(
        new InputStreamReader(System.in));
    byte[] donneesRequete, donneesReponse = new byte[8096];
      
    while (!(requete = entree.readLine()).equals(".")) {
      donneesRequete = requete.getBytes();
      paquetRequete =
	new DatagramPacket(donneesRequete, donneesRequete.length, 
			   adresse, PORT);
      paquetReponse =
	new DatagramPacket(donneesReponse, donneesReponse.length);
      client.send(paquetRequete);
      client.receive(paquetReponse);
      reponse = new String(paquetReponse.getData());
      System.out.println(paquetReponse.getAddress() + " : " + reponse);
    }
    client.close();
  }
}

Serveur « echo » UDP

Serveurs et clients se programment de façon assez symétrique. Le programme suivant crée un DatagramSocket en réception sur le port 1315. Dans une boucle infinie, il crée un paquet requête, le remplit, crée un paquet contenant la réponse et l'émet. Le paquet réponse contient les mêmes données que le paquet requête, ce qui est souhaité pour un serveur d'écho, et a pour adresse de destination l'adresse d'émission du paquet requête. Le même DatagramSocket sert aux communications avec tous les clients sans qu'il soit nécessaire, dans cet exemple, de créer un thread par client.

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

class ServeurEchoUDP {

  public static void main(String[] args) 
    throws UnknownHostException, IOException {
    
    final int PORT = 1315;
    DatagramPacket paquetRequete, paquetReponse;
    DatagramSocket serveur = new DatagramSocket(PORT);
    byte[] donneesRequete = new byte[8096], donneesReponse;
      
    while (true) {
      paquetRequete =
	new DatagramPacket(donneesRequete, donneesRequete.length);
      serveur.receive(paquetRequete);
      paquetReponse =
	new DatagramPacket(paquetRequete.getData(), 
			   paquetRequete.getLength(), 
			   paquetRequete.getAddress(), 
			   paquetRequete.getPort());
      serveur.send(paquetReponse);
    }
  }
}


[Cours Java] [Notes 5] [Notes 7]

Mise à jour :