Previous Up Next

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
&
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: 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

9.5  CORBA

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 : 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


Previous Up Next