Corrigé - Coucou, c`est en construction

publicité
IN328 : RMI - Corrigé
SI
http://personnel.supaero.fr/garion-christophe/IN328
Ce TP va vous permettre de manipuler RMI, l’API d’appel distant fournie par Sun.
1
Contenu
Ce corrigé succinct contient l’essentiel des explications nécessaires pour réaliser le TP sur RMI. Les
sources des classes sont disponibles sur le site. Vous trouverez un fichier de construction build.xml pour
Ant qui permet de construire et de lancer les applications facilement. Il faudra bien sûr personnaliser les
variables que j’ai utilisées (chemin d’accès, machines utilisées etc.).
Les exécutables que j’ai créés prennent tous en paramètre le nom d’une ou deux machines suivant les
questions (serveur RMI, serveur de fichiers).
J’ai choisi d’avoir quatres répertoires distincts contenant le bytecode : un pour le client, un pour le
serveur, un pour le registre et un pour le serveur de fichiers quand on l’utilise. Pour chaque question du
TP, j’ai créé un paquetage spécifique.
Je n’ai pas utilisé Ant pour les « exécutions » du serveur, car on « forke » la machine virtuelle et on
n’a plus l’affichage.
Vous remarquerez également que le fichier Ant est construit maladroitement : je compile les interfaces
distantes et je les copie dans le bon répertoire pour le client. Dans un vrai projet, il vaut mieux créer
toutes les interfaces distantes, les mettre dans un JAR et distribuer ce JAR.
Enfin, vous trouverez une archive contenant toutes les politiques de sécurité utilisées. Par défaut, j’ai
utilisé clervoy comme machine sur laquelle tourne le registre et guerin comme machine sur laquelle
tourne le serveur de fichiers. Vous pouvez avoir quelques problèmes si vous utilisez localhost, dans ce
cas, mettez l’adresse de la machine à la place.
2
Création d’un objet distant et appel d’une méthode
Nous allons dans un premier temps créer un objet qui fournit des services distants. Un client va appeler
ces services. Dans un premier temps, ce client se trouvera sur la même machine que l’objet distant, mais
même dans ce cas, RMI utilise des sockets. On a donc le même comportement que pour un objet se
situant physiquement sur une machine distante. Dans un second temps, nous utiliserons deux machines
différentes (vous vous connecterez via ssh sur une machine Sun du CI, comme clervoy par exemple).
Important : il faudra placer les bytecodes des applications client et serveur (et pour le registre) dans
des répertoires différents pour bien comprendre ce qu’il est nécessaire d’avoir de chaque côté (classes,
stubs, interfaces, positionnement du classpath).
1. définir une interface distante InterfaceBonjour qui définit une méthode afficher(String s) ;
2. définir une classe BonjourDistant qui réalise cette interface et qui étend la classe
java.rmi.server.UnicastRemoteObject ;
3. créer une classe Enregistrement qui possède une méthode main qui enregistre l’objet dans un
registre ;
Ces trois questions permettaient de réaliser la partie serveur de l’application. Rien de bien particulier,
il suffisait de créer une interface étendant java.rmi.Remote et une classe réalisant cette interface.
L’« application » serveur liant l’objet dans le registre utilisait bien sûr la méthode Naming.rebind().
1
Cette méthode est pratique en développement1 , mais ne doit pas être utilisée systématiquement dans
le cadre d’un développement final.
Le nom de l’objet dans la base de registre est un nom long, comme par exemple rmi://localhost/
objetDistant. On aurait très bien pu l’appeler simplement objetDistant. On peut remarquer
qu’ici on a omis le numéro de port, donc par défaut on considère le port 1099. On peut également
définir le numéro de port du serveur en utilisant l’argument en ligne de commande de java.
L’interface développée fait partie du paquetage fr.supaero.rmi.serveur. Les classes développées
font partie du paquetage fr.supaero.rmi.serveur.base et les bytecodes correspondants sont stockés dans un répertoire classesServeur.
4. du côté client, implanter une classe AppelDistant qui possède une méthode main qui cherche une
référence sur l’objet distant et appelle sa méthode afficher ;
Rien de particulier. Il ne fallait pas oublier de transtyper la référence obtenue après le lookup,
car on obtient une référence de type java.rmi.Remote. De même, il faut posséder l’interface
InterfaceBonjour dans son classpath pour pouvoir compiler correctement le client.
La classe fait partie du paquetage fr.supaero.rmi.client.base et le bytecode correspondant est
stocké dans le répertoire classesClient.
NB : vous remarquerez que j’ai détaillé les exceptions qui peuvent être lancées pour que vous voyez
bien quelles sont ces exceptions. On aurait bien sûr pu rattraper toutes les exceptions avec un
catch (Exception e).
5. générer le stub de la classe BonjourDistant en utilisant l’option -v1.2 de rmic pour ne pas générer
de skeleton ;
6. lancer une base de registre grâce à rmiregistry et le serveur. On lancera le registre en prenant de
garde de bien positionner le classpath pour que le stub soit visible.
7. lancer le client. Où s’exécute la méthode de l’objet distant ?
Rien de bien particulier pour ces trois questions. Voici le détail des opérations utilisées :
(a) lancement du registre. J’ai utilisé la commande suivante :
CLASSPATH=/chemin_TP/classesRegistre/ rmiregistry
Je positionne le classpath pour que le registre puisse « voir » l’interface et le stub du serveur.
(b) « exécution » de la classe fr.supaero.rmi.serveur.base.Enregistrement. Rien de bien
particulier, une erreur peut se produire si on n’a pas généré le stub.
(c) « exécution » de la classe fr.supaero.rmi.client.base.AppelDistant. Comme on n’utilise
pas de SecurityManager, on n’a pas accès au chargement dynamique des classes. Il faut donc
que le stub se trouve dans le classpath du client, sinon une exception est levée (ce qui n’est pas
nécessaire si on peut le charger dynamiquement, cf. section 4).
On voit bien ici que la méthode afficher de l’objet distant s’exécute du côté serveur.
A partir de la version 5.0 du JDK, lorsque l’on exporte un stub dans le registre, si le bytecode du
stub n’est pas disponible, on génére un objet de type java.lang.reflect.Proxy à la place du
stub. Le client et le registre n’ont donc plus besoin d’avoir le bytecode du stub dans le classpath
(il faut bien sûr que le client utilise du code et une JVM compatibles avec le JDK 5.0). J’ai
affiché dans AppelDistant l’objet récupéré par l’appel à Naming.lookup. Si les stubs ont été
utilisés par le serveur, le registre et le client, on obtiendra à l’affichage :
Objet distant recupere : BonjourDistant_Stub[UnicastRef [liveRef:
1 Elle
est même nécessaire si on ne veut pas relancer le registre à chaque fois.
2
[endpoint:[134.212.136.180:35046](remote),objID:[705be52f:117e92e1b19:-8000, 0]]]]
ce qui montre bien que l’on utilise le stub (on voit également quel est le port utilisé par le stub
pour communiquer, ici le port 35046 de la machine dont l’adresse IP est 134.212.136.180).
Si on ne dispose pas des stubs (il suffit de ne pas l’avoir dans le CLASSPATH du serveur !), on
obtient alors :
Objet distant recupere : Proxy[InterfaceBonjour,RemoteObjectInvocationHandler
[UnicastRef [liveRef: [endpoint:[134.212.136.180:35058](remote),objID:
ce qui montre bien que l’on utilise un objet de type Proxy et non pas un stub.
Attention toutefois, cette solution suppose que le serveur et le client utilisent une version du
JDK supérieure à la version 5.0.
Dans toute la suite du TP, j’utiliserai les stubs et non pas la classe Proxy. On devra donc avoir
le bytecode des stubs nécessaires dans le classpath.
3
Passage d’un objet en paramètre
Dans cette section, nous allons utiliser une méthode d’un objet distant qui prend en paramètre un
objet se situant chez le client. On créera un nouveau paquetage pour pouvoir réutiliser le code écrit
précédemment facilement.
Important : comme précédemment, nous n’utiliserons pas le chargement dynamique des classes nécessaires. Il faudra donc s’assurer que les classes et stubs nécessaires sont bien dans les classpaths (en
particulier, le serveur va avoir besoin de classes dont se sert le client).
1. créer une interface InterfaceMessage dans le paquetage du serveur qui définit une méthode
getTexte qui renvoie une chaı̂ne de caractères ;
2. créer une classe Message dans le paquetage du client qui réalise cette interface et qui a pour attribut
la chaı̂ne de caractères à renvoyer ;
Rien
de
bien
particulier
ici.
J’ai
choisi
d’utiliser
deux
nouveaux
paquetages,
fr.supaero.rmi.client.objet et fr.supaero.rmi.serveur.objet.
3. modifier les classes précédentes pour que la méthode afficher de InterfaceBonjour prenne un
objet de type InterfaceMessage en paramètre. Que se passe-t-il ?
La modification de InterfaceBonjour et de BonjourDistant était triviale. Lorsque l’on essaye
d’exécuter l’appel client à afficher, une erreur de marshalling est trouvée : la JVM nous indique
que la classe Message n’est pas sérialisable. En effet, nous avons vu en cours qu’un objet passé en
paramètre d’une méthode distante devait soit être sérialisable, soit lui-même distant.
4. écrire une classe MessageSerialisable sérialisable et réalisant InterfaceMessage et l’utiliser dans
AppelDistant. Que se passe-t-il maintenant ?
Cette fois-ci tout fonctionne, à condition que le serveur puisse reconstruire l’objet de type
MessageSerialisable2 . Comme nous n’utilisons pas le chargement dynamique, cela revient à copier
le bytecode de MessageSerialisable dans le CLASSPATH du serveur.
Vous remarquerez que j’ai fait afficher un petit texte dans la méthode getTexte de
MessageSerialisable. Ce texte « apparaı̂t » du côté serveur, ce qui est normal car l’objet est
sérialisé et envoyé au serveur.
2 Sinon
une exception d’unmarshalling est levée dans le serveur.
3
Si on avait utilisé un objet distant, ce texte serait apparu du côté client. J’ai implanté cette solution, les classes correspondantes sont dans les paquetages fr.supaero.rmi.client.distant et
fr.supaero.rmi.serveur.distant. On voit alors qu’il n’y a pas besoin d’enregistrer l’objet distant
de type Message dans un registre, tout se fait « automatiquement » (il faut bien sûr que le serveur
possède le bytecode du stub de Message).
4
Chargement dynamique des classes
Dans les sections précédentes, nous avons supposé que le serveur, le client et le registre disposaient
des stubs et des bytecodes nécessaires à leur bon fonctionnement. Nous allons maintenant utiliser le
chargement dynamique des classes. De cette façon, le serveur et le client ne disposeront que des interfaces
distantes nécessaires à leur compilation.
1. récupérer les classes développées dans la section 2 dans deux nouveaux paquetages,
fr.supaero.rmi.client.dyn et fr.supaero.rmi.serveur.dyn. Modifier la classe cliente pour que
celle-ci puisse utiliser le chargement dynamique des classes ;
Il n’y avait pas grand chose à faire. Comme la classe client ne disposera pas du stub, mais devra le
charger dynamiquement, il ne fallait pas oublier de mettre en place un RMISecurityManager dans
l’application cliente.
J’ai également changé l’application serveur pour utiliser le constructeur de UnicastRemoteObject
permettant de préciser le numéro de port sur lequel le stub attend les connexions. Ceci me permet
de définir précisement la politique de sécurité dont j’ai besoin. J’ai choisi ici le port 1200.
2. lancer un registre sans classpath. On a besoin d’un serveur Web pour servir les fichiers. On va utiliser
un serveur de fichier léger, disponible sur le site sous l’onglet ressources, la classe ClassFileServer.
Pour l’utiliser : java ClassFileServer numPort CHEMIN_VERS_FICHIERS. Lancer ensuite le serveur
en précisant comme codebase "http://nomMachineServeurFichier:numPort/" (ne pas oublier le
« / » final !). Enfin, lancer le client avec un fichier de politique de sécurité adéquat ;
Là encore rien de particulier si on effectuait bien les opérations demandées (ne pas mettre de
classpath pour le registre, lancer le serveur de fichier etc.). Voici le fichier de politique de sécurité
que j’ai utilisé personnellement pour le client :
grant {
// connexions vers le serveur de fichiers
permission java.net.SocketPermission
"guerin.supaero.fr:2000", "connect";
// connexions vers le registre
permission java.net.SocketPermission
"clervoy.supaero.fr:1099", "connect";
// connexions vers le stub
permission java.net.SocketPermission
"clervoy.supaero.fr:1200", "connect";
};
Il ne fallait pas oublié que l”on a également besoin de l’interface InterfaceDistant sur le serveur
de fichiers pour pouvoir reconstruire le stub.
On remarquera que l’on voit bien les appels au serveur de fichier dans les traces de ce dernier lorsque
le registre et le client vont charger les interfaces et les classes dont ils ont besoin.
4
On remarquera enfin que si le client utilise des objets en paramètre de la méthode distante, il peut
également préciser que le bytecode des classes et interfaces correspondantes se trouvent sur le serveur
de fichier (pour que l’objet serveur puisse les reconstruire) via java.rmi.server.codebase.
3. nous allons essayer de « bootstrapper » le client et le serveur. Pour cela, créer un répertoire qui
contiendra les classes de l’application (même celles du client. Il faudra donc modifier la classe
AppelDistant pour qu’elle n’ait plus de méthode main, mais qu’elle appelle la méthode afficher
à sa création), et deux applications qui chargent dynamiquement la classe applicative du serveur et
la classe applicative du client.
C’est le plus gros morceau du TP. Il faut rester méthodique et ne pas se précipiter.
Les classes sont disponibles sur le site dans les paquetages fr.supaero.rmi.client.boot et
fr.supaero.rmi.serveur.boot pour les classes applicatives et dans fr.supaero.rmi.boot pour
les classes de démarrage.
J’ai choisi les machines suivantes :
– le serveur de fichiers tourne sur guerin:2000
– le serveur et le registre tournent sur clervoy
– le client tourne sur dortie
Les classes « applicatives » ne nécessitaient pas de modifications importantes. Seule la classe
AppelDistant devait être modifiée : le traitement qu’elle faisait (appel de la méthode afficher)
devait être encapsulé dans son constructeur qui ne devait pas posséder d’argument (appel à
newInstance). Ceci peut poser problème si l’on veut paramétrer le nom du serveur où se trouve le
registre par exemple.
On pouvait alors lancer un serveur de fichier pointant sur le répertoire contenant ces classes et lancer
un registre sans classpath sur la machine servant de serveur.
Le serveur « bootstrappé » lui se contente de récupérer l’objet serveur dynamiquement via la classe
RMIClassLoader et crée un objet. On remarquera l’utilisation de la classe java.util.Property
qui permet de récupérer les propriétés du système (codebase ici). Il faut préciser dans le fichier de
politique de sécurité que l’on est autorisé à le faire :
grant {
// on autorise les connexions par socket sur le port 2000 du serveur de
// fichiers
permission java.net.SocketPermission "guerin:2000", "connect";
// on autorise les connexions par socket sur le port 1099 de la machine
// pour le registre
permission java.net.SocketPermission "clervoy:1099", "connect";
// on autorise les connexions par socket sur le stub
permission java.net.SocketPermission "clervoy:1200", "accept";
// on autorise les connexions par socket sur la machine appelante
// pour le retour
permission java.net.SocketPermission "dortie:1024-", "accept";
// on autorise la lecture de la propriete du systeme java.rmi.server.codebase
permission java.util.PropertyPermission "java.rmi.server.codebase", "read";
};
On remarquera que je suis obligé d’autoriser les connexions sur dortie (sur laquelle tourne le client)
sur tous les ports utilisateurs, car la socket factory utilisé (ici RMISocketFactory, la factory par
5
défaut) choisit un port anonyme sur le client. Il faut créer sa propre SocketFactory pour choisir un
port spécifique sur le client.
On le lance avec java -Djava.security.policy=/CHEMIN_POLITIQUE/politique_serveur_boot.txt
-Djava.rmi.server.codebase=http://machineContenantClasses:numPort/ BootServer.
Il
faut bien sûr préciser où se trouvent les classes à charger.
On lance de la même façon le client boostrappé. J’ai utilisé dans le constructeur de BonjourDistant
un numéro de port pour pouvoir écrire précisement le fichier de politique de sécurité. Celui-ci est le
suivant :
grant {
// on autorise les connexions par socket sur le port 2000 du serveur de
// fichiers
permission java.net.SocketPermission "guerin:2000", "connect";
// on autorise les connexions par socket sur le registre
permission java.net.SocketPermission "clervoy:1099", "connect";
// on autorise les connexions par socket sur le stub
permission java.net.SocketPermission "clervoy:1200", "connect";
// on autorise la lecture de java.rmi.server.codebase
permission java.util.PropertyPermission "java.rmi.server.codebase", "read";
};
5
Utiliser le mécanisme d’activation
Dans cette section, il faut essayer d’étendre non pas UnicastRemoteServer pour la classe représentant
l’objet distant, mais Activatable.
1. modifier la classe BonjourDistant pour qu’elle étende correctement Activatable ;
Toutes les classes sont disponibles sur le site. Pas de problème particulier pour celle là, il fallait juste
faire attention au constructeur.
2. modifier la classe Enregistrement pour qu’elle enregistre l’objet Activatable. Ne pas oublier de
mettre en place un RMISecurityManager ;
Ne pas oublier de changer les chemins d’accès dans la classe disponible sur le corrigé ! Sinon, il
fallait suivre « tranquillement » les transparents : créer un groupe d’activation, créer le descripteur
de l’objet, l’enregister auprès du service d’activation, et enfin le lier à un nom dans le registre RMI.
J’ai utilisé System.getProperties() pour récupérer les propriétés du système et utiliser l’argument
java.security.policy que je passe à la JVM. De même, je récupére le codebase du serveur de
fichier grâce à la propriété java.rmi.server.codebase.
3. lancer l’application en utilisant un « client de base » précédemment développé. Attention à la
politique de sécurité pour rmid.
J’ai choisi ici de réutiliser la solution utilisant le chargement dynamique des classes. J’ai
par contre réécrit les classes dans des paquetages différents, java.rmi.client.activ et
java.rmi.serveur.activ.
Il ne fallait pas oublier de prendre un fichier de politique de sécurité suffisant. Celui-ci permettait
à l’utilitaire rmid d’exécuter certaines commandes sur le système. J’ai utilisé le fichier suivant (très
laxiste !) :
6
grant {
// on autorise les activation groups a utiliser certaines
// proprietes
permission com.sun.rmi.rmid.ExecOptionPermission "*";
};
J’ai lancé rmid comme suit : rmid -J-Djava.security.policy=politique_rmid.txt -port 1098
Il fallait également positionner le codebase en lançant l’enregistrement de telle sorte
que celui-ci pointe correctement sur le répertoire contenant la classe de l’objet distant.
Par exemple, j’ai lancé : java -Djava.security.policy=politique_server_activ.txt
-Djava.rmi.server.codebase=http://guerin:2000/ fr.supaero.rmi.serveur.activ.Enregistrement.
Je disposais évidemment d’un serveur de fichiers via la classe ClassFileServer qui attendait des
connexions sur le port 2000 de la machine guerin.
J’ai utilisé le fichier de politique suivant pour l’enregistrement :
grant {
// on autorise les connexions par socket sur le port 2000 du serveur de
// fichiers
permission java.net.SocketPermission "guerin:2000", "connect";
// on autorise les connexions par socket sur le port 1099 de la machine
// pour le registre
permission java.net.SocketPermission "clervoy:1099", "connect";
// on autorise les connexions par socket sur la machine
// pour rmid
permission java.net.SocketPermission "clervoy:1098", "connect";
// on autorise les connexions par socket sur la machine
// pour les activations
permission java.net.SocketPermission "clervoy:1024-", "accept";
// on autorise les connexions par socket sur la machine
// pour le stub
permission java.net.SocketPermission "clervoy:1200", "connect";
// on autorise les connexions par socket sur la machine appelante
// pour le retour
permission java.net.SocketPermission "dortie:1024-", "accept";
// on autorise la lecture et l’ecriture des permissions
permission java.util.PropertyPermission "*", "read,write";
// on autorise l’utilisation du runtime
permission java.lang.RuntimePermission "*";
// on autorise les connexions par socket sur le stub
permission java.net.SocketPermission "clervoy:1200", "accept";
};
7
Pour le client, on peut utiliser le fichier de politique suivant (identique à celui de la section 4 sauf
que l’on rajoute l’autorisation de se connecter à rmid sur le port 1098) :
grant {
// connexions vers le serveur de fichiers
permission java.net.SocketPermission
"guerin.supaero.fr:2000", "connect";
// connexions vers le registre
permission java.net.SocketPermission
"clervoy.supaero.fr:1099", "connect";
// connexions vers rmid
permission java.net.SocketPermission
"clervoy.supaero.fr:1098", "connect";
// connexions vers le stub
permission java.net.SocketPermission
"clervoy.supaero.fr:1200", "connect";
};
On s’aperçoit à l’exécution que si l’on arrête la JVM lançant l’enregistrement, lors de l’appel du
client, une machine virtuelle est bien redémarrée. L’affichage se fait dans les traces de rmid.
8
Téléchargement