Introduction à  RMI

Stéphane Genaud

Caractéristiques générales

JNI (Java Native Interface) doit permettre à  du code Java de s'interfacer avec d'autres langages (cibles principales le C/C++) mais son emploi semble délicat et peu convaincant.

Passer des objets est un mécanisme plus puissant que les RPC habituels qui sont limités à  des types primitifs (ou structures de type primitifs, e.g. CORBA).

Appel de procédure à  distance

proxy

L'objet distant possède un représentant (proxy) sur la machine locale; ceci permet d'invoquer une méthode de l'objet B comme si celui-ci était local. C'est la souche (stub) qui matérialise ce représentant.

Avant la version 5 du JDK, les souches sont des fichiers stub.classgénérés avec rmic.

Les souches (stubs)

Quand le client appelle la méthode souche dans la JVM locale, la souche :

Exemple 1

Exemple Simple: les arguments de la méthode invoquée sont des types primitifs (scalaires, e.g. int, float, ..) ou tableaux (e.g. String) mais sérialisables (java.io.Serializable).

Les différents programmes

Le service

Le service est ici un objet dont l'une des méthodes fait l'addition de deux nombres.

L'interface du service

Operation.java
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Operation extends Remote {

    public int addition(int a, int b) throws RemoteException ;

}

Remarque 1. En étendant l'interface java.rmi.Remote, les méthodes de l'interface sont automatiquement appelables depuis d'autres JVM. Tout objet qui implémentera cette interface deviendra "remote".

Remarque 2. On pourrait bien sûr définir d'autres methodes à  côté de addition.

L'implémentation du service

OperationImpl.java
import java.rmi.server.UnicastRemoteObject ;
import java.rmi.RemoteException ;
import java.net.InetAddress.* ;
import java.net.* ;

public class OperationImpl extends UnicastRemoteObject
  implements Operation  {

    public OperationImpl () throws RemoteException {
        super();
    };

    public int addition(int a, int b) throws RemoteException {
      return( a + b) ;
  }
}

Le service hérite de la classe java.rmi.UnicastRemoteObject pour activer l'objet distribué automatiquement.

Remarque. On pourrait définir directement l'enregistrement de l'objet en ajoutant, par exemple dans main :

   public static void main(String args[]) {
     try {
         OperationImpl une_op = new OperationImpl ();
         Naming.rebind("rmi://localhost/Operation",une_op) ;
         System.out.println("Service Operation enregistré");
      }  
      catch(Exception e) {
         System.out.println("Err. enreg. registry");
      }
    }
Cependant, on préfère isoler ce code assez générique dans un programme serveur.

Le serveur

Serveur.java est une classe qui
  1. crée une instance de l'objet demandé (le service),
  2. enregistre ce service dans le rmiregistry pour le rendre public. Deux classes permettent de le faire (choisir une seule des 2 solutions):
    • La classe java.rmi.Naming avec méthode bind() permet d'utiliser des URLs.
    • La classe java.rmi.registry.LocateRegistry et sa méthode getRegistry() est un niveau plus bas (elle est utilisée par Naming). Cette classe permet de créer dynamiquement un registry (avec createRegistry()).
  3. lance un gestionnaire de sécurité.
Dans un premier temps, nous simplifions le code en ne créant pas le gestionnaire de sécurité.
Le code du serveur est très générique : on réutilisera presque toujours la màªme chose pour démarrer des services.

Le serveur

En utilisant la classe java.rmi.registry.LocateRegistry :
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class Serveur {

      public static void main( String [] args) {

      try {
	   OperationImpl une_op = new OperationImpl ();

	   Registry registry = LocateRegistry.getRegistry();

	   registry.bind("Operation",une_op); 
	   System.out.println("Serveur pret"); 
       }
       catch (Exception e) { System.out.println(e) ; }
       }
}

Il y a possibilité de créer le registry dynamiquement (au lieu de lancer rmiregistry sur la ligne de commande) avec

           Registry registry = LocateRegistry.createRegistry(1099);

LocateRegistry.getRegistry() instancie une souche qui implémente l'interface Remote java.rmi.registry.Registry en demandant le registre sur localhost et le port 1099 (défauts). Utiliser getRegistry(String, int) pour spécifier hà´te et port. bind() est ensuite invoquée sur le stub pour que le registre attache le code de une_op au nom arbitraire "Operation".

Exemples :

 Registry registry = LocateRegistry.getRegistry("foo.services.com");
 SomeService service = (SomeService)registry.lookup("toto");
 service.requestService(...);

Le serveur

En utilisant la classe java.rmi.Naming :
import java.rmi.*;
import java.net.*;

public class Serveur {
   public static void main(String [] args) {
  
       try {
         OperationImpl une_op = new OperationImpl ();
         Naming.rebind("rmi://localhost/Operation",une_op) ;
         System.out.println("Serveur pret");
       }
	 catch (RemoteException re) { System.out.println(re) ; }
       catch (MalformedURLException e) { System.out.println(e) ; }
  }
}
Pour des raisons de sécurité les opérations bind, rebind ou unbind se font toujours avec un registry local.
  • L'enregistrement passe par une connexion de type socket sur localhost
  • Pour des raisons de sécurité les opérations bind, rebind ou unbind se font toujours avec un registry local (tournant sur localhost).

Gestionnaire de sécurité serveur

  • Définie par le fichier java.policy
  • Par défaut, celui du JDK, e.g.
    /usr/lib/jvm/java-6-openjdk/jre/lib/security/java.policy
  • Donner plus de permissivité :
        grant {
          permission java.net.SocketPermission 
               "*:80-65535","connect,accept,listen,resolve";
          permission java.security.AllPermission;
        };
       

Le client

Client.java : le code utilisant l'interface Operation.

Avant d'invoquer une méthode de l'interface, il faut obtenir une référence. Comme pour le serveur, deux classes permettent de le faire :

  • Avec la classe de plus haut niveau java.rmi.Naming : méthode lookup()
  • Avec la classe de bas niveau java.rmi.Registry et sa méthode lookup().

Le client

En utilisant la classe java.rmi.Naming :
import java.rmi.* ;
import java.net.MalformedURLException ;
import java.io.*;

public class Client {
  public static void main(String [] args) {
    if (args.length != 1)
        System.out.println("Usage : java Client Serveur");
    else {
        try {
          Operation o =
             (Operation) Naming.lookup("//"+args[0]+"/Operation");
          System.out.println("Client : 33 + 45 = ? ");
          int r = o.addition( 33 , 45 );
          System.out.println("Le serveur a calcule : 33+45="+ r );
        }
        catch (NotBoundException re) { System.out.println(re) ; }
        catch (RemoteException re) { System.out.println(re) ; }
        catch (MalformedURLException e) { System.out.println(e) ; }
     }
   }
}

Exécution

  1. Pour un test sur une seule machine, mettre les programmes du serveur et du client dans deux répertoires différents, serveur et client.
  2. Lancer le serveur

         cd serveur 
         rmiregistry &  # (si pas lance par programme serveur)
         java -Djava.security.policy=java.policy Serveur 
    

  3. Attention :le rmiregistry devra trouver le class file à l'aide de son $JAVA_PATH. Si celui ne contient que '.' et que le class file est dans un autre répertoire: exception.
  4. Lancer le client

        cd client
        java Client localhost
    

Lancer "start rmiregistry" sous windows, ou monopoliser un terminal pour lui.

Lancer le rmiregistry dans le màªme répertoire que le serveur (o๠se trouve OperationImpl_Stub.class) est bien commode car le CLASSPATH contient le répertoire courant par défaut. Si vous lancez rmiregistry ailleurs, il faut que le CLASSPATH pointe vers o๠se trouve OperationImpl_Stub.class.

Exemple 2: Passage d'objets

RMI permet de passer en paramètres des méthodes, des types simples, mais aussi des objets construits.

Comme précédemment, les arguments passés doivent être serializable.

La sérialisation écrit la structure de l'objet dans un flux transmissible sur le réseau, mais n'incorpore pas au flux les méthodes.

Exemple 2: Passage d'objets en paramètres

Imaginons un objet dont le rôle est d'exécuter du code.
pass object

  • Il accepte en entrée un objet quelconque qui a une méthode execute() (appelons Task l'interface correspondante),
  • télécharge dynamiquement le code de l'implémentation (cherche dans codebase),
  • invoque execute(),
  • retourne le résultat au client.

Objet Remote Compute (interface)

Comme précédemment, on doit spécifier au client l'interface offerte par l'objet distant. On la nomme Compute.java.

 
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Compute extends Remote {
    Object executeTask(Task t) throws RemoteException;
}

L'unique méthode accepte un objet de classe Task.

Objet Remote Compute (impl.)

Sur le serveur, l'implémentation ComputeImpl.java :
import java.rmi.server.UnicastRemoteObject ;
import java.rmi.RemoteException ;
import java.net.* ;

public class ComputeImpl extends UnicastRemoteObject implements Compute
{
    public ComputeImpl() throws RemoteException {
       /* constructeur de la classe mere UnicastRemoteObject */
       super();   
    }
    public Object executeTask(Task t) {
       /* les objets passes en arg. doivent fournir une methode execute() */
       return t.execute();   
    }
}

La classtask passé en paramètre

L'interface Task.java : spécifie

import java.io.Serializable;

public interface Task extends Serializable {
    Object execute();
}

L'objet task passé en paramètre (impl)

L'implémentation TaskPi.java:

public class TaskPi implements Task {

    public TaskPi(int digits) {  //constructeur
        this.digits = digits;
    }

    public Object execute() {
        return computePi(digits);
    }

    public static BigDecimal computePi(int digits) {

        /* ... calcul compliqué ...*/
        return pi.setScale(digits,BigDecimal.ROUND_HALF_UP);
    }
}

Le client Client.java

import java.rmi.*;
import java.math.*;

/* Un code qui utilise l'objet remote Compute doit (1) obtenir une reference vers cet objet, (2) creer un objet Task, (3) appeler l'execution de la tache en passant la tache a Compute */

public class Client {
    public static void main(String args[]) {
        if (System.getSecurityManager() == null) {
            System.setSecurityManager(new RMISecurityManager());
        }
        try {
            String name = "rmi://" + args[0] + "/Compute";
            /* obtenir une ref. sur un objet Compute */
            Compute engine = (Compute) Naming.lookup(name);
            /* instancie un objet de calcul */
            TaskPi ma_tache = new TaskPi(Integer.parseInt(args[1]));
            /* faire calculer la tache envoyee et recuperer */
            BigDecimal pi = (BigDecimal) (engine.executeTask(ma_tache));
            System.out.println(pi);
        } catch (Exception e) {
            System.err.println("Exception du client : " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Le serveur Server.java

 
import java.rmi.*;
import java.net.*;

public class Server {
  public static void main(String [] args) {
     if (System.getSecurityManager() == null) {
         System.setSecurityManager(new RMISecurityManager());
     }
     try {
         ComputeImpl engine1 = new ComputeImpl ();
         Naming.rebind("rmi://localhost/Compute",engine1) ;
         System.out.println("Objet Compute enregistré dans le registry.");
         System.out.println("Serveur pret.");
     }
     catch (RemoteException re) { System.out.println(re) ; }
     catch (MalformedURLException e) { System.out.println(e) ; }
  }
}

Comme dans l'exemple de base, le serveur instancie un objet ComputeImpl et l'enregistre dans le registry sous l'identifiant Compute.

Gestionnaire Sécurité

L'une des principales difficultés de RMI est la gestion de la sécurité.

  • Il faut modifier la politique très restrictive de sécurité par défaut ($JAVA_HOME/lib/security/java.policy). La méthode préconisée est d'associer un fichier décrivant la politique avec la JVM lancée (-Djava.security.policy=fichier) (syntaxe).
  • Un programme n'a pas de gestionnaire de sécurité par défaut, et empêche certaines opérations (comme le téléchargement de code). On peut en créer un:

Code base

  1. Quand un client invoque une méthode d'un objet distant, le rmiregistry (lui aussi distant) retourne une référence à  cet objet (stub). Le rmiregistry doit pouvoir accéder aux classes correspondant à  ce stub.
  2. Le rmiregistry cherche d'abord dans son CLASSPATH la définition de la classe.
  3. Ensuite, la recherche se fait dans codebase, spécifié au lancement du serveur (qui enregistre l'objet).
    -Djava.rmi.server.codebase= http://icps.u-strasbg.fr/~genaud/codebase/
  4. Le client pourra alors télécharger la définition des classes de la codebase.

Remarque: en utilisant une codebase, le serveur doit pouvoir accéder avec son CLASSPATH aux definitions de classes, mais le rmiregistry ne doit pas pouvoir le faire.

Compilation : fichiers

client serveur
Sources
Client.java Server.java
Task.java Task.java
TaskPi.java
Operation.java Operation.java
OperationImpl.java
javac *.java
TaskPi.class
OperationImpl.class
OperationImpl_Skel.class

Exercice

Ecivez un système de polling de messages, avec plusieurs clients, un serveur:
  • un client envoie un message vers un serveur connu,
  • le serveur enregistre le message du client,
  • et lui renvoie les N derniers messages non encore lus provenant d'autres clients,
  • le client affiche ces N messages à l'écran et se termine.