9. JAVA RMI
9.1. Introduction
Nous avons vu comment créer des applications réseau à l’aide des outils de communication appelés sockets. Dans une application client/serveur bâtie sur ces outils, le lien entre le client et le serveur est le protocole de communication qu’ils ont adopté pour converser. Les deux applications peuvent être écrites avec des langages différents : Java par exemple pour le client, Perl pour le serveur ou toute autre combinaison. On a bien deux applications distinctes reliées par un protocole de communication connu des deux. Par ailleurs, l’accès au réseau via les sockets n’est pas transparent pour une application Java : elle doit utiliser la classe Socket, classe créée spécialement pour gérer ces outils de communication que sont les sockets.
JAVA RMI (Remote Method Invocation) permet de créer des applications réseau aux caractéristiques suivantes :
- Les applications client/serveur sont des applications Java aux deux extrémités de la communication
- Le client peut utiliser des objets situés sur le serveur comme s’ils étaient locaux
- La couche réseau devient transparente : les applications n’ont pas à se soucier de la façon dont sont transportées les informations d’un point à un autre.
Le dernier point est un facteur de portabilité : si la couche réseau d’une application RMI venait à changer, l’application elle-même n’aurait pas à être ré-écrite. Ce sont les classes RMI du langage Java qui devront être adaptées à la nouvelle couche réseau.
Le principe d’une communication RMI est le suivant :
- Une application Java classique est écrite sur une machine A. Elle va jouer le rôle de serveur. Pour ce faire certains de ses objets vont faire l’objet d’une « publication » sur la machine A sur laquelle l’application s’exécute et deviennent alors des services.
- Une application Java classique est écrite sur une machine B. Elle va jouer le rôle de client. Elle aura accès aux objets/services publiés sur la machine A, c’est à dire que via une référence distante, elle va pouvoir les manipuler comme s’ils étaient locaux. Pour cela, elle aura besoin de connaître la structure de l’objet distant auquel elle veut accéder (méthodes & propriétés).
9.2. Apprenons par l’exemple
La théorie qui sous-entend l’interface RMI n’est pas simple. Pour y voir plus clair, nous allons suivre pas à pas l’écriture d’une application client/serveur utilisant le package RMI de Java. Nous prenons une application que l’on trouve dans de nombreux ouvrages sur RMI : le client invoque une unique méthode d’un objet distant qui lui renvoie alors une chaîne de caractères. Nous présentons ici, une légère variante : le serveur fait l’écho de ce que lui envoie le client. Nous avons déjà présenté dans cet ouvrage, une telle application s’appuyant elle, sur les sockets.
9.2.1. L’application serveur
9.2.1.1. Étape 1 : l’interface de l’objet/serveur
Un objet distant est une instance de classe qui doit implémenter l’interface Remote définie dans le package java.rmi. Les méthodes de l’objet qui seront accessibles à distance sont celles déclarées dans une interface dérivée de l’interface Remote :
import java.rmi.*;
// l'interface distante
public interface interEcho extends Remote{
public String echo(String msg) throws java.rmi.RemoteException;
}
Ici, on déclare donc une interface interEcho déclarant une méthode echo comme accessible à distance. Cette méthode est susceptible de générer une exception de la classe RemoteException, classe qui regroupe tous les erreurs liées au réseau.
9.2.1.2. Étape 2 : écriture de l’objet serveur
Dans l’étape suivante, on définit la classe qui implémente l’interface distante précédente. Cette classe doit être dérivée de la classe UnicastRemoteObject, classe qui dispose des méthodes autorisant l’invocation de méthodes à distance.
import java.rmi.*;
import java.rmi.server.*;
import java.net.*;
// classe implémentant l’écho distant
public class srvEcho extends UnicastRemoteObject implements interEcho{
// constructeur
public srvEcho() throws RemoteException{
super();
}// fin constructeur
// méthode réalisant l’écho
public String echo(String msg) throws RemoteException{
return "[" + msg + "]";
}// fin écho
}// fin classe
Dans la classe précédente, nous trouvons :
- la méthode qui fait l’écho
- un constructeur qui ne fait rien si ce n’est appeler le constructeur de la classe mère. Il est là pour déclarer qu’il peut générer une exception de type RemoteException.
Nous allons créer une instance de cette classe avec une méthode main. Pour qu’un objet/service soit accessible de l’extérieur, il doit être créé et enregistré dans l’annuaire des objets accessibles de l’extérieur. Un client désirant accéder à un objet distant procède en effet de la façon suivante :
- il s’adresse au service d’annuaire de la machine sur laquelle se trouve l’objet qu’il désire. Ce service d’annuaire opère sur un port que le client doit connaître (1099 par défaut). Le client demande à l’annuaire, une référence d’un objet/service dont il donne le nom. Si ce nom est celui d’un objet/service de l’annuaire, celui-ci renvoie au client une référence via laquelle le client va pouvoir dialoguer avec l’objet/service distant.
- à partir de ce moment, le client peut utiliser cet objet distant comme s’il était local
Pour en revenir à notre serveur, nous devons créer un objet de type srvEcho et l’enregistrer dans l’annuaire des objets accessibles de l’extérieur. Cet enregistrement se fait avec la méthode de classe rebind de la classe Naming :
avec
nom : le nom qui sera associé à l’objet distant
obj : l’objet distant
Notre classe srvEcho devient donc la suivante :
import java.rmi.*;
import java.rmi.server.*;
import java.net.*;
// classe implémentant l’écho distant
public class srvEcho extends UnicastRemoteObject implements interEcho{
// constructeur
public srvEcho() throws RemoteException{
super();
}// fin constructeur
// méthode réalisant l’écho
public String echo(String msg) throws RemoteException{
return "[" + msg + "]";
}// fin écho
// création du service
public static void main (String arg[]){
try{
srvEcho serveurEcho=new srvEcho();
Naming.rebind("srvEcho",serveurEcho);
System.out.println("Serveur d’écho prêt");
} catch (Exception e){
System.err.println(" Erreur " + e + " lors du lancement du serveur d’écho ");
}
}// main
}// fin classe
Lorsqu’on lit le programme précédent, on a l’impression qu’il va s’arrêter aussitôt après avoir créé et enregistré le service d’écho. Ce n’est pas le cas. Parce que la classe srvEcho est dérivée de la classe UnicastRemoteObject, l’objet créé s’exécute indéfiniment : il écoute les demandes des clients sur un port anonyme c’est à dire choisi par le système selon les circonstances. La création du service est asynchrone : dans l’exemple, la méthode main crée le service et continue son exécution : elle affichera bien « Serveur d’écho prêt ».
9.2.1.3. Étape 3 : compilation de l’application serveur
Au point où on en est, on peut compiler notre serveur. Nous compilons le fichier interEcho.java de l’interface interEcho ainsi que le fichier srvEcho.java de la classe srvEcho. Nous obtenons les fichiers .class correspondant : interEcho.class et srvEcho.class.
9.2.1.4. Étape 4 : écriture du client
On écrit un client à qui on passe en paramètre l’URL du serveur d’écho et qui
- lit une ligne tapée au clavier
- l’envoie au serveur d’écho
- affiche la réponse que celui-ci envoie
- reboucle en 1 et s’arrête lorsque la ligne tapée est « fin ».
Cela donne le client suivant :
import java.rmi.*;
import java.io.*;
public class cltEcho {
public static void main(String arg[]){
// syntaxe : cltEcho URLService
// vérification des arguments
if(arg.length!=1){
System.err.println("Syntaxe : pg url_service_rmi");
System.exit(1);
}
// dialogue client-serveur
String urlService=arg[0];
BufferedReader in=null;
String msg=null;
String reponse=null;
interEcho serveur=null;
try{
// ouverture du flux clavier
in=new BufferedReader(new InputStreamReader(System.in));
// localisation du service
serveur=(interEcho) Naming.lookup(urlService);
// boucle de lecture des msg à envoyer au serveur d'écho
System.out.print("Message : ");
msg=in.readLine().toLowerCase().trim();
while(! msg.equals("fin")){
// envoi du msg au serveur et réception de la réponse
reponse=serveur.echo(msg);
// suivi
System.out.println("Réponse serveur : " + reponse);
// msg suivant
System.out.print("Message : ");
msg=in.readLine().toLowerCase().trim();
}// while
// c'est fini
System.exit(0);
// gestion des erreurs
} catch (Exception e){
System.err.println("Erreur : " + e);
System.exit(2);
}// try
}// main
}// classe
Il n’y a rien de bien particulier dans ce client si ce n’est l’instruction qui demande une référence du serveur :
On se rappelle que notre service d’écho a été enregistré dans l’annuaire des services de la machine où il se trouve avec l’instruction :
Naming.rebind("srvEcho",serveurEcho);
Le client utilise donc lui aussi une méthode de la classe Naming pour obtenir une référence du serveur qu’il veut utiliser. La méthode lookup utilisée admet comme paramètre l’url du service demandé. Celle-ci a la forme d’une url classique :
avec
rmi : facultatif - protocole rmi
machine : nom ou adresse IP de la machine sur laquelle opère le serveur d’écho - facultatif, par défaut localhost.
port : port d’écoute du service d’annuaire de cette machine - facultatif, par défaut 1099
nom_service : nom sous lequel a été enregistré le service demandé (srvEcho pour notre exemple)
Ce qui est récupéré, c’est une instance de l’interface distante interEcho. Si on suppose que le client et le serveur ne sont pas sur la même machine, lorsqu’on compile le client cltEcho.java, on doit disposer dans le même répertoire, du fichier interEcho.class, résultat de la compilation de l’interface distante interEcho, sinon on aura une erreur de compilation sur les lignes qui référencent cette interface.
9.2.1.5. Étape 5 : génération des fichiers .class nécessaires à l’application client-serveur
Afin de bien comprendre ce qui est du côté serveur et ce qui est du côté client, on mettra le serveur dans un répertoire echo\serveur et le client dans un répertoire echo\client.
Le répertoire du serveur contient les fichiers source suivants :
E:\data\java\RMI\echo\serveur>dir *.java
INTERE~1 JAV 158 09/03/99 15:06 interEcho.java
SRVECH~1 JAV 759 09/03/99 15:07 srvEcho.java
Après compilation de ces deux fichiers source, on a les fichiers .class suivants :
E:\data\java\RMI\echo\serveur>dir *.class
SRVECH~1 CLA 1 129 09/03/99 15:58 srvEcho.class
INTERE~1 CLA 256 09/03/99 15:58 interEcho.class
Dans le répertoire du client, on trouve le fichier source suivant :
ainsi que le fichier interEcho.class qui a été généré lors de la compilation du serveur :
Après compilation du fichier source on a les fichiers .class suivants :
E:\data\java\RMI\echo\client>dir *.class
CLTECH~1 CLA 1 506 09/03/99 16:08 cltEcho.class
INTERE~1 CLA 256 09/03/99 15:59 interEcho.class
Si on tente d’exécuter le client cltEcho, on obtient l’erreur suivante :
E:\data\java\RMI\echo\client>j:\jdk12\bin\java cltEcho rmi://localhost/srvEcho
Erreur : java.rmi.UnmarshalException: error unmarshalling return; nested exception is:
java.lang.ClassNotFoundException: srvEcho_Stub
Si on tente d’exécuter le serveur srvEcho, on obtient l’erreur suivante :
E:\data\java\RMI\echo\serveur>j:\jdk12\bin\java srvEcho
Erreur java.rmi.StubNotFoundException: Stub class not found: srvEcho_Stub; nested exception is:
java.lang.ClassNotFoundException: srvEcho_Stub lors du lancement du serveur d’écho
Dans les deux cas, la machine virtuelle Java indique qu’elle n’a pas trouvé la classe srvEcho_stub. Effectivement, nous n’avons encore jamais entendu parler de cette classe. Dans le client, la localisation du serveur s’est faite avec l’instruction suivante :
Ici, urlservice est la chaîne rmi://localhost/srvEcho avec
Rmi : protocole rmi
Localhost : machine où opère le serveur - ici la même machine sur laquelle le client. La syntaxe est normalement machine:port. En l’absence du port, c’est le port 1099 qui sera utilisé par défaut. A l’écoute de ce port, se trouve le service d’annuaire du serveur.
srvEcho : c’est le nom du service particulier demandé
A la compilation, aucune erreur n’avait été signalée. Il fallait simplement que le fichier interEcho.class de l’interface distante soit disponible.
A l’exécution, la machine virtuelle réclame la présence d’un fichier srvEcho_stub.class si le service demandé est le service srvEcho, de façon générale un fichier X_stub.class pour un service X. Ce fichier n’est nécessaire qu’à l’exécution pas à la compilation du client. Il en est de même pour le serveur. Qu’est-ce donc que ce fichier ?
Sur le serveur, se trouve la classe srvEcho.class qui est notre objet/service distant. Le client, s’il n’a pas besoin de cette classe, a néammoins besoin d’une sorte d’image d’elle afin de pouvoir communiquer avec. En fait, le client n’adresse pas directement ses requêtes à l’objet distant : il les adresse à son image locale srvEcho_stub.class située sur la même machine que lui. Cette image locale srvEcho_stub.class dialogue avec une image de même nature (srvEcho_stub.class) située cette fois sur le serveur. Cette image est créée à partir du fichier .class du serveur avec un outil de Java appelé rmic. Sous Windows, la commande :
va produire, à partir du fichier srvEcho.class deux autres fichiers .class :
E:\data\java\RMI\echo\serveur>dir *.class
SRVECH~2 CLA 3 264 09/03/99 16:57 srvEcho_Stub.class
SRVECH~3 CLA 1 736 09/03/99 16:57 srvEcho_Skel.class
Il y a bien là, le fichier srvEcho_stub.class dont le client et le serveur ont besoin à l’exécution. Il y a également un fichier srvEcho_Skel.class dont pour l’instant on ignore le rôle. On fait une copie du fichier srvEcho_stub.class dans le répertoire du client et du serveur et on supprime le fichier srvEcho_Skel.class. On a donc les fichiers suivants :
du côté serveur :
E:\data\java\RMI\echo\serveur>dir *.class
SRVECH~1 CLA 1 129 09/03/99 15:58 srvEcho.class
INTERE~1 CLA 256 09/03/99 15:58 interEcho.class
SRVECH~1 CLA 3 264 09/03/99 16:01 srvEcho_Stub.class
du côté client :
E:\data\java\RMI\echo\client>dir *.class
CLTECH~1 CLA 1 506 09/03/99 16:08 cltEcho.class
INTERE~1 CLA 256 09/03/99 15:59 interEcho.class
SRVECH~1 CLA 3 264 09/03/99 16:01 srvEcho_Stub.class
9.2.1.6. Étape 6 : Exécution de l’application client-serveur d’écho
Nous sommes prêts pour exécuter notre application client-serveur. Dans un premier temps, le client et le serveur fonctionneront sur la même machine. Il faut tout d’abord lancer notre application serveur. On se rappelle que celle-ci :
- crée le service
- l’enregistre dans l’annuaire des services de la machine sur laquelle opère le serveur d’écho
Ce dernier point nécessite la présence d’un service d’annuaire. Celui-ci est lancé par la commande :
rmiregistry est le service d’annuaire. Il est ici lancé en tâche de fond dans une fenêtre Dos de Windows par la commande start. L’annuaire actif, on peut créer le service d’écho et l’enregistrer dans l’annuaire des services. Là encore, il est lancé en tâche de fond par une commande start :
Le serveur d’écho s’exécute dans une nouvelle fenêtre DOS et affiche comme on le lui avait demandé :
Il ne nous reste plus qu’à lancer et tester notre client :
E:\data\java\RMI\echo\client>j:\jdk12\bin\java cltEcho rmi://localhost/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin
9.2.1.7. Le client et le serveur sur deux machines différentes
Dans l’exemple précédent, le client et le serveur étaient sur une même machine. On les place maintenant sur des machines différentes :
- le serveur sur une machine windows
- le client sur une machine linux
Le serveur est lancé commé précédemment sur la machine Windows. Sur la machine linux, on a transféré les fichiers .class du client :
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir
total 9
drwxr-xr-x 2 serge admin 1024 Mar 10 10:02 .
drwxr-xr-x 4 serge admin 1024 Mar 10 10:01 ..
-rw-r--r-- 1 serge admin 1506 Mar 10 10:02 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
-rw-r--r-- 1 serge admin 3264 Mar 10 10:02 srvEcho_Stub.class
Le client est lancé :
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Erreur : java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
java.rmi.UnmarshalException: error unmarshalling call header; nested exception is:
java.rmi.UnmarshalException: skeleton class not found but required for client version
On a donc une erreur : la machine virtuelle Java réclame apparemment le fichier srvEcho_skel.class qui avait été produit par l’utilitaire rmic mais qui jusqu’à maintenant n’avait pas servi. On le recrée et on le transfère également sur la machine linux :
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir
total 11
drwxr-xr-x 2 serge admin 1024 Mar 10 10:17 .
drwxr-xr-x 4 serge admin 1024 Mar 10 10:01 ..
-rw-r--r-- 1 serge admin 1506 Mar 10 10:02 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
-rw-r--r-- 1 serge admin 1736 Mar 10 10:17 srvEcho_Skel.class
-rw-r--r-- 1 serge admin 3264 Mar 10 10:02 srvEcho_Stub.class
On a la même erreur que précédemment... Alors on réfléchit et on relit les docs sur RMI. On finit par se dire que c’est peut-être le serveur lui-même qui a besoin du fameux fichier srvEcho_Skel.class. On relance alors, sur la machine Windows, le serveur avec les deux fichiers srvEcho_Stub.class et srvEcho_Skel.class présents :
E:\data\java\RMI\echo\serveur>dir *.class
SRVECH~1 CLA 1 129 09/03/99 15:58 srvEcho.class
INTERE~1 CLA 256 09/03/99 15:58 interEcho.class
SRVECH~2 CLA 3 264 10/03/99 9:05 srvEcho_Stub.class
SRVECH~3 CLA 1 736 10/03/99 9:05 srvEcho_Skel.class
E:\data\java\RMI\echo\serveur>start j:\jdk12\bin\java srvEcho
puis sur la machine linux, on teste de nouveau le client et cette fois-ci, ça marche :
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r-- 1 serge admin 1506 Mar 10 10:02 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
-rw-r--r-- 1 serge admin 3264 Mar 10 10:02 srvEcho_Stub.class
shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin
On en déduit donc, que côté serveur les deux fichiers srvEcho_Stub.class et srvEcho_Skel.class doivent être présents. Côté client, seul le fichier srvEcho_Stub.class a été nécessaire jusqu’à maintenant. Il s’était avéré indispensable lorsque le client et le serveur étaient sur la même machine Windows. Sous linux, on l’enlève pour voir...
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r-- 1 serge admin 1506 Mar 10 10:02 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
*** Security Exception: No security manager, stub class loader disabled ***
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
at sun.rmi.server.RMIClassLoader.getClassLoader(RMIClassLoader.java:84)
at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:88)
at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
at java.rmi.Naming.lookup(Naming.java:60)
at cltEcho.main(cltEcho.java:28)
Erreur : java.rmi.UnexpectedException: Unexpected exception; nested exception is:
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
On a une erreur intéressante qui a l’air de dire que la machine virtuelle Java a voulu charger la fameuse classe stub mais qu’il a échoué en l’absence d’un « security manager ». On se dit qu’on a vu quelque chose sur ce thème dans les docs. On s’y replonge... et on trouve que le serveur doit créer et installer un gestionnaire de sécurité (security manager) qui garantisse aux clients qui demandent le chargement de classes que celles-ci sont saines. En l’absence de ce security manager, ce chargement de classes est impossible. Ca semble coller : notre client linux a demandé la classe srvEcho_stub.class dont il a besoin au serveur et celui-ci a refusé en disant qu’aucun gestionnaire de sécurité n’avait été installé. On modifie donc le code de la fonction main du serveur de la façon suivante :
// création du service
public static void main (String arg[]){
// installation d'un gestionnaire de sécurité
System.setSecurityManager(new RMISecurityManager());
// lancement et enregistrement du service
try{
srvEcho serveurEcho=new srvEcho();
Naming.rebind("srvEcho",serveurEcho);
System.out.println("Serveur d’écho prêt");
} catch (Exception e){
System.err.println(" Erreur " + e + " lors du lancement du serveur d’écho ");
}
}// main
On compile et on produit les fichiers srvEcho_stub.class et srvEcho_Skel.class avec l’outil rmic. On lance le service d’annuaire (rmiregistry) puis le serveur et on a une erreur qu’auparavant on n’avait pas !
Erreur java.security.AccessControlException: access denied (java.net.SocketPermission 127.0.0.1:1099 connect,resolve) lors du lancement du serveur d’écho
Le gestionnaire de sécurité semble avoir été trop efficace. On lit de nouveau les docs... On s’aperçoit que lorsqu’un gestionnaire de sécurité est actif, il faut préciser au lancement d’un programme ses droits. Cela se fait avec l’option suivante :
start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho
où
java.security.policy est un mot clé
mypolicy est un fichier texte définissant les droits du programme. Ici, c’est le suivant :
Le programme a ici tous les droits.
On recommence. On se place dans le répertoire du serveur et on fait successivement :
- lancement du service d’annuaire : start j:\jdk12\bin\rmiregistry
- lancement du serveur : start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho
Et cette fois, le serveur d’écho (le client pas encore) se lance correctement. Maintenant vous pouvez faire l’expérience suivante :
- arrêtez le serveur d’écho puis le service d’annuaire
- relancez le service d’annuaire en étant dans un autre répertoire que celui du serveur
- revenez sur le répertoire du serveur lancez le serveur d’écho - vous obtenez l’erreur suivante :
Erreur java.rmi.ServerException: RemoteException occurred in server thread; nes
ted exception is:
java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
java.lang.ClassNotFoundException: srvEcho_Stub lors du lancement du serveur d’écho
On en déduit que le répertoire à partir duquel le service d’annuaire est lancé a de l’importance. Ici, Java n’a pas trouvé la classe srvEcho_stub.class parce que le service d’annuaire n’a pas été lancé du répertoire du serveur. Lors du lancement du serveur, on peut préciser dans quel répertoire se trouvent les classes nécessaires au serveur :
start j:\jdk12\bin\java
-Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/
srvEcho
La commande est sur une unique ligne. Le mot clé java.rmi.server.codebase sert à indiquer l’URL du répertoire contenant les classes nécessaires au serveur. Ici, cette URL précise le protocole file qui est le protocole d’accès aux fichiers locaux et le répertoire contenant les fichiers .class du serveur. Si donc on procède comme suit :
- arrêt du service d’annuaire
- relancer le service d’annuaire à partir d’un autre répertoire que celui du serveur
- dans le répertoire du serveur, lancer celui-ci avec la commande (une seule ligne) :
start j:\jdk12\bin\java
-Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/
srvEcho
Le serveur est alors lancé correctement. On peut donc passer au client. On le teste :
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r-- 1 serge admin 1506 Mar 10 14:28 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
*** Security Exception: No security manager, stub class loader disabled ***
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
at sun.rmi.server.RMIClassLoader.getClassLoader(RMIClassLoader.java:84)
at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:88)
at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
at java.rmi.Naming.lookup(Naming.java:60)
at cltEcho.main(cltEcho.java:31)
Erreur : java.rmi.UnexpectedException: Unexpected exception; nested exception is:
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
On obtient la même erreur signalant l’absence d’un gestionnaire de sécurité. On se dit qu’on s’est peut-être trompé et que c’est le client qui doit se créer son gestionnaire de sécurité. On laisse le serveur avec son gestionnaire de sécurité mais on en crée un pour le client également. La fonction main du client cltEcho.java devient alors :
public static void main(String arg[]){
// syntaxe : cltEcho machine port
// machine : machine où opère le serveur d'écho
// port : port où opère l'annuaire des services sur la machine du service d'écho
// vérification des arguments
if(arg.length!=1){
System.err.println("Syntaxe : pg url_service_rmi");
System.exit(1);
}
// installation d'un gestionnaire de sécurité
System.setSecurityManager(new RMISecurityManager());
// dialogue client-serveur
String urlService=arg[0];
BufferedReader in=null;
String msg=null;
String reponse=null;
interEcho serveur=null;
try{
....
} catch (Exception e){
....
}// try
}// main
On procède ensuite comme suit :
- on recompile cltEcho.java
- on transfère les fichiers .class sur la machine linux
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r-- 1 serge admin 1506 Mar 10 14:28 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
- on lance le client
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
java.io.FileNotFoundException: /e:/data/java/rmi/echo/serveur/srvEcho_Stub.class
at java.io.FileInputStream.<init>(FileInputStream.java)
at sun.net.www.protocol.file.FileURLConnection.connect(FileURLConnection.java:150)
at sun.net.www.protocol.file.FileURLConnection.getInputStream(FileURLConnection.java:170)
at sun.applet.AppletClassLoader.loadClass(AppletClassLoader.java:119)
at sun.applet.AppletClassLoader.findClass(AppletClassLoader.java:496)
at sun.applet.AppletClassLoader.loadClass(AppletClassLoader.java:199)
at sun.rmi.server.RMIClassLoader.loadClass(RMIClassLoader.java:159)
at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:97)
at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
at java.rmi.Naming.lookup(Naming.java:60)
at cltEcho.main(cltEcho.java:31)
File not found when looking for: srvEcho_Stub
Erreur : java.rmi.UnmarshalException: Return value class not found; nested exception is:
java.lang.ClassNotFoundException: srvEcho_Stub
Malgré les apparences, on progresse : l’erreur n’est plus la même. On constate que le client a pu demander au serveur la classe srvEcho_Stub.class, mais que celui-ci ne l’a pas trouvé. Donc, le client doit avoir un gestionnaire de sécurité s’il veut pouvoir demander des classes au serveur.
Si on regarde l’erreur précédente, on voit que le fichier srvEcho_Stub.class a été cherché dans le répertoire e:/data/java/rmi/echo/serveur/ et qu’il n’a pas été trouvé. C’est pourtant bien là qu’il est. Si on regarde plus précisément, la liste des méthodes impliquées dans l’erreur, on trouve celle-ci : sun.net.www.protocol.file.FileURLConnection.getInputStream. Le client semble avoir ouvert un flux avec un objet de type FileURLConnection. On se dit que tout ça a un rapport avec la façon dont on a lancé notre serveur :
start j:\jdk12\bin\java
-Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/
srvEcho
Le message d’erreur semble faire référence à la valeur du mot clé java.rmi.server.codebase. Lorsqu’on regarde les docs de nouveau, on voit que la valeur de ce mot clé est, dans les exemples donnés, toujours : http://.., c.a.d. que le protocole utilisé est http. Il n’apparaît pas clairement comment le client demande et obtient ses classes auprès du serveur. Peut-être les demande-t-il avec l’URL du mot clé java.rmi.server.codebase, URL précisée au lancement du serveur. On décide donc de lancer le serveur par la nouvelle commande suivante :
start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
Le protocole est maintenant http. On est obligé de déplacer les fichiers .class à un endroit accessible au serveur http de la machine sur laquelle seront stockées les classes. Dans notre exemple, le serveur tourne sur une machine windows avec un serveur http PWS de Microsoft. La racine de ce serveur est d:\Inetpub\wwwroot. Aussi procède-t-on de la façon suivante :
- on crée le répertoire d:\Inetpub\wwwroot\rmi\echo
- on y place les fichiers .class du serveur ainsi que le fichier mypolicy
- on lance le serveur Web si ce n’est déjà fait
- on relance le service d’annuaire (rmiregistry)
- on relance le serveur avec la commande
start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
- sur la machine linux, on lance le client :
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r-- 1 serge admin 1622 Mar 10 14:37 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin
Ouf ! Ca marche. Le client a bien réussi à récupérer le fameux fichier srvEcho_Stub.class.
Tout ça nous a donné des idées, et on se demande si le client qui se trouve sur la machine Windows du serveur, fonctionnerait lui aussi sans le fichier srvEcho_Stub.class. On passe dans le répertoire du client, on supprime le fichier srvEcho_Stub.class s’il s’y trouve et on lance le client de la même façon que sous Linux :
E:\data\java\RMI\echo\client>dir *.class
CLTECH~1 CLA 1 622 10/03/99 14:12 cltEcho.class
INTERE~1 CLA 256 09/03/99 15:59 interEcho.class
E:\data\java\RMI\echo\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : nouveau message
Réponse serveur : [nouveau message]
Message : fin
9.2.1.8. Résumé
Côté serveur Windows :
- le serveur a un gestionnaire de sécurité
- il a été lancé avec des options : start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
Côté client Linux ou Windows
- le client a un gestionnaire de sécurité
- sur linux, il a été lancé par java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
- sur windows, il a été lancé par j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
9.2.1.9. Serveur d’écho sur linux, clients sur Windows et Linux
On passe maintenant le serveur sur une machine Linux et on teste des clients linux et windows. La démarche à suivre est la suivante :
- on passe les fichiers .class du serveur sur la machine linux
shiva[serge]:/home/admin/serge/WWW/rmi/echo/serveur#
$ dir
total 11
drwxr-xr-x 2 serge admin 1024 Mar 10 16:15 .
drwxr-xr-x 3 serge admin 1024 Mar 10 16:09 ..
-rw-r--r-- 1 serge admin 256 Mar 10 16:09 interEcho.class
-rw-r--r-- 1 serge admin 1245 Mar 10 16:09 srvEcho.class
-rw-r--r-- 1 serge admin 1736 Mar 10 16:09 srvEcho_Skel.class
-rw-r--r-- 1 serge admin 3264 Mar 10 16:09 srvEcho_Stub.class
- parce que la classe srvEcho_Stub.class va être demandée par les clients, le répertoire choisi pour les classes du serveur est un répertoire accessible au serveur http de la machine Linux. Ici l’URL de ce répertoire est http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serveur
- le service d’annuaire est lancé en tâche de fond : /usr/local/bin/jdk/rmiregistry &
- le serveur est lancé en tâche de fond : /usr/local/bin/jdk/bin/java
-Djava.rmi.server.codebase=http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serveur/
srvEcho &
On peut tester les clients. Le client Windows d’abord.
- on passe dans le répertoire du client sur la machine windows
- on lance le client par la commande :
E:\data\java\RMI\echo\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://shiva.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : fin
On teste le client Linux :
shiva[serge]:/home/admin/serge/java/rmi/echo/client#
$ java cltEcho srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin
A noter que pour le client linux qui fonctionne sur la même machine que le serveur d’écho, il n’a pas été besoin de préciser de machine dans l’URL du service demandé.
9.3. Deuxième exemple : Serveur SQL sur machine Windows
9.3.1. Le problème
Nous avons vu dans le chapitre JDBC comment gérer des bases de données relationnelles. Dans les exemples présentés, les applications et la base de données utilisée étaient sur la même machine Windows. On se propose ici, d’écrire un serveur RMI sur une machine Windows, qui permettrait aux clients distants d’exploiter les bases ODBC publiques du poste sur lequel se trouve le serveur.

Le client RMI pourrait faire 3 opérations :
- se connecter à la base de son choix
- émettre des requêtes SQL
- fermer la connexion
Le serveur exécute les requêtes SQL du client et lui envoie les résultats. C’est son travail essentiel et c’est pourquoi nous l’appellerons un serveur SQL.
Nous appliquons les différentes étapes vues précédemment avec le serveur d’écho.
9.3.2. Étape 1 : l’interface distante
L’interface distante est l’interface qui liste les méthodes du serveur RMI qui seront accessibles aux clients RMI. Nous utiliserons l’interface suivante :
import java.rmi.*;
// l'interface distante
public interface interSQL extends Remote{
public String connect(String pilote, String url, String id, String mdp)
throws java.rmi.RemoteException;
public String[] executeSQL(String requete, String separateur)
throws java.rmi.RemoteException;
public String close()
throws java.rmi.RemoteException;
}
Le rôle des différentes méthodes est le suivant :
Connect : le client se connecte à une base de données distante dont il donne le pilote, l’url JDBC ainsi que son identité id et son mot de passe mdp pour accéder à cette base. Le serveur lui rend une chaîne de caractères indiquant le résultat de la connexion :
executeSQL : le client demande l’exécution d’une requête SQL sur la base à laquelle il est connecté. Il indique le caractère qui doit séparer les champs dans les résultats qui lui sont renvoyés. Le serveur renvoie un tableau de chaînes :
pour une requête de mise à jour de la base, n étant le nombre de lignes mises à jour
si la requête a généré une erreur
si la requête n’a généré aucun résultat
si la requête a généré des résultats. Les lignes ainsi renvoyées par le serveur sont les lignes résultats de la requête.
close : le client ferme sa connexion avec la base distante. Le serveur renvoie une chaîne indiquant le résultat de cette fermeture :
9.3.3. Étape 2 : Écriture du serveur
Le source Java du serveur SQL suit. Sa compréhension nécessite d’avoir assimilé la gestion JDBC des bases de données ainsi que la construction des serveurs RMI. Les commentaires du programme devraient faciliter sa compréhension.
// packages importés
import java.rmi.*;
import java.rmi.server.*;
import java.sql.*;
import java.util.*;
// classe srvSQL
public class srvSQL extends UnicastRemoteObject implements interSQL{
// données globales de la classe
private Connection DB;
// ------------- constructeur
public srvSQL() throws RemoteException{
super();
}
// --------------- connect
public String connect(String pilote, String url, String id,
String mdp) throws RemoteException{
// connexion à la base url grâce au driver pilote
// identification avec identité id et mot de passe mdp
String resultat=null; // résultat de la méthode
try{
// chargement du pilote
Class.forName(pilote);
// demande de connexion
DB=DriverManager.getConnection(url,id,mdp);
// ok
resultat="200 Connexion réussie";
} catch (Exception e){
// erreur
resultat="500 Echec de la connexion (" + e + ")";
}
// fin
return resultat;
}
// ------------- executeSQL
public String[] executeSQL(String requete, String separateur)
throws RemoteException{
// exécute une requête SQL sur la base DB
// et met les résultats dans un tableau de chaînes
// données nécessaires à l'exécution de la requête
Statement S=null;
ResultSet RS=null;
String[] lignes=null;
Vector resultats=new Vector();
String ligne=null;
try{
// création du conteneur de la requête
S=DB.createStatement();
// exécution requête
if (! S.execute(requete)){
// requête de mise à jour
// on renvoie le nombre de lignes mises à jour
lignes=new String[1];
lignes[0]="100 "+S.getUpdateCount();
return lignes;
}
// c'était une requête d'interrogation
// on récupère les résultats
RS=S.getResultSet();
// nombre de champs du Resultset
int nbChamps=RS.getMetaData().getColumnCount();
// on les exploite
while(RS.next()){
// création de la ligne des résultats
ligne="101 ";
for (int i=1;i<nbChamps;i++)
ligne+=RS.getString(i)+separateur;
ligne+=RS.getString(nbChamps);
// ajout au vecteur des résultats
resultats.addElement(ligne);
}// while
// fin de l'exploitation des résultats
// on libère les ressources
RS.close();
S.close();
// on rend les résultats
int nbLignes=resultats.size();
if (nbLignes==0){
lignes=new String[1];
lignes[0]="501 Pas de résultats";
} else {
lignes=new String[resultats.size()];
for(int i=0;i<lignes.length;i++)
lignes[i]=(String) resultats.elementAt(i);
}//if
return lignes;
} catch (Exception e){
// erreur
lignes=new String[1];
lignes[0]="500 " + e;
return lignes;
}// try-catch
}// executeSQL
// --------------- close
public String close() throws RemoteException {
// ferme la connexion à la base de données
String resultat=null;
try{
DB.close();
resultat="200 Base fermée";
} catch (Exception e){
resultat="500 Erreur à la fermeture de la base ("+e+")";
}
// renvoi du résultat
return resultat;
}
// ----------- main
public static void main (String[] args){
// gestionnaire de sécurité
System.setSecurityManager(new RMISecurityManager());
// lancement du service
srvSQL serveurSQL=null;
try{
// création
serveurSQL=new srvSQL();
// enregistrement
Naming.rebind("srvSQL",serveurSQL);
// suivi
System.out.println("Serveur SQL prêt");
} catch (Exception e){
// erreur
System.err.println("Erreur lors du lancement du serveur SQL ("+ e +")");
}// try-catch
}// main
}// classe
9.3.4. Écriture du client RMI
Le client du serveur RMI est appelé avec les paramètres suivants :
urlserviceAnnuaire : url RMI du service d’annuaire qui a enregistré le serveur SQL
pilote : pilote que doit utiliser le serveur SQL pour gérer la base de données
urlBase : url JDBC de la base de données à gérer
id : identité du client ou null si pas d’identité
mdp : mot de passe du client ou null si pas de mot de passe
separateur : caractère que le serveur SQL doit utiliser pour séparer les champs des lignes résultats d’une requête
Voici un exemple de paramètres possibles :
avec ici :
nom rmi du serveur SQL
le pilote usuel des bases avec interface odbc
pour utiliser une base articles déclarée dans la liste des bases publiques ODBC de la machine Windows
pas d’identité
pas de mot de passe
les champs des résultats seront séparés par une virgule
Une fois lancé avec les paramètres précédents, le client suit les étapes suivantes :
- il se connecte au serveur RMI srvSQL, donc un serveur RMI sur la même machine que le client
- il demande la connexion à la base de données articles
- il demande à l’utilisateur de taper une requête SQL au clavier
- il l’envoie au serveur SQL
- il affiche à l’écran les résultats renvoyés par le serveur
- il redemande à l’utilisateur de taper une requête SQL au clavier. Il s’arrêtera lorsque la requête est fin.
Le texte Java du client suit. Les commentaires devraient suffire à sa compréhension.
import java.rmi.*;
import java.io.*;
public class cltSQL {
// données globales de la classe
private static String syntaxe =
"syntaxe : cltSQL urlServiceAnnuaire pilote urlBase id mdp separateur";
private static BufferedReader in=null;
private static interSQL serveurSQL=null;
public static void main(String arg[]){
// syntaxe : cltSQL urlServiceAnnuaire separateur pilote url id mdp
// urlServiceAnnuaire : url de l'annuaire des services RMI à contacter
// pilote : pilote à utiliser pour la base de données à exploiter
// urlBase : url jdbc de la base à exploiter
// id : identité de l'utilisateur
// mdp : son mot de passe
// separateur : chaîne séparant les champs dans les résultats d'une requête
// vérification du nb d'arguments
if(arg.length!=6)
erreur(syntaxe,1);
// init paramètres de la connexion à la base de données
String urlService=arg[0];
String pilote=arg[1];
String urlBase=arg[2];
String id, mdp, separateur;
if(arg[3].equals("null")) id=""; else id=arg[3];
if(arg[4].equals("null")) mdp=""; else mdp=arg[4];
if(arg[5].equals("null")) separateur=" "; else separateur=arg[5];
// installation d'un gestionnaire de sécurité
System.setSecurityManager(new RMISecurityManager());
// dialogue client-serveur
String requete=null;
String reponse=null;
String[] lignes=null;
String codeErreur=null;
try{
// ouverture du flux clavier
in=new BufferedReader(new InputStreamReader(System.in));
// suivi
System.out.println("--> Connexion au serveur RMI en cours...");
// localisation du service
serveurSQL=(interSQL) Naming.lookup(urlService);
// suivi
System.out.println("--> Connexion à la base de données en cours");
// demande de connexion initiale à la base de données
reponse=serveurSQL.connect(pilote,urlBase,id,mdp);
// suivi
System.out.println("<-- "+reponse);
// analyse réponse
codeErreur=reponse.substring(0,3);
if(codeErreur.equals("500"))
erreur("Abandon sur erreur de connexion à la base",3);
// boucle de lecture des requêtes à envoyer au serveur SQL
System.out.print("--> Requête : ");
requete=in.readLine().toLowerCase().trim();
while(! requete.equals("fin")){
// envoi de la requête au serveur et réception de la réponse
lignes=serveurSQL.executeSQL(requete,separateur);
// suivi
afficheLignes(lignes);
// requête suivante
System.out.print("--> Requête : ");
requete=in.readLine().toLowerCase().trim();
}// while
// suivi
System.out.println("--> Fermeture de la connexion à la base de données distante");
// on clôt la connexion
reponse=serveurSQL.close();
// suivi
System.out.println("<-- " + reponse);
// fin
System.exit(0);
// gestion des erreurs
} catch (Exception e){
erreur("Abandon sur erreur : " + e,2);
}// try
}// main
// ----------- AfficheLignes
private static void afficheLignes(String[] lignes){
for (int i=0;i<lignes.length;i++)
System.out.println("<-- " + lignes[i]);
}// afficheLignes
// ------------ erreur
private static void erreur(String msg, int exitCode){
// affichage msg d'erreur
System.err.println(msg);
// libération éventuelle des ressources
try{
in.close();
serveurSQL.close();
} catch(Exception e){}
// on quitte
System.exit(exitCode);
}// erreur
}// classe
9.3.5. Étape 3 : création des fichiers .class
- le serveur est compilé
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\javac interSQL.java
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\javac srvSQL.java
E:\data\java\RMI\sql\serveur>dir *.class
INTERS~1 CLA 451 12/03/99 17:54 interSQL.class
SRVSQL~1 CLA 3 238 12/03/99 17:54 srvSQL.class
- les fichiers Stub et Skel sont créés
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\rmic srvSQL
E:\data\java\RMI\sql\serveur>dir *.class
INTERS~1 CLA 451 12/03/99 17:54 interSQL.class
SRVSQL~1 CLA 3 238 12/03/99 17:54 srvSQL.class
SRVSQL~2 CLA 4 491 12/03/99 17:56 srvSQL_Stub.class
SRVSQL~3 CLA 2 414 12/03/99 17:56 srvSQL_Skel.class
- on transfére les fichiers interSQL.class, srvSQL_Stub.class, srvSQL_Skel.class dans le répertoire du client
E:\data\java\RMI\sql\client>dir
CLTSQL~1 JAV 3 486 11/03/99 11:39 cltSQL.java
INTERS~1 CLA 451 11/03/99 10:55 interSQL.class
SRVSQL~1 CLA 4 491 11/03/99 13:19 srvSQL_Stub.class
SRVSQL~2 CLA 2 414 11/03/99 13:19 srvSQL_Skel.class
- on compile le client
E:\data\java\RMI\sql\client>j:\jdk12\bin\javac cltSQL.java
E:\data\java\RMI\sql\client>dir *.class
INTERS~1 CLA 451 11/03/99 10:55 interSQL.class
CLTSQL~1 CLA 2 839 12/03/99 18:00 cltSQL.class
SRVSQL~1 CLA 4 491 11/03/99 13:19 srvSQL_Stub.class
SRVSQL~2 CLA 2 414 11/03/99 13:19 srvSQL_Skel.class
9.3.6. Étape 4 : Tests avec serveur & client sur même machine windows
- le service d’annuaire est lancé dans un répertoire autre que celui du serveur et du client
- on place le fichier mypolicy suivant dans les répertoires du client et du serveur
- on lance le serveur
E:\data\java\RMI\sql\serveur>start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/sql/serveur/ srvSQL
- on lance le client
E:\data\java\RMI\sql\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltSQL srvSQL
sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,
--> Connexion au serveur RMI en cours...
--> Connexion à la base de données en cours
<-- 200 Connexion réussie
--> Requête : select nom, stock_actu from articles order by stock_actu desc
<-- 101 vélo,31
<-- 101 essai3,13
<-- 101 skis nautiques,13
<-- 101 canoé,13
<-- 101 panthère,11
<-- 101 léopard,11
<-- 101 cachalot,10
<-- 101 fusil,10
<-- 101 arc,10
--> Requête : update articles set stock_actu=stock_actu-1 where stock_actu<=11
<-- 100 5
--> Requête : select nom,stock_actu from articles order by stock_actu asc
<-- 101 cachalot,9
<-- 101 fusil,9
<-- 101 arc,9
<-- 101 panthère,10
<-- 101 léopard,10
<-- 101 essai3,13
<-- 101 skis nautiques,13
<-- 101 canoé,13
<-- 101 vélo,31
--> Requête : fin
--> Fermeture de la connexion à la base de données distante
<-- 200 Base fermée
9.3.7. Étape 5 : Tests avec serveur sur machine windows et client sur machine linux
- on arrête éventuellement le serveur et le service d’annuaire
- on transfère les fichiers .class du client sur une machine linux
shiva[serge]:/home/admin/serge/java/rmi/sql/client#
$ dir *.class
-rw-r--r-- 1 serge admin 2839 Mar 11 14:37 cltSQL.class
-rw-r--r-- 1 serge admin 451 Mar 11 14:37 interSQL.class
- les fichiers du serveur sont mis dans un répertoire accessible au serveur http de la machine windows
D:\Inetpub\wwwroot\rmi\sql>dir
INTERS~1 CLA 451 11/03/99 10:55 interSQL.class
SRVSQL~1 CLA 3 238 11/03/99 13:19 srvSQL.class
SRVSQL~2 CLA 4 491 11/03/99 13:19 srvSQL_Stub.class
SRVSQL~3 CLA 2 414 11/03/99 13:19 srvSQL_Skel.class
MYPOLICY 81 08/06/98 15:01 mypolicy
- on relance le service d’annuaire
- on relance le serveur avec des paramètres différents que ceux utilisés dans le test précédent
D:\Inetpub\wwwroot\rmi\sql>start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/sql/ srvSQL
- on lance le client sur la machine linux
/usr/local/bin/jdk/bin/java cltSQL rmi://tahe.istia.univ-angers.fr/srvSQL sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,
--> Requête : select nom,stock_actu,stock_mini from articles order by nom
<-- 101 arc,9,8
<-- 101 cachalot,9,6
<-- 101 canoé,13,7
<-- 101 essai3,13,9
<-- 101 fusil,9,8
<-- 101 léopard,10,7
<-- 101 panthère,10,7
<-- 101 skis nautiques,13,8
<-- 101 vélo,31,8
--> Requête : update articles set stock_actu=stock_mini where stock_mini<=7
<-- 100 4
--> Requête : select nom,stock_actu,stock_mini from articles order by nom
<-- 101 arc,9,8
<-- 101 cachalot,6,6
<-- 101 canoé,7,7
<-- 101 essai3,13,9
<-- 101 fusil,9,8
<-- 101 léopard,7,7
<-- 101 panthère,7,7
<-- 101 skis nautiques,13,8
<-- 101 vélo,31,8
--> Requête : fin
--> Fermeture de la connexion à la base de données distante
<-- 200 Base fermée
9.3.8. Conclusion
On a là une application intéressante en ce qu’elle permet l’accès à une base de données à partir d’un poste quelconque du réseau. On aurait très bien pu l’écrire de façon traditionnelle avec des sockets et ce qui est d’ailleurs demandé dans un exercice du chapitre sur les bases de données. Si on écrivait cette application de façon traditionnelle :
- on aurait un client et un serveur qui pourraient être écrits avec des langages différents
- le client et le serveur communiqueraient par échange de lignes de texte et auraient un dialogue qui pourrait être du genre :
où les deux premiers paramètres spécifient où trouver le serveur, les quatre suivants indiquant les paramètres de connexion à la base de données à exploiter
Le serveur pourrait répondre par quelque chose du genre :
pour demander au serveur d’exécuter une requête SQL sur la base connectée au client. separateur étant le caractère séparateur des champs dans les lignes de la réponse.
Le serveur pourrait répondre quelque chose du genre
pour une requête de mise à jour de la base, n étant le nombre de lignes mises à jour
si la requête a généré une erreur
si la requête n’a généré aucun résultat
si la requête a généré des résultats. Les lignes ainsi renvoyées par le serveur sont les lignes résultats de la requête.
pour fermer la connexion avec la base distante. Le serveur pourrait renvoyer une chaîne indiquant le résultat de cette fermeture :
On voit ici que si on est capable de construire une application traditionnelle avec un protocole du genre précédent, on peut en déduire une structure possible du serveur RMI. Là où dans le protocole on a une phrase du client vers le serveur du genre :
on pourra avoir au sein du serveur RMI une méthode
et cette méthode accessible au client devra donc faire partie de l’interface publiée du serveur.
Pour en terminer là, notons que notre serveur ne gère actuellement qu’un client : il ne peut en gérer plusieurs, tel qu’il est écrit actuellement. En effet, si un client se connecte à une base B1, un objet Connection DB=DB1 est créé par le serveur. Si un second client demande une connexion à une base B2, le serveur le note en faisant Connection DB=DB2, cassant ainsi la connexion du premier client à la base B1.
9.4. Exercices
9.4.1. Exercice 1
Étendre le serveur SQL précédent afin qu’il gère plusieurs clients.
9.4.2. Exercice 2
Écrire l’applet Java de commerce électronique présentée dans les exercices du chapitre JDBC afin qu’elle travaille avec le serveur RMI de l’exercice précédent.