Communication par sockets

Chapitres traités   

Le langage Java permet une communication entre machines qui s'appuie sur le protocole IP (Internet Protocol), protocole de base du réseau Internet. Il y a plusieurs façons de faire communiquer des machines.

Nous pouvons simplement télécharger une ressource Internet référencée par une URL (Uniform Resource Locator) ; nous avons déjà utilisé cette possibilité dans le chapitre consacré aux applets. Nous pouvons employer pour cela la classe java.net.URL. Si nous voulons disposer d'un certain contrôle sur le téléchargement de données, nous pouvons obtenir des fonctionnalités supplémentaires grâce à la classe java.net.URLConnection qui sera utilisée dans le chapitre communication Applet-Servlet.

Nous pouvons communiquer également par le mode "connecté" en utilisant le protocole TCP (Transmission Control Protocol). Nous établissons entre deux machines une connexion par laquelle nous ouvrons des flux de données, connexion que l'on conserve tout le temps de la communication. Les classes ServerSocket et Socket du paquetage java.net doivent être employées pour ce type de communication. Des classes du paquetage java.io sont alors utilisées pour établir les flux de données.

Pour en savoir plus sur les Applets.
Pour en savoir flux sur les flux de données.

Choix du chapitreConnexion à un serveur

Avant d'écrire notre premier programme sur les réseaux, nous allons utiliser le client Telnet pour établir des connexions à certaines machines qui proposent des services sur le réseau.

Attention, par défaut le client Telnet n'est pas actif sous Windows Vista. Il est nécessaire de l'activer dans la rubrique Fonctionnalités de Windows du Panneau de configuration.

Vous vous êtes peut-être déjà servi de Telnet pour communiquer avec un ordinateur distant en utilisant les commandes de son propre système d'exploitation. Vous pouvez également l'utiliser pour établir une connexion à un service quelconque sur des ordinateurs reliés à votre réseau ou sur Internet (Si le pare-feu le permet). Dans ce cas là, il suffit de respecter le protocole lié au service demandé.

Rappelons qu'un service est un programme qui fonctionne en permanence (démon sous UNIX) dès que l'ordinateur est sous tension. Il est également à l'écoute du réseau pour capturer la moindre requête venant de l'extérieur, et dès lors, il va rendre le service demandé. Il est possible d'avoir plusieurs services sur une même machine, et pour les différencier, nous utilisons un numéro qui est appelé numéro de port. Ces numéros sont standardisés comme par exemple :
  1. le 13 pour le service date ITS (Internet Time Service).
  2. le 21 pour le service FTP,
  3. le 23 pour le service Telnet,
  4. le 25 pour le service SMTP,
  5. le 80 pour le service HTTP, etc.

Pour établir la connexion avec la machine distante sur le réseau (tube de communication), il suffit de connaître le nom de la machine (nom de l'hôte) et le numéro du service (numéro de port). Ces tubes de communication sont appelés des Sockets.

Exemple de mise en oeuvre

Nous allons essayer de nous connecter au service date proposé par un serveur adapté sur Internet :

  1. Lancez le logiciel client Telnet.
  2. Etablissez ensuite la connexion au serveur time.nist.org (Situé aux Etats-Unis à @IP 192.43.244.18) à l'aide du client Telnet. Pour cela nous utilisons la commande open. Rappelez-vous que par défaut ce service est réglé sur le port 13.
    open time.nist.gov 13
    ou
    open 192.43.244.18 13
  3. Que se passe-t-il ? Nous venons de nous connecter au service date que la plupart des serveurs UNIX implémentent en permanence. Le serveur auquel vous vous êtes connecté se trouve au NIST à Boulder dans le Colorado, et fournit l'heure d'une horloge atomique au césium. Naturellement, le temps affiché n'est pas parfaitement précis à cause des délais de propagation des informations sur le réseau. Par convention, le service date est toujours rattaché au port 13.



    Le programme du serveur fonctionne en permanence sur la machine distante, attendant un paquet du réseau qui essaierait de communiquer avec le port 13. Lorsque le système d'exploitation de l'ordinateur distant reçoit le paquet contenant une requête de connexion sur le port 13, le processus d'écoute du serveur est activé et la connexion est établie. Cette connexion demeure jusqu'à ce qu'elle soit arrêtée par l'une des deux parties.

    Lorsque vous avez ouvert une session Telnet sur le port 13 de l'ordinateur distant, une partie indépendante du logiciel réseau à converti la chaîne de texte time.nist.gov en une adresse IP associée, c'est à dire 192.43.244.18. Puis le logiciel a envoyé une requête de connexion à cet ordinateur, en spécifiant le port 13.

    Une fois que cette connexion a été établie, le programme de l'ordinateur distant a renvoyé un ensemble de données, puis il a terminé la connexion. Bien sûr, dans le cas plus général, les clients et les serveurs entament des dialogues beaucoup plus poussés avant que la connexion ne soit interrompue.

Connexion à un serveur par un petit programme Java

Notre premier exemple de programme réseau effectue la même chose que ce que nous venons de faire avec Telnet, c'est à dire se connecter au port 13 du service date sur le serveur time.nist.gov.

Le résultat renvoyé par le service est un simple texte qu'il est possible de formater afin de le rendre plus compréhensible pour nous.

Codage correspondant
package reseau;

import java.awt.*;
import java.awt.event.*;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
import javax.swing.*;

public class Client extends JFrame {
   private JTextField résultat = new JTextField("Cliquez sur le bouton de connexion pour connaître la date...");
   private JToolBar outils = new JToolBar();

   public Client() {
      super("Service Date");
      résultat.setBackground(Color.YELLOW);
      add(outils, BorderLayout.NORTH);
      add(résultat);
      outils.add(new AbstractAction("Connexion à time.nist.gov") {
         public void actionPerformed(ActionEvent e) {
            try {
               Socket connexion = new Socket("time.nist.gov", 13);
               Scanner réponse = new Scanner(connexion.getInputStream());
               if (réponse.hasNextLine()) {
                  réponse.next();
                  Scanner date = new Scanner(réponse.next());
                  date.useDelimiter("-");
                  int année = 2000+date.nextInt();
                  String mois = date.next();
                  String jour = date.next();
                  résultat.setText("  "+jour+"-"+mois+"-"+année+"   "+réponse.next());
               }
            }
            catch (UnknownHostException ex) {
               setTitle("Impossible de se connecter au service");
            }
            catch (IOException ex) {
               setTitle("Gros problème de communication");
            }
         }
      });
      pack();
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   public static void main(String[] args) { new Client(); }
}
Lorsque nous établissons une communication réseau, nous devons prendre en compte un certain nombre de critères :
  1. Localiser la machine distante et plus précisément le service à interroger, respectivement au moyen de son adresse IP et de son numéro de service (port). Ceci est réalisé très simplement au travers de la classe java.net.Socket.
    • La première ligne de la gestion d'événement ouvre une socket, qui est en fait une abstraction du réseau et qui permet d'établir une communication en entrée et en sortie avec le service. L'adresse de l'ordinateur distant est passée au constructeur de la socket avec le numéro du service désiré. Si la connexion ne peut pas être ouverte, une UnknowHostException est déclenchée. Si une autre erreur survient, une IOException apparaît.

      Socket connexion = new Socket("time.nist.gov", 13);

    • Une fois que la socket est ouverte, les méthodes getOutputStream() et getInputStream() de java.net.Socket peuvent être sollicitées pour renvoyer respectivement les objets OutputStream et InputStream que vous pouvez utiliser par la suite comme n'importe quel autre flux.

      Evidemment, la classe Socket est très simple d'emploi, car Java masque toute la complexité inhérente à l'établissement d'une connexion réseau et à l'envoi de données. Le paquetage java.net vous fournit quand à lui une interface de programmation tout-à-fait semblable à celle que vous obtiendriez avec un simple fichier.

  2. Respecter le protocole établi par le serveur. Il faut savoir par exemple qui parle en premier et surtout, il faut prendre en compte le type de l'information envoyé, ce qui se traduit généralement à choisir un flux d'information adapté.

    Dans le cas qui nous préoccupe, seul le serveur dialogue, sans information de retour. La réponse envoyée est un simple texte qui peut toutefois être interprété très simplement en prenant le flux de lecture de texte représenté par la classe Scanner.

  3. Finalement, pour obtenir la réponse souhaitée, il suffit de se connecter au flot d'octets envoyé par le serveur au moyen de la méthode getInputStream() de la socket et de le rattacher au flux de plus haut niveau afin de formater correctement l'information :

    Scanner réponse = new Scanner(connexion.getInputStream());

  4. Tout le reste ensuite est connu. Nous pouvons procéder exactement de la même façon que lors d'une lecture de fichier, sauf qu'ici l'information n'est pas sur le disque dur mais transite sur le réseau. Finalement les deux lignes les plus importantes sont celles que nous venons d'évoquer. Grâce à ces deux petites lignes, nous pouvons faire une gestion extrêmement simple des informations issues du réseau :

    Socket connexion = new Socket("time.nist.gov", 13);
    Scanner
    réponse = new Scanner(connexion.getInputStream());

Pour en savoir plus sur les flux et les fichiers
§

Le timeout des sockets

Le principe de base d'un dialogue sur le réseau, c'est de lire le flux d'octets provenant du service jusqu'à ce que l'ensemble de l'information soit récupérée. Dans ce cas de figure, il est tout-à-fait possible que le temps de transfert soit très long, qu'il soit même beaucoup trop important, suite à un problème éventuel de connexion réseau.

Il est envisageable de contrôler le temps de transfert maximum toléré pour une application au delà duquel nous considérons qu'un problème de communication réseau doit certainement se produire. Ce temps maximum est appelé timeout. La classe Socket possède la méthode setSoTimeout() qui précise la valeur de la durée d'attente maximum en milliseconde à ne pas dépasser.

Socket connexion = new Socket("time.nist.gov", 13);
connexion.setSoTimeout(10000); // 10 secondes d'attente maximum

Si le timeout est atteint avant la fin de lecture complète de l'information attendue, une InterruptedIOException est levée. Vous pouvez ainsi intercepter cette exception et réagir au timeout.

Dans l'écriture précédente il existe toutefois un petit soucis. Effectivement, durant la phase de construction de la connexion, le timeout n'est pas encore précisé. Le temps d'attente peut alors être infini si un problème réseau apparaît. Nous pouvons palier à ce problème en proposant la création d'une socket non-connectée, puis en la connectant plus tard avec le timeout requis, comme suit :

Socket connexion = new Socket();
connexion.connect(new InetSocketAddress("time.nist.gov", 13), 10000);

Dans cette étude, nous ne traitons que du protocole TCP (Transmission Control Protocol). Ce protocole établit une connexion fiable entre deux ordinateurs. La plate-forme Java supporte également le pseudo-protocole UDP (User Datagram Protocol), qui peut être employé pour envoyer des paquets (aussi appelés datagrammes) plus rapidement qu'avec TCP. L'inconvénient majeur de l'UDP est que les paquets peuvent arriver dans le désordre, voire même égarés. C'est donc à l'ordinateur qui reçoit ces paquets de les remettre dans le bon ordre, et de demander un nouvel envoi des paquets perdus. L'UDP est donc plus adapté à des applications pour lesquelles les paquets peuvent être égarés, comme des flux de données audio ou vidéo, ou de mesures continues.

Demi-fermeture

Lorsqu'un client envoi une requête au serveur, ce dernier doit pouvoir déterminer la fin de celle-ci. Pour cette raison, de nombreux protocoles Internet (comme SMTP) sont orienté vers la ligne. D'autres protocoles contiennent un en-tête qui spécifie la taille des données de la requête. En effet, indiquer la fin des données de la requête est plus difficile qu'écrire des données dans un fichier. En effet, il suffit de femer le fichier. En fermant une socket, vous vous déconnectez immédiatement du serveur.

La demi-fermeture résout ce problème. Vous pouvez fermer le flux de sortie d'une socket, au moyen de la méthode shutdownOutput(), ce qui indique au serveur la fin des données de la requête, mais permet de conserver le flux d'entrée ouvert afin que vous puissiez lire la réponse. Dans ce cas, le serveur lit simplement l'entrée jusqu'à ce qu'il atteigne la fin du flux d'entrée. A ce sujet, il existe également la méthode shutdownInput() qui permet quant-à elle de fermer le flux d'entrée et de concerver le flux de sortie (utilisation plus rare).

Bien entendu, ce protocole n'est utile que pour des services à opération unique comme le HTTP où le client se connecte, émet une requête, intercepte la réponse, puis se déconnecte.

Exemple de mise en oeuvre

A titre d'exemple, je vous propose de créer une petite application qui nous permet de consulter la page d'accueil d'un site quelconque, sans interprétation, c'est-à-dire en visualisant uniquement le balisage HTML, ce que les navigateurs appellent couramment le code source :

Attention pour demander la page d'accueil du site, vous devez passer par le protocole HTTP en proposant la commande suivante (il faut tenir compte des espaces) :

GET / HTTP/1.0 suivi de deux retours chariots



Codage correspondant
package reseau;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.util.Scanner;
import javax.swing.*;

public class Client extends JFrame {
   private JTextField adresse = new JTextField();
   private JFormattedTextField port = new JFormattedTextField(80);
   private JTextArea éditeur = new JTextArea(20, 38);
   private JToolBar outils = new JToolBar();

   public Client() {
      super("Editeur HTML");
      éditeur.setBackground(Color.YELLOW);
      éditeur.setEditable(false);
      adresse.setColumns(20);
      port.setColumns(4);
      add(outils, BorderLayout.NORTH);
      add(new JScrollPane(éditeur));
      outils.add(new JLabel("Adresse : "));
      outils.add(adresse);
      outils.addSeparator();
      outils.add(new JLabel("Port : "));
      outils.add(port);
      outils.add(new AbstractAction("Connexion") {
         public void actionPerformed(ActionEvent e) {
            try {
               Socket connexion = new Socket(adresse.getText(), (Integer) port.getValue());
               PrintWriter requête = new PrintWriter(connexion.getOutputStream(), true);
               Scanner réponse = new Scanner(connexion.getInputStream());               
               requête.println("GET / HTTP/1.0\n");
               connexion.shutdownOutput();
               éditeur.setText("");
               while (réponse.hasNextLine()) éditeur.append(réponse.nextLine()+"\n");
            }
            catch (IOException ex) {
               JOptionPane.showMessageDialog(Client.this, adresse.getText(), "Adresse non valide", JOptionPane.ERROR_MESSAGE);
            }
         }
      });
      pack();
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   public static void main(String[] args) { new Client(); }
}
La classe java.net.Socket
Socket(String hôte, int port)
Crée une socket et la connecte à un port de l'ordinateur distant.
Socket()
Crée une socket qui n'a pas encore été connectée.
void connect(SocketAddress adresse)
void connect(SocketAddress adresse, int timeout)
Relie cette socket à l'adresse donnée (ou s'arrête si l'intervalle de temps spécifié a expiré).
boolean isConnected()
Précise si la socket est connectée.
void close()
Ferme la socket.
boolean isClosed()
Précise si la socket est fermée.
InputStream getInputStream()
Récupère le flot de données à lire sur la socket (octets venat du réseau).
OutputStream getOutputStream()
Récupère le flot de données à écrire sur la socket (octets envoyés sur le réseau).
void setSoTimeout(int timeout)
Définit la valeur de timeout en lecture sur cette socket. Si cette valeur est atteinte, une InterruptedIOException est déclenchée.
void shutdownOutput()
Définit le flux de sortie sur "Fin de flux".
void shutdownInput()
Définit le flux d'entrée sur "Fin de flux".
boolean isOutputShutdown()
Précise si la sortie est fermée.
boolean isInputShutdown()
Précise si l'entrée est fermée.
InetAddress getInetAddress()
Renvoie l'adresse Internet.
La classe java.net.InetSocketAddress
InetSocketAddress(String hôte, int port)
Construit un objet d'adresse avec l'ordinateur distant et le port donnés, ce qui résout le nom de l'hôte en cours de la construction. Si le nom de l'hôte ne peut être résolu, la propriété unresolved de l'objet de l'adresse est validé.
boolean isUnresolved()
Précise si l'objet de l'adresse ne peut être résolu.

Les adresses internet

En général, vous n'avez pas besoin de vous préoccuper des adresses Internet, qui sont composés de quatre (ou six pour IPv6) octets de la forme 192.43.244.18. Cependant, vous pouvez avoir recours à la classe InetAddress si vous souhaitez traduire un nom d'ordinateur en adresse Internet et inversement.

Les classes Socket et ServerSocket disposent d'une méthode getInetAddress() qui renvoie un objet InetAddress.
§

  1. La méthode statique getByName() renvoie un objet InetAddress correspondant à un hôte. Ainsi, le code suivant renvoie un objet InetAddress qui encapsule la séquence de quatre octets suivante : 192.43.244.18 :

    InetAddress adresseDistante = InetAddress.getByName("time.nist.gov");

  2. Vous pouvez également accéder à chacun des octets par la méthode getAddress() :

    byte[ ] octets = adresseDistante.getAddress();

  3. Certains sites dont le trafic est très important peuvent correspondre à plusieurs adresses Internet, pour réduire leur charge. Par exemple, le nom java.sun.com représente trois adresses Internet différentes. L'une d'entre elle est choisie au hasard lorsque la connexion est ouverte. Vous pouvez retrouver ces adresses grâce à la méthode getAllByName() :

    InetAddress[ ] adresses = InetAddress.getAllByName("java.sun.com");

  4. Pour terminer, vous aurez parfois besoin de votre propre adresse. Si vous vous contentez de demander l'adresse localhost, vous obtiendrez systématiquement 127.0.0.1, ce qui n'est pas très intéressant. Il vaut donc mieux utiliser la méthode getLocalHost() pour obtenir votre propre adresse :

    InetAddress adresseLocale = InetAddress.getLocalHost();

La classe java.net.InetAddress
static InetAddress getByName(String hôte)
static InetAddress[] getAllByName(String hôte)
Ces méthodes construisent une adresse Internet, ou un tableau d'adresses Internet correspondant à un nom d'ordinateur distant spécifié.
static InetAddress getLocalHost()
Construit une adresse Internet pour l'ordinateur local.
byte[] getAddress()
Renvoie un tableau d'octets contenant une adresse numérique au format décimal.
String getHostAddress()
Renvoie une chaîne contenant des valeurs décimales séparées par des points, comme "192.43.244.18".
String getHostName()
Renvoie le nom de l'ordinateur.
Exemple de mise en oeuvre

Je vous propose de reprendre et de modifier l'exemple du client qui fait appel au service de date. Nous proposons cette fois-ci un afiichage supplémentaire : les @IP des ordinateurs distant et local.

Affichage des différentes adresses des machines sur le réseau, en plus de la date.

Codage correspondant
package reseau;

import java.awt.*;
import java.awt.event.*;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
import javax.swing.*;

public class Client extends JFrame {
   private JTextField résultat = new JTextField("Cliquez sur le bouton de connexion pour connaître la date...");
   private JToolBar outils = new JToolBar();

   public Client() {
      super("Service Date");
      résultat.setBackground(Color.YELLOW);
      add(outils, BorderLayout.NORTH);
      add(résultat);
      outils.add(new AbstractAction("Connexion à time.nist.gov") {
         public void actionPerformed(ActionEvent e) {
            try {
               Socket connexion = new Socket("time.nist.gov", 13);
               Scanner réponse = new Scanner(connexion.getInputStream());
               if (réponse.hasNextLine()) {
                  réponse.next();
                  Scanner date = new Scanner(réponse.next());
                  date.useDelimiter("-");
                  int année = 2000+date.nextInt();
                  String mois = date.next();
                  String jour = date.next();
                  String adresseDistante = connexion.getInetAddress().getHostAddress();
                  String adresseLocale = connexion.getInetAddress().getLocalHost().getHostAddress();
                  résultat.setText(" ("+adresseDistante+" --> "+adresseLocale+")     "+jour+"-"+mois+"-"+année+"   "+réponse.next());
               }
            }
            catch (UnknownHostException ex) {
               setTitle("Impossible de se connecter au service");
            }
            catch (IOException ex) {
               setTitle("Gros problème de communication");
            }
         }
      });
      pack();
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   public static void main(String[] args) { new Client(); }
}

 

Choix du chapitre Implémentation de serveurs

Maintenant que nous avons implémenté un client simple qui reçoit des données d'un serveur sur Internet (ou sur le réseau local), intéressons-nous à la fabrication d'un serveur simple qui devra envoyer des informations sur Internet (ou sur le réseau local). Une fois que ce serveur (service) sera lancé, il devra attendre qu'un client se connecte à l'un de ces port. Nous choisissons le port 8189, qui n'est utilisé par aucun service standard.

  1. La classe java.net.ServerSocket permet de monter un service. Cette classe sera ensuite utilisée pour établir une communication par le réseau au moyen d'un point de comminication qu'est la socket. Dans notre cas, le service doit se rattacher au port 8189 :

    ServerSocket service = new ServerSocket(8189);

  2. La méthode accept() demande au programme d'attendre indéfiniment jusqu'à ce qu'un client se connecte sur ce port. Une fois qu'un ordinateur s'y est connecté en envoyant une requête adéquate sur le réseau, cette méthode renvoie un objet Socket qui représente la connexion établie :

    Socket client = service.accept(); // méthode blocante

  3. Vous pouvez vous servir ensuite de cet objet pour lire et pour écrire au travers de cette socket, comme nous l'avons déjà fait dans le chapitre précédent :

    Scanner requête = new Scanner(client.getInputStream());
    PrintWriter réponse = new PrintWriter(client.getOutputStream());

    Tout ce que le serveur envoie à son flux de sortie devient un flux d'entrée pour le programme client, et toutes les sorties du client deviennent les entrées du serveur.
    §

    Pour l'instant, dans ces exemples nous transmettons du texte. Nous pourrions bien sûr transmettre des données binaires, il faudrait alors utiliser respectivement les classes DataInputStream et DataOutputStream. Pour transmettre des objets en série, il faudrait utiliser ObjectInputStream et ObjectOutputStream.

  4. Dans ce serveur très simple, nous nous contentons de lire les données du client, une ligne à la fois, et de les afficher. Cela met en évidence le fait que ce programme reçoit bien les données du client.
    package reseau;
    
    import java.io.*;
    import java.net.*;
    import java.util.Scanner;
    
    public class ServeurEcho  {
       public static void main(String[] args) throws IOException {
          ServerSocket service = new ServerSocket(8189);
          Socket client = service.accept(); // attend la connexion du client
          Scanner requête = new Scanner(client.getInputStream());
          PrintWriter réponse = new PrintWriter(client.getOutputStream(), true);
    
          réponse.println("Bonjour, tapez OK pour sortir");
          while (requête.hasNextLine()) {
             String ligne = requête.nextLine();
             réponse.println("Echo : "+ligne);
             if (ligne.trim().equalsIgnoreCase("OK")) break;
          }
          client.close(); // fermeture de la connexion du client
       }
    }    

    Un serveur réel ferait des claculs intermédiaires sur les données reçues et renverrait sa réponse.
    §

  5. Pour terminer, il ne faut pas oublier de fermer la socket ouverte (fermeture de la connexion réseau).

Nous pouvons tester ce programme en utilisant Telnet. Une fois que le serveur est lancé, établissons la connexion au port 8189 sur l'ordinateur qui propose le service :



Lorsque nous nous connectons, nous obtenons le message d'invite "Bonjour, tapez OK pour sortir". Saisissons n'importe quoi, le serveur nous répond en nous renvoyant notre message précédé de "Echo : ". Pour nous déconnecter, il suffit de taper "ok" ou "OK". Le programme du serveur sera alors également arrêté.

La classe java.net.ServerSocket
ServerSocket(int port) throws IOException
Crée un service qui examine un port.
Socket accept()
Attend une connexion. Cette méthode bloque le thread courant jusqu'à ce que la connexion soit établie. Cette méthode renvoie un objet Socket grâce auquel le programme peut communiquer avec le client.
void close() throws IOException
Clôture le service.
Exemple de mise en place d'un service date personnel

Codage correspondant
package reseau;

import java.io.*;
import java.net.*;
import java.text.DateFormat;
import java.util.Date;

public class ServeurDate  {
   public static void main(String[] args) throws IOException {
      ServerSocket service = new ServerSocket(13);
      Socket client = service.accept(); // attend la connexion du client
      PrintWriter réponse = new PrintWriter(client.getOutputStream(), true);
      réponse.println(DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.MEDIUM).format(new Date()));
      client.close();
   }
}

 

Choix du chapitre Servir plusieurs clients

Le serveur de l'exemple précédent possède un inconvénient. Supposons que nous voulions permettre à plusieurs clients de se connecter en même temps à notre serveur. Typiquement, un serveur est exécuté en permanence sur un ordinateur dédié à cette tâche, et plusieurs clients sur Internet peuvent se connecter à ce serveur simultanément. Si le serveur ne peut pas gérer plusieurs connexions en même temps, un utilisateur pourra le monopoliser en restant connecté longtemps. Mais nous pouvons faire beaucoup mieux grâce à la magie des threads.

Pour en savoir plus sur les threads.
§

  1. Chaque fois que le programme a établi une nouvelle connexion, c'est à dire qu'il a accepté une requête, nous allons créer un nouveau thread qui sera chargé de la gestion de la connexion entre le serveur et ce client. Le programme reviendra alors en arrière et attendra la prochaine connexion. Pour que cela puisse se produire, la boucle principale doit ressembler à ceci :

  2. La classe ThreadConnexion dérive de Thread et contient la boucle de communication avec le client dans la méthode run().

    Dans l'absolue, il est généralement préférable d'implémenter l'interface Runnable plutôt que de faire un héritage directement depuis la classe Thread. De même, il vaut mieux utiliser la classe Scanner en lieu et place de la classe BufferedReader.

  3. Comme chaque nouvelle connexion lance un nouveau thread, plusieurs clients peuvent se connecter au serveur en même temps. Effectuez le test en lançant dans un premier temps le service ServeurThread, et ensuite, à partir de plusieurs ordinateurs, exécutez le programme Telnet en établissant la connexion vers l'ordinateur hôte au port 8189. Cette fois-ci, le numéro de la connexion a été rajouté sur le message de retour.

 

Choix du chapitre Programmes Client-Serveur

Pour faire la synthèse de ce que nous venons de voir, nous allons créer deux systèmes client-serveur plus utiles :

  1. D'une part un module de conversion entre les €uros et les francs : nous prévoyons un système multi-client.
  2. D'autre part un système de transfert de photos sur le réseau. Cette fois-ci, nous attendons que le premier client ait envoyé la photo avant de prendre en compte un deuxième client éventuel.

Conversion entre les €uros et les francs

Le premier système, permet de réaliser des conversions entre les €uros et les francs.

Au début le client doit préciser la localisation du service. Une fois que le connexion est établie, elle reste opérationnelle jusqu'à ce que le client clôture l'application graphique.

Mise en oeuvre du service
package reseau;

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

public class ServeurConversion  {
   public static void main(String[] args) throws IOException {
      ServerSocket service = new ServerSocket(1234);
      System.out.println("Service (1234) en fonction ...");
      while (true) {
         System.out.println("Attente d'un nouveau client ...");
         Socket client = service.accept(); // attend la connexion d'un client
         new Thread(new Connexion(client)).start();
      }
   }
}

class Connexion implements Runnable {
   private Socket client;
   private final double TAUX = 6.55957;
   private double €uro, franc;

   public Connexion(Socket client) { this.client = client; }

   public void run() {
      String adresse = client.getInetAddress().getHostAddress();
       try {
         DataInputStream requête = new DataInputStream(client.getInputStream());
         DataOutputStream réponse = new DataOutputStream(client.getOutputStream());
         System.out.println("Client ("+adresse+") connecté ...");
         while (true) {
            String demande = requête.readUTF();
            if (demande.equals("EuroFranc")) {
               €uro = requête.readDouble();
               franc = €uro * TAUX;
               réponse.writeDouble(franc);
            }
            if (demande.equals("FrancEuro")) {
               franc = requête.readDouble();
               €uro = franc / TAUX;
               réponse.writeDouble(€uro);
            }
         }
      } 
      catch (Exception ex) { System.err.println("Fin de la connexion avec le client ("+adresse+")");    }
   }   
}
Mise en oeuvre de la partie cliente
package reseau;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import java.text.*;
import javax.swing.*;

public class ClientConversion extends JFrame {
   private JTextField adresseIP = new JTextField(12);
   private JFormattedTextField euro = new JFormattedTextField(NumberFormat.getCurrencyInstance());
   private JFormattedTextField franc = new JFormattedTextField(new DecimalFormat("#,##0.00 F"));
   private JToolBar outils = new JToolBar("Localisation du serveur");
   private Socket connexion;
   private DataOutputStream requête;
   private DataInputStream réponse;

   public ClientConversion() {
      super("Convertisseur €uro <-> Franc");
      euro.setValue(0.0);
      franc.setValue(0.0);
      add(outils, BorderLayout.NORTH);
      outils.add(new JLabel("Adresse IP : "));
      outils.add(adresseIP);
      outils.add(new AbstractAction("Connexion") {
         public void actionPerformed(ActionEvent e) {
            try {
               connexion = new Socket(adresseIP.getText(), 1234);
               requête = new DataOutputStream(connexion.getOutputStream());
               réponse = new DataInputStream(connexion.getInputStream());
            }
            catch (UnknownHostException ex) { setTitle("Service non accessible"); }
            catch (IOException ex) { setTitle("Problème de communication réseau"); }
         }
      });
      outils.add(new AbstractAction("  F  ") {
         public void actionPerformed(ActionEvent e) {
            try {
               requête.writeUTF("EuroFranc");
               Number valeur = (Number) euro.getValue();
               requête.writeDouble(valeur.doubleValue());
               franc.setValue(réponse.readDouble());
            }
            catch (IOException ex) { setTitle("Le calcul n'a pas être fait correctement");}
         }
      });
      outils.add(new AbstractAction("") {
         public void actionPerformed(ActionEvent e) {
            try {
               requête.writeUTF("FrancEuro");
               Number valeur = (Number) franc.getValue();
               requête.writeDouble(valeur.doubleValue());
               euro.setValue(réponse.readDouble());
            }
            catch (IOException ex) { setTitle("Le calcul n'a pas être fait correctement");}
         }
      });
      add(euro);
      add(franc, BorderLayout.SOUTH);
      pack();
      setLocationRelativeTo(null);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   public static void main(String[] args) { new ClientConversion(); }
}

Transfert d'images sur le réseau

A titre d'exemple, pour valider à la fois le fonctionnement du réseau, mais égalemenent la bonne gestion des flux, je vous propose de visualiser automatiquement une photo numérique sur le serveur qui est envoyé par un client sur le réseau local. Nous devons donc élaborer deux applications :

  1. La première, qui correspond au client du réseau, doit récupérer des photos présentes sur le poste local et les afficher ensuite dans la zone principale de la fenêtre. Ainsi, vous avez la possibilité de choisir la photo qui vous plait afin de l'envoyer au service de visionnage.
  2. La deuxième, sur le serveur, en attente d'éventuels transferts sur le réseau, affiche la photo envoyée par la première application.

Application cliente qui envoi les photos ............................................. Serveur qui reçoit les photos

photos.PanneauImage.java
package photos;

import javax.swing.JComponent;
import java.awt.Graphics;
import java.awt.image.BufferedImage;

class PanneauImage extends JComponent {
  private BufferedImage image;
  private double ratio;
   
  public void change(BufferedImage image) {
    if (image!=null) {
      this.image = image;
      ratio = (double)image.getWidth()/image.getHeight();  
      repaint();
    }
  }
   
  @Override
  protected void paintComponent(Graphics surface) {
    if (image!=null)
      surface.drawImage(image, 0, 0, this.getWidth(), (int)(this.getWidth()/ratio), null);   
  }   
}
photos.ClientPhotos.java
package photos;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.*;
import javax.imageio.*;

public class ClientPhotos extends JFrame implements ActionListener {
  private String répertoire = "J:/Stockage/";
  private String[] liste;
  private PanneauImage panneau = new PanneauImage();
  private JComboBox choix;
  private JButton envoyer = new JButton("Envoyer la photo");

  public ClientPhotos() {
    liste = new File(répertoire).list();
    choix = new JComboBox(liste);
    panneau.change(récupérer());
    choix.addActionListener(this);
    envoyer.addActionListener(this);
    setSize(500, 400);
    setTitle("Envoi de photos");
    add(choix, BorderLayout.NORTH);
    add(envoyer, BorderLayout.SOUTH);
    add(panneau);
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    setVisible(true);
  }
     
  private BufferedImage récupérer() {
    try {
      BufferedImage photo = ImageIO.read(new File(répertoire+choix.getSelectedItem()));
      return photo;
    }
    catch (Exception ex) {
      setTitle("Problème de localisation des photos");
      return null;
    }      
  }
  
  public static void main(String[] args) { new ClientPhotos(); }
 
  public void actionPerformed(ActionEvent e) {
    if (e.getSource() == choix) {
      panneau.change(récupérer());
    }
    else if (e.getSource() == envoyer) {       
      try {
        File fichier = new File(répertoire+choix.getSelectedItem());
        byte[] octets = new byte[(int)fichier.length()];
        FileInputStream photo = new FileInputStream(fichier);
        photo.read(octets);
        envoyer(octets);
      }
      catch (IOException ex) { setTitle("Problème avec le fichier"); }
    }
  }
    
  private void envoyer(byte[] octets) {
    try {
      Socket connexion = new Socket("localhost", 7777);
      ObjectOutputStream fluxRéseau = new ObjectOutputStream(connexion.getOutputStream());
      fluxRéseau.writeObject(octets);
      connexion.close();
    } 
    catch (IOException ex) { setTitle("Problème avec le serveur"); }
  }
}
photos.ServeurPhotos.java
package photos;

import java.io.*;
import java.net.*;
import javax.swing.JFrame;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;

public class ServeurPhotos extends JFrame {
  private static PanneauImage panneau = new PanneauImage();
    
  public ServeurPhotos() {
    setSize(500, 400);
    setTitle("Visionneuse");
    add(panneau);
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    setVisible(true);
  }

  public static void main(String[] args) throws Exception {
    new ServeurPhotos();
    activerService();
  }

  public static void activerService() throws Exception {
    ServerSocket service = new ServerSocket(7777);
     while (true) {
       Socket client = service.accept();
       ObjectInputStream fluxRéseau = new ObjectInputStream(client.getInputStream());
       byte[] octets = (byte[]) fluxRéseau.readObject();
       ByteArrayInputStream fluxImage = new ByteArrayInputStream(octets);
       BufferedImage photo = ImageIO.read(fluxImage);
       panneau.change(photo); 
    }
  }
}  

 

Choix du chapitreLes sockets interruptibles

Lorsque nous nous connectons à une socket, le thread courant se bloque jusqu'à ce que le connexion s'établisse ou lorsque le timeout arrive à expiration. De la même manière, lorsque nous faisons une lecture ou une écriture d'informations au travers de la socket, le thread courant se bloque jusqu'à que l'opération de transfert soit définitivement terminée ou, encore une fois lorsque le timeout arrive à expiration.

Dans une application normale, ou l'interactivité avec l'utilisateur est prépondérante, nous devons lui proposer, à tout moment, d'annuler la communication avec le service distant notamment si la connexion a du mal à s'établir. Cependant, nous ne pouvons pas mettre en oeuvre un thread concurrent, en faisant appel notamment à la méthode interrupt() tant que la socket n'est pas en fonctionnement.

  1. Pour permettre l'interruption d'une socket à tout moment, vous devez utiliser la classe SocketChannel qui se trouve dans le paquetage java.nio.channels. Ouvrez la socket de la façon suivante :

    SocketChannel canal = SocketChannel.open(new InetSocketAddress(hôte, port));

    Un canal ne possède pas de flux associés. Par contre, il dispose de méthodes read() et write() qui utilisent un objet Buffer. Ces méthodes sont déclarées respectivement dans les interfaces ReadableByteChannel et WritableByteChannel.

  2. Si vous désirez avoir affaire avec des buffers, vous pouvez utiliser la class Scanner pour lire directement à partir de la classe SocketChannel, parce que la classe Scanner dispose d'un constructeur avec un paramètre de type ReadableByteChannel :

    Scanner lecture = new Scanner(canal);

  3. Pour connecter un flux de sortie à partir d'un canal, vous avez la possibilité de prendre la méthode statique java.nio.channels.Channels.newOutputStream() :

    OutputStream écriture = Channels.newOutputStream(canal);

  4. De même, de façon symétrique, il existe la méthode statique java.nio.channels.Channels.newInputStream() pour permettre une connexion à un flux d'entrée standard :

    InputStream lecture = Channels.newInputStream(canal);

    Voilà tout ce que vous deviez savoir sur ce sujet. A chaque fois qu'un thread est interrompu durant l'opération d'ouverture, de lecture ou d'écriture sur une socket, le système n'est plus bloqué, mais se termine par une exception.

La classe java.net.InetSocketAddress
InetSocketAddress(String hôte, int port)
Construit un objet d'adresse avec l'ordinateur distant et le port donnés, ce qui résout le nom de l'hôte en cours de la construction. Si le nom de l'hôte ne peut être résolu, la propriété unresolved de l'objet de l'adresse est validé.
boolean isUnresolved()
Précise si l'objet de l'adresse ne peut être résolu.
La classe java.nio.channels.SocketChannel
static SocketChannel open(SocketAdress adresse)
Ouvre un canal de socket et se connecte au service spécifié par l'adresse.
La classe java.nio.channels.Channels
static InputStream newInputStream(ReadableByteChannel canal)
Propose un flux en entrée à partir du canal spécifié en paramètre.
static OutputStream newOutputStream(WritableByteChannel canal)
Propose un flux en sortie à partir du canal spécifié en paramètre.

 

Choix du chapitreConnexion à des URLs

Pour accéder à un serveur Web depuis un programme client Java, il serait préférable de travailler à plus haut niveau, plutôt que de mettre en place une gestion par socket, avec les flux d'entrée-sortie associés. Le plus difficile, lorsque nous passons par une architecture classique, est la mise en oeuvre des requêtes issues du protocole HTTP. Dans ce chapitre, nous allons découvrir comment mettre en place un système très performant, grâce à des classes adaptées et tout-à-fait compétentes dans ce genre de protocole.

URL et URI

Les classes URL et URLConnection encapsulent la plupart des problèmes liés à la récupération d'informations sur un site distant.

  1. Voici comment spécifier une adresse URL :

    URL adresse = new URL("http://www.unsite.fr");

    La plate-forme Java prend en charge les ressources suivantes : http: https: ftp: file: et jar:
    §

  2. Si vous souhaitez simplement aller chercher le contenu des ressources, vous pouvez utiliser la méthode openStream() de la classe URL. Cette méthode produit un InputStream. Grâce à cet objet de flux, vous lirez facilement le contenu de la ressource :

    InputStream flux = adresse.openStream();
    Scanner
    requête = new Scanner(flux);

Le paquetage java.net fait une distinction utile entre les URL (adresses Internet) et les URI (identificateurs de ressources).
  1. Une URI représente la construction de syntaxe pure, qui spécifie les différentes parties de la chaîne de ressource Web.
  2. Une URL est en réalité une sorte d'URI, à savoir une URI disposant de suffisamment d'informations pour localiser une ressource.
  3. D'autres URI, comme mailto:emmanuel.remy@wanadoo.fr ne sont pas des agents de localisation ; il n'existe en effet pas de données permettant de localiser cet identificateur. Cette URI est appelée URN (nom des ressources uniformes).
  4. Dans la bibliothèque Java, la classe URI ne possède pas de méthodes permettant d'accéder à la ressource spécifiée par l'identificateur : son unique objectif est l'analyse.
  5. Au contraire, la classe URL peut ouvrir un flux vers la ressource. Pour cette raison, la classe URL fonctionne uniquement avec des identificateurs d'une catégorie que la bibliothèque Java sait gérer, comme http: https: ftp: file: et jar:

Spécification des URI (et donc également des URL)

Pour savoir à quel moment une classe URI peut être nécessaire, il faut envisager le complexité des URL. Par exemple :

http://maps.google.fr/maps?hl=fr&tab=wl
ftp://utilisateur:motdepasse@ftp.site.com/pub/file.txt

La spécification de l'URI donne les règles de la création de ces identifiants :

  1. Une URI possède la syntaxe :

    [schéma:]spécification du schéma[#fragment]

    Ici le texte [ ... ] dénote une partie optionnelle et les caractères : et # figurent littéralement dans l'identifiant.
    §

  2. Si la partie schéma: est présente, l'URI est dite absolue. Autrement, elle est dite relative.
  3. Une URI absolue est opaque si spécification du schéma ne commence par par un / comme :

    mailto:emmanuel.remy@wanadoo.fr

  4. Toutes les URI absolues non opaques et toutes les URI relatives sont hiérarchiques. Par exemple :

    http://emmanuel.remy@wanadoo.fr
    ../../java/net/Socket.html#Socket()

  5. La spécification du schéma d'une URI hiérarchique affiche la structure :

    [//autorité][chemin][?requête]

  6. Pour les URI basées sur des serveurs, la partie autorité possède la forme :

    [utilisateur@]hôte[:port]

  7. L'un des objectif de la classe URI est d'analyser un identifiant et de le diviser en plusieurs composants. Voici deux exemple d'URI :

    http://maps.google.fr/maps:80?hl=fr&tab=wl#partie
    ftp://utilisateur:motdepasse@ftp.site.com/pub/file.txt

    A partir de ces deux exemples, nous pouvons récupérer cahque composant avec des méthodes adaptées :
  8. L'autre objectif de la classe URI est la gestion des identifiants absolues et relatifs.Si vous disposez d'une URI absolue comme :

    http://docs.masociété.com/api/java/net/ServerSocket.html

    et d'une URI relative comme :

    ../../java/net/Socket.html#Socket()

    Vous pouvez alors associer les deux dans une URI absolue :

    http://docs.masociété.com/api/java/net/Socket.html#Socket()

    Cette procédure s'appelle la résolution d'une URL relative. La procédure opposée est appelée revitalisation.
    §

    Par exemple, supposons que vous ayez une URI de base :

    http://docs.masociété.com/api

    et d'une URI :

    http://docs.masociété.com/api/java/lang/String.html

    l'URI relative serait :

    java/lang/String.html

    La classe URI prend en charge ces deux opérations :

    relative = base.relativize(combinée)
    combinée = base.resolve(relative)

Beaucoup de méthodes sont communes pour les deux classes URL et URI. La classe URI propose des méthodes supplémentaires qui vont au dela de la localisation spécifique des URL.
§

Récupérer des informations sur un site distant

Si vous désirez obtenir plus d'informations sur une ressource particulière, il vous faudra recourrir à la classe URLConnection, qui fournit un contrôle plus précis que la classe URL de base. Lorsque vous travaillez avec un objet URLConnection, vous devez respecter un processus bien précis, qui voici :

  1. Appelez la méthode openConnection() de la classe URL pour obtenir un objet URLConnection :

    URL adresse = new URL("http://www.unsite.fr");
    URLConnection
    connexion = adresse.openConnexion();

  2. Définissez certaines propriétés avec les méthodes suivantes :

    Nous reviendrons sur ces méthodes un peu plus loin dans cette section.
    §

  3. Connectez-vous à la ressource distante en appelant la méthode connect() :

    connexion.connect();

    En plus d'établir une connexion de socket avec un serveur, cette méthode demande aussi au serveur des informations d'en-tête.
    §

  4. Après vous être connecté au serveur, vous pouvez lui demander des informations d'en-tête. Il existe deux méthodes pour passer en revue les champs des en-têtes : getHeaderFieldKey() et getHeaderField(). Il existe également une méthode getHeadersFields() qui obtient un objet Map standard contenant les champs d'en-tête. Par souci de simplicité, les méthode suivantes récupèrent les champs standards :
  5. Pour terminer, vous pouvez accéder aux données de la ressource spécifiée. La méthode getInputStream() fournit un flux de données en entrée permettant de lire les informations. Il s'agit du même flux de données que celui envoyé par la méthode openStream() de la classe URL. Il existe également une méthode getObject(), mais dans la pratique elle n'est pas très utile. Les objets renvoyés par les types de contenus standard (comme text/plain et image/gif) nécessitent des classes de la hiérarchie com.sun pour être traitées.

    Attention, les méthodes getInputStream() et getOutputStream() de la classe URLConnection ne sont pas identiques à celles de la classe Socket. La classe URLConnection est assez magique en coulisses, en particulier pour la gestion des en-têtes de requêtes et de réponses. Pour cette raison, il est important que vous suiviez bien les étapes de configuration de la connexion.

Examinons maintenant certaines méthodes en détail.
  1. Il existe plusieurs méthodes pour définir les propriétés d'une connexion avant de vous connecter à un serveur. Les deux plus importantes sont setDoInput() et setDoOutput(). Par défaut, une connexion fournit un flux de données d'entrée en lecture, amis aucun flux de sortie en écriture. Si vous désirez cependant un flux de sortie (par exemple pour envoyer des données à un serveur Web (comme lors d'une communication entre une applet et une servlet), vous devrez appeler :

    connexion.setDoOutput(true);

    Pour en savoir plus sur la communication entre applet et servlet.
    §

  2. Puis, vous pouvez vouloir définir certains en-têtes de requêtes. Ceux-ci sont envoyés avec la commande de requête au serveur. En voici un exemple :

    GET www.serveur.com/index.html HTTP/1.0
    Referer: http://www.quelquechose.com/liens.html
    Proxy-Connection: Keep-Alive
    User-Agent: Mozilla/4.76 (Windows ME; U) Opera 9.26 [fr]
    Host: www.serveur.com
    Accept: text/html, image/gif, image/jpeg, image/png, */*
    Accept-Language: fr
    Accept-Charset: iso-8859-1, *, utf-8
    Cookie: orangemilano=192218887821987

  3. La méthode setIfModifiedSince() indique à la connexion que vous n'êtes intéressé que par les données qui ont été modifiées depuis une certaine date.
  4. Les méthodes setUsecaches() et setAllowUserInteraction() ne sont utilisées que dans les applets. La méthode setUsesCaches() demande au navigateur de commencer par vérifier sa mémoire cache. La méthode setAllowUserInteraction() permet à une application d'afficher une boîte de dialogue pour demander à l'utilisateur son nom et son mot de passe pour les ressources protégées par un mot de passe.

    Ces paramètres nont aucun effet en dehors des applets.
    §

  5. Pour terminer, il existe une méthode générale, setRequestProperty(), qui vous permet de définir n'importe quelle paire nom/valeur correspondant à un protocole particulier. Malheureusement, ces paramètres ne sont pas très bien documentés et ne circulent que par du bouche à oreille entre les programmeurs. Par exemple, si vous voulez accéder à une page Web protégée par mot de passe, vous devez respecter les étapes suivantes :
  6. Une fois que vous avez appelé la méthode connect(), vous pouvez demander des informations d'en-tête. Commençons par énumérer les champs d'en-tête. Cette classe se distingue par la mise en place d'un nouveau protocole. L'appel suivant récupère la n'ième clé d'un en-tête, où n doit commencer à 1. Elle renvoie null si n vaut 0 ou s'il est supérieur au nombre de champ de l'en-tête.

    String clé = connexion.getHeaderFieldKey(n);

    Il n'existe aucune méthode pour renvoyer directement le nombre de champs d'un en-tête. Il suffit alors d'appeler getHeaderFieldKey() jusqu'à obtenir null.
    §

  7. De même, l'appel suivant renvoi la n'ième valeur :

    String valeur = connexion.getHeaderField(n);

    Il existe maintenant une méthode identique qui permet de spécifier, sous forme de chaîne de caractères, l'intitulé de la clé :

    String valeur = connexion.getHeaderField("Content-Length");

  8. Heureusement, il existe une méthode plus générique, intitulée getHeaderFields() qui renvoie une carte de champs d'en-tête de réponse auquel vous pouvez accéder :

    Map champsEnTête = connexion.getHeaderFields();

    Voici un ensemble de champs d'en-têtes typiques d'un requête HTTP :

    Date: Wed, 3 Jun 2009 11:34:38 GMT
    Server: Apache/2.2.2 (Unix)
    Last-Modified: Thu, 17 Apr 2009 22:42:03 GMT
    Accept-Ranges: bytes
    Content-Length: 4813
    Connection: close
    Content-Type: text/html

  9. Pour plus de simplicité, six méthodes récupèrent directement les valeurs des types d'en-têtes les plus courants, et les convertissent en types numériques lorsque cela est approprié.
    Nom du champ Méthode Type de retour
    Date getDate() long
    Expires getExpiration() long
    Last-Modified getLastModified() long
    Content-Length getContentLength() int
    Content-Type getContentType() String
    Content-Encoding getContentEncoding() String

    Les méthodes dont le type de retour est long renvoient le nombre de secondes écoulées depuis le 1er janvier 1970, GMT.
    §

Exemple de mise en oeuvre

Pour conclure tout ce chapitre, je vous propose de mettre en oeuvre un tout petit navigateur juste capable d'affiché le contenu d'une page Web. Nous profiterons de ce navigateur pour ajouter un onglet qui nous donnera toutes les indications de consultation du serveur Web distant :

  1. Examen global de l'URL proposée
  2. Caractéristiques du serveur Web

Fonctionnement global du navigateur

Codage correspondant
package navigateur;

import javax.swing.*;
import java.net.*;
import java.text.MessageFormat;

public class Navigateur extends JFrame  {
   private JTabbedPane onglets = new  JTabbedPane();
   private JTextArea caractéristiques = new JTextArea();

   public Navigateur(URL url) throws Exception {
      super(url.toExternalForm());
      onglets.addTab("Page Web", new JScrollPane(new JEditorPane(url)));
      onglets.addTab("Caractéristiques du serveur", caractéristiques);
      analyse(url);
      add(onglets);
      setSize(800, 700);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }

   private void analyse(URL url) throws Exception {
      caractéristiques.append("Autorité : "+url.getAuthority()+'\n');
      caractéristiques.append("Hôte : "+url.getHost()+'\n');
      caractéristiques.append("Chemin : "+url.getPath()+'\n');
      caractéristiques.append("Protocole : "+url.getProtocol()+'\n');
      caractéristiques.append("Requête : "+url.getQuery()+'\n');
      caractéristiques.append("Ancre : "+url.getRef()+'\n');
      caractéristiques.append("Info utilisateur : "+url.getUserInfo()+'\n');
      caractéristiques.append("Port par défaut : "+url.getDefaultPort()+'\n');
      caractéristiques.append("Port : "+url.getPort()+'\n');
      caractéristiques.append("Fragment : "+url.toURI().getFragment()+'\n');
      caractéristiques.append("Schéma : "+url.toURI().getScheme()+'\n');
      caractéristiques.append("Spécification du schéma : "+url.toURI().getSchemeSpecificPart()+'\n');

      URLConnection connexion = url.openConnection();
      String motifDate = "{0, date, full} {0, time, medium}";
      String motifNombre = "{0, number, integer} octets";
      
      caractéristiques.append("Type de contenu : "+connexion.getContentType()+'\n');
      caractéristiques.append("Taille du document : "+MessageFormat.format(motifNombre, connexion.getContentLength())+'\n');
      caractéristiques.append("Type d'encodage : "+connexion.getContentEncoding()+'\n');
      caractéristiques.append("Instant de téléchargement : "+MessageFormat.format(motifDate, connexion.getDate())+'\n');
      caractéristiques.append("Dernières modification : "+MessageFormat.format(motifDate, connexion.getLastModified())+'\n');
      caractéristiques.append("Date d'expiration : "+MessageFormat.format(motifDate, connexion.getExpiration())+'\n');
      caractéristiques.append("Serveur : "+connexion.getHeaderField("Server")+'\n');
      caractéristiques.append("Type accepté : "+connexion.getHeaderField("Accept-Ranges")+'\n');
   }

   public static void main(String[] args) { 
      String adresse = JOptionPane.showInputDialog("URL :");
      try { new  Navigateur(new URL(adresse));  }
      catch (Exception ex) { JOptionPane.showMessageDialog(null, "Mauvaise URL : "+adresse);}
   }
}

Nous disposons maintenant de tous les outils nécessaire pour mettre en oeuvre une petite application "Chat". Nous en limiterons les performances avec la possibilité de se connecter à un instant donné à un seul interlocuteur.

Je rappelle qu'un Chat est la fois client et serveur. Au départ, lorsque le logiciel est lancé, le service est mise en route et attend une connexion éventuelle venant de l'extérieur. Lorsque le contact s'établie, vous pouvez envoyer vos messages dans la zone de saisie. Votre interlocuteur le reçoit alors dans la zone principale de la fenêtre. Vous pouvez demander vous-même à vous connecter avec votre interlocuteur, il suffit de placer alors le nom de l'hôte dans la zone prévue à cet effet.