Chapter 9 Remote Method Invocation
Le RMI (Remote Method Invocation) permet d'invoquer des méthodes d'un objet distant, c'est-à-dire
appartenant à une autre JVM, sur une autre machine. Cela permet donc de définir
des
architectures de type client/serveur, comme les
``Remote Procedure Calls'' POSIX.
RMI se rapproche de plus en plus de CORBA (qui est ``indépendant'' du langage), et que l'on
traitera rapidement en fin de chapitre.
Pour plus de détails, on se reportera à [RR]
(duquel on a tiré certains exemples). On pourra
également consulter la page
http://java.sun.com/products/jdk/rmi/.
9.1 Architecture
Une application RMI (ou client/serveur de façon générale) se compose d'un
serveur de méthodes, et de clients. Un serveur est essentiellement une instance
d'une classe ``distante'' que l'on peut référencer
d'une autre machine, et sur laquelle on peut faire agir des méthodes ``distantes''.
Il faut donc définir une
classe qui implémente la méthode distante (serveur),
dont les méthodes renvoient des objets pouvant ``transiter sur le réseau''. Ce sont
les objets instances de classes implémentant l'interface Serializable.
Ce sont des classes dont les objets peuvent
être transcrits en ``stream'', c'est-à-dire en flots d'octets.
La plupart des classes (et de leurs sous-classes) de base
String, HashTable, Vector, HashSet, ArrayList etc. sont Serializable.
On peut aussi ``forcer'' une classe à implémenter Serializable, mais cela est souvent un peu
délicat. Il y a plusieurs méthodes selon les cas, et il faut parfois, dans les cas les plus
difficiles, définir les méthodes:
private void writeObject(ObjectOutputStream aOutputStream) throws IOException;
private void readObject(ObjectInputStream aInputStream)
throws ClassNotFoundException, IOException;
responsables respectivement de décrire un objet sous forme de flot d'octets et de reconstituer l'état
d'un objet à partir d'un flot d'octets.
Dans le cas où on passe une classe Serializable, il faut que la définition de cette
classe soit connue (c'est-à-dire copiée sur les différentes machines, ou accessible par NFS)
des clients et du serveur.
Il peut y avoir à gérer la politique de sécurité (sauf pour les objets ``simples'', comme
String). On en reparlera dans un deuxième temps.
Plus généralement, une méthode d'un serveur peut également renvoyer des objets instances de classes
Remote.
Les classes Remote sont elles des classes dont
les instances sont des objets ordinaires dans l'espace d'adressage
de leur JVM, et pour lesquels
des ``pointeurs'' sur ces objets peuvent être envoyés
aux autres espaces d'adressage.
Il faut également
programmer un ou plusieurs
clients qui utilisent les méthodes distantes et initialisent un
``registre'' d'objets distants (``rmiregistry'') qui associe aux noms d'objets
l'adresse des machines qui les contiennent.
Pour pouvoir compiler séparément (c'est bien nécessaire dans le cas distribué!)
serveurs et clients, il faut définir les interfaces des classes utilisées. Il y a un
certain nombre de fichiers générés par des outils RMI qui vont également se révéler
indispensables.
9.2 Exemple: RMI simple
Prenons un premier exemple, où l'on retourne une valeur Serializable:
il va s'agir d'un
``Hello World'' distribué. On construit les classes de la figure 9.1.
Figure 9.1: Les classes d'un ``Hello World'' distribué''
Définissons d'abord l'interface de l'objet distant:
import java.rmi.*;
public interface HelloInterface extends Remote {
public String say() throws RemoteException;
}
Son unique méthode say est celle qui va afficher à l'écran du client ``Hello World''.
Son implémentation est:
import java.rmi.*;
import java.rmi.server.*;
public class Hello extends UnicastRemoteObject
implements HelloInterface {
private String message;
public Hello(String msg) throws RemoteException {
message = msg; }
public String say() throws RemoteException {
return message;
}
}
On peut compiler ces deux sources JAVA:
javac HelloInterface.java
javac Hello.java
ce qui crée HelloInterface.class et Hello.class.
Il faut maintenant créer les ``stubs'' et les ``squelettes''. Le rôle des stubs et des squelettes est
le suivant. Le fournisseur de service exporte un type de référence. Lorsque le client reçoit cette référence,
RMI charge un stub qui transcrit les appels à cette référence en un appel au fournisseur.
Ce processus de marshalling utilise la serialization (séquentialisation) des objets.
Du côté du serveur, un squelette effectue l'unmarshalling et invoque la méthode adéquate du serveur.
La création des stubs et squelette se fait
par la commande rmic sur le code de l'objet distant:
rmic Hello
Cela crée Hello_Stub.class et Hello_Skel.class.
Passons maintenant au client:
import java.rmi.*;
public class HelloClient {
public static void main(String[] argv) {
try {
HelloInterface hello =
(HelloInterface) Naming.lookup
("//cher.polytechnique.fr/Hello");
System.out.println(hello.say());
} catch(Exception e) {
System.out.println("HelloClient exception: "+e);
}
}
}
Ici on a supposé que le serveur - c'est-à-dire la classe sur laquelle seront repercutées
les demandes d'un client -
sera toujours sur cher. On verra dans d'autres exemples, des méthodes
plus souples.
Enfin, le serveur lui-même est la classe HelloServer suivante:
import java.rmi.*;
public class HelloServer {
public static void main(String[] argv) {
try {
Naming.rebind("Hello",new Hello("Hello, world!"));
System.out.println("Hello Server is ready.");
} catch(Exception e) {
System.out.println("Hello Server failed: "+e);
}
}
}
On peut maintenant compiler et démarrer le serveur
en faisant bien
attention au CLASSPATH (qui doit au moins contenir . et/ou les répertoires contenant
les .class nécessaires, accessibles de toutes les machines sous NFS).
javac HelloClient.java
javac HelloServer.java
Pour exécuter les programmes client et serveur, il faut démarrer le registre
(ou serveur) de noms:
rmiregistry &
ou éventuellement, rmiregistry port
& où port
est un numéro de port sur lequel le démon
rmiregistry va communiquer. Ce numéro est par défaut 1099, mais quand plusieurs utilisateurs utilisent la
même machine, il faut que plusieurs démons cohabitent sur la même machine, mais communiquent sur des ports distinct.
On peut maintenant démarrer le serveur (Hello):
[goubaul1@cher HelloNormal]$ java HelloServer
Hello Server is ready.
Enfin, on démarre des clients et on lance une exécution:
[goubaul1@loire HelloNormal]$ java HelloClient
Hello, world!
9.3 RMI avec Callback
L'idée vient de la programmation ``événementielle'', typique d'interfaces graphiques, par exemple
AWT, que vous avez vu en cours de tronc-commun.
Les ``clients'' vont s'enregistrer auprès d'un serveur.
Le ``serveur'' va les ``rappeler'' uniquement lorsque certains événements se produisent.
Le client n'a pas ainsi à faire de ``l'active polling'' (c'est-à-dire à demander des
nouvelles continuellement au serveur) pour être mis au courant des événements.
Le problème est, comment notifier un objet (distant) de l'apparition d'un événement?
Il suffit en fait de passer la référence de l'objet à rappeler au serveur chargé de suivre les
événements.
A l'apparition de l'événement, le serveur va invoquer la méthode de notification du client.
Ainsi,
pour chaque type d'événement, on crée une interface spécifique (pour le client qui veut en
être notifié),
et les clients potentiels à notifier doivent s'enregistrer auprès d'une implémentation de cette
interface.
Cela implique que ``clients'' et ``serveurs'' sont tous à leur tour ``serveurs'' et ``clients''.
Voici un petit exemple, tiré de [RR], dont les classes et interfaces sont résumées à la
figure 9.2.
Figure 9.2: Organisation des classes, pour un RMI avec callback
On définit l'interface associée à un événement particulier, ici un
changement de température:
interface TemperatureListener extends java.rmi.Remote {
public void temperatureChanged(double temperature)
throws java.rmi.RemoteException;
}
C'est la méthode de notification de tout client intéressé par cet événement. C'est
forcément un
objet Remote.
L'interface du serveur d'événements
doit au moins pouvoir permettre l'inscription et la désinscription de clients voulant être notifiés:
interface TemperatureSensor extends java.rmi.Remote {
public double getTemperature() throws
java.rmi.RemoteException;
public void addTemperatureListener
(TemperatureListener listener)
throws java.rmi.RemoteException;
public void removeTemperatureListener
(TemperatureListener listener)
throws java.rmi.RemoteException; }
Le serveur
doit être une sous-classe de UnicastRemoteObject (pour être un serveur),
doit implémenter l'interface TemperatureListener pour pouvoir rappeler les clients en
attente,
ainsi que Runnable ici pour pouvoir avoir un thread indépendant
qui simule les changements de température.
En voici l'implémentation:
import java.util.*;
import java.rmi.*;
import java.rmi.server.*;
public class TemperatureSensorServer
extends UnicastRemoteObject
implements TemperatureSensor, Runnable {
private volatile double temp;
private Vector list = new Vector();
Le vecteur list contiendra la liste des clients.
On peut maintenant écrire le constructeur (qui prend une température initiale)
et une méthode de récupération de la température:
public TemperatureSensorServer()
throws java.rmi.RemoteException {
temp = 98.0; }
public double getTemperature()
throws java.rmi.RemoteException {
return temp; }
On a aussi des méthodes d'ajout et de retrait de clients:
public void addTemperatureListener
(TemperatureListener listener)
throws java.rmi.RemoteException {
System.out.println("adding listener -"+listener);
list.add(listener); }
public void removeTemperatureListener
(TemperatureListener listener)
throws java.rmi.RemoteException {
System.out.println("removing listener -"+listener);
list.remove(listener); }
On construit également un thread responsable du changement aléatoire de la température:
public void run()
{ Random r = new Random();
for (;;)
{ try {
int duration = r.nextInt() % 10000 +2000;
if (duration < 0) duration = duration*(-1);
Thread.sleep(duration); }
catch(InterruptedException ie) {}
int num = r.nextInt();
if (num < 0)
temp += .5;
else
temp -= .5;
notifyListeners(); } }
notifyListeners() est la méthode suivante, chargée de diffuser le changement d'événements
à tous les clients enregistrés:
private void notifyListeners() {
for (Enumeration e = list.elements(); e.hasMoreElements();)
{ TemperatureListener listener =
(TemperatureListener) e.nextElement();
try {
listener.temperatureChanged(temp);
} catch(RemoteException re) {
System.out.println("removing listener -"+listener);
list.remove(listener); } } }
on fait simplement appel, pour chaque client, à la méthode de notification
temperatureChanged.
Enfin, on enregistre le service auprès du rmiregistry (éventuellement fourni à la ligne de commande, contrairement au premier exemple de RMI simple):
public static void main(String args[]) {
System.out.println("Loading temperature service");
try {
TemperatureSensorServer sensor =
new TemperatureSensorServer();
String registry = "localhost";
if (args.length >= 1)
registry = args[0];
String registration = "rmi://"+registry+
"/TemperatureSensor";
Naming.rebind(registration,sensor);
et on démarre le thread
en charge de changer aléatoirement la température, et de gérer des exceptions:
Thread thread = new Thread(sensor);
thread.start(); }
catch (RemoteException re) {
System.err.println("Remote Error - "+re); }
catch (Exception e) {
System.err.println("Error - "+e); } } }
Passons maintenant aux clients:
import java.rmi.*;
import java.rmi.server.*;
public class TemperatureMonitor extends UnicastRemoteObject
implements TemperatureListener {
public TemperatureMonitor() throws RemoteException {}
Il étend UnicastRemoteObject car c'est un serveur également!
De même, il implémente
TemperatureListener. On remarquera qu'on a un
constructeur vide: c'est celui d'Object qui est appelé.
Maintenant, on effectue la recherche du service serveur d'événements:
public static void main(String args[]) {
System.out.println("Looking for temperature sensor");
try {
String registry = "localhost";
if (args.length >= 1)
registry = args[0];
String registration = "rmi://"+registry+
"/TemperatureSensor";
Remote remoteService = Naming.lookup(registration);
TemperatureSensor sensor = (TemperatureSensor)
remoteService;
Et on crée un moniteur que l'on enregistre auprès du serveur d'événements:
double reading = sensor.getTemperature();
System.out.println("Original temp : "+reading);
TemperatureMonitor monitor = new TemperatureMonitor();
sensor.addTemperatureListener(monitor);
On n'oublie pas de gérer les différentes exceptions possibles:
} catch(NotBoundException nbe) {
System.out.println("No sensors available"); }
catch (RemoteException re) {
System.out.println("RMI Error - "+re); }
catch (Exception e) {
System.out.println("Error - "+e); } }
Enfin, on implémente la méthode de rappel:
public void temperatureChanged(double temperature)
throws java.rmi.RemoteException {
System.out.println("Temperature change event : "
+temperature);
}
On peut maintenant compiler le tout:
[goubaul1@cher Ex3]$ javac *.java
[goubaul1@cher Ex3]$ rmic TemperatureMonitor
[goubaul1@cher Ex3]$ rmic TemperatureSensorServer
Puis exécuter le programme distribué:
[goubaul1@cher Ex3]$ rmiregistry &
[goubaul1@cher Ex3]$ java TemperatureSensorServer
Loading temperature service
On crée un premier client (sur loire):
[goubaul1@loire Ex3]$ rmiregistry &
[goubaul1@loire Ex3]$ java TemperatureMonitor cher
Looking for temperature sensor
Original temp : 100.0
Temperature change event : 99.5
Temperature change event : 100.0
Temperature change event : 100.5
Temperature change event : 100.0
Temperature change event : 100.5
Temperature change event : 101.0
Temperature change event : 100.5
Temperature change event : 100.0
Temperature change event : 100.5
Temperature change event : 101.0
Temperature change event : 101.5
On voit alors sur la console de cher:
adding listener -TemperatureMonitor_Stub[RemoteStub
[ref: [endpoint:[129.104.254.64:3224](remote),
objID:[6e1408:f29e197d47:-8000, 0]]]]
Rajoutons un moniteur sur doubs:
[goubaul1@doubs Ex3]$ rmiregistry &
[goubaul1@doubs Ex3]$ java TemperatureMonitor cher
Looking for temperature sensor
Original temp : 101.5
Temperature change event : 102.0
Temperature change event : 102.5
Temperature change event : 103.0
Temperature change event : 102.5
Temperature change event : 103.0
Temperature change event : 103.5
Temperature change event : 102.5
Temperature change event : 102.0
Ce qui produit sur cher:
adding listener -TemperatureMonitor_Stub[RemoteStub
[ref: [endpoint:[129.104.254.57:3648](remote),
objID:[6e1408:f29de7882e:-8000, 0]]]]
On voit bien que les températures et événements sont synchronisés avec l'autre client
sur loire:
Temperature change event : 102.0
Temperature change event : 102.5
Temperature change event : 103.0
Temperature change event : 102.5
Temperature change event : 103.0
Temperature change event : 103.5
^C
[goubaul1@loire Ex3]$
On a interrompu le moniteur sur loire, du coup, on voit à la console, sur cher:
removing listener -TemperatureMonitor_Stub[RemoteStub
[ref: [endpoint:[129.104.254.64:3224](remote),
objID:[6e1408:f29e197d47:-8000, 0]]]]
On interrompt par Control-C sur doubs, et on voit sur cher:
removing listener -TemperatureMonitor_Stub[RemoteStub
[ref: [endpoint:[129.104.254.57:3648](remote),
objID:[6e1408:f29de7882e:-8000, 0]]]]
9.4 RMI avec réveil de serveur
Il reste des problèmes avec les méthodes précédentes.
Même si les services sont peu souvent utilisés, il faut que les
serveurs tournent en permanence.
De plus les serveurs doivent créer et exporter les objets distants. Tout cela
représente une consommation inutile de mémoire et de CPU.
Depuis JAVA 2 on peut activer à distance un objet distant.
Cela
permet d'enregistrer un service RMI sans l'instancier,
le service RMI défini par cette méthode étant inactif
jusqu'à ce qu'un client y fasse appel, et ``réveille'' ainsi le
serveur.
Un processus démon est chargé d'écouter les requêtes et
de réveiller les services: rmid.
A la place du service, une sorte de ``proxy'' est enregistré
auprès du serveur de services RMI (rmiregistry).
Contrairement aux serveurs instances de UnicastRemoteObject,
cette ``fausse'' référence ne s'exécute que pendant un cours instant,
pour inscrire le service auprès du rmiregistry, puis
aux moments des appels au service.
Quand le client appelle ce service, le rmiregistry
fait appel à cette fausse référence.
Celle-ci vérifie si elle a un pointeur sur le vrai service.
Au cas où elle n'en a pas,
elle fait appel au démon rmid pour créer une
instance du service (cela prend un certain temps, la première fois),
puis transmet l'appel au service nouvellement créé.
En pratique
cela est transparent pour le client:
le client n'a en rien besoin de savoir comment le service est
implémenté (activation à distance, ou comme serveur
permanent).
La création du service est un peu différente du cas
du serveur résident, mais son code reste similaire.
Les différentes classes et interfaces sont représentées à la figure
9.3.
Celles-ci font appel aux paquetages RMI suivants:
-
java.rmi définit l'interface RemoteInterface,
et les exceptions,
- java.rmi.activation (depuis JAVA2): permet l'activation
à distance des objets,
- java.rmi.dgc: s'occupe du ramassage de miettes dans
un environnement distribué,
- java.rmi.registry fournit l'interface permettant de
représenter un
rmiregistry, d'en créer un, ou d'en
trouver un,
- java.rmi.server fournit les classes et interfaces pour
les serveurs RMI.
On pourra se reporter à la documentation en ligne:
http://java.sun.com/j2se/1.4.2/docs/guide/rmi/index.html
Figure 9.3: Les différentes classes et interfaces RMI
Commençons par examiner la façon dont on peut définir
l'interface du serveur
activable à distance.
L'interface du service ne contient que les méthodes désirées
sans argument, mais devant renvoyer une exception de type
java.rmi.RemoteException. Ceci est imposé par rmic:
public interface InterfaceObjetActivable
extends java.rmi.Remote
{ public void MethodeA() throws java.rmi.RemoteException;
public void MethodeB() throws java.rmi.RemoteException;
... }
Cette interface peut être en fait implémentée par un objet activable
à distance ou un service permanent UnicastRemoteObject.
L'implémentation
du serveur
doit être une instance de
java.rmi.activation.Activatable,
(en fait, il y a aussi moyen de faire autrement)
et doit implémenter un constructeur particulier,
ainsi que les méthodes désirées MethodeA etc.
On aura typiquement:
public class ImplementationObjetActivable extends
java.rmi.activation.Activatable
implements InterfaceObjetActivable
{
public ImplementationObjetActivable (
java.rmi.activation.ActivationID activationID,
java.rmi.MashalledObject data) throws
java.rmi.RemoteException
{ super(activationID,0); }
(appelle le constructeur de la classe parent Activatable(ID,0))
Enfin, il faudra implémenter la méthode rendant le service attendu:
public void doSomething()
{
...
}
}
L'activation de l'objet
est
en général incluse dans le main de l'implémentation
de l'objet (service). Le
code est assez compliqué du fait que doivent être gérés
la politique de sécurité,
les propriétés et droits du code exécutable,
et enfin l'inscription auprès de rmid et rmiregistry.
9.4.1 Exemple d'une ``lampe''
On va construire les classes représentées à la figure 9.4.
Figure 9.4: Classes pour la lampe activable à distance
L'interface du serveur activable est:
package examples;
import java.rmi.*;
public interface RMILightBulb extends Remote {
public void on() throws RemoteException;
public void off() throws RemoteException;
public boolean isOn() throws RemoteException;
}
Et un client possible est décrit plus bas.
Remarquez
qu'il serait le même si on avait utilisé une implémentation
du serveur comme instance de UnicastRemoteObject.
package examples;
import java.rmi.*;
public class LightBulbClient {
public static void main(String args[]) {
System.out.println("Recherche d'une lampe...");
A ce point du code, on doit utiliser rmiregistry:
/* voir Server */
System.setSecurityManager(new RMISecurityManager());
try {
String registry = "localhost";
if (args.length >= 1)
registry = args[0];
String registration = "rmi://"+registry+
"/ActivatableLightBulbServer";
Remote remoteService = Naming.lookup(registration);
RMILightBulb bulbService = (RMILightBulb) remoteService;
Cela permet de spécifier la machine où se trouve le serveur, et le rmiregistry correspondant.
Maintenant on procède à l'appel aux services:
System.out.println("Appel de bulbservice.on()");
bulbService.on();
System.out.println("Lampe : "+bulbService.isOn());
System.out.println("Appel de bulbservice.off()");
bulbService.off();
System.out.println("Lampe : "+bulbService.isOn());
Enfin on doit récuper les exceptions:
} catch(NotBoundException nbe) {
System.out.println("Pas de lampe dans le
repertoire de services!");
} catch(RemoteException re) {
System.out.println("RMI Error - "+re);
} catch(Exception e) {
System.out.println("Error - "+e);
}
}
}
Le serveur activable à distance est maintenant:
package examples;
import java.rmi.*;
import java.rmi.activation.*;
public class ActivatableLightBulbServer
extends Activatable implements RMILightBulb {
public ActivatableLightBulbServer
(ActivationID activationID,
MarshalledObject data)
throws RemoteException {
super(activationID,0);
setBulb(false); }
On arrive enfin aux services eux-mêmes:
public boolean isOn() throws RemoteException {
return getBulb();
}
public void setBulb(boolean value)
throws RemoteException {
lightOn = value; }
public boolean getBulb() throws RemoteException {
return lightOn; } }
private boolean lightOn;
public void on() throws RemoteException {
setBulb(true);
}
public void off() throws RemoteException {
setBulb(false);
}
On en arrive au main de Setup et à la gestion de la
politique de sécurité:
package examples;
import java.rmi.*;
import java.rmi.activation.*;
import java.util.Properties;
public class Setup {
public static void main(String[] args) throws Exception {
System.setSecurityManager(new RMISecurityManager());
Properties props = new Properties();
props.put("java.security.policy",
"/users/profs/info/goubaul1/Cours03/RMI/Ex2/activepolicy");
La politique de sécurité doit être spécifiée à la ligne
de commande; on le verra plus loin quand on parle de l'exécution.
On doit également créer un descripteur de groupe:
ActivationGroupDesc.CommandEnvironment ace = null;
ActivationGroupDesc exampleGroup =
new ActivationGroupDesc(props, ace);
qui
permet d'associer des propriétés et des droits à l'exécution.
On récupére le descripteur du démon rmid, puis
on y enregistre le descripteur de groupe:
ActivationGroupID agi =
ActivationGroup.getSystem().registerGroup(exampleGroup);
On crée maintenant un descripteur d'activation du code.
Au sein du groupe nouvellement créé,
on passe le nom de la classe, l'endroit où se trouve le code, et une
donnée optionnelle, une version ``serialized'' de l'objet
distant (de type MarshalledObject):
String location =
"file:/users/profs/info/goubaul1/Cours03/RMI/Ex2/";
MarshalledObject data = null;
ActivationDesc desc = new ActivationDesc
(agi, "examples.ActivatableLightBulbServer",
location, data);
Revenons au code.
On indique l'endroit où se trouve le
code exécutable:
java.io.File location = new java.io.File(".");
String strLoc = "file://"
+URLEncoder.encode(location.getAbsolutePath(),"UTF-8");
System.out.println("Code \`a ex\'ecuter : "+strLoc);
L'encodage URLEncoder permet d'être compatible avec
les systèmes Windows etc. On peut aussi utiliser un serveur
http://... pour le chargement dynamique de classes.
Sur un petit système en effet, on peut gérer les stubs créés: il suffit en effet
de recopier, ou d'avoir accès par NFS à tous ces fichiers
sur toutes les machines considérées. Mais ce n'est pas pratique
tout le temps, car cela limite singulièrement la distance entre le client
et le serveur. En fait, on
peut acccéder aux serveurs par adresse http:
java -Djava.rmi.server.codebase=http://hostname:port/path
On enregistre maintenant l'objet auprès du rmid:
RMILightBulb stub = (RMILightBulb)Activatable.register(desc);
System.out.println("Got stub for ActivatableLightBulbServer");
Cela
renvoie un ``stub'' que l'on peut enregistrer auprès du serveur
de services rmiregistry:
Naming.rebind("ActivatableLightBulbServer", mri);
System.out.println("Exported ActivatableLightBulbServer");
System.exit(0); } }
Cela est
similaire au cas d'implémentation par UnicastRemoteObject -
sauf que c'est le ``stub'' et pas l'objet lui-même qui est inscrit.
9.4.2 Complément: politiques de sécurité
Un programme JAVA peut dans certaines circonstances avoir à s'associer une ``politique de
sécurité'' donnant les droits d'objets provenant de certaines autres machines.
C'est typiquement le cas pour des objets Serializable transitant entre applications distantes.
Par
exemple, la connection par socket à une machine distante passe par la méthode
checkConnect(String host, int port) du SecurityManager courant
(en fait, c'est une sous-classe RMISecurityManager pour les politiques concernant RMI),
définissant la politique
de sécurité courante. En cas de non autorisation, on obtient des messages du type:
java.security.AccessControlException: access denied
(java.net.SocketPermission 127.0.0.1:1099 connect,resolve)
Pour associer une politique de sécurité à un code JAVA, il faut
construire un objet instance de SecurityManager,
surcharger les fonctions check... dont on veut changer la politique de sécurité,
et invoquer la méthode de la classe System, setSecurityManager, en lui passant
cet objet créé plus haut.
On fera par exemple:
System.setSecurityManager(new RMISecurityManager() {
public void checkConnect(String host, int port) {}
public void checkConnect(String host, int port,
Object context) {}
});
Ce code
utilise une classe anonyme, sous-classe de RMISecurityManager
dans laquelle les méthodes
checkConnect retournent faux. On n'a donc
aucune permission de créer des sockets pour communiquer avec
d'autres machines.
A partir de JAVA 1.2, une autre méthode est fournie en plus.
Toutes les méthodes check... font appel à la méthode checkPermission
à laquelle on passe le type de permission souhaité.
Par exemple checkConnect appelle checkPermission sur un objet SocketPermission.
La liste des permissions autorisées est gérée par la classe Permissions.
Chaque programme a une politique de sécurité courante, instance de Policy,
qui est Policy.get Policy().
On peut modifier la politique de sécurité courante (comme avant avec le
SecurityManager) en faisant appel à Policy.setPolicy.
Un fichier de permissions (lu au démarrage d'un programme) de la forme:
grant codeBase "file:/home/goubault/-" {
permission java.security.AllPermission;
};
donnera tous les droits à tous les programmes dans /home/goubault/
Un autre exemple un peu plus fin serait:
grant codeBase "file:/home/goubault/-" {
permission java.net.SocketPermission
"129.104.254.54:1024-", "connect, accept";
}
qui permet d'utiliser les sockets sur loire.polytechnique.fr, avec un numéro de
port supérieur à 1024.
On peut aussi utiliser policytool pour générer ces fichiers. En pratique,
pour utiliser le fichier de permissions en question (fichier), à l'exécution du main
de la classe maclasse, tout en spécifiant le répertoire contenant le code, on fera:
java -Djava.security.policy=FICHIER
-D-Djava.rmi.server.codebase=file:/LOCATION
MACLASSE
Revenons maintenant à l'exemple de la section précédente.
Il faut le compiler, et créer les fichiers stubs et squelettes:
javac -d . RMILightBulb.java
javac -d . LightBulbClient.java
javac -d . Setup.java
javac -d . ActivatableLightBulbServer.java
rmic ActivatableLightBulbServer
(le -d . pour créer les répertoires correspondants aux packages)
On lance maintenant les démons
sur la machine correspondant au serveur (ici loire). On commence par
lancer rmiregistry (avec un CLASSPATH ne contenant pas les .class appelés par
le client)
puis par spécifier la politique de sécurité. Par exemple ici, on donne toutes
les permissions (dans le fichier activepolicy):
grant {
permission java.security.AllPermission;
};
Enfin on lance rmid -J-Djava.security.policy=activepolicy
Exécutons enfin ce programme:
[goubaul1@loire Ex2]$ java
-Djava.security.policy=activepolicy
-Djava.rmi.server.codebase=file:/. examples.Setup
Got the stub for the ActivatableLightBulbServer
Exported ActivatableLightBulbServer
(pour spécifier la politique de sécurité)
On exécute le client
sur cher:
[goubaul1@cher Ex2]$ java -Djava.security.policy=activepolicy
examples.LightBulbClient loire
Looking for light bulb service
Invoking bulbservice.on()
Bulb state : true
Invoking bulbservice.off()
Bulb state : false
Compararons brièvement avec une implémentation de type
UnicastRemoteObject
Le serveur lui-même (voir ~goubaul1/RMI/Ex1) est:
public class RMILightBulbImpl extends
java.rmi.server.UnicastRemoteObject
implements RMILightBulb {
public RMILightBulbImpl()
throws java.rmi.RemoteException {
setBulb(false);
}
private boolean lightOn;
public void on()
throws java.rmi.RemoteException {
setBulb(true); }
public void off()
throws java.rmi.RemoteException {
setBulb(false); }
public boolean isOn()
throws java.rmi.RemoteException {
return getBulb(); }
public void setBulb(boolean value) {
lightOn = value; }
public boolean getBulb() {
return lightOn; } }
Mais, l'interface du serveur reste la même:
public interface RMILightBulb extends java.rmi.Remote {
public void on() throws java.rmi.RemoteException;
public void off() throws java.rmi.RemoteException;
public boolean isOn() throws java.rmi.RemoteException;
}
Et on a exactement le même client.
L'exécution se ferait de la façon suivante:
sur cher (serveur):
[goubaul1@cher Ex1]$ java LightBulbServer
Loading RMI service
RemoteStub [ref: [endpoint:[129.104.254.54:1867](local),
objID:[0]]]
Et sur loire (client):
[goubaul1@loire Ex1]$ java LightBulbClient cher
Looking for light bulb service
Invoking bulbservice.on()
Bulb state : true
Invoking bulbservice.off()
Bulb state : false
Corba est une architecture distribuée dans laquelle des clients
émettent des requêtes à destination d'objets, qui s'exécutent dans des
processus serveurs. Ces objets répondent aux clients par la
transmission d'informations bien que ces deux éléments (client et
serveur) soient localisés dans des espaces mémoire différents,
généralement sur des machines distantes.
Lors du développement d'une application Corba, le dialogue entre les
différents constituants est rendu totalement transparent pour le
développeur. Le serveur déclare mettre à disposition ses objets et le
client se contente de demander une connexion à un ou à certains de ces
objets, sans pour autant en connaître obligatoirement la localisation
ou le format. Dans ces deux cas, l'ORB (Object Request Broker) se
charge de localiser les objets, de les charger en mémoire et de
transmettre au serveur les demandes du client (voir la Figure
9.5). Il assure ensuite des
opérations de gestion ou de maintenance, comme la
gestion des erreurs ou leur destruction. Dans cette architecture,
l'application
cliente ne se préoccupe pas des détails d'implémentation des objets
serveurs, elle se contente de s'y connecter et de les utiliser.
L'ORB prend en charge la communication entre les différents composants
du système distribué.
Le dialogue Corba, entre le client, le serveur et l'ORB est décrit à la figure
9.5.
Le cycle de développement CORBA est représenté à la figure
9.6.

Figure 9.5: Le dialogue CORBA

Figure 9.6: Le cycle de développement CORBA
On commence par
écrire l'interface de l'objet en IDL. C'est-à-dire que l'on
définit dans
le langage de description d'interfaces Corba (``Interface Definition Language'')
des opérations disponibles sur l'objet.
Ensuite, on
compile l'IDL. Cela engendre des modules stub (pour le client)
et skeleton (pour le serveur).
Ces modules gèrent l'invocation des méthodes distantes.
Enfin, on implémente l'objet. On dérive une classe à partir du squelette
généré à l'étape précédente. Les données et méthodes de cette classe doivent
correspondre à celles qui sont décrites dans l'IDL. Lors de l'appel des
méthodes sur l'objet, le code défini sera exécuté par l'application serveur.
Ensuite, on
rédige l'application serveur : ce code sert à initialiser l'objet
et à le mettre à disposition des différentes applications clientes.
On compile le serveur. Cela génère l'exécutable de l'application serveur.
On réalise l'application cliente : cette application se connecte à l'objet
distant pour lui demander d'exécuter des méthodes. Elle traite ensuite les
données transmises par le serveur à l'issue de l'exécution des traitements distants.
Enfin, on compile le client. Cela génère l'exécutable de l'application client.
Donnons un exemple de l'écriture de l'interface de l'objet en IDL:
interface Vecteur {
typedef double vect[100]; // Definition du type vect
attribute long size; // size est un attribut
// de type long (entier)
attribute vect T ; // T est un attribut de
// type vect
double maxElement(); // maxElement est une
// methode retournant un double
void mult(in double a); // mult est une methode
// prenant pour parametre
// un double transmis
}; //par le client (mot cle in)
Comme on le voit dans cet exemple, les
principaux types définis par le langage IDL sont short et
long pour les entiers, float et double pour
les flottants, char et string pour les caractères.
Les
types composés sont les tableaux ainsi que d'autres structures
à l'aide des mots clés enum, struct ou union, etc.
Le mot clé attribute appartient au langage IDL, tout comme in
qui indique le sens de passage du paramètre (du client vers le serveur).
Symétriquement le type de passage des paramètres out
permet de réaliser un passage par adresse: par exemple
void maxElement(out double m); Le type inout
peut être utilisé pour les paramètres devant être lus et écrits.
On compile l'interface IDL par la commande idlj -fall Vecteur.idl.
Les principaux fichiers qui sont générés sont les suivants :
-
VecteurOperations.java : définition de l'interface qui contient
la signature des méthodes de l'objet Vecteur.
- Vecteur.java : définition de l'interface Vecteur qui
hérite de VecteurOperations.
- VecteurHelper.java : regroupe des méthodes destinées à vous
aider lors de l'utilisation des objets Vecteur.
- VecteurHolder.java : outils pour prendre en charge le passage
de paramètres avec les méthodes de l'objet Vecteur.
- _VecteurStub.java : stub de l'objet Vecteur.
Cette classe sera utilisée dans le code de notre client de façon
implicite, elle représentera l'objet distant en mettant à notre
disposition les méthodes qui s'y trouvent.
- _VecteurImplBase.java : squelette de l'objet Vecteur.
C'est la classe de base de notre futur objet vecteur. On en dérivera
une classe pour coder le fonctionnement réel de notre objet.
L'implémentation de l'objet se fait en
créant la classe de l'objet que nous souhaitons mettre à disposition: pour cela, on utilise le squelette
généré précédemment.
la classe VecteurImplem que nous allons créer étendra la classe _VecteurImplBase
produite par le compilateur IDL
et implémentera les méthodes définies par l'interface
VecteurOperations.java:
public class VecteurImplem extends _VecteurImplBase {
private int size=100;
private double T[];
VecteurImplem(double[] Tab) {
for(int i=0;i<size;i++) {
T[i]=Tab[i]; } }
VecteurImplem() {} ;
public int size() {
return size; }
public void size(int newSize) {
size = newSize; }
public double[] T() {
return T; }
public void T(double[] newT) {
for(int i=0;i<size;i++) {
T[i]=newT[i]; } }
public double maxElement () {
double m = T[0];
for (int i=1;i<size;i++) {
if (T[i]>m) { m = T[i]; } };
return m; }
public void mult(double a) {
for (int i=0;i<size;i++) {
T[i]=T[i]*a; } } }
La classe implémentant l'objet qui nous intéresse
doit implémenter toutes les méthodes définies
par l'interface VecteurOperations.
On doit donc déclarer en particulier
les attributs définis dans l'interface IDL.
Il faut naturellement définir
les méthodes de création des objets de la classe.
La méthode maxElement
devra retourner l'élément maximal du vecteur et la méthode mult
devra multiplier tous
les éléments du vecteur par la valeur du paramètre.
Pour le serveur CORBA,
nous allons créer une application
hôte, qui contiendra l'objet Vecteur:
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;
import org.omg.CORBA.*;
public class VecteurServeur {
public static void main(String args[]) {
try{
// Cree et initialise l'ORB
ORB orb = ORB.init(args, null);
// Cree le servant et l'enregistre sur l'ORB
VecteurImplem vecteurRef = new VecteurImplem();
orb.connect(vecteurRef);
// Obtention de la reference du RootPOA
org.omg.CORBA.Object objRef =
orb.resolve_initial_references("NameService");
NamingContext ncRef = NamingContextHelper.narrow(objRef);
// Enregistre la reference de l'objet
NameComponent nc = new NameComponent("Vecteur", "");
NameComponent path[] = {nc};
ncRef.rebind(path, vecteurRef);
// attend les invocations de clients
java.lang.Object sync = new java.lang.Object();
synchronized (sync) {
sync.wait(); }
} catch (Exception e) {
System.err.println("ERROR: " + e);
e.printStackTrace(System.out); } } }
Les principales actions réalisées par le serveur sont,
la création d'une instance de l'ORB,
d'une instance du servant (l'implémentation d'un objet Vecteur) et
son enregistrement sur l'ORB.
On obtient ainsi une référence à un objet CORBA pour le nommage et pour y enregistrer
le nouvel objet sous le nom ``Vecteur''.
Le serveur attend ensuite l'invocation de méthodes de l'objet.
On pourra se reporter à
http://java.sun.com/j2se/1.4.2/docs/guide/corba/index.html
pour plus de détails.
On a maintenant une application autonome qui, lors de son lancement, crée un
objet Vecteur et le met à disposition des autres applications.
Tout comme le
serveur, le client devra initialiser l'ORB. Il pourra ensuite se connecter
à l'objet et l'utiliser:
import org.omg.CosNaming.*;
import org.omg.CORBA.*;
public class VecteurClient {
public static void main(String args[]) {
try{
// Cree et initialise l'ORB.
ORB orb = ORB.init(args, null);
// Obtention de la reference du RootPOA
org.omg.CORBA.Object objRef =
orb.resolve_initial_references("NameService");
NamingContext ncRef = NamingContextHelper.narrow(objRef);
// trouve la r\'ef\'erence \`a l'objet
NameComponent nc = new NameComponent("Vecteur", "");
NameComponent path[] = {nc};
Vecteur vecteurRef = VecteurHelper.narrow
(ncRef.resolve(path));
// Utilisation de l'objet
...
} catch (Exception e) {
System.out.println("ERROR : " + e) ;
e.printStackTrace(System.out); } } }
Le client initialise ainsi l'ORB et obtient une référence au contexte de nommage.
Il y recherche ensuite ``Vecteur'' et reçoit une référence de cet objet Corba.
Il peut ensuite invoquer les méthodes de l'objet.
Pour plus de détails, on pourra se reporter à
http://java.sun.com/products/jdk/1.2/docs/guide/
idl/tutorial/GSapp.html.
Maintenant on compile toutes les applications ainsi écrites:
javac *.java
Pour l'exécution,
on procéde dans l'ordre suivant.
On commence par initialiser le serveur de noms:
tnameserv -ORBInitialPort 1050
1050 est le port utilisé par le serveur. Sous Solaris par exemple, il faut être root pour
utiliser un port inférieur à 1024.
Ensuite on initialise le serveur par
java VecteurServer -ORBInitialHost namerserverhost -ORBInitialPort 1050
nameserverhost est le nom de la machine sur laquelle s'exécute le
serveur de noms.
Enfin, on exécute le client par:
java VecteurClient -ORBInitialHost namerserverhost -ORBInitialPort 1050