Java corba et rmi documentation de cours de base
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:
- À la base, Java est entièrement basé sur le concept de l'orienté-objet. Les concepts de réutilisabilité (héritage), de protection de l'information (encapsulation) et de l'accès dynamique (polymorphisme) sont directement utilisables en Java. De plus, Java ne permet pas l'héritage multiple.
- Il permet le transfert transparent d’objets d'une application à une autre et ces objets ne subissent pas de changement lors des transferts (l'intégrité des objets transférés est respectée).
- Son utilisation est sécuritaire (les classes de gestion de la sécurité sont disponibles et le programmeur peut, s'il le désire, programmer les siennes).
- Contrairement à un Framework comme MFC, il est relativement facile et rapide d'écrire des applications Java et d'utiliser les classes RMI.
- Il permet un accès facile et simplifié aux systèmes déjà en place (utilisant des langages comme Java, C, C++, ...).
- Il élimine le coulage de mémoire par l'utilisation du Garbage Collection.
- Il permet l'exécution en parallèle d'applications (locale ou distante).
- Le transfert de comportements (objets) d'une application à une autre permet d'exploiter toute la force des Design Patterns (qui s'appuient très souvent sur des comportements).
- Depuis JDK 1.2, Java supporte l’activation d’objets distants.
- Depuis JDK 1.3, Java supporte les spécifications IIOP (Internet Inter-ORB Protocol) du Object management Group (OMG). Cette nouvelle fonctionnalité de Java (maintenant appelée RMI-IIOP) permet l'intégration de Java et de CORBA.
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 :
- Définir une interface publique qui hérite de java.rmi.Remote.
- Dans cette interface, déclarer les méthodes qui seront vues et utilisées par les clients distants. Chacune de ces méthodes doit définir java.rmi. RemoteException.
- Créer l’implémentation du serveur qui hérite de Java.rmi.UnicastRemoteObject et qui implémente l’interface définie en 1.
- Implémenter les méthodes définies dans l'interface (et possiblement d’autres) dans l’implémentation du serveur.
- La méthode main() du serveur exporte les méthodes en liant l’objet serveur au gestionnaire de registres (préalablement lancé par l’application, ou par l’usager avec l’utilitaire rmiregistry)
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.
- Un objet local est implémenté localement avec une classe Java alors qu’un objet distant est rendu publique avec une interface héritant de java.rmi.Remote. Son implémentation est réalisée par la classe Java qui implémente l’objet distant (le serveur).
- Pour un objet local, une nouvelle instance d'un objet est réalisée localement par l'opérateur new alors qu’une nouvelle instance d'un objet distant est réalisée sur l'ordinateur hôte par l'opérateur new. Un client ne peut directement créer un nouvel objet distant (à moins d'utiliser Remote Object Activation).
- Un objet local est accédé directement à l'aide d'une variable référence à l'objet alors qu’un objet distant est accédé à l'aide d'une variable référence à l'objet qui pointe vers le Stub.
- Dans une application non-distribuée, la référence à un objet pointe directement vers l'objet dans le Heap alors que, pour un objet distant, une référence distante est un pointeur à un objet Stub dans le Heap local. Le Stub contient l'information qui lui permet de se connecter à un objet distant.
- Dans une application non-distribuée, un objet est considéré vivant s'il y a au moins une référence qui lui est rattachée. Dans un environnement distribué, il peut survenir des problèmes détruisant une connexion par exemple. Un objet est considéré comme ayant une référence distante vivante s'il a été accédé depuis un certain temps (The Least Period).
- Quand une référence locale à un objet a été posée à nulle, cet objet est un candidat pour le Garbage Collection. Le Garbage Collector distant fonctionne avec le Garbage Collector local. S'il n'y a pas de référence distante et que toutes les références aux objets distants ont été posées à nulle, alors les objets distants deviennent des candidats pour le Garbage Collector.
- Si un objet local implémente la méthode Finalize(), cette méthode est appelée avant que le Garbage Collection ne fasse son travail. Si un objet distant implémente la méthode Unreferenced(), cette méthode de l'objet non référencé est appelée quand toutes les références distantes sont nulles.
- Le compilateur Java force un programme à considérer toutes les exceptions. RMI force les programmes à considérer toutes les exceptions de type RemoteException qui peuvent survenir.
- Comme pour les interfaces locales, les interfaces distantes peuvent contenir des champs statiques. Un initialiseur est lancé pour chaque machine virtuelle qui charge l'interface distante, créant une nouvelle variable statique. Une copie séparée de cette variable existe dans chaque machine virtuelle.
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:
- Le client doit d'abord obtenir une référence au serveur. Il utilise donc la méthode Naming.lookup().
- La méthode lookup() utilise le Stub pour faire un appel distant au rmiregistry.
- Le rmiregistry retourne la référence de l'objet distant demandé à la couche transport (côté client). Chaque référence distante contient le nom du serveur et le port que les clients peuvent utiliser pour se connecter à la machine virtuelle contenant l'objet distant.
- Une fois la référence du serveur obtenue, la couche transport (côté client) utilise le nom et le numéro de port pour ouvrir une connexion de type socket au serveur.
- La référence obtenue est transférée à la couche application (côté client).
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.