IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java

  Chapitre 15 - Informatique distribuée

pages : 1 2 3 4 5 6 7 8 9 10 11 12 

La même logique est utilisée pour le Socket renvoyé par accept( ). Si accept( ) échoue, nous devons supposer que le Socket n'existe pas et qu'il ne monopolise aucune ressource, et donc qu'il n'est pas besoin de le nettoyer. Au contraire, si accept( ) se termine avec succès, les instructions qui suivent doivent se trouver dans un bloc try-finally pour que Socketsoit toujours nettoyé si l'une d'entre elles échoue. Il faut porter beaucoup d'attention à cela car les sockets sont des ressources importantes qui ne résident pas en mémoire, et on ne doit pas oublier de les fermer (car il n'existe pas en Java de destructeur qui le ferait pour nous).

Le ServerSocket et le Socket fournis par accept( ) sont imprimés sur System.out. Leur méthode toString( ) est donc appelée automatiquement. Voici le résultat :

ServerSocket[addr=0.0.0.0,PORT=0,localport=8080]
Socket[addr=127.0.0.1,PORT=1077,localport=8080]

En raccourci, on peut voir comment cela « colle » avec ce que fait le client.

Le reste du programme consiste seulement à ouvrir des fichiers pour lire et écrire, sauf que InputStream et OutputStream sont créés à partir de l'objet Socket. Les deux objets InputStream et OutputStream sont convertis en objets Reader et Writer au moyen des « classes de conversion InputStreamReader et OutputStreamWriter, respectivement. On aurait pu travailler directement avec les classes Java 1.0 InputStream et OutputStream, mais pour la sortie il y a un avantage certain à utiliser l'approche Writer. C'est évident avec PrintWriter, qui possède un constructeur surchargé prenant en compte un deuxième argument, un flag boolean indiquant que le tampon de sortie doit être automatiquement vidé après chaque println( ) (mais nonaprès les instructions print( )). Chaque fois qu'on écrit sur out, son buffer doit être vidé afin que l'information parte sur le réseau. Le vidage du tampon est important dans cet exemple particulier car aussi bien le serveur que le client attendent de l'autre une ligne complète avant de la traiter. Si le tampon n'est pas vidé à chaque ligne, l'information ne circule pas sur le réseau tans que le buffer n'est pas plein, ce qui occasionnerait de nombreux problèmes dans cet exemple.

Lorsqu'on écrit des programmes réseau, il faut être très attentif à l'utilisation du vidage automatique de buffer. Chaque fois que l'on vide un buffer, un paquet est créé et envoyé. Dans notre exemple, c'est exactement ce que nous recherchons, puisque si le paquet contenant la ligne n'est pas envoyé, l'échange entre serveur et client sera stoppé. Dit d'une autre manière, la fin de ligne est la fin du message. Mais dans de nombreux cas, les messages ne sont pas découpés en lignes et il sera plus efficace de ne pas utiliser le vidage automatique de buffer et à la place de laisser le gestionnaire du buffer décider du moment pour construire et envoyer un paquet. De cette manière, les paquets seront plus importants et le processus plus rapide.

Remarquons que, comme tous les flux qu'on peut ouvrir, ceux-ci sont tamponnés. À la fin de ce chapitre, vous trouverez un exercice montrant ce qui se passe lorsqu'on ne tamponne pas les flux (tout est ralenti).

La boucle while infinie lit les lignes depuis le BufferedReader inet écrit sur System.out et PrintWriter out. Remarquons que in et out peuvent être n'importe quel flux, ils sont simplement connectés au réseau.

Lorsque le client envoie une ligne contenant juste le mot END, « la boucle est interrompue et le programme ferme le Socket.

Et voici le client :

//: c15:JabberClient.java
// Client simplifié se contentant d'envoyer
// des lignes au serveur et de lire les lignes
// que le serveur lui envoie.
import java.net.*;
import java.io.*;

public class JabberClient {
  public static void main(String[ « args)
      throws IOException {
// Appeler getByName() avec un argument null revient
    // à utiliser une adresse IP spéciale "Boucle Locale"
// pour faire des tests réseau sur une seule machine.

     InetAddress addr =
      InetAddress.getByName(null);
    // Il est également possible d'utiliser
    // l'adresse ou le nom:
    // InetAddress addr =
    //    InetAddress.getByName("127.0.0.1");
    // InetAddress addr =
    //    InetAddress.getByName("localhost");
    System.out.println("addr = " + addr);
    Socket socket =
      new Socket(addr, JabberServer.PORT);
    // Le code doit être inclus dans un bloc
    // try-finally afin de garantir
    // que socket sera fermé:
    try {
      System.out.println("socket = " + socket);
      BufferedReader in =        new BufferedReader(
          new InputStreamReader(
            socket.getInputStream()));
      // Le tampon de sortie est automatiquement
      // vidé par PrintWriter:
      PrintWriter out =        new PrintWriter(
          new BufferedWriter(
            new OutputStreamWriter(
              socket.getOutputStream())),true);
      for(int i = 0; i < 10; i ++) {
        out.println("howdy " + i);
        String str = in.readLine();
        System.out.println(str);
      }
      out.println("END");
    } finally {
      System.out.println("closing...");
      socket.close();
    }
  }
} ///:~

La méthode main( ) montre qu'il existe trois manières de produire l'InetAddress de l'adresse IP de la boucle locale  : avec null, localhost, ou bien l'adresse réservée et explicite 127.0.0.1. Bien entendu, si l'on désire se connecter à une machine du réseau, il suffit d'y substituer son adresse. Lorsque InetAddress addr est imprimée (via l'appel automatique de sa méthode toString( )), voici le résultat :

localhost/127.0.0.1

Lorsque getByName( ) a été appelée avec un argument null, elle a cherché par défaut localhost, ce qui a fourni l'adresse spéciale 127.0.0.1.

Remarquons que le Socket nommé socket est créé avec InetAddress ainsi que le numéro de port. Pour comprendre ce qui se passe lorsqu'on imprime un de ces objets Socket, il faut se souvenir qu'une connexion Internet est déterminée de manière unique à partir de quatre données : clientHost, clientPortNumber, serverHost, et serverPortNumber. Lorsque le serveur démarre, il prend en compte le port qui lui est assigné (8080) sur la machine locale (127.0.0.1). Lorsque le client démarre, le premier port suivant disponible sur sa machine lui est alloué, 1077 dans ce cas, qui se trouve sur la même machine (127.0.0.1) que le serveur. Maintenant, pour que les données circulent entre le client et le serveur, chacun doit savoir où les envoyer. En conséquence, pendant la connexion au serveur connu, le client envoie une adresse de retour afin que le serveur sache où envoyer ses données. Ce qu'on peut voir dans la sortie de l'exemple côté serveur :

Socket[addr=127.0.0.1,port=1077,localport=8080]

Cela signifie que le serveur vient d'accepter une demande de connexion provenant de 127.0.0.1 sur le port 1077 alors qu'il est à l'écoute sur le port local (8080). Du côté client :

Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]

ce qui signifie que le client vient d'établir une connexion à 127.0.0.1 sur le port 8080 en utilisant le port local 1077.

Il faut remarquer que chaque fois qu'on relance le client, le numéro du port local est incrémenté. Il commence à 1025 (un après le bloc de ports réservé) et ne cesse d'augmenter jusqu'à ce qu'on reboote la machine, auquel cas il recommence à 1025 (sur les machines UNIX, lorsque la limite supérieure de la plage accordée aux sockets est atteinte, on recommence avec la plus petite valeur possible).

L'objet Socket créé, le processus consistant à en faire un BufferedReader puis un PrintWriter est le même que pour le serveur (encore une fois, dans les deux cas on commence par un Socket). Ici, le client entame la conversation en envoyant la chaîne « [howdy] » suivie d'un nombre. Remarquons que le buffer doit être vidé à nouveau (ce qui est automatique via le deuxième argument du constructeur de PrintWriter). Si le buffer n'est pas vidé, la conversation est complètement suspendue parce que le « howdy » initial ne sera jamais envoyé (le buffer n'est pas assez rempli pour que l'envoi se fasse automatiquement). Chaque ligne renvoyée par le serveur est écrite sur System.out pour vérifier que tout fonctionne correctement. Pour arrêter l'échange, le client envoie le mot connu » END. Si le client ne se manifeste plus, le serveur lance une exception.

Remarquons qu'ici aussi le même soin est apporté pour assurer que la ressource réseau que représente le Socket est relâchée correctement, au moyen d'un bloc try-finally.

Les sockets fournissent une connexion » dédiée « qui persiste jusqu'à ce qu'elle soit explicitement déconnectée (la connexion dédiée peut encore être rompue de manière non explicite si l'un des deux côtés ou un lien intermédiaire de la connexion se plante). La conséquence est que les deux parties sont verrouillées en communication et que la connexion est constamment ouverte. Cela peut paraître une approche logique du travail en réseau, en fait on surcharge le réseau. Plus loin dans ce chapitre on verra une approche différente du travail en réseau, dans laquelle les connexions seront temporaires.

Servir des clients multiples

Le programme JabberServer fonctionne, mais ne peut traiter qu'un client à la fois. Dans un serveur typique, on désire traiter plusieurs clients en même temps. La réponse est le multithreading, et dans les langages ne supportant pas cette fonctionnalité cela entraîne toutes sortes de complications. Dans le Chapitre 14 nous avons vu que le multithreading de Java est aussi simple que possible, si l'on considère le multithreading comme un sujet quelque peu complexe. Parce que gérer des threads est relativement simple en Java, écrire un serveur prenant en compte de multiples clients est relativement facile.

L'idée de base est de construire dans le serveur un seul ServerSocket et d'appeler accept( ) pour attendre une nouvelle connexion. Au retour d'accept( ), on crée un nouveau thread utilisant le Socket résultant, thread dont le travail est de servir ce client particulier. Puis on appelle à nouveau accept( ) pour attendre un nouveau client.

Dans le code serveur suivant, on remarquera qu'il ressemble à l'exemple JabberServer.java sauf que toutes les opérations destinées à servir un client particulier ont été déplacées dans une classe thread séparée :

//: c15:MultiJabberServer.java
// Un serveur utilisant le multithreading
// pour traiter un nombre quelconque de clients.
import java.io.*;
import java.net.*;

class ServeOneJabber extends Thread {
  private Socket socket;
  private BufferedReader in;
  private PrintWriter out;
  public ServeOneJabber(Socket s)
      throws IOException {
    socket = s;
    in =
      new BufferedReader(
        new InputStreamReader(
          socket.getInputStream()));
    // Autoriser l'auto-vidage:
    out =
      new PrintWriter(
        new BufferedWriter(
          new OutputStreamWriter(
            socket.getOutputStream())), true);
    // Si l'un des appels ci-dessus résulte en une
    // exception, l'appelant a la responsabilité
    // de fermer socket. Sinon le thread
    // s'en chargera.
    start(); // Appelle run()
  }
  public void run() {
    try {
      while (true) {  
        String str = in.readLine();
        if (str.equals("END")) break;
        System.out.println("Echoing: " + str);
        out.println(str);
      }
      System.out.println("closing...");
    } catch(IOException e) {
      System.err.println("IO Exception");
    } finally {
      try {
        socket.close();
      } catch(IOException e) {
        System.err.println("Socket not closed");
      }
    }
  }
}

public class MultiJabberServer {  
  static final int PORT = 8080;
  public static void main(String[ « args)
      throws IOException {
    ServerSocket s = new ServerSocket(PORT);
    System.out.println("Server Started");
    try {
      while(true) {
// On attend ici jusqu'à avoir
// une demande de connexion:
        Socket socket = s.accept();
        try {
          new ServeOneJabber(socket);
        } catch(IOException e) {
          // En cas d'échec, fermer l'objet socket,
          // sinon le thread le fermera:
          socket.close();
        }
      }
    } finally {
      s.close();
    }
  }
} ///:~

Chaque fois qu'un nouveau client se connecte, le thread ServeOneJabber prend l'objet Socket produit par accept( ) dans main( ). Puis, comme auparavant, il crée un BufferedReader et un objet PrintWriter(avec auto-vidage du buffer) à partir du Socket. Finalement, il appelle la méthode spéciale start( ) de la classe Thread qui initialise le thread puis appelle run( ). On réalise ainsi le même genre de traitement que dans l'exemple précédent : lire quelque chose sur la socket et le renvoyer en écho jusqu'à l'arrivée du signal spécial » END.

À nouveau, il nous faut penser à notre responsabilité en ce qui concerne la ressource socket. Dans ce cas, la socket est créée hors de ServeOneJabber et donc la responsabilité doit être partagée. Si le constructeur de ServeOneJabber échoue, il suffit qu'il lance une exception vers l'appelant, qui nettoiera alors le thread. Mais dans le cas contraire, l'objet ServeOneJabber a la responsabilité de nettoyer le thread, dans sa méthode run( ).

Remarquons la simplicité de MultiJabberServer. Comme auparavant, on crée un ServerSocket, et on appelle accept( ) pour autoriser une nouvelle connexion. Mais cette fois, la valeur de retour de accept( ) (un Socket) est passée au constructeur de ServeOneJabber, qui crée un nouveau thread afin de prendre en compte cette connexion. La connexion terminée, le thread se termine tout simplement.

Si la création du ServerSocket échoue, à nouveau une exception est lancée par main( ). En cas de succès, le bloc try-finally extérieur garantit le nettoyage. Le bloc try-catch intérieur protège d'une défaillance du constructeur ServeOneJabber  ; en cas de succès, le thread ServeOneJabber fermera la socket associée.

Afin de tester que le serveur prend réellement en compte plusieurs clients, le programme suivant crée beaucoup de clients (sous la forme de threads) et se connecte au même serveur. Le nombre maximum de threads autorisés est fixé par la constante final int MAX_THREADS.

//: c15:MultiJabberClient.java
// Client destiné à tester MultiJabberServer
// en lançant des clients multiple.
import java.net.*;
import java.io.*;

class JabberClientThread extends Thread {
  private Socket socket;
  private BufferedReader in;
  private PrintWriter out;
  private static int counter = 0;
  private int id = counter++;
  private static int threadcount = 0;
  public static int threadCount() {
    return threadcount;
  }
  public JabberClientThread(InetAddress addr) {
    System.out.println("Making client " + id);
    threadcount++;
    try {
      socket =
        new Socket(addr, MultiJabberServer.PORT);
    } catch(IOException e) {
      System.err.println("Socket failed");
      // Si la création de socket échoue,
      // il n'y a rien à nettoyer.
    }
    try {    
      in =
        new BufferedReader(
          new InputStreamReader(
            socket.getInputStream()));
      // Autoriser l'auto-vidage du tampon:
      out =
        new PrintWriter(
          new BufferedWriter(
            new OutputStreamWriter(
              socket.getOutputStream())), true);
      start();
    } catch(IOException e) {
      // socket doit être fermé sur n'importe quelle
      // erreur autre que celle de son constructeur:
      try {
        socket.close();
      } catch(IOException e2) {
        System.err.println("Socket not closed");
      }
    }
    // Sinon socket doit être fermé par
    // la méthode run() du thread.
  }
  public void run() {
    try {
      for(int i = 0; i < 25; i++) {
        out.println("Client " + id + ": " + i);
        String str = in.readLine();
        System.out.println(str);
      }
      out.println("END");
    } catch(IOException e) {
      System.err.println("IO Exception");
    } finally {
      // Toujours fermer:
      try {
        socket.close();
      } catch(IOException e) {
        System.err.println("Socket not closed");
      }
      threadcount--; // Fin de ce thread
    }
  }
}

public class MultiJabberClient {
  static final int MAX_THREADS = 40;
  public static void main(String[ « args)
      throws IOException, InterruptedException {
    InetAddress addr =
      InetAddress.getByName(null);
    while(true) {
      if(JabberClientThread.threadCount()
         < MAX_THREADS)
        new JabberClientThread(addr);
      Thread.currentThread().sleep(100);
    }
  }
} ///:~

Ce livre a été écrit par Bruce Eckel ( télécharger la version anglaise : Thinking in java )
Ce chapitre a été traduit par Jean-Pierre Vidal, Alban Peignier ( groupe de traduction )
télécharger la version francaise (PDF) | Commandez le livre en version anglaise (amazon) | télécharger la version anglaise
pages : 1 2 3 4 5 6 7 8 9 10 11 12 
Penser en Java 2nde édition - Sommaire |  Préface |  Avant-propos | Chapitre : 1  2  3  4  5  6  7  8  9  10  11  12  13  14  15 |  Annexe : A B C D  | Tables des matières - Thinking in Java