TP JAVA - RMI
Ada Diaconescu, Bertrand Dupouy et Frank Singhoff
SOMMAIRE
Déroulement du TP :
Il se divise en trois parties :
- la première partie : montre le fonctionnement élémentaire de RMI - à utiliser cet exemple pour mettre en place la seconde et la troisième partie ;
- la seconde partie : montre un modèle client-serveur - à développer ;
- la troisième partie : montre un modèle agents mobiles - à développer.
Documentation :
Le support du cours se trouve
ici
RMI permet l'appel d'une méthode appartenent à un objet distant,
c'est à dire géré par une autre JVM que la JVM locale.
L'objet distant (le serveur) qui propose l'appel de ses méthodes par
des objets distants (des clients) doit offrir :
- une interface qui décrit l'ensemble des méthodes visibles à distance :
- cette interface doit hériter de java.rmi.Remote, ce qui rend les paramètres des méthodes sérialisables (marshallable),
- chaque méthode exportée doit lever l'exception
java.rmi.RemoteException,
on remarquera que l'interface distante est décrite en Java : il n'y a pas de langage de description spécifique comme IDL ou celui de rpcgen.
- une classe qui implémente cette interface ;
- une instance de cette classe - cette instance doit être enregistée sous un certain nom auprès du service de désignation de RMI (rmiregistry) :
- appel à registry.rebind(‹service_name›, stub).
L'appelant (le client) doit obtenir une référence
sur l'objet distant (le serveur) :
- utiliser LocateRegistry afin d'obtenir une référence locale sur le registry distant
(s'exécutant sur la machine du serveur) :
Registry registry = LocateRegistry.getRegistry(host, port);
L'objet registry se conduit en représentant local du serveur de noms distant.
- appel à registry.lookup(‹service_name›) qui interroge rmiregistry
sur la machine distante.
Le stub est téléchargé à ce moment (lors du lookup) ;
Il faudrait convertir le type du stub retourné au type de l'objet distant (ex : Hello)
Les souches (fichier dont le nom est
terminé par _stub) destinées au client et au
serveur sont produites par rmic.
L'appel à la méthode distante est, dans les faits, traduite en appel
local au stub fourni par le serveur. C'est ce dernier qui fait l'appel
distant.
Recopier les fichiers se trouvant dans
http://perso.telecom-paristech.fr/~diacones/tp-rmi/exo1_tp.zip
Pour décompresser l'archive dans un répertoire exo1 :
unzip exo1_tp.zip -d exo1
On y trouve :
- un répertoire src - avec le code source de l'exo1 :
- hello/Hello.java : l'interface distante ;
- server/HelloServer.java : la classe du serveur, implantant l'interface Hello - sa méthode readMessage retourne un message aux clients ;
- client/HelloClient.java : fait un appel à distance (readMessage) au serveur et affiche le résultat à la console (en mode texte) ;
- client/HelloClientGraphic.java : la variante graphique du HelloClient pour afficher le résultat (message) dans une fenêtre graphique.
- un fichier security.policy pour configurer le gestionnaire de sécurité RMI (SecurityManager).
Commentaires sur le fichier Hello.java - l'interface distante - qui se réduit aux lignes suivantes :
public interface Hello extends java.rmi.Remote{
String readMessage() throws java.rmi.RemoteException;
}
- Remote signifie que la méthode readMessage (de toutes les classes implantant cette interface
Hello) pourra être appelée depuis une JVM autre que la JVM locale.
- HelloServer.java implémente cette interface :
public class HelloServer implements Hello{ ... }
- Pour compiler les fichiers sources :
- Créer un répertoire bin :
> mkdir bin
- Déplacez-vous dans le répertoire src (où se trouve le code source java) :
> cd src
- Compiler le code java (dans l'ordre) :
> javac -d ../bin hello/*.java
> javac -d ../bin server/*.java
> javac -d ../bin client/*.java
On obtient alors le code compilé (fichiers binaires *.class) dans le répertoire
bin :
hello/Hello.class, server/HelloServer.class, client/HelloClient.class, ...
- Pour configurer le gestionnaire de sécurité RMI (SecurityManager) :
- Copier le fichier security.policy dans le répertoire bin
> cp ../security.policy ../bin
- Pour crér le stub pour les clients distants :
- Déplacez-vous dans le répertoire bin (où se trouve la classe
server/HelloServer.class) :
> cd ../bin
- Exécuter le compilateur rmi
> rmic -keepgenerated server.HelloServer
Ce qui produit les fichiers suivants :
server/HelloServer_Stub.java et server/HelloServer_Stub.class
L'option keepgenerated permet de garder le fichier source (server/HelloServer_Stub.java)
correspondant au fichier bytecode (server/HelloServer_Stub.class).
Pour commencer, on va exécuter l'application répartie dans un cas particulier, plus simple.
Dans ces cas, le partage de code - ex : pour les stubs - sera assuré par NFS (Network File System - installé sur les machines de TP) : le client et le serveur, même s'ils ne sont pas sur la même machine, partagent le même disque.
- En étant dans le répertoire bin (où se trouvent les fichiers .class) :
- Lancer rmiregistry en arrière plan, en donnant un numéro
de port particulier : 53000 + numéro de casier
(si on ne donne pas de port, rmiregistry écoute sur
le port 1099).
- Commande générique :
rmiregistry <votre port spécifique>&
- Exemple spécifique - sur la machine roxane avec le numéro de casier 007 :
> rmiregistry 53007&
- Lancer le serveur server.HelloServer sur la même machine que rmiregistry
- Commande générique :
java -Djava.security.policy=security.policy server.HelloServer <votre port spécifique> <votre message>
- Exemple spécifique - toujours sur la machine roxane :
> java -Djava.security.policy=security.policy server.HelloServer 53007 hello
Ce serveur affiche alors :
HelloServer bound: HelloService
- Lancer le client client.HelloClient sur une autre machine
- Commande générique :
> java -Djava.security.policy=security.policy client.HelloClient <votre port spécifique> <la machine du serveur>
- Exemple spécifique :
> java -Djava.security.policy=security.policy client.HelloClient 53007 roxane
Le client devrait alors afficher à la console le message du serveur (ex : hello)
- Note : pour afficher le message via une fenêtre graphique, lancer client.HelloClientGraphic (à la place du client.HelloClient) comme avant ;
On doit voir alors apparaître une fenêtre grisée sur laquelle est écrite
la chaine qui a été émise par le serveur (ex : hello).
Attention, dans ce cas : assurez-vous de lancer le client sur la machine locale (et non pas via une session ssh) afin de pouvoir visualiser la partie graphique.
Dans le cas général - sans partage de code via NFS - il faut indiquer où se trouvent les stubs :
- Option 1 : dans le système de fichiers local au serveur rmi ;
- Option 2 : sur un serveur accesible aux clients et aux serveurs rmi - ex : serveur http ou ftp.
Ceci permet le transfert de la classe stub (et de toute autre classe requise à l'exécution du client), à partir d'un endroit conu et accessible, vers la machine du client, pendant l'exécution.
Pour ce faire, il faut d'abord copier les fichiers à partager (ex : Hello.class) dans un répertoire dédié accessible par le rmiregistry du serveur :
- On utilisera le répertoire $HOME/public_html/rmi-codebase/ ;
- Copier hello/Hello.class dans ce répertoire. On doit avoir : $HOME/public_html/rmi-codebase/hello/Hello.class.
Ensuite, lors du lancement du rmiregistry et du serveur rmi, il faudrait indiquer l'endroit où se trouvent ces classes - on utilisera le paramètre dédié codebase.
Option 1 - protocole file :
- Lancer le rmiregistry depuis n'importe quel répertoire :
ex : > rmiregistry -J-Djava.rmi.server.codebase=file:/$HOME/public_html/rmi-codebase/ 53007&
- Lancer le serveur depuis le répertoire bin de l'exercise (si non, utiliser le paramè classpath pour indiquer à la JVM l'endroit où se trouvent les classes du serveur).
ex : > java -Djava.security.policy=security.policy -Djava.rmi.server.codebase=file:/$HOME/public_html/rmi-codebase/ server.HelloServer 53007 hello
- Lancer le client sur une autre machine (comme avant).
ex : > java -Djava.security.policy=security.policy client.HelloClient 53007 roxane
Option 2 - protocole http :
Attention : dans les commmandes ci-dessous, remplacer bien le texte <votre nom d'utilisateur unix> par votre vrais nom d'utilisateur.
- Lancer le rmiregistry depuis n'importe quel répertoire :
ex : > rmiregistry -J-Djava.rmi.server.codebase=http://perso.telecom-paristech.fr/~<votre nom d'utilisateur unix>/rmi/rmi-codebase/ 53007&
- Lancer le serveur depuis le répertoire bin de l'exercise (si non, utiliser le paramè classpath pour indiquer à la JVM l'endroit où se trouvent les classes du serveur).
ex : > java -Djava.security.policy=security.policy -Djava.rmi.server.codebase=http://perso.telecom-paristech.fr/~<votre nom d'utilisateur unix>/rmi-codebase/ server.HelloServer 53007 hello
- Lancer le client sur une autre machine (comme avant).
ex : > java -Djava.security.policy=security.policy client.HelloClient 53007 roxane
Pour faciliter la résolution des exercices suivants, répondez aux
questions ci-dessous en consultant le fichier produit par
rmic -keepgenerated :
- comment le serveur rend-t-il accessible à distance la méthode readMessage ?
- que doit faire le client pour invoquer la méthode distante readMessage ?
Voir dans le stub, exécuté localement par le client, l'appel
à la méthode distante qui se fait par : ref.invoke où :
- ref est un objet du type RemoteRef ;
- les objets passés en paramétres sont "marshallés" ;
- le dernier argument est un identifiant de la méthode sur le serveur.
On se propose de simuler le scénario suivant :
un cuisinier cherche des ingrédients pour préparer un repas au
moindre coût.
Il va chercher les ingrédients un par un, en procédant ainsi :
- pour un ingrédient donné, il interroge une liste de magasins, appelés
Mag1 à MagN. Chacun de ces magasins se trouve sur une machine différente.
Chaque magasin est géré par un processus appelé StoreManager,
- le cuisinier est modélisé par un processus StoreClient qui s'exécute sur une machine quelconque.
Ce processus va interroger, pour un ingrédient donné, une suite de processus du type StoreManager
- à la fin de la consultation il affiche àl'écran le nom du site où se trouve le magasin proposant
cet ingrédient au prix le plus bas.
L'interrogation d'un processus du type StoreManager se fera par l'appel à distance de la méthode
getPrice qui aura été rendue visible dans l'interface Store implémentée
par GerantMagasin.
Schéma du mécanisme d'interrogation :
Voici le contenu de l'interface Store.java :
/**
* remote interface for Store services
*/
public interface Store extends java.rmi.Remote {
/**
* get the price of the given ingredient
* note: this method can be called remotely
* @param ingredient: the ingredient whose price should be returned
* @return: the price of the ingredient
* @throws java.rmi.RemoteException
*/
float getPrice(String ingredient) throws java.rmi.RemoteException;
}
Remote signifie que la méthode getPrice des classes implantant cette interface Store
peut être appelée depuis une JVM autre que la JVM locale.
StoreManager.java implémente cette interface :
public class StoreManager implements Store {
...
}
et StoreClient.java utilise la méthode publique getPrice pour interroger les magasins.
Copier les fichiers se trouvant dans
http://perso.telecom-paristech.fr/~diacones/tp-rmi/exo2_tp.zip
Pour décompresser l'archive dans un répertoire exo2 :
> unzip exo2_tp.zip -d exo2
On y trouve :
- un répertoire src - avec le code source de l'exo2 :
- store/Store.java : l'interface distante du magasin ;
cette interface définit une méthode getPrice(String ingredient) qui retourne le prix de l'ingrédient donné en paramètre, tel que proposé par le magasin questioné;
- manager/StoreManager.java : la classe du serveur, implantant l'interface Store - l'implantation de sa méthode getPrice retourne le prix des produits aux clients ;
- client/StoreClient.java (à compléter) : fait un appel à distance (getPrice) à tous les serveurs de type Store et affiche le prix le moins cher à la console (en mode texte) ;
- client/StoreClientGraphic.java : la variante graphique du StoreClient pour afficher le résultat (message) dans une fenêtre graphique.
- un fichier security.policy pour configurer le gestionnaire de sécurité RMI (SecurityManager).
- des fichiers pour configurer les magasins avec différents prix d'ingrédients - Mag1, Mag2 et Mag3.
Dans cet exercice on vous demande d'abord de compléter
le fichier source StoreClient.java puis
de lancer toute l'application.
-
Dans StoreClient.java, compléter les lignes
où se trouvent des //TODO ..., c'est à dire :
- //TODO
String name = ...;
- //TODO
for (int i = 2; i < ... ; i++) {
- //TODO
Registry registry = LocateRegistry.getRegistry(..., ...);
- //TODO
price = ...;
- Une fois ceci fait, on compile et on lance l'application .
- Pour compiler les fichiers sources :
- Créer un répertoire bin :
> mkdir bin
- Déplacez-vous dans le répertoire src (où se trouve le code source java) :
> cd src
- Compiler le code java (dans l'ordre) :
> javac -d ../bin store/*.java
> javac -d ../bin manager/*.java
> javac -d ../bin client/*.java
On obtient alors le code compilé (fichiers binaires *.class) dans le répertoire
bin :
store/Store.class, manager/StoreManager.class, client/StoreClient.class, ...
- Pour configurer le gestionnaire de sécurité RMI (SecurityManager) :
- Copier le fichier security.policy dans le répertoire bin
> cp ../security.policy ../bin
- Pour pouvoir configurer chaque magasin :
- Copier les fichier Mag1, Mag2 et Mag3 dans le répertoire bin
> cp ../Mag* ../bin
- Optionel: pour crér et visualiser le stub pour les clients distants :
- Déplacez-vous dans le répertoire bin (où se trouve la classe manager/StoreManager.class) :
> cd ../bin
- Exécuter le compilateur rmi
> rmic -keepgenerated server.HelloServer
Ce qui produit les fichiers suivants :
manager/StoreManager_Stub.java et manager/StoreManager_Stub.class
- Lancer rmiregistry sur chacune des 3 machines où vont tourner
les serveurs (les instances de StoreManager) - sur le même port :
> rmiregistry <votre port spécifique>
- Lancer les serveurs sur chacune de ces 3 machines differentes,
correpondant aux trois magasins : Mag1, Mag2, Mag3, respectivement
> java -Djava.security.policy=security.policy manager.StoreManager <votre port spécifique> Mag<x>
- Lancer le client sur une machine quelconque :
> java -Djava.security.policy=security.policy client.StoreClient <votre port spécifique> <votre nom de produit> <la liste des machines des serveurs>
- Exécuter l'application exo2 en partageant le code binaire pendant l'exécution (ex : via un serveur http, sans utiliser le partage de NFS) - voir l'exercice I.4)
- Remplacer les deux tableaux - ingredientNames et ingredientPrices - par un ArrayList contenant des objets d'une nouvelle classe - Product - où chaque produit contient un nom et un prix.
- Modifier le format des fichiers MagX de façon à avoir sur chaque ligne un ingrédient et son prix.
On reprend l'exemple précédent que l'on va traiter
en mettant en oeuvre des agents mobiles.
Le client va créer un agent au lieu d'interroger lui-même
les sites détenteurs de magasins.
Cet agent va passer de site en site, et sur chacun de ces sites
entrer dans le magasin pour y lire le prix de l'ingrédient recherché.
S'il y a lieu, il met à jour le prix minimal.
Une fois tous les sites consultés, l'agent donne au client le nom du site
qui propose l'ingrédient recherché au prix le plus bas, ainsi que ce prix.
Comment mettre en oeuvre cette application ?
Le client va lancer sa demande depuis un site qui sera appelé
par la suite "initiateur".
Ce client va initialiser un tableau de sites à parcourir, puis exécuter sur
le premier site de la liste la méthode
migrate, à laquelle il a passé l'agent en argument. Ceci fait migrer l'agent de hôte en hôte jusqu'à arriver de nouveau sur la hôte initiateur.
Le schéma suivant indique les accès aux objets distants lors du lancement
initial de l'agent :
- L'initiateur invoque à distance la méthode migrate
sur le premier site de la liste des sites à consulter, appelé ici "Host"
- L'agent est récupéré par "Host" depuis le site initiateur
lors de l'exécution de migrate. L'agent sera exécuté sur "Host".
Deux types objets vont modéliser ces différents acteurs : Host et Agent.
- L'interface Host est la suivante :
public interface Host extends Remote{
void migrate(Agent agent) throws RemoteException;
}
Remote signifie que les méthodes de cet objet Host
peuvent être appelées depuis une JVM autre que la JVM locale.
Il y aura deux implémentations de cette interface Host :
- une pour les sites détenant un magasin (HostImplem.java) : dans ce cas la méthode migrate
consulte le prix puis fait migrer l'agent vers le site suivant ;
- une pour le site initiateur (Initiator.java) : dans ce cas la méthode migrate se contente
d'afficher le nom du site proposant le prix le plus bas, ainsi que ce prix.
- L'interface de l'objet Agent est la suivante :
public interface Agent extends Serializable{
void getMinPrice(String[] storeIngredients, Float[] storePrices);
String getNextHostName();
boolean isNextInitiator();
void displayResult();
int getRmiRegistryPort();
}
Agent doit être une interface Serializable.
En effet, Serializable indique que les objets de type Agent utilisés seront
sérialisés et normalisés lors de l'appel distant ("marshalling").
Les trois premières méthodes (getMinPrice, getNextHostName et isNextInitiator) servent aux sites consultés ; la quatrième (displayResult) est utilisée par l'initiateur.
On remarquera que ces deux interfaces Agent et Host
sont complétement indépendantes de l'application actuellement traitée :
- l'agent peut effectuer un traitement quelconque ;
- l'hote suivant peut être déterminé par n'importe quel algorithme.
De façon plus générale, cette méthode permet d'écrire le code d'un serveur
avant de savoir quelle application il va exécuter.
Copier les fichiers se trouvant dans
http://perso.telecom-paristech.fr/~diacones/tp-rmi/exo3_tp.zip
Pour décompresser l'archive dans un répertoire exo3 :
> unzip exo3_tp.zip -d exo3
On y trouve :
- un répertoire src - avec le code source de l'exo3 :
- service/Host.java : l'interface distante de du hô ;
- service/Agent.java : l'interface de l'agent ;
- manager/HostImplem.java (à compléter) : la classe du serveur, implantant l'interface Host ;
- manager/AgentImplem.java : la classe de l'agent, implantant l'interface Agent ;
- manager/Store.java : la classe du magasin ;
- manager/MySecurity.java et manager/MyHostname.java : aident au développement de l'application ;
- client/Initiator.java (à compléter) : la classe de l'initiateur ;
- client/AgentThread.java (à compléter) : la classe qui permet de lancer dans un nouveau Thread chaque traitement de l'agent sur un nouveau hôte, et de débloquer ainsi la hôte précédente ;
- un fichier security.policy pour configurer le gestionnaire de sécurité RMI (SecurityManager).
- des fichiers pour configurer les magasins avec différents prix d'ingrédients - Mag1, Mag2 et Mag3.
Comme on l'a dit, un site consulté (HostImplem.java) va implémenter l'interface Host
en faisant :
public class HostImplem implements Host{...}
Les points importants du programme (HostImplem.java), sont les suivants :
- l'appel au constructeur, qui met l'objet en attente d'éventuels appels distants ;
- la méthode main, qui appelle en particulier :
- le RMISecurityManager
- registry.rebind pour enregistrer les objets proposés par ce site
auprès de rmiregistry ;
- la méthode migrate est implantée sous forme de thread pour que
le traitement effectué par cet agent ne bloque pas le fonctionnement de l'hôte.
Le site initiateur est celui sur lequel se trouve le client qui déclenche
le fonctionnement de l'agent mobile.
Ce site va, comme le précédent, implémenter de façon spécifique l'interface Host :
public class Initiator implements Host{ ... }
Les points importants du code sont les suivants :
- l'appel au constructeur qui met l'objet en attente d'éventuels appels distants ;
- la méthode main qui appelle en particulier :
- le RMISecurityManager
- l'appel à registry.rebind pour enregistrer l'initiateur auprès de rmiregistry (permetant au dernier hôte de le localiser et de l'appeler â distance)
- l'appel à registry.lookup pour retrouver la référence du site auquel on s'adresse en premier.
Le stub est téléchargé à ce moment (lors du lookup).
- la méthode migrate se contente d'afficher un résultat.
Elle sera appelée par le site sur lequel se trouve le dernier magasin à consulter.
Remarque importante :
le stub permettant l'accès aux méthodes proposées par un objet distant
est téléchargé lors de la recherche de cet objet par registry.lookup.
Le téléchargement est fait par le serveur web donné par le paramètre
server.codebase défini au lancement du serveur qui
implémente l'objet distant.
Dans notre cas, HostImplem_stub sera téléchargé lors
de l'exécution de la ligne suivante :
Host remoteHost = (Host)registry.lookup(hostServiceName);
ce stub sera utilisé pour accéder à la méthode distante
migrate pendant l'exécution de :
remoteHost.migrate(agent);
L'agent de notre application, appelé AgentImplem, est implémenté à partir
de l'interface Agent, comme indiqué ci-dessous :
public class AgentImplem implements Agent{
private Float myMinPrice = new Float(Float.MAX_VALUE);
private String myIngredient="";
private String myMinHost = "";
...
}
et dans le code de l'initiateur (Initiator.java), on trouve :
public static void main(String args[]){
...
AgentImplem agent = new AgentImplem(Integer.parseInt(args[0]), hostsToVisit, args[1]);//args[0] - rmi-registry port; args[1] product; args[2] host {host} ...
... //obtain the first remote host to visit
remoteHost.migrate(agent);//migrate the agent towards the first remote host
...
}
Le site (HostImplem) qui offre la méthode migrate va créer un thread pour
gérer l'agent, comme on l'indique ici :
public void migrate(Agent agent){
AgentThread myThread = new AgentThread(agent, myStore);
myThread.start();
}
la classe AgentThread hérite de Thread :
public class AgentThread extends Thread{ ... }
On vous demande :
- de compléter trois fichiers ;
- de lancer l'application ;
- de suivre son fonctionnement.
Fichiers à compléter :
Dans les fichiers AgentThread.java, Initiator.java et HostImplem.java
compléter les lignes où se trouvent des //TODO ....
Note : n'oublier pas de compiler avant de tenter le lancement - utiliser l'exemple des exercices précédents.
Pour lancer l'application :
- Lancer rmiregistry sur chacune des 3 machines où vont tourner
les serveurs (les instances de HostImplem) - sur le même port :
> rmiregistry <votre port spécifique>
- Lancer les serveurs sur chacune de ces 3 machines differentes,
correpondant aux trois magasins : Mag1, Mag2, Mag3, respectivement
> java -Djava.security.policy=security.policy manager.HostImplem <votre port spécifique> Mag<x>
- Lancer rmiregistry sur la machine du client (car l'initiateur doit s'enregistrer en tant qu'objet distant) ;
- Lancer le client cette machine là :
> java -Djava.security.policy=security.policy client.Initiator <votre port spécifique> <votre nom de produit> <la liste des machines des serveurs>
Lorsque l'application fonctionnera, suivre et expliquer les différents messages affichés par les hôtes distants ainsi que par l'initiateur.
- Exécuter l'application exo3 en partageant le code binaire pendant l'exécution (ex : via un serveur http, sans utiliser le partage de NFS) - voir l'exercice I.4)
- Remplacer les tableaux avec des ArrayList - ex : dans la classe AgentImpl.java ..etc
- Remplacer les attributs publiques (ex : dans la classe Store) par des attributs privés, accessibles via des méthodes get et set.
©(Copyright)
ada.diaconescu _at_ telecom-paristech.fr ;
dupouy@enst.fr ;
singhoff@enst.fr.