Skip to content

9. JAVA RMI

9.1. Einführung

Wir haben gesehen, wie man Netzwerkanwendungen mithilfe von Kommunikationswerkzeugen namens Sockets erstellt. In einer auf diesen Werkzeugen basierenden Client-Server-Anwendung ist die Verbindung zwischen Client und Server das Kommunikationsprotokoll, das sie für die Kommunikation verwenden. Die beiden Anwendungen können in verschiedenen Sprachen geschrieben sein: beispielsweise Java für den Client, Perl für den Server oder eine beliebige andere Kombination. Wir haben also tatsächlich zwei unterschiedliche Anwendungen, die durch ein beiden bekanntes Kommunikationsprotokoll verbunden sind. Darüber hinaus ist der Netzwerkzugriff über Sockets für eine Java-Anwendung nicht transparent: Sie muss die Socket-Klasse verwenden, eine Klasse, die speziell zur Verwaltung dieser als Sockets bezeichneten Kommunikationswerkzeuge entwickelt wurde.

Java RMI (Remote Method Invocation) ermöglicht es Ihnen, Netzwerkanwendungen mit den folgenden Eigenschaften zu erstellen:

  1. Client/Server-Anwendungen sind Java-Anwendungen an beiden Enden der Kommunikation
  2. Der Client kann Objekte auf dem Server so nutzen, als wären sie lokal
  3. Die Netzwerkschicht wird transparent: Anwendungen müssen sich keine Gedanken darüber machen, wie Informationen von einem Punkt zum anderen transportiert werden.

Der letzte Punkt ist ein Faktor für die Portabilität: Sollte sich die Netzwerkschicht einer RMI-Anwendung ändern, müsste die Anwendung selbst nicht neu geschrieben werden. Es sind die RMI-Klassen der Java-Sprache, die an die neue Netzwerkschicht angepasst werden müssten.

Das Prinzip der RMI-Kommunikation ist wie folgt:

  1. Eine Standard-Java-Anwendung wird auf Rechner A geschrieben. Sie fungiert als Server. Dazu werden einige ihrer Objekte auf Rechner A, auf dem die Anwendung läuft, „veröffentlicht“ und werden so zu Diensten.
  2. Eine klassische Java-Anwendung wird auf Rechner B geschrieben. Sie fungiert als Client. Sie hat Zugriff auf die auf Rechner A veröffentlichten Objekte/Dienste; das heißt, über eine Remote-Referenz kann sie diese so manipulieren, als wären sie lokal. Dazu muss sie die Struktur des Remote-Objekts kennen, auf das sie zugreifen möchte (Methoden und Eigenschaften).

9.2. Lernen wir dies anhand eines Beispiels

Die Theorie hinter der RMI-Schnittstelle ist nicht einfach. Um die Sache zu verdeutlichen, werden wir Schritt für Schritt durch den Prozess der Erstellung einer Client-Server-Anwendung unter Verwendung des RMI-Pakets von Java gehen. Wir verwenden eine Anwendung, die in vielen Büchern über RMI zu finden ist: Der Client ruft eine einzelne Methode eines Remote-Objekts auf, die dann eine Zeichenkette zurückgibt. Hier stellen wir eine leichte Abwandlung vor: Der Server gibt das, was der Client ihm sendet, unverändert zurück. Eine solche Anwendung haben wir in diesem Buch bereits vorgestellt, allerdings eine, die auf Sockets basiert.

9.2.1. Die Serveranwendung

9.2.1.1. Schritt 1: Die Objekt-/Server-Schnittstelle

Ein Remote-Objekt ist eine Klasseninstanz, die die im Paket java.rmi definierte Remote-Schnittstelle implementieren muss. Die Methoden des Objekts, auf die remote zugegriffen werden kann, sind diejenigen, die in einer von der Remote-Schnittstelle abgeleiteten Schnittstelle deklariert sind:

import java.rmi.*;

 // remote interface p
ublic interface interEcho extends Remote{ 
     public String echo(String msg) throws java.rmi.RemoteException
; }

Hier deklarieren wir eine interEcho-Schnittstelle, die eine echo-Methode als remote zugänglich deklariert. Diese Methode kann eine Ausnahme der Klasse RemoteException auslösen, die alle netzwerkbezogenen Fehler umfasst.

9.2.1.2. Schritt 2: Schreiben des Server-Objekts

Im nächsten Schritt definieren wir die Klasse, die die zuvor definierte Remote-Schnittstelle implementiert. Diese Klasse muss von der Klasse „UnicastRemoteObject“ abgeleitet sein, die Methoden bereitstellt, die den Aufruf von Remote-Methoden ermöglichen.

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

// class implementing remote echo
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // manufacturer
    public srvEcho() throws RemoteException{
        super();
    }// manufacturer end

    // method performing the echo
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// fine echo
}// end of class

In der vorherigen Klasse finden wir:

  1. die Methode, die das Echo ausführt
  2. einen Konstruktor, der nichts anderes tut, als den Konstruktor der übergeordneten Klasse aufzurufen. Er dient dazu, zu deklarieren, dass eine RemoteException ausgelöst werden kann.

Wir werden eine Instanz dieser Klasse mit einer main-Methode erstellen. Damit ein Objekt/Dienst von außen zugänglich ist, muss es erstellt und im Verzeichnis der extern zugänglichen Objekte registriert werden. Ein Client, der auf ein Remote-Objekt zugreifen möchte, geht wie folgt vor:

  1. Er kontaktiert den Verzeichnisdienst auf dem Rechner, auf dem sich das gewünschte Objekt befindet. Dieser Verzeichnisdienst läuft auf einem Port, den der Client kennen muss (standardmäßig 1099). Der Client fordert vom Verzeichnis eine Referenz auf ein Objekt/einen Dienst an und gibt dessen Namen an. Wenn dieser Name einem Objekt/Dienst im Verzeichnis entspricht, gibt das Verzeichnis eine Referenz an den Client zurück, über die der Client mit dem Remote-Objekt/Dienst kommunizieren kann.
  2. Von diesem Zeitpunkt an kann der Client dieses Remote-Objekt so nutzen, als wäre es lokal

Zurück zu unserem Server: Wir müssen ein Objekt vom Typ srvEcho erstellen und es im Verzeichnis der extern zugänglichen Objekte registrieren. Diese Registrierung erfolgt mithilfe der Methode rebind der Klasse *Naming*:

Naming.rebind(String nom, Remote obj)

wobei

name: der Name, der dem Remote-Objekt zugeordnet wird

obj: das Remote-Objekt

Unsere srvEcho-Klasse sieht daher wie folgt aus:

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

// class implementing remote echo
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // manufacturer
    public srvEcho() throws RemoteException{
        super();
    }// manufacturer end

    // method performing the echo
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// fine echo

    // service creation
    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 ");
        }
    }// hand
}// end of class

Beim Lesen des vorherigen Programms scheint es, als würde es unmittelbar nach dem Erstellen und Registrieren des Echo-Dienstes beendet werden. Dies ist jedoch nicht der Fall. Da die Klasse srvEcho von der Klasse *UnicastRemoteObject* abgeleitet ist, läuft das erstellte Objekt unbegrenzt weiter: Es wartet auf Client-Anfragen an einem anonymen Port, d. h. einem Port, der vom System je nach den Umständen ausgewählt wird. Die Erstellung des Dienstes erfolgt asynchron: Im Beispiel erstellt die main-Methode den Dienst und setzt die Ausführung fort; sie zeigt „Echo-Server bereit“ an.

9.2.1.3. Schritt 3: Kompilieren der Serveranwendung

Nun können wir unseren Server kompilieren. Wir kompilieren die Datei „interEcho.java“ aus der Schnittstelle „interEcho“ sowie die Datei „srvEcho.java“ aus der Klasse „srvEcho“. Dabei erhalten wir die entsprechenden .class-Dateien: „interEcho.class“ und „srvEcho.class“.

9.2.1.4. Schritt 4: Schreiben des Clients

Wir schreiben einen Client, der die URL des Echo-Servers als Parameter entgegennimmt und

  1. eine über die Tastatur eingegebene Zeile liest
  2. sie an den Echo-Server sendet
  3. die Antwort anzeigt
  4. kehrt zu Schritt 1 zurück und stoppt, wenn die eingegebene Zeile „end“ lautet.

Dies führt zu folgendem Client:

import java.rmi.*;
 import java.io.*

; public class cltEch

    o { public static void main(String a
        rg[]){ // syntax : cltEcho URLService

        // argument verification
        if(arg.length!=1){
            System.err.println("Syntaxe : pg url_service_rmi");
            System.exit(1);
        }

        // client-server dialogue
        String urlService=arg[0];
        BufferedReader in=null;
        String msg=null;
        String reponse=null;
        interEcho serveur=null;

        try{
            // open keyboard flow
            in=new BufferedReader(new InputStreamReader(System.in));
            // service location
            serveur=(interEcho) Naming.lookup(urlService);                
            // loop for reading msg to be sent to echo server
            System.out.print("Message : ");
            msg=in.readLine().toLowerCase().trim();
            while(! msg.equals("fin")){
                // send msg to server and receive response
                reponse=serveur.echo(msg);
                // follow-up
                System.out.println("Réponse serveur : " + reponse);
                // next msg
                System.out.print("Message : ");                
                msg=in.readLine().toLowerCase().trim();
            }// while
            // it's over
            System.exit(0);
        // error management        
        } catch (Exception e){
            System.err.println("Erreur : " + e);
            System.exit(2);
        }// try
    }// hand
}// class                

An diesem Client ist nichts Besonderes, abgesehen von der Anweisung, die eine Serverreferenz abruft:

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

Erinnern Sie sich daran, dass unser Echo-Dienst im Dienstverzeichnis des Rechners, auf dem er sich befindet, mit folgendem Befehl registriert wurde:


            Naming.rebind("srvEcho",serveurEcho);

Der Client verwendet daher ebenfalls eine Methode der Naming-Klasse, um eine Referenz auf den Server zu erhalten, den er nutzen möchte. Die verwendete Lookup-Methode nimmt die URL des angeforderten Dienstes als Parameter entgegen. Diese URL hat die Form einer Standard-URL:

    rmi://machine:port/nom_service

wobei

rmi: optional – RMI-Protokoll

machine: Name oder IP-Adresse des Rechners, auf dem der Echo-Server läuft – optional, Standard ist localhost.

port: Listening-Port des Verzeichnisdienstes dieses Rechners – optional, Standardwert 1099

service_name: Name, unter dem der angeforderte Dienst registriert wurde (in unserem Beispiel srvEcho)

Als Rückgabewert wird eine Instanz der Remote-Schnittstelle interEcho zurückgegeben. Angenommen, Client und Server befinden sich nicht auf demselben Rechner, muss beim Kompilieren des Clients cltEcho.java die Datei interEcho.class – das Ergebnis der Kompilierung der Remote-Schnittstelle interEcho – im selben Verzeichnis vorhanden sein; andernfalls tritt an den Zeilen, die auf diese Schnittstelle verweisen, ein Kompilierungsfehler auf.

9.2.1.5. Schritt 5: Erstellen der für die Client-Server-Anwendung erforderlichen .class-Dateien

Um klar zwischen den serverseitigen und den clientseitigen Komponenten zu unterscheiden, werden wir den Server im Verzeichnis echo\server und den Client im Verzeichnis echo\client ablegen.

Das Server-Verzeichnis enthält die folgenden Quelldateien:

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

Nach der Kompilierung dieser beiden Quelldateien erhalten wir die folgenden .class-Dateien:

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

Im Client-Verzeichnis finden wir die folgende Quelldatei:

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

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

sowie die Datei interEcho.class, die bei der Server-Kompilierung generiert wurde:

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

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

Nach der Kompilierung der Quelldatei erhalten wir die folgenden .class-Dateien:

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

Wenn wir versuchen, den cltEcho-Client auszuführen, erhalten wir folgende Fehlermeldung:

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

Wenn Sie versuchen, den srvEcho-Server auszuführen, erhalten Sie die folgende Fehlermeldung:

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

In beiden Fällen meldet die Java Virtual Machine, dass sie die Klasse srvEcho_stub nicht finden konnte. Tatsächlich haben wir noch nie zuvor von dieser Klasse gehört. Im Client wurde der Server mit der folgenden Anweisung gefunden:

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

Hier ist urlService die Zeichenfolge rmi://localhost/srvEcho mit

RMI: RMI-Protokoll

Localhost: der Rechner, auf dem der Server läuft – in diesem Fall derselbe Rechner wie der Client. Die Syntax lautet normalerweise Rechner:Port. Wenn kein Port angegeben wird, wird standardmäßig Port 1099 verwendet. Der Verzeichnisdienst des Servers hört auf diesem Port.

srvEcho: Dies ist der Name des spezifischen Dienstes, der angefordert wird

Bei der Kompilierung wurden keine Fehler gemeldet. Die Datei interEcho.class für die Remote-Schnittstelle musste lediglich verfügbar sein.

Zur Laufzeit benötigt die virtuelle Maschine die Datei srvEcho_stub.class, wenn der angeforderte Dienst der srvEcho-Dienst ist; generell wird für einen Dienst X die Datei X_stub.class benötigt. Diese Datei ist nur zur Laufzeit erforderlich, nicht während der Client-Kompilierung. Dasselbe gilt für den Server. Was genau ist also diese Datei?

Auf dem Server befindet sich die Klasse srvEcho.class, die unser Remote-Objekt bzw. -Dienst ist. Der Client benötigt, auch wenn er diese Klasse nicht benötigt, dennoch eine Art Abbild davon, um mit ihr zu kommunizieren. Tatsächlich sendet der Client seine Anfragen nicht direkt an das Remote-Objekt: Er sendet sie an sein lokales Abbild srvEcho_stub.class, das sich auf demselben Rechner wie er selbst befindet. Dieses lokale Abbild, srvEcho_stub.class, kommuniziert mit einem ähnlichen Abbild (srvEcho_stub.class), das sich auf dem Server befindet. Dieses Abbild wird aus der .class-Datei des Servers mithilfe eines Java-Tools namens rmic erstellt. Unter Windows lautet der Befehl:

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

erzeugt aus der Datei „srvEcho.class“ zwei weitere .class-Dateien:

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

Hier haben wir die Datei srvEcho_stub.class, die sowohl der Client als auch der Server zur Laufzeit benötigen. Es gibt auch eine Datei srvEcho_Skel.class, deren Zweck derzeit unbekannt ist. Wir kopieren die Datei srvEcho_stub.class in die Client- und Server-Verzeichnisse und löschen die Datei srvEcho_Skel.class. Wir haben nun die folgenden Dateien:

auf der Serverseite:

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

auf der Client-Seite:

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. Schritt 6: Ausführen der Client-Server-Echo-Anwendung

Wir sind bereit, unsere Client-Server-Anwendung auszuführen. Zunächst laufen Client und Server auf demselben Rechner. Zunächst müssen wir unsere Serveranwendung starten. Erinnern Sie sich daran, dass dies:

  • den Dienst
  • und registriert ihn im Dienstverzeichnis des Rechners, auf dem der Echo-Server läuft

Dieser letzte Schritt erfordert einen Registrierungsdienst. Dieser wird mit dem Befehl gestartet:

start j:\jdk12\bin\rmiregistry

rmiregistry ist der Registrierungsdienst. Hier wird er im Hintergrund in einem Windows-Eingabeaufforderungsfenster mit dem Befehl „start“ gestartet. Sobald die Registrierung aktiv ist, können wir den Echo-Dienst erstellen und ihn in der Dienstregistrierung registrieren. Auch dieser wird im Hintergrund mit einem „start“-Befehl gestartet:

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

Der Echo-Server läuft in einem neuen DOS-Fenster und zeigt die angeforderte Ausgabe an:

Serveur d’écho prêt

Jetzt müssen wir nur noch unseren Client starten und testen:

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. Client und Server auf zwei verschiedenen Rechnern

Im vorherigen Beispiel befanden sich Client und Server auf demselben Rechner. Nun platzieren wir sie auf verschiedenen Rechnern:

  • den Server auf einem Windows-Rechner
  • der Client auf einem Linux-Rechner

Der Server wird wie zuvor auf dem Windows-Rechner gestartet. Auf dem Linux-Rechner wurden die .class-Dateien des Clients übertragen:

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

Der Client wird gestartet:

$ 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

Wir haben also einen Fehler: Die Java Virtual Machine benötigt offenbar die Datei srvEcho_skel.class, die vom Dienstprogramm rmic generiert wurde, aber bisher noch nicht verwendet wurde. Wir erstellen sie neu und übertragen sie auch auf den Linux-Rechner:

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

Wir erhalten denselben Fehler wie zuvor... Also überlegen wir es uns noch einmal und lesen die RMI-Dokumentation erneut durch. Schließlich kommen wir zu dem Schluss, dass der Server selbst vielleicht die berühmte Datei srvEcho_Skel.class benötigt. Wir starten dann den Server auf dem Windows-Rechner neu, wobei sowohl die Datei srvEcho_Stub.class als auch die Datei srvEcho_Skel.class vorhanden sind:

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

Anschließend testen wir den Client erneut auf dem Linux-Rechner, und diesmal funktioniert er:

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

Wir können daher schlussfolgern, dass auf der Serverseite sowohl die Datei srvEcho_Stub.class als auch die Datei srvEcho_Skel.class vorhanden sein müssen. Auf der Clientseite war bisher nur die Datei srvEcho_Stub.class erforderlich. Sie hatte sich als unverzichtbar erwiesen, als sich Client und Server auf demselben Windows-Rechner befanden. Unter Linux werden wir sie entfernen, um zu sehen, was passiert...

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

Wir haben einen interessanten Fehler, der darauf hindeutet, dass die Java Virtual Machine versucht hat, die berüchtigte Stub-Klasse zu laden, dies jedoch aufgrund des Fehlens eines „Security Managers“ nicht geschafft hat. Wir erinnern uns, in der Dokumentation etwas darüber gelesen zu haben. Wir tauchen noch einmal tiefer ein … und stellen fest, dass der Server einen Security Manager erstellen und installieren muss, um den Clients, die das Laden von Klassen anfordern, zu garantieren, dass die Klassen sicher sind. Ohne diesen Security Manager ist das Laden von Klassen unmöglich. Das scheint zu passen: Unser Linux-Client hat die benötigte Datei „srvEcho_stub.class“ vom Server angefordert, und der Server hat dies mit der Begründung abgelehnt, dass kein Security Manager installiert sei. Also ändern wir den Code der Hauptfunktion des Servers wie folgt:

     // service creation 
    public static void main (String arg[]){

         // installation of a security manager Sy
        stem.setSecurityManager(new RMISecurityManager()); 

        // service launch and registration
        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 ");
        }
    }// hand

Wir kompilieren und generieren die Dateien srvEcho_stub.class und srvEcho_Skel.class mit dem Tool rmic. Wir starten den Verzeichnisdienst (rmiregistry) und anschließend den Server, und erhalten eine Fehlermeldung, die zuvor nicht aufgetreten ist!

Erreur java.security.AccessControlException: access denied (java.net.SocketPermission 127.0.0.1:1099 connect,resolve)  lors du lancement du serveur d’écho

Der Sicherheitsmanager scheint zu streng gewesen zu sein. Wir sehen uns die Dokumentation noch einmal an... Wir stellen fest, dass man bei aktivem Sicherheitsmanager die Berechtigungen des Programms beim Start angeben muss. Dies geschieht mit der folgenden Option:

<mark style="background-color: #ffff00">start j:\\jdk12\\bin\\java -Djava.security.policy=mypolicy srvEcho</mark>

wobei

java.security.policy ein Schlüsselwort ist

mypolicy eine Textdatei ist, die die Berechtigungen des Programms definiert. Hier lautet sie wie folgt:

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

Das Programm verfügt hier über alle Berechtigungen.

Fangen wir noch einmal von vorne an. Wechseln Sie in das Server-Verzeichnis und führen Sie Folgendes aus:

  • Starten Sie den Verzeichnisdienst: start j:\jdk12\bin\rmiregistry
  • Starten Sie den Server: start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho

Und dieses Mal startet der Echo-Server (der Client jedoch noch nicht) korrekt. Nun können Sie Folgendes versuchen:

  • Stoppen Sie den Echo-Server und anschließend den Verzeichnisdienst
  • Starten Sie den Verzeichnisdienst neu, während Sie sich in einem anderen Verzeichnis als dem des Servers befinden
  • kehren Sie zum Serververzeichnis zurück starten Sie den Echo-Server – Sie erhalten die folgende Fehlermeldung:
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

Daraus lässt sich schließen, dass es eine Rolle spielt, aus welchem Verzeichnis der Verzeichnisdienst gestartet wird. In diesem Fall konnte Java die Datei „srvEcho_stub.class“ nicht finden, da der Verzeichnisdienst nicht aus dem Serververzeichnis gestartet wurde. Beim Starten des Servers können Sie das Verzeichnis angeben, in dem sich die vom Server benötigten Klassen befinden:

start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

Der Befehl steht in einer einzigen Zeile. Das Schlüsselwort <mark style="background-color: #ffff00">java.rmi.server.codebase</mark> wird verwendet, um die URL des Verzeichnisses anzugeben, das die vom Server benötigten Klassen enthält. In diesem Fall gibt die URL das file-Protokoll an – das Protokoll für den Zugriff auf lokale Dateien – sowie das Verzeichnis, das die .class-Dateien des Servers enthält. Wenn Sie also wie folgt vorgehen:

  • den Verzeichnisdienst anhalten
  • starten Sie den Verzeichnisdienst aus einem anderen Verzeichnis als dem des Servers neu
  • starten Sie im Serververzeichnis den Server mit dem Befehl (eine einzige Zeile):
 start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

Der Server läuft nun einwandfrei. Wir können nun zum Client übergehen. Testen wir ihn:

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

Wir erhalten denselben Fehler, der auf das Fehlen eines Sicherheitsmanagers hinweist. Wir vermuten, dass wir einen Fehler gemacht haben und dass der Client seinen eigenen Sicherheitsmanager erstellen muss. Wir belassen den Server mit seinem Sicherheitsmanager, erstellen aber auch einen für den Client. Die Hauptfunktion des Clients cltEcho.java lautet dann:

public static void main(String arg[]){
         // syntax : cltEcho machine po
        rt // machine: machine where the echo server 
        operates // port: port where the service directory operates on the echo servic

        e machine // argument ve
        rification if(ar
            g.length!=1){ System.err.println("Syntaxe : pg u
            rl_service_rmi"
        )

        ; System.exit(1); } // installation
         of a security manager System.setSecurityManager(n

        ew RMISecurityManager()); 
         // client-server dia
        logue String urlServi
        ce=arg[0]; Buf
        feredReader in=null;
         String msg=null; S

        tring reponse=null; interEcho serveur=null; try{
            ....
        } catch (Exception e){
            ....
        }// try
    }// hand

Wir gehen dann wie folgt vor:

  • cltEcho.java neu kompilieren
  • die .class-Dateien auf den Linux-Rechner übertragen
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
  • Starten Sie den 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

Trotz des ersten Anscheins machen wir Fortschritte: Der Fehler ist nicht mehr derselbe. Wir sehen, dass der Client die Datei srvEcho_Stub.class vom Server anfordern konnte, der Server sie jedoch nicht finden konnte. Daher muss der Client über einen Sicherheitsmanager verfügen, wenn er Klassen vom Server anfordern will.

Wenn wir uns den vorherigen Fehler ansehen, stellen wir fest, dass die Datei srvEcho_Stub.class im Verzeichnis e:/data/java/rmi/echo/server/ gesucht und nicht gefunden wurde. Doch genau dort befindet sie sich. Wenn wir uns die Liste der am Fehler beteiligten Methoden genauer ansehen, finden wir diese: sun.net.www.protocol.file.FileURLConnection.getInputStream. Der Client scheint eine Verbindung über ein FileURLConnection-Objekt geöffnet zu haben. Wir vermuten, dass all dies damit zusammenhängt, wie wir unseren Server gestartet haben:

start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

Die Fehlermeldung scheint sich auf den Wert des Schlüsselworts java.rmi.server.codebase* zu beziehen. Wenn wir uns die Dokumentation noch einmal ansehen, stellen wir fest, dass der Wert dieses Schlüsselworts in den angegebenen Beispielen immer http://.. lautet, d. h., das verwendete Protokoll ist HTTP. Es ist unklar, wie der Client seine Klassen vom Server anfordert und erhält. Möglicherweise fordert er sie über die URL an, die durch das Schlüsselwort java.rmi.server.codebase* angegeben wird, das beim Start des Servers festgelegt wird. Wir beschließen daher, den Server mit dem folgenden neuen Befehl zu starten:

start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho

Das Protokoll ist nun HTTP. Wir müssen die .class-Dateien an einen Ort verschieben, auf den der HTTP-Server auf dem Rechner, auf dem die Klassen gespeichert werden sollen, zugreifen kann. In unserem Beispiel läuft der Server auf einem Windows-Rechner mit einem Microsoft PWS-HTTP-Server. Das Stammverzeichnis dieses Servers lautet d:\Inetpub\wwwroot. Daher gehen wir wie folgt vor:

  • Erstellen Sie das Verzeichnis d:\Inetpub\wwwroot\rmi\echo
  • Legen Sie dort die .class-Dateien des Servers und die Datei mypolicy ab
  • Starten Sie den Webserver, falls er noch nicht läuft
  • Starten Sie den Registrierungsdienst (rmiregistry) neu
  • Starten Sie den Server mit dem Befehl neu
start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
  • Starten Sie auf dem Linux-Rechner den 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

Puh! Es funktioniert. Der Client hat die Datei „srvEcho_Stub.class“ erfolgreich abgerufen.

All dies hat uns auf einige Ideen gebracht, und wir fragen uns, ob der Client, der auf dem Windows-Rechner des Servers läuft, auch ohne die Datei „srvEcho_Stub.class“ funktionieren würde. Wir navigieren zum Verzeichnis des Clients, löschen die Datei „srvEcho_Stub.class“, falls sie vorhanden ist, und starten den Client auf die gleiche Weise wie unter 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. Zusammenfassung

Windows-Serverseite:

  • Der Server verfügt über einen Sicherheitsmanager
  • Er wurde mit den folgenden Optionen gestartet: start j:\jdk12\bin\java -Djava.security.policy=mypolicy

-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho

Client-Seite (Linux oder Windows)

  • Der Client verfügt über einen Sicherheitsmanager
  • Unter Linux wurde es mit `java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho` gestartet
  • Unter Windows wurde er mit j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho gestartet

9.2.1.9. Echo-Server unter Linux, Clients unter Windows und Linux

Wir werden nun den Server auf einen Linux-Rechner verlegen und Linux- und Windows-Clients testen. Die Vorgehensweise ist wie folgt:

  • Verschieben Sie die .class-Dateien des Servers auf den Linux-Rechner
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
  • Da die Datei srvEcho_Stub.class von Clients angefordert wird, muss das für die Serverklassen gewählte Verzeichnis für den HTTP-Server des Linux-Rechners zugänglich sein. Hier lautet die URL für dieses Verzeichnis http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serveur
  • Der Registrierungsdienst wird im Hintergrund gestartet: /usr/local/bin/jdk/rmiregistry &
  • Der Server wird im Hintergrund gestartet: /usr/local/bin/jdk/bin/java

-Djava.rmi.server.codebase=http://shiva.istia.univ-angers.fr/~serge/rmi/echo/server/

srvEcho &

Wir können die Clients testen. Zuerst den Windows-Client.

  • Navigieren Sie auf dem Windows-Rechner zum Client-Verzeichnis
  • Starten Sie den Client mit dem folgenden Befehl:
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

Testen des Linux-Clients:

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

Beachten Sie, dass für den Linux-Client, der auf demselben Rechner wie der Echo-Server läuft, keine Angabe eines Rechners in der URL des angeforderten Dienstes erforderlich war.

9.3. Zweites Beispiel: SQL-Server auf einem Windows-Rechner

9.3.1. Das Problem

Im JDBC-Kapitel haben wir gesehen, wie man relationale Datenbanken verwaltet. In den dortigen Beispielen befanden sich die Anwendungen und die verwendete Datenbank auf demselben Windows-Rechner. Hier schlagen wir vor, einen RMI-Server auf einem Windows-Rechner zu schreiben, der es Remote-Clients ermöglicht, auf die öffentlichen ODBC-Datenbanken auf dem Rechner zuzugreifen, auf dem der Server gehostet wird.

Image

Der RMI-Client könnte drei Operationen ausführen:

  • eine Verbindung zur Datenbank seiner Wahl herstellen
  • SQL-Abfragen senden
  • die Verbindung schließen

Der Server führt die SQL-Abfragen des Clients aus und sendet die Ergebnisse an den Client zurück. Dies ist seine Hauptfunktion, weshalb wir ihn als SQL-Server bezeichnen.

Wir wenden die verschiedenen Schritte an, die wir zuvor beim Echo-Server gesehen haben.

9.3.2. Schritt 1: Die Fernbedienungsschnittstelle

Die Remote-Schnittstelle ist die Schnittstelle, die die RMI-Servermethoden auflistet, auf die RMI-Clients zugreifen können. Wir verwenden die folgende Schnittstelle:

import java.rmi.*;

 // remote interface p
ublic interface interSQL extends Remote{ 
     public String connect(String pilote, String url, String id, String mdp
        ) throws java.rmi.RemoteExcept
    ion; public String[] executeSQL(String requete, String separa
        teur) throws java.rmi.RemoteE
    xception; public Str
        ing close() throws java.rmi.Re
moteException; }

Die verschiedenen Methoden haben folgende Funktionen:

Connect: Der Client stellt eine Verbindung zu einer Remote-Datenbank her und übergibt dabei den Treiber, die JDBC-URL sowie seine ID und sein Passwort für den Zugriff auf die Datenbank. Der Server gibt eine Zeichenkette zurück, die das Ergebnis der Verbindung angibt:

    200 - Connexion réussie
    500 - Echec de la connexion

executeSQL: Der Client fordert die Ausführung einer SQL-Abfrage in der Datenbank an, mit der er verbunden ist. Er gibt das Zeichen an, das die Felder in den zurückgegebenen Ergebnissen trennen soll. Der Server gibt ein Array von Zeichenfolgen zurück:

    100 n
    für eine Datenbank-Update-Abfrage, wobei n die Anzahl der aktualisierten Zeilen ist
    500 msg d’erreur
    wenn die Abfrage einen Fehler erzeugt hat
    501 Pas de résultats
    wenn die Abfrage keine Ergebnisse lieferte
    101 ligne1
    101 ligne2
    101 ...
wenn die Abfrage Ergebnisse zurückgegeben hat. Die vom Server zurückgegebenen Zeilen sind die Abfrageergebnisse.

close: Der Client schließt seine Verbindung zur Remote-Datenbank. Der Server gibt eine Zeichenkette zurück, die das Ergebnis dieses Schließvorgangs angibt:

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

9.3.3. Schritt 2: Schreiben des Servers

Nachfolgend finden Sie den Java-Quellcode für den SQL-Server. Um ihn zu verstehen, sind Kenntnisse im Bereich der JDBC-Datenbankverwaltung und der Erstellung von RMI-Servern erforderlich. Die Programmkommentare sollen das Verständnis erleichtern.

// imported packages
import java.rmi.*;
import java.rmi.server.*;
import java.sql.*;
import java.util.*;

// class srvSQL
public class srvSQL extends UnicastRemoteObject implements interSQL{

    // global class data
    private Connection DB;

    // ------------- manufacturer
    public srvSQL() throws RemoteException{
        super();
    }

    // --------------- connect
    public String connect(String pilote, String url, String id,
        String mdp) throws RemoteException{

        // connection to url database via driver
        // identification with identity id and password mdp

        String resultat=null;            // result of the method
        try{
            // loading the driver
            Class.forName(pilote);
            // connection request
            DB=DriverManager.getConnection(url,id,mdp);
            // ok
            resultat="200 Connexion réussie";
        } catch (Exception e){
            // error
            resultat="500 Echec de la connexion (" + e + ")";
        }
        // end
        return resultat;
    }            

    // ------------- executeSQL
    public String[] executeSQL(String requete, String separateur)
        throws RemoteException{

        // executes a SQL query on the DB database
        // and puts the results in an array of strings

        // data required to execute the request
        Statement S=null;
        ResultSet RS=null;
        String[] lignes=null;
        Vector resultats=new Vector();
        String ligne=null;

        try{
            // create query container
            S=DB.createStatement();
            // request execution
            if (! S.execute(requete)){
                // update request
                // returns the number of lines updated
                lignes=new String[1];
                lignes[0]="100 "+S.getUpdateCount();
                return lignes;
            }
            // it was a query request
            // retrieve results
            RS=S.getResultSet();
            // number of Resultset fields
            int nbChamps=RS.getMetaData().getColumnCount();
            // we exploit them
            while(RS.next()){
                // create results line
                ligne="101 ";
                for (int i=1;i<nbChamps;i++)
                    ligne+=RS.getString(i)+separateur;
                ligne+=RS.getString(nbChamps);
                // add to results vector
                resultats.addElement(ligne);
            }// while
            // end of results analysis
            // free up resources
            RS.close();
            S.close();
            // we return the results
            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){
            // error
            lignes=new String[1];
            lignes[0]="500 " + e;
            return lignes;
        }// try-catch
    }// executeSQL

    // --------------- close
    public String close() throws RemoteException {
        // closes database connection
        String resultat=null;
        try{
            DB.close();
            resultat="200 Base fermée";
        } catch (Exception e){
            resultat="500 Erreur à la fermeture de la base ("+e+")";
        }
        // return result
        return resultat;
    }

    // ----------- hand
    public static void main (String[] args){

        // security manager
        System.setSecurityManager(new RMISecurityManager());

        // service launch
        srvSQL serveurSQL=null;
        try{
            // creation
            serveurSQL=new srvSQL();
            // registration
            Naming.rebind("srvSQL",serveurSQL);
            // follow-up
            System.out.println("Serveur SQL prêt");
        } catch (Exception e){
            // error
            System.err.println("Erreur lors du lancement du serveur SQL ("+ e +")");
        }// try-catch
    }// hand

}// class

9.3.4. Erstellen des RMI-Clients

Der RMI-Server-Client wird mit den folgenden Parametern aufgerufen:

    urlserviceAnnuaire pilote urlBase id mdp separateur

directoryServiceURL: RMI-URL des Verzeichnisdienstes, bei dem der SQL-Server registriert ist

driver: Treiber, den der SQL-Server zur Verwaltung der Datenbank verwenden muss

urlBase: JDBC-URL der zu verwaltenden Datenbank

id: Client-ID oder null, falls keine ID vorhanden ist

password: Client-Passwort oder null, falls kein Passwort vorhanden ist

separator: Zeichen, das der SQL-Server verwenden muss, um Felder in den Ergebniszeilen einer Abfrage zu trennen

Hier ist ein Beispiel für mögliche Parameter:

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

wobei hier:

urlserviceAnnuaire    srvSQL
RMI-Name des SQL-Servers
pilote     sun.jdbc.odbc.JdbcOdbcDriver
der Standardtreiber für Datenbanken mit einer ODBC-Schnittstelle
urlBase    jdbc:odbc:articles
um eine Artikeldatenbank zu verwenden, die in den öffentlichen ODBC-Datenbanken des Windows-Rechners aufgeführt ist
id    null
keine Identität
mdp    null
kein Passwort
separateur    , 
die Felder in den Ergebnissen werden durch ein Komma getrennt

Nach dem Start mit den oben genannten Parametern führt der Client folgende Schritte aus:

  • Er stellt eine Verbindung zum RMI-Server „srvSQL“ her, einem RMI-Server, der sich auf demselben Rechner wie der Client befindet
  • Er fordert eine Verbindung zur Artikeldatenbank an
connect(‘’sun.jdbc.odbc.JdbcOdbcDriver’’, ‘‘jdbc:odbc:articles’’, ’’’’, ’’’’)
  • Der Benutzer wird aufgefordert, eine SQL-Abfrage einzugeben
  • sie sendet diese an den SQL-Server
executeSQL(requete, ’’,’’);
  • Es zeigt die vom Server zurückgegebenen Ergebnisse auf dem Bildschirm an
  • Es fordert den Benutzer erneut auf, eine SQL-Abfrage über die Tastatur einzugeben. Der Vorgang wird beendet, sobald die Abfrage abgeschlossen ist.

Der Java-Client-Code folgt. Die Kommentare sollten zum Verständnis ausreichen.

import java.rmi.*;
import java.io.*;

public class cltSQL {

    // global class data
    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[]){
        // syntax : cltSQL urlServiceAnnuaire separator driver url id mdp
        // urlServiceAnnuaire : url of the directory of services RMI to contact
        // driver: driver to be used for the database to be processed
        // urlBase: jdbc url of the database to be used
        // id: user identity
        // mdp: password
        // separator: string separating fields in query results

        // check number of arguments
        if(arg.length!=6)
            erreur(syntaxe,1);

        // init database connection parameters
        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 of a security manager
        System.setSecurityManager(new RMISecurityManager());

        // client-server dialogue
        String requete=null;
        String reponse=null;
        String[] lignes=null;
        String codeErreur=null;

        try{
            // open keyboard flow
            in=new BufferedReader(new InputStreamReader(System.in));
            // follow-up
            System.out.println("--> Connexion au serveur RMI en cours...");
            // service location
            serveurSQL=(interSQL) Naming.lookup(urlService);
            // follow-up
            System.out.println("--> Connexion à la base de données en cours");
            // initial database connection request
            reponse=serveurSQL.connect(pilote,urlBase,id,mdp);
            // follow-up
            System.out.println("<-- "+reponse);
            // response analysis
            codeErreur=reponse.substring(0,3);
            if(codeErreur.equals("500")) 
                erreur("Abandon sur erreur de connexion à la base",3);
            // loop for reading requests to be sent to the server SQL
            System.out.print("--> Requête : ");
            requete=in.readLine().toLowerCase().trim();
            while(! requete.equals("fin")){
                // send request to server and receive response
                lignes=serveurSQL.executeSQL(requete,separateur);
                // follow-up
                afficheLignes(lignes);
                // following request
                System.out.print("--> Requête : ");                
                requete=in.readLine().toLowerCase().trim();
            }// while
            // follow-up
            System.out.println("--> Fermeture de la connexion à la base de données distante");
            // close the connection
            reponse=serveurSQL.close();
            // follow-up
            System.out.println("<-- " + reponse);
            // end
            System.exit(0);
        // error management        
        } catch (Exception e){
            erreur("Abandon sur erreur : " + e,2);
        }// try
    }// hand

    // ----------- AfficheLignes
    private static void afficheLignes(String[] lignes){
        for (int i=0;i<lignes.length;i++)
            System.out.println("<-- " + lignes[i]);
    }// afficheLignes

    // ------------ error
    private static void erreur(String msg, int exitCode){
        // error msg display
        System.err.println(msg);
        // possible release of resources
        try{
            in.close();
            serveurSQL.close();
        } catch(Exception e){}
        // we leave
        System.exit(exitCode);
    }// error

}// class    

9.3.5. Schritt 3: Erstellen von .class-Dateien

  • Der Server wird kompiliert
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
  • Die Stub- und Skel-Dateien werden erstellt
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
  • Verschieben Sie die Dateien interSQL.class, srvSQL_Stub.class und srvSQL_Skel.class in das Client-Verzeichnis
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
  • Kompilieren des Clients
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. Schritt 4: Testen mit Server und Client auf demselben Windows-Rechner

  • Der Verzeichnisdienst wird in einem anderen Verzeichnis als dem des Servers und des Clients gestartet
F:\>start j:\jdk12\bin\rmiregistry
  • Legen Sie die folgende mypolicy-Datei in den Verzeichnissen von Client und Server ab
grant {
    // Allow everything for now
    permission java.security.AllPermission;
};
  • Starten Sie den Server
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
  • Starten Sie den 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 ,

--> Connection to server RMI in progress...
--> Current database connection
<-- 200 Successful connection
--> Requête : select nom, stock_actu from articles order by stock_actu desc
<-- 101 vélo.31
<-- 101 test3.13
<-- 101 water skis,13
<-- 101 canoeing,13
<-- 101 panther,11
<-- 101 leopard,11
<-- 101 sperm whale,10
<-- 101 rifle,10
<-- 101 arc,10
--> Query: 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 sperm whale,9
<-- 101 rifle,9
<-- 101 arc.9
<-- 101 panther,10
<-- 101 leopard,10
<-- 101 test3.13
<-- 101 water skis,13
<-- 101 canoeing,13
<-- 101 vélo.31
--> Query: end
--> Closing the remote database connection
<-- 200 Closed base

9.3.7. Schritt 5: Testen mit einem Server auf einem Windows-Rechner und einem Client auf einem Linux-Rechner

  • Beenden Sie gegebenenfalls den Server und den Verzeichnisdienst
  • Übertragen Sie die .class-Dateien des Clients auf einen Linux-Rechner
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
  • Die Serverdateien befinden sich in einem Verzeichnis, auf das der HTTP-Server des Windows-Rechners zugreifen kann
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
  • Starten Sie den Verzeichnisdienst neu
F:\>start j:\jdk12\bin\rmiregistry
  • Starten Sie den Server mit anderen Parametern als denen, die im vorherigen Test verwendet wurden
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
  • Starten Sie den Client auf dem Linux-Rechner
/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 sperm whale,9,6
<-- 101 canoeing,13.7
<-- 101 test3,13,9
<-- 101 rifle,9,8
<-- 101 leopard,10.7
<-- 101 panther,10.7
<-- 101 water skis,13.8
<-- 101 bicycle,31,8
--> Query: 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 sperm whale,6,6
<-- 101 canoeing,7,7
<-- 101 test3,13,9
<-- 101 rifle,9,8
<-- 101 leopard,7,7
<-- 101 panther,7,7
<-- 101 water skis,13.8
<-- 101 bicycle,31,8
--> Query: end
--> Closing the remote database connection
<-- 200 Closed base

9.3.8. Fazit

Dies ist eine interessante Anwendung, da sie den Zugriff auf eine Datenbank von jedem Arbeitsplatzrechner im Netzwerk aus ermöglicht. Wir hätten sie durchaus auf herkömmliche Weise unter Verwendung von Sockets schreiben können, was eigentlich in einer Übung im Kapitel über Datenbanken verlangt wird. Wenn wir diese Anwendung auf herkömmliche Weise schreiben würden:

  • hätten wir einen Client und einen Server, die in verschiedenen Sprachen geschrieben sein könnten
  • würden Client und Server durch den Austausch von Textzeilen kommunizieren und einen Dialog führen, der etwa so aussehen könnte:
client : connect machine port pilote urlBase id mdp

wobei die ersten beiden Parameter angeben, wo der Server zu finden ist, und die nächsten vier die Verbindungsparameter für die zu verwendende Datenbank angeben

Der Server könnte etwa wie folgt antworten:

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

um den Server aufzufordern, eine SQL-Abfrage auf der mit dem Client verbundenen Datenbank auszuführen. separator ist das Zeichen, das zur Trennung der Felder in den Antwortzeilen verwendet wird.

Der Server könnte mit etwa folgendem antworten

    100 n
    auf eine Datenbank-Update-Abfrage, wobei n die Anzahl der aktualisierten Zeilen ist
    500 msg d’erreur
    wenn die Abfrage einen Fehler erzeugt hat
    501 Pas de résultats
    wenn die Abfrage keine Ergebnisse zurückgegeben hat
    101 ligne1
    101 ligne2
    101 ...
wenn die Abfrage Ergebnisse zurückgegeben hat. Die vom Server zurückgegebenen Zeilen sind die Abfrageergebnisse.
client : close

um die Verbindung zur Remote-Datenbank zu schließen. Der Server gibt möglicherweise eine Zeichenkette zurück, die das Ergebnis dieses Schließvorgangs angibt:

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

Hier sehen wir, dass wir, wenn wir eine herkömmliche Anwendung unter Verwendung eines Protokolls des oben beschriebenen Typs erstellen können, eine mögliche Struktur für den RMI-Server ableiten können. An der Stelle im Protokoll, an der wir eine Nachricht vom Client an den Server vom Typ:

    commande param1 param2 ... paramq

können wir eine Methode innerhalb des RMI-Servers haben

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

und diese Methode, auf die der Client zugreifen kann, muss daher Teil der veröffentlichten Schnittstelle des Servers sein.

Abschließend sei angemerkt, dass unser Server derzeit nur einen Client bedient: In seiner aktuellen Form kann er nicht mehrere Clients bedienen. Wenn sich ein Client mit der Datenbank B1 verbindet, erstellt der Server ein Connection-Objekt mit DB=DB1. Wenn ein zweiter Client eine Verbindung zur Datenbank B2 anfordert, baut der Server diese mit Connection DB=DB2 auf und unterbricht damit die Verbindung des ersten Clients zur Datenbank B1.

9.4. Übungen

9.4.1. Übung 1

Erweitern Sie den vorherigen SQL-Server so, dass er mehrere Clients bedienen kann.

9.4.2. Übung 2

Schreiben Sie das in den Übungen des JDBC-Kapitels vorgestellte Java-E-Commerce-Applet so, dass es mit dem RMI-Server aus der vorherigen Übung funktioniert.