Java corba et rmi documentation de cours de base
2.0 Java RMI (Remote Method Invocation)
Java RMI propose une méthode relativement simple et puissante pour distribuer des applications sur des machines virtuelles différentes (une machine virtuelle étant une instance de l’interpréteur de Bytecode).
C’est à partir de JDK 1.1 que le concept original de machine virtuelle de Java a été étendu à un nouveau modèle dans lequel les classes et les objets peuvent fonctionner dans un environnement réseau (les machines virtuelles peuvent être localisées à la fois sur le même ordinateur ou encore sur un réseau).
Toutes les fonctionnalités de Java sont disponibles au programmeur RMI (entre autres celles de la sécurité, du processus de Serialization des données, de la connexion aux systèmes déjà existants par JNI et aux bases de données par JDBC). Les principaux avantages à utiliser RMI (et Java) sont les suivants:
Les architectes de Sun ont réussi à rendre transparente l’utilisation d’objets distants. Après avoir localisé les objets appropriés, le programmeur utilise les méthodes de ces objets comme si ces dernières faisaient partie de son application. Tout le travail de codage, de décodage, de vérification et de transfert est effectué par RMI.
2.1 Vue globale de Java RMI
Toutes les applications distribuées utilisant RMI sont faites de la même façon. Elles sont divisées en au moins deux modules; le client et le serveur. Le serveur rend disponibles des objets aux clients en les enregistrant au gestionnaire de registres rmiregistry.
Du point de vue du serveur, les méthodes d’un objet sont rendues disponibles pour les clients distants grâce aux opérations suivantes :
Le client, ayant un lien local ou réseau avec l’ordinateur du serveur, contacte le gestionnaire de registres pour connaître l’emplacement exact de l’objet distant cherché (en spécifiant l’adresse IP ou DNS du serveur et le nom de l’objet exporté). Une fois l’adresse connue, le client n’a plus à contacter ce service. Il effectue les transactions directement avec le serveur en utilisant l'adresse obtenue.
Lors du passage d’objets d’une machine virtuelle à une autre, Java RMI traite les objets distants d’une façon différente des objets non-distants. Les objets distants sont accédé par le biais d'un Stub, lequel agit comme un représentant de l’objet distant (un Proxy) auprès du client. Un appel à une méthode distante passe par le Stub et ce dernier est responsable de transmettre l’appel à l’objet distant. Un Stub implémente la même interface que l’objet distant. Ceci explique pourquoi un client ne peut voir que les méthodes déclarées dans l’interface de l’objet distant.
Il existe des nuances importantes entre un objet local et un objet distant (vue du client), en voici une liste non-exhaustive.
En Java, le chargeur de classe est responsable de charger dynamiquement en mémoire les classes nécessaires à une application. Différent types de Class Loader existent et Java RMI utilise la classe RMIClassLoader. RMIClassLoader a pour fonction de charger le Stub et le Skeleton utilisés par le système RMI ainsi que d'autres classes utilitaires secondaires.
En général, RMIClassLoader tentera de charger les classes à partir du système local en utilisant la variable d'environnement CLASSPATH. Si les classes ne peuvent être chargées localement, RMIClassLoader tentera d'extraire le URL de l'objet Marshalled (Serialized, voir plus loin) et de l'utiliser comme Codebase pour localiser et télécharger les classes cherchées.
2.2 Le passage de paramètres
Il existe des nuances importantes concernant le passage de paramètres dans une application utilisant une seule machine virtuelle et une application distribuée utilisant plusieurs machines virtuelles. Ces différences sont expliquées dans cette section.
Pour un machine virtuelle Java simple:
En général, Java passe les paramètres par valeur. Quand un paramètre est passé à une méthode, Java fait une copie de la valeur du paramètre, place cette copie sur le Stack et exécute la méthode. Quand la méthode a besoin du paramètre, un accès est fait au Stack pour prendre la valeur du paramètre. Les valeurs retournées par les méthodes sont copiées de la même façon.
Pour les types simples (boolean, byte, short, int, long, char, float ou double) le processus est simple. Par contre, le processus de passage d’objets comme paramètres est plus complexe. En Java, les objets résident dans le Heap et sont accédé par des variables références. L’exemple suivant montre le processus:
String laString = "La valeur de la String" ; System.out.println(laString) ;
Dans la deuxième ligne de code, une copie de la valeur de la référence à l'objet laString est placée sur le Stack. La méthode utilise la copie de cette référence pour accéder à l’objet. Il est important de mentionner que la méthode recevant la copie de la référence à un objet comme paramètre a un impact définitif sur l'objet. Si par exemple, la méthode modifie un des paramètres de l'objet en question, la modification est tout de suite effective et définitive (même à la sortie de la méthode).
Pour un machine virtuelle Java RMI:
Quand un paramètre de type simple est passé à une méthode d’un objet distant, RMI passe ce paramètre par valeur en effectuant une copie du type simple, et en transmettant cette copie à la méthode distante. Si la méthode retourne un type simple, il est aussi est retourné par valeur.
Lorsqu’un objet est passé à une méthode distante, le processus est différent de celui décrit plus haut (pour une machine virtuelle simple). RMI envoi une copie de l’objet lui-même (pas une copie de sa référence). L’objet est passé par valeur, ce n’est pas sa référence qui est transmise. De la même façon, le retour d’objet par une méthode transmet une copie de l’objet lui-même.
Le passage d’objets entre machines virtuelles pose le problème des objets complexes (héritage, agrégation, composition, association, ...). Pour que la machine virtuelle qui reçoit l’objet soit en mesure de reconstituer cet objet dans toute sa complexité, il faut que toute la structure de l’objet soit transférée. Le programmeur doit donc porter attention aux objets qu’il désire transférer. De grosses structures peuvent engorger exagérément le CPU des ordinateurs, ainsi que la largeur de bande disponible.
RMI règle le problème du transfert des objets complexes en utilisant le Object Serialization. Ce processus transforme l’objet (et ses références) en une suite linéaire (nommé Stream) facilement transférable entre machines virtuelles. Les objets ainsi linéarisés (Marshalled) sont ensuite délinéarisés (Unmarshalled) sur réception.
RMI introduit un autre type de paramètre qui est la référence à un objet distant (permettant les Callback par exemple). L’exemple suivant illustre les Callbacks (l'exemple du chapitre 5 contient également un Callback) :
// Deux objets dont les classes ont été définies ailleurs GestionnaireBanque gestionnaireBanque ; Compte referenceAuCompte ;
// L’adresse RMI du service
String adresseRMIService = "rmi://Banque/ServiceGestionBanque" ;
Try
{
// Trouver l’adresse de l’objet distant
gestionnaireBanque = (GestionnaireBanque)
Naming.Lookup(adresseRMIService) ;
// Obtenir la reference
referenceAuCompte = gestionnaireBanque.getCompte("Celine Dion") ;
// MMmmm, voir le contenu du compte (pour ensuite transférer les $)
System.out.Println(referenceAuCompte.getLeContenu() ) ;
}
catch (RemoteException e)
{
System.out.println("Erreur : RemoteException dans le programme XXX :
" + e ) ;
System.exit(1) ;
}
La référence au compte de Céline Dion (l’objet) permet d’invoquer les méthodes de cet objet et de consulter le contenu de celui-ci.
D’une machine virtuelle à l’autre, les valeurs sont passées de façon à ne pas dépendre des plates-formes.
2.3 La communication entre les applications distantes
La communication RMI est essentiellement réalisée en utilisant 4 couches inter-reliées; la couche application, la couche Proxy ou Stub, la couche Remote Reference et la couche Transport (voir la figure 1). L'utilisation d'une architecture divisée en plusieurs couches permet la modification d'une de ces couches sans affecter le reste du système. Par exemple, la couche de transport pourrait être remplacée par une autre couche utilisant le protocole UDP et ce, sans affecter les autres couches (TCP de TCP/IP est habituellement utilisé) .
2.3.1 La couche application
La première couche est constituée par les modules client et serveur. On y retrouve les appels de haut niveau pour exporter les objets et y accéder.
L'architecture RMI est basée sur le fait que le comportement exporté (les méthodes) est défini dans l'interface (définition des services). L'interface ne contient pas de code exécutable et elle hérite de java.rmi.Remote. L'interface Remote est essentiellement utilisée pour déclarer aux éventuels clients l'accessibilité à distance d'un objet. Son implémentation est réalisée dans une classe du module serveur.
Le client et le serveur sont exécutés en utilisant deux machines virtuelles différentes.
Figure 1. Couches de communication associées à Java RMI (partiellement tidée de [2]).
2.3.2 La couche Proxy ou Stub
Les deux parties de cette couche (le Stub et le Skeleton) sont générées lors de la compilation de l'implémentation et de l'interface. L'utilitaire rmic est utilisé pour effectuer la compilation (voir la section 4.4). Dans la version 1.2 de JDK, le compilateur ne génère qu'un Stub, le Skeleton n'est plus nécessaire.
Comme il a été mentionné plus haut, le Stub est le lien entre le client et l'objet distant. Cette couche intercepte donc les appels faits aux méthodes des objets distants par le client. Elle les redirige ensuite vers le service RMI approprié.
Du point de vue du client, l'objet distant est d'abord localisé, créé et il est ensuite retypé (Casted) du nom de l'interface. Lorsque le client effectue un appel à une des méthodes de l'objet distant, cet appel est transféré au Stub. Ce dernier effectue ensuite le travail de base (Marshalling) pour envoyer l'information vers le serveur sous la forme de Streams. Rendue au serveur (Skeleton), l'information subit la transformation inverse (Unmarshalling) et la méthode est appelée avec les bons paramètres. Si la méthode produit un retour, cette information subit le chemin inverse vers le client. Pendant la connexion entre un serveur et un client, Java remplace les objets distants par leur Stub, peu importe leur localisation dans la structure.
Le Skeleton (ou le serveur lui-même pour JDK 1.2) est localisé du côté serveur et fonctionne directement avec l'implémentation des méthodes exportées. Ses fonctions sont essentiellement les mêmes que celles du Stub, en plus de recevoir les appels de ce dernier.
2.3.3 La couche de références distantes (Remote Reference Layer)
Cette couche définit et supporte la sémantique des invocations de la connexion RMI. Elle interprète et gère les références des objets distants. La couche de références distantes est en fait une couche abstraite entre le Stub, le Skeleton et les protocoles de communication (qui sont gérés par la couche de transport). Les interactions entre ces deux couches seront toujours effectuées comme si la connexion était de type connecté (avec un Stream), mais le transport de l'information peut quand même être réalisé en utilisant un protocole non-connecté (UDP de TCP/IP).
Dans la version 1.1 de JDK, cette couche connecte les clients aux objets distants par une connexion de type un à un (Unicast). Avant qu'un client ne puisse utiliser les services d'un objet distant, le service doit être instantié sur le serveur et exporté au système RMI.
Dans JDK 1.2, cette couche a subi des améliorations visant à supporter des objets distants dormants (Remote Objet Activation). Quand un appel à une méthode d'un objet distant dormant est effectué par le client, RMI détermine l'état du service. S'il est effectivement dormant, RMI instantit l'objet et restore son état.
D'autres types de connexion sont également possibles. Par exemple, en mode Multicast un simple Stub pourrait simultanément envoyer une requête à plusieurs implémentations distantes d'un objet et accepter la première réponse.
2.3.4 La couche transport (Transport Layer)
C'est la couche de transport qui effectue la connexion entre les machines virtuelles et ce, même en présence d'obstacles réseaux (murs de feu). Elle est responsable de la mise en contact des machines virtuelles, de la gestion des connexions, de la vérification des connexions mortes et de la réception de demandes de connexion distantes.
Toutes les connexions sont des Streams et utilisent le protocole TCP de TCP/IP. Il est également possible d'utiliser le protocole UDP à la place de TCP. Les connexions sont basées sur les adresses IP et les numéros de port des ordinateurs communiquant. Un nom DNS peut être utilisé à la place de l'adresse IP.
En parallèle avec TCP/IP, RMI utilise un protocole propriétaire de bas niveau appelé Java Remote Method Protocol (JRMP). Deux versions de JRMP existent. La première est associée à la version 1.1 de JDK. Elle oblige l'utilisation d'un Skeleton du côté serveur. La deuxième version est apparue avec la version 1.2 de JDK et élimine l'utilisation du Skeleton. Le compilateur rmic peut générer les classes de la couche Proxy selon les deux versions (avec le paramètre -v1.1 ou -v1.2).
Comme il a été mentionné plus haut, l'architecture en couches du processus de communication permet le remplacement du protocole de bas niveau (de la couche de transport) par d'autres alternatives. Il est également possible de modifier cette couche afin de permettre le transport de Streams encryptés, l'intégration de nouveaux algorithmes de sécurité et l'optimisation de performance. Ces changements n'auront aucun impact sur les couches supérieures.
Même si deux machines virtuelles fonctionnent sur le même ordinateur, la connexion RMI doit passer par le service TCP/IP du système d'opération. La couche de transport permet donc les connexions RMI locales (sur le même ordinateur) ou réseau.
2.4 Le problème des murs de feu (Fire Walls)
Dans le domaine des applications de type client/serveur, les murs de feu posent souvent un problème aux concepteurs de logiciels. Typiquement, ces murs bloquent tout le trafic réseau sauf celui associé à certains ports bien identifiés (comme le port 80 par exemple).
Comme la couche de transport de RMI utilise des sockets (ces ports sont bloqués par les murs de feu) pour faciliter la communication, le JRMP (Java Remote Method Protocol) se trouve bloqué par les murs de feu. Les designers de RMI ont anticipé ce problème et ont développé une solution à l'intérieur de la couche de transport de RMI. Pour traverser les murs de feu, RMI encapsule les appels aux objets distants à l'intérieur de requêtes HTTP POST.
Cas d'un client derrière un mur de feu:
Quand la couche de transport tente d'établir une communication avec le serveur et qu'un mur de feu empêche la connexion, RMI relance la tentative de connexion en encapsulant l'appel (JRMP) dans une requête HTTP POST. L'en-tête de la requête POST prend alors la forme suivante:
…
Comme la plupart des murs de feu reconnaissent le protocole HTTP, le serveur Proxy est habituellement capable de transférer l'appel à l'extérieur du mur et sur le bon port (celui que le serveur écoute).
Cas du client et du serveur derrière deux murs de feu:
Dans ce cas, le mur de feu empêche la réception des appels, la couche de transport RMI utilise une autre méthode. Elle place l'appel (JRMP) dans des Packets HTTP et les transfère au port 80 du serveur. L'en-tête de la requête POST prend alors la forme suivante:
Ceci cause l'exécution d'un script CGI, lequel invoque la machine virtuelle Java, décode les Packets et transfère l'appel au serveur sur le bon port. Le retour d'information s'effectue de la même manière.
Il est évident que des dégradations de performances accompagnent bien souvent ces deux solutions.
2.5 Le gestionnaire de registres dans RMI (Naming) et la connexion client
Une étape importante permettant la réalisation de la communication entre le client et le serveur est le processus d'identification des objets distants sur le réseau (Naming). C'est en utilisant les API de RMI ( Naming.lookup() ) que le client obtient l'adresse complète de la localisation de l'objet distant et qu'il arrive à se connecter au serveur et ainsi, à invoquer les méthodes des objets distants.
RMI utilise le gestionnaire de registres (côté serveur) qui sert en quelque sorte de bottin téléphonique tant pour le serveur (pour s'enregistrer) que pour le client (pour y trouver une adresse de l'objet cherché). Chaque ordinateur hôte, offrant un service local ou distant d'objets et qui accepte les requêtes des clients pour ces objets, doit avoir un gestionnaire de registres qui fonctionne (par défaut sur le port 1099).
Le gestionnaire de registres peut être lancé par l'utilisateur à l'aide de rmiregistry. Les API de RMI permettent de plus de lancer ce service à même l'application serveur (dans l'application du chapitre 5 de ce document, le serveur lance lui-même le gestionnaire de registres).
Une fois le service lancé, le serveur peut associer les objets qu'il exporte à un nom public, reconnaissable par les éventuels clients. L'application serveur est ensuite placée en attente de clients. Normalement, une application serveur, pouvant être connectée avec plusieurs clients simultanément, traite ces derniers dans des Threads différents.
Comme il a été mentionné plus haut, le client accède le gestionnaire de registres (côté serveur) en utilisant la méthode lookup() de la classe Naming pour localiser l'objet cherché. Il utilise un URL (Uniform Resource Locator) de la forme:
rmi://[:/]
où le nom de l'ordinateur hôte doit être reconnu dans un réseau local ou par un service de nom de domaine (DNS) sur Internet. Le numéro du port doit être spécifié seulement si le service de registes côté serveur utilise un autre port que le 1099.
Les étapes à la connexion du client sont les suivantes:
2.6 La hiérarchie des classes et des interfaces en Java RMI
La figure 2 illustre la hiérarchie et les dépendances d'un objet exportable. La définition des différentes composantes de cette figure est la suivante: La classe java.lang.Object est la classe de base à tous les objets en Java. La classe java.rmi.server.RemoteObject est une interface héritant de Object et de l'interface java.rmi.server.Remote. La classe java.rmi.server. RemoteServer hérite de RemoteObject.
Dans l'interface qui définit les méthodes distantes, le programmeur n'utilise (par héritage) que l'interface Remote. Dans l'implémentation de l'interface, il n'utilise (par héritage) que la classe java.rmi.server.UnicastRemoteObject (et possiblement Activable) et n'implémente que l'interface MonInterfaceDistante.
Figure 2. Hiérarchie des objets exportés (partiellement tiré de [2]).
Le lecteur trouvera une brève description des interfaces, des classes et des Exceptions de Java RMI (JDK 1.2) en annexe 1 de ce document.