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 

De tous temps, la programmation des machines interconnectées s'est avérée difficile, complexe, et sujette à erreurs.

Le programmeur devait connaître le réseau de façon détaillée, et parfois même son hardware. Il était généralement nécessaire d'avoir une bonne compréhension des différentes couches du protocole réseau, et il existait dans chacune des bibliothèques de réseau un tas de fonctions, bien entendu différentes, concernant la connexion, l'empaquetage et le dépaquetage des blocs d'information, l'émission et la réception de ces blocs, la correction des erreurs et le dialogue. C'était décourageant.

Toutefois, l'idée de base de l'informatique distribuée n'est pas vraiment complexe, et elle est encapsulée de manière très agréable dans les bibliothèques Java. Il faut pouvoir :

  • obtenir quelques informations de cette machine-là et les amener sur cette machine-ci, ou vice versa. Ceci est réalisé au moyen de la programmation réseau de base ;
  • se connecter à une base de données, qui peut résider quelque part sur le réseau. Pour cela, on utilise la Connectivité Bases de Données Java : Java DataBase Connectivity (JDBC), qui est une encapsulation des détails confus, et spécifiques à la plate-forme, du SQL (Structured Query Language - langage structuré d'interrogation de bases de données - utilisé pour de nombreux échanges avec des bases de données) ;
  • fournir des services via un serveur Web. C'est le rôle des servlets Java et des Pages Serveur Java : Java Server Pages (JSP) ;
  • exécuter de manière transparente des méthodes appartenant à des objets Java résidant sur des machines distantes, exactement comme si ces objets résidaient sur la machine locale. Pour cela, on utilise l'Invocation de Méthode Distante de Java : Remote Method Invocation (RMI) ;
  • utiliser du code écrit dans d'autres langages, tournant sous d'autres architectures. C'est l'objet de Common Object Request Broker Architecture (CORBA), qui est directement mis en oeuvre par Java ;
  • séparer les questions relatives à la connectivité de la logique concernant le résultat cherché, et en particulier les connexions aux bases de données incluant la gestion des transactions et la sécurité. C'est le domaine des Enterprise JavaBeans (EJB). Les EJB ne représentent pas réellement une architecture distribuée, mais les applications qui en découlent sont couramment utilisées dans un système client-serveur en réseau ;
  • ajouter et enlever, facilement et dynamiquement, des fonctionnalités provenant d'un réseau considéré comme un système local. C'est ce que propose la fonctionnalité Jini de Java.

Ce chapitre a pour but d'introduire succinctement toutes ces fonctionnalités. Il faut noter que chacune représente un vaste sujet, et pourrait faire l'objet d'un livre à elle toute seule, aussi ce chapitre n'a d'autre but que de vous rendre ces concepts familiers, et en aucun cas de faire de vous un expert (toutefois, vous aurez largement de quoi faire avec l'information que vous trouverez ici à propos de la programmation réseau, des servlets et des JSP).

La programmation réseau

Une des grandes forces de Java est de pouvoir travailler en réseau sans douleur. Les concepteurs de la bibliothèque réseau de java en ont fait quelque chose d'équivalent à la lecture et l'écriture de fichiers, avec la différence que les « fichiers » résident sur une machine distante et que c'est elle qui décide de ce qu'elle doit faire au sujet des informations que vous demandez ou de celles que vous envoyez. Autant que possible, les menus détails du travail en réseau ont été cachés et sont pris en charge par la JVM et l'installation locale de Java. Le modèle de programmation utilisé est celui d'un fichier ; en réalité, la connexion réseau (Une » socket « - littéralement douille, ou prise, NdT) est encapsulée dans des objets stream, ce qui permet d'utiliser les mêmes appels de méthode que pour les autres objets stream. De plus, les fonctionnalités de multithreading de Java viennent à point lorsqu'on doit traiter plusieurs connexions simultanées.

Cette section introduit le support réseau de Java en utilisant des exemples triviaux.

Identifier une machine

Il semble évident que, pour pouvoir appeler une machine depuis une autre, en ayant la certitude d'être connecté à une machine en particulier, il doit exister quelque chose comme un identifiant unique sur le réseau. Les anciens réseaux se contentaient de fournir des noms de machines uniques sur le réseau local. Mais Java travaille sur l'Internet, et cela nécessite un moyen d'identifier chaque machine de manière unique par rapport à toutes les autres dans le monde entier. C'est la raison d'être de l'adresse IP (Internet Protocol - protocole internet, NdT) qui existe sous deux formes :

  1. La forme familière la forme DNS (Domain Name System, Système de Nommage des Domaines). Mon nom de domaine est bruceeckel.com, et si j'avais dans mon domaine un ordinateur nommé Opus, son nom de domaine serait Opus.bruceeckel.com. C'est exactement le type de nom que vous utilisez lorsque vous envoyez du courrier à quelqu'un, et il est souvent associé à une adresse World Wide Web.
  2. Sinon, on peut utiliser la forme du quadruplet pointé, c'est à dire quatre nombre séparés par des points, par exemple 123.255.28.120.

Dans les deux cas, l'adresse IP est représentée en interne comme un nombre sur 32 bits [72] (et donc chaque nombre du quadruplet ne peut excéder 255), et il existe un objet spécial Java pour représenter ce nombre dans l'une des formes décrites ci-dessus en utilisant la méthode de la bibliothèque java.net : static InetAddress.getByName( ). Le résultat est un objet du type InetAddress qu'on peut utiliser pour construire une socket, « comme on le verra plus loin.

Pour montrer un exemple simple d'utilisation de InetAddress.getByName( ), considérons ce qui se passe lorsque vous êtes en communication avec un Fournisseur d'Accès Internet (FAI) - Internet Service Provider (ISP). Chaque fois que vous vous connectez, il vous assigne une adresse IP temporaire. Tant que vous êtes connecté, votre adresse IP est aussi valide que n'importe quelle autre adresse IP sur Internet. Si quelqu'un se connecte à votre machine au moyen de votre adresse IP, alors il peut se connecter à un serveur Web ou FTP qui tournerait sur votre machine. Bien entendu, il faudrait qu'il connaisse votre adresse IP, mais puisqu'une nouvelle vous est assignée à chaque connexion, comment pourrait-il faire ?

Le programme qui suit utilise InetAddress.getByName( ) pour récupérer votre adresse IP. Pour qu'il fonctionne, vous devez connaître le nom de votre ordinateur. Sous Windows 95/98, aller à Paramètres, « Panneau de Contrôle, « Réseau, « et sélectionnez l'onglet » Identification. » Le Nom d'ordinateur est le nom à utiliser sur la ligne de commande.

//: c15:WhoAmI.java
// Affiche votre adresse de réseau lorsque
// vous êtes connectés à Internet.
import java.net.*;

public class WhoAmI {
  public static void main(String[ « args)
      throws Exception {
    if(args.length != 1) {
      System.err.println(
        "Usage: WhoAmI MachineName");
      System.exit(1);
    }
    InetAddress a =
      InetAddress.getByName(args[0]);
    System.out.println(a);
  }
} ///:~

Supposons que ma machine ait pour nom » peppy. « Une fois connecté au FAI, je lance le programme :

java WhoAmI peppy

En retour, j'obtiens un message tel que celui-ci (bien entendu, l'adresse est différente à chaque connexion) :

peppy/199.190.87.75

Si je donne cette adresse à un ami et qu'il existe un serveur Web tournant sur mon ordinateur, il peut s'y connecter en allant à l'URL http://199.190.87.75 (du moins tant que je reste connecté). Ceci est parfois une manière commode de distribuer de l'information à d'autres personnes, ou encore de tester la configuration d'un site avant de l'installer sur un « vrai » serveur.

Serveurs et clients

La finalité d'un réseau est de permettre à deux machines de se connecter et ainsi de se « parler ». Une fois que les deux machines se sont trouvées l'une l'autre, elles peuvent entamer une agréable conversation bi-directionnelle. Mais qu'est-ce que chacune peut donc rechercher chez l'autre ? Et d'abord, comment trouvent-elles l'autre ? Tout se passe à peu près comme lorsqu'on est perdu dans un parc d'attractions : une des machines doit attendre l'appel de l'autre sans bouger : « Hé, où êtes-vous ? »]

La machine qui attend est appelée serveur, celle qui cherche client. Cette distinction n'est importante que tant que le client cherche à se connecter au serveur. Une fois les machines connectées, la communication se traduit par un processus bi-directionnel et on n'a plus à se préoccuper de savoir qui est le serveur et qui est le client.

Le rôle du serveur est donc d'être en attente d'une demande de connexion, ceci est réalisé par l'objet serveur qu'on crée dans ce but. Le rôle du client est de tenter d'établir une connexion avec un serveur, ceci est réalisé avec un objet client qu'on crée pour cela. Une fois la connexion établie, aussi bien du côté serveur que du côté client, elle se transforme magiquement en un objet flux d'E/S, et dès lors on peut traiter cette connexion comme une lecture ou une écriture dans un fichier. Ainsi, une fois la connexion établie, il ne reste qu'à utiliser les commandes d'E/S familières vues au Chapitre 11. C'est un des aspects agréables des fonctions de Java en réseau.

Tester les programmes hors réseau

Pour toutes sortes de raisons, il se peut qu'on ne dispose pas de machines client et serveur, pas plus que d'un réseau pour tester nos programmes. Par exemple lorsqu'on réalise des exercices dans une situation d'apprentissage, ou bien qu'on écrive des programmes qui ne sont pas encore suffisamment stables pour être mis sur le réseau. Les créateurs du protocole internet (IP) ont bien appréhendé cette question, et ont créé une adresse spéciale appelée localhost qui représente une adresse IP en « boucle locale » permettant d'effectuer des tests en se passant de la présence d'un réseau. Voyez ci-dessous la manière générique de réaliser cette adresse en Java :

InetAddress addr = InetAddress.getByName(null);

En utilisant getByName( ) avec un argument null, cette fonction utilise par défaut localhost. InetAddress est utilisée pour désigner une machine particulière, et on doit l'initialiser avant d'aller plus loin. On ne peut pas manipuler le contenu d'une InetAddress (mais il est possible de l'imprimer, comme on va le voir dans l'exemple suivant). L'unique manière de créer une InetAddress passe par l'une des méthodes membre static surchargées de cette classe : getByName( ) (habituellement utilisée), getAllByName( ) ou getLocalHost( ).

Une autre manière de réaliser l'adresse de la boucle locale est d'utiliser la chaîne localhost:

InetAddress.getByName("localhost");

(en supposant que localhost « est décrit dans la table des hôtes de la machine), ou encore en se servant de la forme « quadruplet pointé » pour désigner l'adresse IP réservée à la boucle locale :

InetAddress.getByName("127.0.0.1");

Les trois formes aboutissent au même résultat.

Les Ports  : un emplacement unique dans la machine

Une adresse IP n'est pas suffisante pour identifier un serveur, en effet plusieurs serveurs peuvent coexister sur une même machine. Chaque machine IP contient aussi des ports, et lorsqu'on installe un client ou un serveur il est nécessaire de choisir un port convenant aussi bien à la connexion du client qu'à celle du serveur ; si vous donnez rendez-vous à quelqu'un, l'adresse IP représentera le quartier et le port sera le nom du bar.

Le port n'est pas un emplacement physique dans la machine, mais une abstraction de programmation (surtout pour des raisons de comptabilité). Le programme client sait comment se connecter à la machine via son adresse IP, mais comment se connectera-t-il au service désiré (potentiellement, l'un des nombreux services de cette machine) ? C'est pourquoi les numéros de port représentent un deuxième niveau d'adressage. L'idée sous-jacente est que si on s'adresse à un port particulier, en fait on effectue une demande pour le service associé à ce numéro de port. Un exemple simple de service est l'heure du jour. Typiquement, chaque service est associé à un numéro de port unique sur une machine serveur donnée. Il est de la responsabilité du client de connaître à l'avance quel est le numéro de port associé à un service donné.

Les services système réservent les numéros de ports 1 à 1024, il ne faut donc pas les utiliser, pas davantage que d'autres numéros de ports dont on saurait qu'ils sont utilisés. Le premier nombre choisi pour les exemples de ce livre est le port 8080 (en souvenir de la vénérable vieille puce 8 bits Intel 8080 de mon premier ordinateur, une machine CP/M).

Les sockets

Une socket est une abstraction de programmation représentant les extrémités d'une connexion entre deux machines. Pour chaque connexion donnée, il existe une socket sur chaque machine, on peut imaginer un câble virtuel reliant les deux machines, chaque extrémité enfichée dans une socket. Bien entendu, le hardware sous-jacent ainsi que la manière dont les machines sont connectées ne nous intéressent pas. L'essentiel de l'abstraction est qu'on n'a pas à connaître plus que ce qu'il est nécessaire.

En Java, on crée un objet Socket pour établir une connexion vers une autre machine, puis on crée un InputStream et un OutputStream (ou, avec les convertisseurs appropriés, un Reader et un Writer) à partir de ce Socket, afin de traiter la connexion en tant qu'objet flux d'E/S. Il existe deux classes Socket basées sur les flux : ServerSocket utilisé par un serveur pour [écouter « les connexions entrantes et Socket utilisé par un client afin d'initialiser une connexion. Lorsqu'un client réalise une connexion socket, ServerSocket retourne (via la méthode accept( )) un Socket correspondant permettant les communications du côté serveur. À ce moment-là, on a réellement établi une connexion Socket à Socket et on peut traiter les deux extrémités de la même manière, car elles sont alors identiques. On utilise alors les méthodes getInputStream( ) et getOutputStream( ) pour réaliser les objets InputStream et OutputStream correspondants à partir de chaque Socket. À leur tour ils peuvent être encapsulés dans des classes buffers ou de formatage tout comme n'importe quel objet flux décrit au Chapitre 11.

La construction du mot ServerSocket est un exemple supplémentaire de la confusion du plan de nommage des bibliothèques Java. ServerSocket aurait dû s'appeler «  ServerConnector »ou bien n'importe quoi qui n'utilise pas le mot Socket. Il faut aussi penser que ServerSocket et Socket héritent tous deux de la même classe de base. Naturellement, les deux classes ont plusieurs méthodes communes, mais pas suffisamment pour qu'elles aient une même classe de base. En fait, le rôle de ServerSocketest d'attendre qu'une autre machine se connecte, puis à ce moment-là de renvoyer un Socket réel. C'est pourquoi ServerSocket semble mal-nommé, puisque son rôle n'est pas d'être une socket mais plus exactement de créer un objet Socketlorsque quelqu'un se connecte.

Cependant, un ServerSocket crée physiquement un » serveur « ou, si l'on préfère, une « prise » à l'écoute sur la machine hôte. Cette « prise » est à l'écoute des connexions entrantes et renvoie une « prise » établie (les points terminaux et distants sont définis) via la méthode accept( ). La confusion vient du fait que ces deux « prises » (celle qui écoute et celle qui représente la communication établie) sont associées à la même « prise » serveur. La « prise » qui écoute accepte uniquement les demandes de nouvelles connexions, jamais les paquets de données. Ainsi, même si ServerSocket n'a pas beaucoup de sens en programmation, il en a physiquement.]

Lorsqu'on crée un ServerSocket, on ne lui assigne qu'un numéro de port. Il n'est pas nécessaire de lui assigner une adresse IP parce qu'il réside toujours sur la machine qu'il représente. En revanche, lorsqu'on crée un Socket, il faut lui fournir l'adresse IP et le numéro de port sur lequel on essaie de se connecter (toutefois, le Socket résultant de la méthode ServerSocket.accept( ) contient toujours cette information).

Un serveur et un client vraiment simples

Cet exemple montre l'utilisation minimale d'un serveur et d'un client utilisant des sockets. Le serveur se contente d'attendre une demande de connexion, puis se sert du Socket résultant de cette connexion pour créer un InputStream et un OutputStream. Ces derniers sont convertis en Reader et Writer, puis encapsulés dans un BufferedReader et un PrintWriter. À partir de là , tout ce que lit le BufferedReader est renvoyé en écho au PrintWriter jusqu'à ce qu'il reconnaisse la ligne » END, « et dans ce cas il clôt la connexion.

Le client établit une connexion avec le serveur, puis crée un OutputStreamet réalise le même type d'encapsulation que le serveur. Les lignes de texte sont envoyées vers le PrintWriterrésultant. Le client crée également un InputStream (ici aussi, avec la conversion et l'encapsulation appropriées) afin d'écouter le serveur (c'est à dire, dans ce cas, simplement les mots renvoyés en écho).

Le serveur ainsi que le client utilisent le même numéro de port, et le client se sert de l'adresse de boucle locale pour se connecter au serveur sur la même machine, ce qui évite de tester en grandeur réelle sur un réseau (pour certaines configurations, on peut être amené à se connecter physiquement à un réseau afin que le programme fonctionne, même si on ne communique pas sur ce réseau.)

Voici le serveur :

//: c15:JabberServer.java
// Serveur simplifié dont le rôle se limite à        
// renvoyer en écho tout ce que le client envoie.
import java.io.*;
import java.net.*;

public class JabberServer {  
  // Choisir un port hors de la plage 1-1024:
  public static final int PORT = 8080;
  public static void main(String[ « args)
      throws IOException {
    ServerSocket s = new ServerSocket(PORT);
    System.out.println("Started: " + s);
    try {
      // Le programme stoppe ici et attend
      // une demande de connexion:
      Socket socket = s.accept();
      try {
        System.out.println(
          "Connection accepted: "+ socket);
        BufferedReader in =
          new BufferedReader(
            new InputStreamReader(
              socket.getInputStream()));
        // Le tampon de sortie est vidé
        // automatiquement par PrintWriter:
        PrintWriter out =
          new PrintWriter(
            new BufferedWriter(
              new OutputStreamWriter(
                socket.getOutputStream())),true);
        while (true) {  
          String str = in.readLine();
          if (str.equals("END")) break;
          System.out.println("Echoing: " + str);
          out.println(str);
        }
      // Toujours fermer les deux sockets...
      } finally {
        System.out.println("closing...");
        socket.close();
      }
    } finally {
      s.close();
    }
  }
} ///:~

On remarque que ServerSocket ne nécessite qu'un numéro de port, et non une adresse IP (puisqu'il tourne sur cette machine !). Lorsqu'on appelle accept( ) la méthode bloque jusqu'à ce qu'un client tente de se connecter. Cela signifie qu'elle est en attente d'une demande de connexion, mais elle ne bloque pas les autres processus (voir le Chapitre 14). Une fois la connexion établie, accept( ) renvoie un objet Socket qui représente cette connexion.

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