|
RMI (Remote Method Invocation) est une API Java permettant de manipuler des objets distants (objet qui existe dans un autre espace adresse soit dans la même machine soit dans une machine différente) de manière transparente pour l'utilisateur, c'est-à-dire de la même façon que si l'objet était sur la machine virtuelle. Dans les premières version de JAVA, RMI est une solution pure Java, contrairement à la norme Corba de l'OMG (Object Management Group) permettant de manipuler des objets à distance avec n'importe quel langage et sur n’importe quelle plate-forme. Corba est toutefois beaucoup plus compliqué à mettre en oeuvre, c'est la raison pour laquelle de nombreux développeurs se tournent généralement vers RMI d’ou la nécessité d'évoluér l’API pour qu’elle devienne compatible avec CORBA. En particulier, elle prend en charge des appels sous forme de IIOP (Internet Inter-ORB Protocol) un protocole de CORBA.
Architecture de RMI
En RMI la transmission de données se fait à travers un système de couches, basées sur le modèle OSI afin de garantir une interopérabilité. Quant aux connexions, elles sont effectuées grâce à un protocole propriétaire JRMP (Java Remote Method Protocol) basé sur TCP/IP.
- Le stub (souche) et le skeleton (traduisez squelette), respectivement sur le client et le serveur, assurent la conversion des communications avec l'objet distant.
- La couche de référence (RRL, remote Reference Layer) est chargée du système de localisation afin de fournir un moyen aux objets d'obtenir une référence à l'objet distant. On l'appelle généralement l’annuaire RMI.
- La couche de transport permet d'écouter les appels entrants ainsi que d'établir les connexions et le transport des données sur le réseau.
Lorsqu'un client désire invoquer une méthode d'un objet distant, il effectue les opérations suivantes :
Il localise l'objet distant grâce à un service d’annuaire (Registry)
1- Il obtient dynamiquement une image virtuelle de l'objet distant (stub ).
2-
Le stub possède exactement la même interface que l'objet distant.
3-
Le stub sérialise les appels de la méthode distante, puis les transmet au serveur sous forme de flux de données. Ce qu’on appel "marshalise".
4-
Le Skeleton"déserialise" les données envoyées par le stub ("démarshalise"), puis appelle la méthode en local
5-
Le Skeleton récupère les résultats puis les marshalisent.
6-
Le stub démarshalise les données provenant du Skeleton et les transmet au client
Notons qu'a partir de la version 1.2, le Skeleton a été remplacé par un Stub.
Pour réaliser un programme RMI, on doit absolument passer par 5 étapes essentielles:
1- Définir l'interface pour la classe distante qui hérite de l'interface Remote et déclarer les méthodes publiques globales de l'objet. De plus ces méthodes doivent pouvoir lancer une exception de type RemoteException (BonjourInterface). Une interface, littéralement parlant est un dispositif ou un système que les entités indépendantes emploient pour agir l'un sur l'autre. En java ou autre langage qui utilise des interfaces, c’est une définition d’un protocole comportementale qui peut être mis en application par n'importe quelle classe dans n'importe quel niveau hiérarchique de cette dernière. En plus clair, les interfaces sont utilisées pour :
- Détecter les similitudes parmi des classes sans forcer artificiellement leurs rapports.
- Déclaration des méthodes qu'on s'attend à ce qu'une ou plusieurs classes peuvent implémenter.
- Indiquer un objet d’interface sans indiquer sa classe.
BonjourInterface.java
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface BonjourInterface extends Remote {
public String direBonjour() throws RemoteException;
}
|
2- Définir la classe distance qui implémente l’interface (Bonjour) et hérite de la classe UnicastRemoteObject ou Activatable (utilisant elle-même les classes Socket et SocketServer, permettant la communication par protocole TCP) - dans notre cas on va s’intéresser à UnicastRemoteObject pour débuter – afin de créer un lien au système de RMI et aussi initialisation à distance d'objet. Rappelons que les objets de cette classe doivent être Serializable, un mécanisme qui permet de sérialiser des objets en flux réseau pour faciliter leur transfère.
Bonjour.java
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class Bonjour extends UnicastRemoteObject implements BonjourInterface {
private String message;
public Bonjour(String msg) throws RemoteException {
message = msg;
}
public String direBonjour() throws RemoteException {
return message;
}
}
|
N.B :vous remarquerez que ce cas la class Bonjour n’implémente pas la classe Serializable parce qu’elle dispose d'une propriété de type String qui est déjà Serializable.
3- Générer les classes Stub (souche) et Skeleton (squelette) en utilisant la commande : #rmic Bonjour
4- Lancer le service d’annuaire RMI en utilisant la commande:#rmiregistryou intsancier le en ivocant la méthode createRegistry de la classe LocateRegistry puis lancer l'application serveur (Server) qui instancie l'objet distant et le publie.
Server.java
import java.rmi.Naming;
public class Server {
public static void main(String[] args) {
try {
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://localhost/Bonjour", new Bonjour("Salut!"));
System.out.println("L'objet est publié");
} catch (RemoteException e) {
System.out.println("Erreur de publication: " + e);
}
} catch (MalformedURLException e) {
System.out.println("Erreur de publication: " + e);
}
}
}
|
5- Créer un programme client (Client) pour accéder aux méthodes de l’objet distant.
Client.java
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class Client {
public static void main(String[] args) {
try {
BonjourInterface monObjet = (BonjourInterface) Naming
.lookup("rmi://localhost/Bonjour");
System.out.println(monObjet.direBonjour());
} catch (MalformedURLException e) {
System.out.println("Erreur :" + e.getMessage());
} catch (RemoteException e) {
System.out.println("Erreur :" + e.getMessage());
} catch (NotBoundException e) {
System.out.println("Erreur :" + e.getMessage());
}
}
}
|
L’exécution du client se passe comme n’importe quel autre programme Java toutefois le cadre de notre exemple suppose que les classes suivantes soient accessibles dans le classpath.
- Coté serveur : Bonjour.class, Bonjour_Stub.class, BonjourInterface.class et Serveur.class.
Le bind envoie au registry une instance de type Stub d'où la nécessité de l'avoir.
- Registry (dans le cas ou il n'est pas intégré au serveur): BonjourInterface.class,Bonjour_Stub.class.
rmiregistry décode les objets qui lui sont passés et donc a besoin des classes et interfaces correspondantes au type des Stubs. Ce comportement est particulier à cette implantation de référence du service de registry.
- Coté client
Bonjour_Stub.class, BonjourInterface.class et Client.class.
Ici c’est la présence des classes de Stub qui peut surprendre. Dans de nombreux cas de déploiement, comme pour les Applets, ceci ne pose aucun problème puisque c’est le serveur qui déploie le code client. Dans d’autres cas on pourra s’orienter vers un chargement dynamique des classes un concept qui n'est encore au point par rapport à CORBA si on croit les spécialistes.
Chargement dynamique.
Un client RMI peut se trouver dans une situation où il ne dispose pas de classe Stub de ce fait son exécution relèvera une exception du type ClassNotFoundException. Dans ce cas nous pouvons se tourner vers le chargement dynamique pour cela il faut:
- Placer l'ensemble des classes du serveur dans espace web accessible par le client.
- Mettre en place un SecurityManager(gestionnaire de sécurité) pour pouvoir charger des classes à distant soit on ajoutant cette ligne de code System.setSecurityManager(new SecurityManager())soit par option lancement de la J.V.M–Djava.security.manager cette opération implique la mise en place d'un fichier de privilèges afin de résoudre et de se connecter au hôte de registry. Ce fichier policy contenant une entrée de ce type : permission java.net.SocketPermission “host:1024-”, “connect” ;
- Exécuter le serveur on fournissant l'URL du chargement dynamique des classes.
java -Djava.rmi.server.codebase="http://localhost/export/ Server
- Pour terminer exécuter le client.
java -Djava.security.manager -Djava.security.policy=priv.policy Client
CallBack
Dans l'exemple précédant, nous avons utilisé un modèle synchrone c'est-à-dire que le client doit attendre une réponse de la part du serveur avant de pourvoir faire quoi que ce soit d'autre. Ce modèle fonctionne parfaitement mais est très restrictif, en particulier dans les situations où le traitement sur le serveur peut être relativement long.
Dans cette section nous allons étudier une technique dite "Callback" qui nous permet de construire un modèle plutôt asynchrone. Cette technique consiste à avoir une souche serveur et souche client de chaque cote et quand le client invoque une méthode distante il transmet sa référence pour que le serveur puisse le recontacter- en invoquant une méthode distant- pour lui transmettre le résultat de son appel, comme le montre le diagrame de séquance ci-dessous.
Comme veut la coutume on définit les deux interfaces distantes une pour chaque coté NumberGen pour le serveur et NumberAsk pour le client.
NumberGen.java
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface NumberGen extends Remote {
void needNumber(NumberAsk ci) throws RemoteException;
}
|
NumberAsk.java
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface NumberAsk extends Remote {
void takeNumber(int i) throws RemoteException;
}
|
Puis on écrit deux programmes NumberGenImpl qui représente l'implantation de l'interface distante du serveur et NumberAskImpl pour celle du client.
NumberGenImpl.java
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.util.Random;
public class NumberGenImpl extends UnicastRemoteObject implements NumberGen,
Runnable {
private NumberAsk myRemoteInterface;
public NumberGenImpl() throws RemoteException {
}
public void run() {
System.out.println("Un client demande un numéro");
try {
Thread.sleep(3000);
Random rand = new Random();
myRemoteInterface.takeNumber(rand.nextInt());
} catch (Exception e) {
}
}
public void needNumber(NumberAsk ci) throws RemoteException {
myRemoteInterface = ci;
Thread i = new Thread(this, "ServerImpl");
i.start();
}
public static void main(String[] args) {
try {
LocateRegistry.createRegistry(1099);
NumberGenImpl si = new NumberGenImpl();
Naming.rebind("rmi://localhost/ServerImpl", si);
System.out.println("Serveur en ligne.");
} catch (Exception e) {
e.printStackTrace();
}
}
}
|
NumberAskImpl.java
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class NumberAskImpl implements NumberAsk {
public static void main(String[] args) {
NumberAskImpl ClientImpl = new NumberAskImpl();
try {
UnicastRemoteObject.exportObject(ClientImpl);
// sa référence afin de pouvoir invoquer ses méthodes distantes
NumberGen si = (NumberGen) Naming
.lookup("rmi://localhost/ServerImpl");
si.needNumber(ClientImpl);
System.out.println("Je suis libre et je peux autre chose en attendant");
} catch (Exception e) {
e.printStackTrace();
}
}
public void takeNumber(int i) throws RemoteException {
System.out.println("D'apres le serveur mon numéro est: " + i);
}
}
|
Pour tester, nous exécutons le programme serveur en premier puis le client.
Activable
Jusqu'à maintenait nous avons travaillé avec des objets qui ne sont pas nécessairement des instances situées dans un serveur actif. Parfois nous avons le désire de déclarer un objet pour l’’exportation” et ensuite arrêter la JVM et lorsqu’un client demande une référence l’objet est ressuscité au sein d’une JVM active. Pour cela nous devons travailler aves des objets particuliers autrement dit des objets "Activatable" se sont, en générale des objets distants qui héritent de la classe Activatable leur fournissant tout le support nécessaire pour qu'ils puissent être persistant.
C'est objets sont gérer par un démon système rmid qui enregistrer ces objets et les activer l' avantage est d'évite d'avoir des objets serveurs actifs en permanence même s'ils ne sont pas utilise -cela est trop coûteux en terme de ressource- aussi d’avoir des références d'objets persistantes pour qu’en cas de crash d'objet serveur le démon peut le relancer avec la même référence aussi les clients continuent à utiliser la même référence.
Un objet activable appartiens un groupe d'activation d'une part correspond à une JVM et d'autre part est chargé de surveiller, d'activer/réactiver les objets. Lors qu’un client invoque une méthode sur un objet activable, si la JVM du groupe n'est pas en cours d'exécution, elle est lancée et si l'objet n'est pas actif il est instancie avec les informations transmises lors de l'enregistrement (nom de sa classe, URL chargement bytecode de la classe dans le cas ou elle n’est pas chargeable pas CLASSPATH et les données pour le constructeur de la classe).
Pour la réalisation d'un exemple nous modifions l'implémentation du précédent objet Bonjour pour le rendre activable.(BonjourActivatable)
BonjourActivatable.java
import java.rmi.MarshalledObject;
import java.rmi.RemoteException;
import java.rmi.activation.Activatable;
import java.rmi.activation.ActivationID;
public class BonjourActivatable extends Activatable implements BonjourInterface {
public BonjourActivatable(ActivationID id, MarshalledObject data)
throws RemoteException {
super(id, 0);
}
public String direBonjour() {
return "Salut";
}
}
|
Puis nous écrivons un programme qui permet de configurer et d'instancier l'objet activable.(InstallBonjour)
InstallBonjour.java
import java.rmi.MarshalledObject;
import java.rmi.Naming;
import java.rmi.RMISecurityManager;
import java.rmi.activation.Activatable;
import java.rmi.activation.ActivationDesc;
import java.rmi.activation.ActivationGroup;
import java.rmi.activation.ActivationGroupDesc;
import java.rmi.activation.ActivationGroupID;
import java.util.Properties;
public class InstallBonjour {
public static void main(String[] args) throws Exception {
System.setSecurityManager(new RMISecurityManager());
Properties props = new Properties();
props.put("java.security.policy", "priv.policy");
//Un descripteur de groupe, instance de ActivationGroupDesc
ActivationGroupDesc.CommandEnvironment ace = null;
ActivationGroupDesc exampleGroup = new ActivationGroupDesc(props, ace);
ActivationGroupID agi = ActivationGroup.getSystem().registerGroup(
exampleGroup);
String location = args[0];
MarshalledObject data = null;
ActivationDesc desc = new ActivationDesc(agi, "BonjourActivatable",
location, data);
BonjourInterface mri = (BonjourInterface) Activatable.register(desc);
System.out.println("j'ai recu le S_tub");
Naming.rebind("//localhost/Bonjour", mri);
System.out.println("Exported Bonjour");
System.exit(0);
}
}
|
Nous générons le classe Stub (rmic BonjourActivatable), nous lançons le registry (rmiregistry) puis le démon rmid en lui fournissant un fichier de privilège, qui définit les autorisations nécessaire pour que ActivationGroupDescriptor puisse lancer une JVM pour un groupe d'activation:
#rmid -J-Djava.security.policy=priv.policy
Nous exécutons le programme InstallBonjour:
#java -Djava.security.policy=policy InstallBonjour "file:///c:/rmi/activale"
L'url "file:///c:/rmi/activale" permet au daemon de charger le bytecode de la classe si elle n’est pas dans CLASSPATH. Pour terminer, nous exécutons le client.
RMI et FireWall
RMI ouvre des connexions TCP directes sur des ports anonymes lorsqu'on ne les spécifie pas, alors il lui sera impossible de passer à travers des "firewalls". Pour cela il y a deux solutions envisageables :
1- La première est la solution la plus fiable, elle consiste à enregistrer l’objet en utilisant un port donné ex:
"UnicastRemoteObject.exportObject(ClientImpl,4450)" puis configurer le firewalls afin qu’il laisse passer des paquets à travers ce dernier.
2- La deuxième est plus barbare -si j'ose dire- mais utile dans le cas ou nous n'avons pas accès aux paramètres du firewall. Cette solution consiste à encapsuler les requêtes RMI dans des requêtes HTTP POST (tunneling http). Dans ce cas, nous supposons que le "firewall" bloque le trafic RMI mais laisse passer le trafic HTTP, le client doit positionner la propriété « http.proxyHost » et essaie de passer par un proxy web local et de le faire contacter le serveur à l'adresse http://localhost:4450.
Un deuxième scénario pour cette solution s'impose. Le cas ou le client est derrière un "firewall". Dans ce cas il faudra prévoir un autre niveau de redirection le serveur dispose d'un programme Common Getaway Interface (CGI) qui redirige la requête. Vers l'objet du serveur RMI, le client essaie de contacter le serveur à l'adresse http://localhost/cgi-bin/java-rmi?forward=4450.
C’est une solution qui fonctionne mais des pertes de performances et des aléas de sécurité éventuelle s'opposent. Cela dit à n'utiliser que lorsqu'on ne peut faire autrement.
Télécharger l'archive complet de ce tutoriel (RMI.zip)
|