Les références distantes (R.M.I.)

images/brownbelt.png

Ce chapitre constitue une introduction simplifiée à R.M.I..

R.M.I. permet de demander des services à des objets distants c’est à dire des objets situés dans une autre J.V.M (éventuellement active sur une autre machine). Bien que demandant un peu d’intendance de mise en place, R.M.I permet une architecture élégante et performante pour réaliser des échanges client-serveur.

La notion de service distant

Imaginons une application bancaire très simple qui gère des comptes. Voici, par exemple, les grandes lignes de la classe Compte :

public class Compte {

   public synchronized void depot(BigDecimal val) {
     // ...
   }

   public synchronized void retrait(BigDecimal val) throws ExceptionDecouvert{
      // ...
   }

   public BigDecimal getSolde() {
      return ...;
   }
}

et voici la réalisation du “guichet” qui nous permet d’obtenir une référence sur une instance de Compte lorsqu’on connait le nom du client:

public class Banque {
   //
   public Compte compteClient (String client) {
      return ...;
   }
}

On voudrait maintenant accéder aux services de cette application depuis un poste distant. Ici une instance de Banque “vit” sur le serveur, et les instances de Compte restent également sur cette même JVM. On voudrait donc accéder aux services rendus par ces instances depuis d’autres machines sur le réseau.

Comment le code résidant sur l’application cliente va-t-il voir les objets distants?

La définition d’un service distant

Le principe du découplage entre une demande de service et sa réalisation prend ici tout sons sens. Le code demandeur du service du coté client n’a pas à connaître le type effectif de l’objet coté serveur: il va y avoir entre les partenaires un accord de définition d’interface de service.

Un autre principe important entre en jeu: la présence du réseau n’est pas transparente. On ne définit pas un service distant de la même manière qu’un service local (rendu dans la même J.V.M): en effet tout appel de méthode implique un transfert de données sur une ligne de communication et cette opération peut échouer pour des raisons liées aux entrée/sortie distantes.

La définition des services distants de Compte et de Banque passe par une définition d’interface particulière et par un requires du module java.rmi :

// dans module java.rmi!
import java.rmi.* ;

public interface CompteDistant extends Remote{
   public void depot(BigDecimal val) throws RemoteException ;
   public void retrait(BigDecimal val) throws ExceptionDecouvert, RemoteException ;
   public BigDecimal getSolde() throws RemoteException ;
}
import java.rmi.* ;
public interface BanqueDistante extends Remote{
   public CompteDistant compteClient (String client) throws RemoteException ;
}
  • chaque interface de service R.M.I (Remote Method Invocation) doit hériter de l’interface de marquage java.rmi.Remote.
  • chaque méthode du contrat doit impérativement déclarer la propagation d’une RemoteException (en plus des exceptions qui leur sont propres).

Principe de la communication entre objets distants

images/rmi/principe.png

  • Du coté serveur un objet “vu de l’extérieur” doit être exporté. Le mécanisme correspondant va mettre en place un thread d’écoute pour traiter les demandes de services. Sur les anciennes versions de RMI il existait explicitement des classes d’objets chargés de gérer RMI coté serveur: les Skeletons.
  • Du coté client un objet particulier appelé “talon” (Stub) gère le dialogue avec l’objet distant. C’est ce Stub qui est utilisé par l’application cliente au travers de l’interface de service (CompteDistant dans l’exemple). Sur les anciennes versions les classes de stub étaient générées statiquement; maintenant c’est un mandataire dynamique (Proxy) qui est automatiquement généré.
  • Les deux codes chargés de gérer les aspect réseaux vont collaborer pour établir des Sockets les maintenir, les rétablir, gérer le protocole d’invocation de méthode distante, et gérer des échanges pour un garbage-collector distribué

    Point important: tous les paramètres des méthodes, leur résultat, les exceptions susceptibles d’être propagées sont tous linéarisés. Ils doivent donc tous être d’un type primitif ou Serializable

Exportation d’un objet

Le package java.rmi.server traite de l’exportation des objets RMI. Une des solutions possibles consiste à définir l’objet exporté comme une sous-classe de UnicastRemoteObject:

import java.rmi.* ;
import java.rmi.server.* ;

// code par délégation!
// les IDE permettent de générer ce code automatiquement
public class CompteExporte extends UnicastRemoteObject
                implements CompteDistant {
   private Compte compte ;
   public CompteExporte (Compte cpt) throws RemoteException {
      compte=cpt ;
   }
   public void depot(BigDecimal val)throws RemoteException {
      compte.depot(val) ;
   }
   public void retrait(BigDecimal val) throws ExceptionDecouvert, RemoteException {
      compte.retrait(val) ;
   }
   public BigDecimal getSolde() throws RemoteException{
      return compte.getSolde() ;
   }
}
  • L’objet exporté implante l’interface de service distante

) Tout constructeur d’objet exporté doit explicitement propager l’exception RemoteException (ne serait-ce que parce que tous les constructeurs de UnicastRemoteObject le font!)

Sur le même principe le code de l’objet Banque :

import java.rmi.* ;
import java.rmi.server.* ;
public class BanqueExportee extends UnicastRemoteObject
        implements BanqueDistante {
   private Banque bank ;
   // juste pour l'exemple
   // en réalité la banque peut déjà exister!
   // on va considérer que le constructeur délègue à un singleton
   public BanqueExportee()throws RemoteException {
       bank = new Banque() ;
   }
   public CompteDistant compteClient (String client) throws RemoteException{
     // on peut faire partager le même Compte
     // à plusieurs codes clients!(bien entendu le programmeur
     // doit gérer cette concurrence d’accès)
       return new CompteExporte(bank.compteClient(client)) ;
    }
}

Le comportement des mandataires

images/rmi/exemple.png

Prenons par exemple un code client qui réalise les opérations suivantes:

BanqueDistante banqueDistante ;
..... // codes divers
CompteDistant cpt = banqueDistante.compteClient(client);

La référence banqueDistante est typée par l’interface de service, mais l’objet qui va rendre le service est le mandataire (stub)

Ce talon va appeler l’objet serveur, lui signifier l’appel de la méthode et lui passer l’argument linéarisé.

L’objet serveur va renvoyer en résultat une instance de mandataire sur CompteExporte qui représente la référence distante.

Cette référence distante est vue par le code client comme de type CompteDistant et c’est sur cette référence que seront invoquées les méthodes correspondantes:

cpt.depot(mnt);//le talon communique avec l’objet correspondant

Mais comment a-t-on amorcé ce processus, comment a-t-on obtenu la première reférence distante sur un objet BanqueExportee ?

Annuaire d’objets distants

Pour obtenir une référence sur un objet distant l’application cliente doit s’adresser à un agent “bien connu”, pour lui demander un objet référencé par un nom. Inversement une application serveur qui souhaite rendre un service distant doit appeler cet annuaire pour enregistrer une référence sous ce nom (dans l’exemple nous avons choisi le mot-clef “banque”). En RMI cette application d’intermédiation est appelée registry.

images/rmi/registry.png

L’application de référence fournie avec le JDK est rmiregistry :

  • Elle doit être lancée sur le même hôte réseau que le serveur.
  • Elle “écoute” par défaut sur le port 1099

On peut la lancer avec la commande:

rmiregistry [num_port]

Le serveur: enregistrement auprès du Registry

Voici un code de serveur très simple:

...
import java.rmi.* ;
public class Serveur {
   public static void main (String[] args) throws Exception {
      BanqueExportee bank = new BanqueExportee() ;
      Naming.rebind("banque", bank) ;
   }
}
  • On créé un objet Banque serveur
  • On l’enregistre auprès du registry sous le nom “banque” Cet appel a pour effet d’envoyer une instance de mandataire sur BanqueExportee au registry.

Il y a deux méthodes statiques possibles dans la classe Naming :

  • bind(..) pour un enregistrement définitif,
  • rebind(..) pour des re-enregistrements successifs.

On notera que le programme ne s’arrête pas: il y a en effet maintenant un thread d’écoute à l’attente des demandes de services sur l’objet exporté. Utiliser UnicastRemoteObject.unexportObject() pour retirer un objet du runtime RMI.

Le client: demande à l’annuaire

Un code de client très simple :

...
import java.rmi.* ;
public class Client {
   public static void main (String[] arg) throws Exception {
      String hote = args[0] ;
      ........
      BanqueDistante bk =
          (BanqueDistante) Naming.lookup("rmi://"+hote+"/banque") ;
      CompteDistant cpt = bk.compteClient(nomClient) ;
      cpt.depot(depot) ;
      cpt.retrait(retrait) ;
      ........;
   }
}
  • La demande de reférence distante initiale au registry se fait au moyen de la méthode Naming.lookup() . Comme cette méthode rend un résultat de type Object noter la nécessité d’opérer un transtypage (cast).
  • L’argument de désignation est une chaîne en format URL de la forme:

    rmi://hote:port/clef
  • hote est la désignation de la machine hôte sur laquelle tourne le registry
  • la désignation du port est optionnelle (si le registry écoute sur un port différent du port par défaut 1099)
  • clef est le mot-clef sous lequel est enregistré le service.
  • on notera que cette désignation est aussi valable pour le bind

    //code coté serveur
    Naming.rebind(“rmi://localhost:6666/banque”, bank );
    // ici registry écoute sur port 6666

Check-list pour une mise en oeuvre simple

Le cadre de notre exemple suppose que les classes suivantes soient accessibles dans le classpath des applications:

SERVEUR:

Banque.class CompteDistant.class
BanqueDistante.class CompteExporte.class
BanqueExportee.class
ExceptionDecouvert.class
Compte.class Serveur.class

CLIENT:

BanqueDistante.class CompteDistant.class
 Client.class ExceptionDecouvert.class

Compléments

Les compléments techniques suivants constituent une annexe de référence

Téléchargement dynamique des codes de classe

Un partenaire d’un échange RMI peut se trouver dans une situation où il ne dispose pas d’un code de classe:

  • parce qu’une classe de Stub n’a pas été installée localement
  • parce que le type effectif d’un objet accompagnant un appel de méthode n’est pas connu localement. Du fait du polymorphisme on peut avoir en effet un type runtime différent du type déclaré dans le “contrat” de l’interface Remote. On notera que cette incertitude peut exister pour chacun des partenaires (client ou serveur -de fait la distinction traditionnelle entre client et serveur peut devenir peu pertinente: un “serveur” pouvant prendre l’initiative d’un échange vers un objet exporté par le client-).

Contexte de sécurité

Le premier point est que toute J.V.M qui télécharge dynamiquement du code doit mettre en place un SecurityManager (s’il n’est pas déjà présent):

System.setSecurityManager(new SecurityManager());

La présence d’un environnement de sécurité nécessite une mise en place d’une politique de sécurité:

java -Djava.security.policy=myrmi.policy pack.Client .....

Ce fichier policy contenant une entrée de type :

permission java.net.SocketPermission “host:1024-”, “connect” ;

On peut également tenter de réduire le nombre de ports admis en spécifiant : le port du rmiregistry, un port que les objets serveurs ont choisi (voir constructeur spécial de UnicastRemoteObject).

Mise en place du téléchargement des classes

Les objets impliqués dans des échanges R.M.I. contiennent une annotation qui permet d’indiquer une URL d’origine de la classe correspondante. Le ClassLoader local qui “désérialize” l’objet peut prendre en compte cette URL pour charger la classe en fonction du protocole indiqué dans l’URL. De manière pratique le protocole le plus utilisé est http. Ce qui suppose la mise en place d’un serveur http pour exporter le code des classes.

Pour annoter correctement les objets impliqués :

  • Soit fixer la propriété java.rmi.server.codebase dans le contexte de la JVM exportatrice.
  • Soit fixer un ensemble d’URL accessibles et les passer à un URLClassLoader. Exemple :

    public static void main(String[] args) throws Exception {......
        ....
       URL[] tbURL = new URL[args.length] ;
       for (String st: args) {
          tbURL[ix] = new URL(st) ;
       }
       URLClassLoader classLoad = new URLClassLoader( tbURL) ;
       // à condition que le module soit visible
       Remote ss = (Remote) classLoad.loadClass(nomClasseRacine).newInstance();
       // ici la classe “racine” a un constructeur sans paramètre
       //attention les classes dépendantes ne doivent pas être vue du classpath local
       Naming.rebind("Server", ss) ;
    }

Dans les deux cas il est souhaitable d’organiser un déploiement dans des jars et d’utiliser des URLs comme:

jar:http://server:8080/mesclasses.jar!/

Attention: dans ce cas si on utilise rmiregistry comme registry prendre soin de lui retirer toute visibilité directe sur les classes concernées (sinon il reconstituera des annotations erronées au moment où il envoie le Stub au client).

Personnalisation des mécanismes sous-jacents à RMI

La classe java.rmi.server.UnicastRemoteObject dispose de constructeurs ou de méthode de classe qui permettent :

  • de fixer le numero de port utilisé pour “exporter” l’objet (utile dans un contexte sous SecurityManager).
  • de personnaliser les Sockets sous-jacentes (via RMIServerSocketFactory et RMIClientSocketFactory). On peut ainsi par exemple mettre en place des sockets UDP, des Sockets SSL -voir package javax.rmi.ssl-), ou des Sockets avec compression des données, etc. voir: docs/guide/rmi/socketfactory/index.html
  • Pour une liste des Properties de configuration voir : docs/guide/rmi/spec/rmi-properties.html.

Par ailleurs RMI est automatiquement capable de transformer une requête bloquée par un pare-feu en appel HTTP POST.

Serveurs dormants, serveurs résilients

Les objets exportés ne sont pas nécessairement des instances situées dans un serveur actif. On peut tout à fait déclarer un objet pour l’’exportation” et ensuite arréter la JVM serveur.

Lorsqu’un client demande une référence l’objet est ressuscité au sein d’une JVM active. Ces objet particuliers sont Activatable et un démon système particulier rmid sert à enregistrer ces objets et à les activer (notons que rmid joue aussi le rôle de registry). Voir docs/guide/rmi/activation.html