Skip to content

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 :

  1. Les applications client/serveur sont des applications Java aux deux extrémités de la communication
  2. Le client peut utiliser des objets situés sur le serveur comme s’ils étaient locaux
  3. 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 :

  1. 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.
  2. 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 :

  1. la méthode qui fait l’écho
  2. 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 :

  1. 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.
  2. à 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 :

Naming.rebind(String nom, Remote obj)

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

  1. lit une ligne tapée au clavier
  2. l’envoie au serveur d’écho
  3. affiche la réponse que celui-ci envoie
  4. 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 :

            serveur=(interEcho) Naming.lookup(urlService);

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 :

    rmi://machine:port/nom_service

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 :

E:\data\java\RMI\echo\client>dir *.java

CLTECH~1 JAV         1 427  09/03/99  16:08 cltEcho.java

ainsi que le fichier interEcho.class qui a été généré lors de la compilation du serveur :

E:\data\java\RMI\echo\client>dir *.class

INTERE~1 CLA           256  09/03/99  15:59 interEcho.class

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 :

            serveur=(interEcho) Naming.lookup(urlService);              

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 :

E:\data\java\RMI\echo\serveur>j:\jdk12\bin\rmic srvEcho

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 :

start j:\jdk12\bin\rmiregistry

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 :

E:\data\java\RMI\echo\serveur>start j:\jdk12\bin\java srvEcho

Le serveur d’écho s’exécute dans une nouvelle fenêtre DOS et affiche comme on le lui avait demandé :

Serveur d’écho prêt

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

java.security.policy est un mot clé

mypolicy est un fichier texte définissant les droits du programme. Ici, c’est le suivant :

grant {
    // Allow everything for now
    permission java.security.AllPermission;
};

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.

Image

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 :

    200 - Connexion réussie
    500 - Echec 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 :

    100 n

    pour une requête de mise à jour de la base, n étant le nombre de lignes mises à jour

    500 msg d’erreur

    si la requête a généré une erreur

    501 Pas de résultats

    si la requête n’a généré aucun résultat

    101 ligne1
    101 ligne2
    101 ...

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 :

    200 Base fermée
    500 Erreur lors de la fermeture de la base (msg d’erreur)

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 pilote urlBase id mdp separateur

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 :

    srvSQL sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

avec ici :

urlserviceAnnuaire  srvSQL

nom rmi du serveur SQL

pilote  sun.jdbc.odbc.JdbcOdbcDriver

le pilote usuel des bases avec interface odbc

urlBase jdbc:odbc:articles

pour utiliser une base articles déclarée dans la liste des bases publiques ODBC de la machine Windows

id  null

pas d’identité

mdp null

pas de mot de passe

separateur  , 

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
connect(‘’sun.jdbc.odbc.JdbcOdbcDriver’’, ‘‘jdbc:odbc:articles’’, ’’’’, ’’’’)
  • il demande à l’utilisateur de taper une requête SQL au clavier
  • il l’envoie au serveur SQL
executeSQL(requete, ’’,’’);
  • 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
F:\>start j:\jdk12\bin\rmiregistry
  • on place le fichier mypolicy suivant dans les répertoires du client et du serveur
grant {
    // Allow everything for now
    permission java.security.AllPermission;
};
  • 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
F:\>start j:\jdk12\bin\rmiregistry
  • 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 :
client : connect machine port pilote urlBase id mdp

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 :

200 - Connexion réussie
500 - Echec de la connexion
client : executeSQL requete, separateur

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

    100 n

    pour une requête de mise à jour de la base, n étant le nombre de lignes mises à jour

    500 msg d’erreur

    si la requête a généré une erreur

    501 Pas de résultats

    si la requête n’a généré aucun résultat

    101 ligne1
    101 ligne2
    101 ...

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.

client : close

pour fermer la connexion avec la base distante. Le serveur pourrait renvoyer une chaîne indiquant le résultat de cette fermeture :

    200 Base fermée
    500 Erreur lors de la fermeture de la base (msg d’erreur)

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 :

    commande param1 param2 ... paramq

on pourra avoir au sein du serveur RMI une méthode

    String commande(param1,param2,..., paramq)

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.