Skip to content

9. JAVA RMI

9.1. Einleitung

Wir haben gesehen, wie man Netzwerkanwendungen mithilfe der Kommunikationswerkzeuge 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 den Austausch verwenden. Beide Anwendungen können in unterschiedlichen Sprachen geschrieben sein: beispielsweise Java für den Client, Perl für den Server oder eine beliebige andere Kombination. Es handelt sich also um zwei getrennte Anwendungen, die durch ein beiden bekanntes Kommunikationsprotokoll miteinander verbunden sind. Darüber hinaus ist der Netzwerkzugriff über Sockets für eine Java-Anwendung nicht transparent: Sie muss die Klasse Socket verwenden, die speziell zur Verwaltung dieser Kommunikationswerkzeuge, der sockets, entwickelt wurde.

JAVA und RMI (Remote Method Invocation) ermöglichen die Erstellung von Netzwerkanwendungen mit folgenden Eigenschaften:

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

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

Das Prinzip einer RMI-Kommunikation ist wie folgt:

  1. Eine klassische Java-Anwendung wird auf einem Rechner A geschrieben. Dieser übernimmt die Rolle des Servers. Zu diesem Zweck werden einige ihrer Objekte auf dem Rechner A, auf dem die Anwendung ausgeführt wird, „veröffentlicht“ und werden so zu Diensten.
  2. Eine klassische Java-Anwendung wird auf einem Rechner B geschrieben. Sie übernimmt die Rolle des Clients. Sie hat Zugriff auf die auf Rechner A veröffentlichten Objekte/Dienste, d. h., über eine Remote-Referenz kann sie diese so bearbeiten, 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 anhand eines Beispiels

Die Theorie hinter der Schnittstelle RMI ist nicht einfach. Um einen besseren Überblick zu erhalten, werden wir Schritt für Schritt die Erstellung einer Client-Server-Anwendung unter Verwendung des Java-Pakets RMI verfolgen. Wir nehmen eine Anwendung, die in vielen Büchern über RMI zu finden ist: Der Client ruft eine einzige Methode eines Remote-Objekts auf, das ihm daraufhin eine Zeichenkette zurückgibt. Hier stellen wir eine leichte Abwandlung vor: Der Server gibt das, was ihm der Client sendet, einfach wieder zurück. Wir haben in diesem Buch bereits eine solche Anwendung vorgestellt, die auf Sockets basiert.

9.2.1. Die Serveranwendung

9.2.1.1. Schritt 1: Die Schnittstelle des Objekts/Servers

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

import java.rmi.*;

// die Remote-Schnittstelle
public interface interEcho extends Remote{
    public String echo(String msg) throws java.rmi.RemoteException;
}

Hier wird also eine Schnittstelle interEcho deklariert, die eine Methode echo als remote zugänglich deklariert. Diese Methode kann eine Ausnahme der Klasse RemoteException auslösen, eine Klasse, die alle netzwerkbezogenen Fehler zusammenfasst.

9.2.1.2. Schritt 2: Erstellen des Serverobjekts

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 über Methoden verfügt, die den Aufruf von Methoden aus der Ferne ermöglichen.

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

// Klasse, die das Remote-Echo implementiert
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // Konstruktor
    public srvEcho() throws RemoteException{
        super();
    }// Ende des Konstruktors

    // Methode zur Umsetzung des Echos
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// Ende des Echos
}// Ende der Klasse

In der oben genannten Klasse finden wir:

  1. die Methode, die das Echo ausgibt
  2. einen Konstruktor, der nichts anderes tut, als den Konstruktor der übergeordneten Klasse aufzurufen. Er dient dazu, anzugeben, dass er eine Ausnahme vom Typ RemoteException auslösen kann.

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

  1. Er wendet sich an den Verzeichnisdienst des Rechners, 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 für ein Objekt/einen Dienst an, dessen Namen er angibt. Handelt es sich bei diesem Namen um den eines Objekts/Dienstes im Verzeichnis, sendet dieses dem Client eine Referenz zurück, über die der Client mit dem entfernten Objekt/Dienst kommunizieren kann.
  2. Ab diesem Zeitpunkt kann der Client dieses entfernte Objekt so nutzen, als wäre es lokal

Um auf unseren Server zurückzukommen: Wir müssen ein Objekt vom Typ srvEcho erstellen und es im Verzeichnis der von außen zugänglichen Objekte registrieren. Diese Registrierung erfolgt mit der Methode der Klasse rebind der Klasse Naming:

Naming.rebind(String nom, Remote obj)

mit

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

obj: das Remote-Objekt

Unsere Klasse srvEcho sieht somit wie folgt aus:

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

// Klasse, die das Remote-Echo implementiert
public class srvEcho extends UnicastRemoteObject implements interEcho{

    // Konstruktor
    public srvEcho() throws RemoteException{
        super();
    }// Ende des Konstruktors

    // Methode zur Umsetzung des Echos
    public String echo(String msg) throws RemoteException{
        return  "["  + msg + "]";
    }// Ende des Echos

    // Erstellung des Dienstes
    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
}// Ende der Klasse

Wenn man das vorherige Programm liest, hat man den Eindruck, dass es unmittelbar nach dem Erstellen und Speichern des Echo-Dienstes beendet wird. Das ist jedoch nicht der Fall. Da die Klasse srvEcho von der Klasse UnicastRemoteObject abgeleitet ist, wird das erstellte Objekt unbegrenzt ausgeführt: 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 Methode main den Dienst und setzt ihre Ausführung fort: Sie zeigt tatsächlich „Echo-Server bereit“ an.

9.2.1.3. Schritt 3: Kompilierung der Serveranwendung

An dieser Stelle können wir unseren Server kompilieren. Wir kompilieren die Datei „interEcho.java“ der Schnittstelle „interEcho“ sowie die Datei „srvEcho.java“ der Klasse „srvEcho“. Wir erhalten die entsprechenden Dateien .class: interEcho.class und srvEcho.class.

9.2.1.4. Schritt 4: Schreiben des Clients

Wir schreiben einen Client, dem wir die Datei URL vom Echo-Server als Parameter übergeben und der

  1. eine über die Tastatur eingegebene Zeile liest
  2. an den Echo-Server sendet
  3. die vom Echo-Server gesendete Antwort anzeigt
  4. zurück zu Schritt 1 springt und beendet wird, sobald die eingegebene Zeile „Ende“ lautet.

Daraus ergibt sich folgender Client:

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

public class cltEcho {

    public static void main(String arg[]){
        // Syntax: cltEcho URLService

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

        // Client-Server-Dialog
        String urlService=arg[0];
        BufferedReader in=null;
        String msg=null;
        String reponse=null;
        interEcho serveur=null;

        try{
            // Öffnen des Tastaturstroms
            in=new BufferedReader(new InputStreamReader(System.in));
            // Lokalisierung des Dienstes
            serveur=(interEcho) Naming.lookup(urlService);                
            // Schleife zum Einlesen der an den Echo-Server zu sendenden Nachrichten
            System.out.print("Message : ");
            msg=in.readLine().toLowerCase().trim();
            while(! msg.equals("fin")){
                // Senden der Nachricht an den Server und Empfang der Antwort
                reponse=serveur.echo(msg);
                // Weiterverfolgung
                System.out.println("Réponse serveur : " + reponse);
                // nächste Nachricht
                System.out.print("Message : ");                
                msg=in.readLine().toLowerCase().trim();
            }// while
            // Fertig
            System.exit(0);
        // Fehlerbehandlung        
        } catch (Exception e){
            System.err.println("Erreur : " + e);
            System.exit(2);
        }// try
    }// main
}// Klasse                

An diesem Client ist nichts Besonderes, abgesehen von der Anweisung, die eine Referenz vom Server anfordert:

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

Wir erinnern uns, dass unser Echo-Dienst im Dienstverzeichnis des Rechners, auf dem er sich befindet, mit folgender Anweisung registriert wurde:


            Naming.rebind("srvEcho",serveurEcho);

Der Client nutzt also ebenfalls eine Methode der Klasse Naming, um eine Referenz des Servers zu erhalten, den er verwenden möchte. Die verwendete lookup-Methode akzeptiert als Parameter die URL des angeforderten Dienstes. Diese hat die Form einer klassischen URL:

    rmi://machine:port/nom_service

mit

rmi: optional – RMI-Protokoll

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

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

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

Abgerufen wird eine Instanz der Remote-Schnittstelle interEcho. Angenommen, Client und Server befinden sich nicht auf demselben Rechner, dann muss beim Kompilieren des Clients cltEcho.java muss im selben Verzeichnis die Datei interEcho.class vorhanden sein, die das Ergebnis der Kompilierung der Remote-Schnittstelle interEcho ist; andernfalls tritt bei den Zeilen, die auf diese Schnittstelle verweisen, ein Kompilierungsfehler auf.

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

Um besser zu verstehen, was auf der Serverseite und was auf der Clientseite geschieht, legen wir den Server in einem Verzeichnis namens echo\serveur und den Client in einem Verzeichnis namens echo\client ab.

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 erhält man 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 Verzeichnis des Kunden befindet sich 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 Kompilierung des Servers 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 erhält man 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

Versucht man, den Client cltEcho auszuführen, erhält man 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 man versucht, den Server srvEcho auszuführen, wird folgende Fehlermeldung angezeigt:

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 gefunden hat. Tatsächlich haben wir noch nie von dieser Klasse gehört. Im Client erfolgte die Lokalisierung des Servers mit folgender Anweisung:

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

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

Rmi: RMI-Protokoll

Localhost: Rechner, auf dem der Server läuft – hier derselbe Rechner wie der Client. Die Syntax lautet normalerweise „Rechner:Port“. Fehlt der Port, wird standardmäßig Port 1099 verwendet. Auf diesem Port lauscht der Verzeichnisdienst des Servers.

srvEcho: Dies ist der Name des angeforderten Dienstes

Bei der Kompilierung wurden keine Fehler gemeldet. Es musste lediglich die Datei „interEcho.class“ der Remote-Schnittstelle verfügbar sein.

Bei der Ausführung fordert die virtuelle Maschine das Vorhandensein einer Datei srvEcho_stub.class, wenn der angeforderte Dienst der Dienst srvEcho ist; im Allgemeinen eine Datei X_stub.class für einen Dienst X. Diese Datei wird nur bei der Ausführung benötigt, nicht bei der Kompilierung des Clients. Das Gleiche gilt für den Server. Was ist das also für eine Datei?

Auf dem Server befindet sich die Klasse srvEcho.class, die unser Remote-Objekt bzw. -Dienst ist. Der Client benötigt diese Klasse zwar nicht, benötigt jedoch eine Art Abbild davon, um mit ihr kommunizieren zu können. Tatsächlich richtet der Client seine Anfragen nicht direkt an das Remote-Objekt, sondern an dessen lokales Abbild srvEcho_stub.class, das sich auf demselben Rechner wie er befindet. Dieses lokale Abbild srvEcho_stub.class kommuniziert mit einem Abbild derselben Art (srvEcho_stub.class), das sich wiederum auf dem Server befindet. Dieses Image wird aus der Datei .class auf dem Server mit einem Java-Tool 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 Dateien „.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

Hier befindet sich tatsächlich die Datei srvEcho_stub.class, die sowohl der Client als auch der Server zur Ausführung benötigen. Außerdem gibt es eine Datei srvEcho_Skel.class, deren Funktion derzeit noch unbekannt ist. Wir erstellen eine Kopie der Datei „srvEcho_stub.class“ im Verzeichnis des Clients und des Servers und löschen die Datei „srvEcho_Skel.class“. Somit haben wir folgende 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 werden Client und Server auf demselben Rechner laufen. Zunächst müssen wir unsere Serveranwendung starten. Wir erinnern uns, dass diese:

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

Für diesen letzten Schritt ist ein Verzeichnisdienst erforderlich. Dieser wird mit dem folgenden Befehl gestartet:

start j:\jdk12\bin\rmiregistry

rmiregistry ist der Verzeichnisdienst. Er wird hier im Hintergrund in einem Windows-DOS-Fenster mit dem Befehl „start“ gestartet. Sobald das Verzeichnis aktiv ist, können wir den Echo-Dienst erstellen und ihn im Dienstverzeichnis registrieren. Auch hier wird er im Hintergrund mit dem Befehl start gestartet:

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

Der Echo-Server läuft in einem neuen Fenster unter dem Namen DOS und zeigt, wie gewünscht, Folgendes 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 werden sie auf unterschiedlichen Rechnern platziert:

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

Der Server wird wie zuvor auf dem Windows-Rechner gestartet. Auf den 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

Es liegt also ein Fehler vor: Die Java-Virtual-Machine fordert offenbar die Datei „srvEcho_skel.class“ an, die vom Dienstprogramm „rmic“ erstellt wurde, bisher jedoch noch nicht verwendet wurde. Wir erstellen sie neu und übertragen sie ebenfalls 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

Es tritt derselbe Fehler wie zuvor auf... Also überlegen wir kurz und lesen die Dokumentation zu RMI noch einmal durch. Schließlich kommen wir zu dem Schluss, dass vielleicht der Server selbst die besagte Datei srvEcho_Skel.class benötigt. Wir starten den Server auf dem Windows-Rechner also neu, wobei die beiden Dateien srvEcho_Stub.class und srvEcho_Skel.class vorhanden sind, ebenso wie :

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 es:

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

Daraus lässt sich also ableiten, dass auf der Serverseite die beiden Dateien srvEcho_Stub.class und srvEcho_Skel.class vorhanden sein müssen. Auf der Client-Seite 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 entfernen wir sie mal, 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 hier einen interessanten Fehler, der anscheinend besagt, dass die Java-Virtual-Machine versucht hat, die besagte Klasse stub zu laden, dies jedoch fehlgeschlagen ist, da kein „Security Manager“ vorhanden war. Wir erinnern uns, dass wir in der Dokumentation etwas zu diesem Thema gelesen haben. Wir schauen noch einmal nach … und stellen fest, dass der Server einen Sicherheitsmanager (Security Manager) erstellen und installieren muss, der den Clients, die das Laden von Klassen anfordern, garantiert, dass diese sicher sind. Fehlt dieser Sicherheitsmanager, ist das Laden der Klassen nicht möglich. Das scheint zu passen: Unser Linux-Client hat die benötigte Klasse srvEcho_stub.class vom Server angefordert, und dieser hat dies mit der Begründung abgelehnt, dass kein Security Manager installiert worden sei. Wir ändern daher den Code der Funktion main auf dem Server wie folgt:

    // Einrichtung des Dienstes
    public static void main (String arg[]){

        // Installation eines Sicherheitsmanagers
        System.setSecurityManager(new RMISecurityManager());

        // Starten und Registrieren des Dienstes
        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 ");
        }
    }// Hauptseite

Man kompiliert und erzeugt die Dateien srvEcho_stub.class und srvEcho_Skel.class mit dem Tool rmic. Man startet den Verzeichnisdienst (rmiregistry) und anschließend den Server, woraufhin ein Fehler auftritt, der 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 effektiv gewesen zu sein. Wir lesen die Dokumentation noch einmal durch … Dabei stellen wir fest, dass bei aktivem Sicherheitsmanager beim Start eines Programms dessen Berechtigungen angegeben werden müssen. Dies geschieht mit der folgenden Option:

start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho

wobei

java.security.policy ein Schlüsselwort ist

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

grant {
    // Vorläufig alles zulassen
    permission java.security.AllPermission;
};

Das Programm verfügt hier über alle Rechte.

Wir fangen noch einmal von vorne an. Wechseln Sie in das Serververzeichnis und führen Sie nacheinander folgende Schritte aus:

  • Starten des Verzeichnisdienstes: start j:\jdk12\bin\rmiregistry
  • Starten des Servers: start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho

Und dieses Mal startet der Echo-Server (der Client noch nicht) korrekt. Nun können Sie folgendes Experiment durchführen:

  • Beenden 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 Verzeichnis des Servers zurück Starten Sie den Echo-Server – Sie erhalten 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 ableiten, dass das Verzeichnis, aus dem der Verzeichnisdienst gestartet wird, eine Rolle spielt. In diesem Fall hat Java die Klasse „srvEcho_stub.class“ nicht gefunden, da der Verzeichnisdienst nicht aus dem Serververzeichnis gestartet wurde. Beim Start des Servers kann angegeben werden, in welchem Verzeichnis sich die für den Server erforderlichen 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 java.rmi.server.codebase dient dazu, den Pfad URL des Verzeichnisses anzugeben, das die für den Server erforderlichen Klassen enthält. Hier gibt dieses URL das „file“-Protokoll an, das für den Zugriff auf lokale Dateien zuständig ist, sowie das Verzeichnis, das die .class-Dateien des Servers enthält. Wenn man also wie folgt vorgeht:

  • den Verzeichnisdienst anhalten
  • Neustart des Verzeichnisdienstes aus einem anderen Verzeichnis als dem des Servers
  • im Verzeichnis des Servers diesen mit dem folgenden Befehl (in einer einzigen Zeile) starten:
 start j:\jdk12\bin\java 
-Djava.security.policy=mypolicy 
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/ 
srvEcho

Der Server wurde nun erfolgreich gestartet. 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

Es tritt derselbe Fehler auf, der auf das Fehlen eines Sicherheitsmanagers hinweist. Man geht davon aus, dass man sich vielleicht geirrt hat und dass der Client seinen eigenen Sicherheitsmanager erstellen muss. Man belässt den Server mit seinem Sicherheitsmanager, erstellt aber auch einen für den Client. Die Funktion „main“ des Clients cltEcho.java sieht dann wie folgt aus:

public static void main(String arg[]){
        // Syntax: cltEcho Maschinenport
        // Rechner: Rechner, auf dem der Echo-Server läuft
        // Port: Port, über den das Dienstverzeichnis auf dem Rechner des Echo-Dienstes läuft

        // Überprüfung der Argumente
        if(arg.length!=1){
            System.err.println("Syntaxe : pg url_service_rmi");
            System.exit(1);
        }

        // Installation eines Sicherheitsmanagers
        System.setSecurityManager(new RMISecurityManager());

        // Client-Server-Kommunikation
        String urlService=arg[0];
        BufferedReader in=null;
        String msg=null;
        String reponse=null;
        interEcho serveur=null;

        try{
            ....
        } catch (Exception e){
            ....
        }// try
    }// main

Anschließend gehen wir wie folgt vor:

  • cltEcho.java wird neu kompiliert
  • die .class-Dateien werden 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
  • Man startet 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 stellen fest, dass der Client die Klasse srvEcho_Stub.class vom Server anfordern konnte, dieser sie jedoch nicht gefunden hat. Der Client muss also über einen Sicherheitsmanager verfügen, wenn er Klassen vom Server anfordern will.

Betrachtet man den vorherigen Fehler, so sieht man, dass die Datei srvEcho_Stub.class im Verzeichnis e:/data/java/rmi/echo/serveur/ gesucht und nicht gefunden wurde. Dabei befindet sie sich doch genau dort. Betrachtet man die Liste der vom Fehler betroffenen Methoden genauer, findet man folgende: sun.net.www.protocol.file.FileURLConnection.getInputStream. Der Client scheint einen Datenstrom mit einem Objekt vom Typ FileURLConnection geöffnet zu haben. Man könnte vermuten, dass all dies mit der Art und Weise 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 man sich die Dokumentation noch einmal ansieht, stellt man fest, dass der Wert dieses Schlüsselworts in den angegebenen Beispielen immer lautet: http://.., c.a.d. Das verwendete Protokoll ist HTTP. Es ist nicht eindeutig ersichtlich, wie der Client seine Klassen vom Server anfordert und erhält. Möglicherweise fordert er sie mit dem Schlüssel „URL“ an, wobei die Schlüssel „java.rmi.server.codebase“ und „URL“ beim Start des Servers angegeben wurden. 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. Die Dateien .class müssen an einen Ort verschoben werden, auf den der HTTP-Server des Rechners, auf dem die Klassen gespeichert werden, Zugriff hat. In unserem Beispiel läuft der Server auf einem Windows-Rechner mit einem HTTP-Server von Microsoft (PWS). Das Stammverzeichnis dieses Servers ist d:\Inetpub\wwwroot. Daher gehen wir wie folgt vor:

  • Man erstellt das Verzeichnis d:\Inetpub\wwwroot\rmi\echo
  • dort werden die Dateien .class des Servers sowie die Datei mypolicy abgelegt
  • man startet den Webserver, falls dies noch nicht geschehen ist
  • man startet den Verzeichnisdienst (rmiregistry) neu
  • man startet den Server neu mit dem Befehl
start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
  • Auf dem Linux-Rechner starten wir 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 besagte Datei „srvEcho_Stub.class“ erfolgreich abgerufen.

Das Ganze hat uns auf eine Idee gebracht, und wir fragen uns, ob der Client, der sich auf dem Windows-Server befindet, auch ohne die Datei srvEcho_Stub.class funktionieren würde. Wir wechseln in das Verzeichnis des Clients, löschen die Datei srvEcho_Stub.class, falls sie dort 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. Résumé

Auf der Windows-Serverseite:

  • Der Server verfügt über einen Sicherheitsmanager
  • Er wurde mit 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

Auf der Client-Seite (Linux oder Windows)

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

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

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

  • Wir übertragen die Dateien .class vom Server 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 Klasse srvEcho_Stub.class von den Kunden angefordert wird, ist das für die Serverklassen ausgewählte Verzeichnis ein Verzeichnis, auf das der HTTP-Server des Linux-Rechners zugreifen kann. Hier lautet der Pfad zu URL in diesem Verzeichnis: http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serveur
  • Der Verzeichnisdienst 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/serveur/

srvEcho &

Nun können die Clients getestet werden. Zunächst der Windows-Client.

  • Wechseln Sie in das Verzeichnis des Clients auf dem Windows-Rechner
  • Wir starten den Client mit dem 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

Man testet den Linux-Client:

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 des Rechners im URL des angeforderten Dienstes erforderlich war.

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

9.3.1. Das Problem

Im Kapitel JDBC haben wir gesehen, wie man relationale Datenbanken verwaltet. In den vorgestellten Beispielen befanden sich die Anwendungen und die verwendete Datenbank auf demselben Windows-Rechner. Hier soll ein RMI-Server auf einem Windows-Rechner eingerichtet werden, der es Remote-Clients ermöglicht, die öffentlichen ODBC-Datenbanken des Rechners zu nutzen, auf dem sich der Server befindet.

Image

Der Client RMI könnte drei Vorgänge ausführen:

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

Der Server führt die Abfragen SQL des Clients aus und sendet ihm die Ergebnisse zurück. Das ist seine wesentliche Aufgabe, weshalb wir ihn als SQL-Server bezeichnen.

Wir wenden die verschiedenen zuvor beim Echo-Server behandelten Schritte an.

9.3.2. Schritt 1: Die Remote-Schnittstelle

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

import java.rmi.*;

// die Remote-Schnittstelle
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;
}

Die verschiedenen Methoden haben folgende Funktionen:

Connect: Der Client stellt eine Verbindung zu einer Remote-Datenbank her, deren pilote, die url, die JDBC sowie seine Identität id und sein Passwort mdp angibt, um auf diese Datenbank zuzugreifen. Der Server gibt ihm 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 Abfrage SQL in der Datenbank an, mit der er verbunden ist. Er gibt das Trennzeichen an, das die Felder in den zurückgegebenen Ergebnissen trennen soll. Der Server gibt ein Array von Zeichenfolgen zurück:

    100 n

bei einer Abfrage zur Aktualisierung der Datenbank, wobei n die Anzahl der aktualisierten Zeilen ist

    500 msg d’erreur

, wenn die Abfrage einen Fehler verursacht hat

    501 Pas de résultats

wenn die Abfrage kein Ergebnis geliefert hat

    101 ligne1
    101 ligne2
    101 ...

wenn die Abfrage Ergebnisse geliefert hat. Die vom Server zurückgegebenen Zeilen sind die Ergebniszeilen der Abfrage.

close: Der Client schließt seine Verbindung zur entfernten 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: Server-Code

Es folgt der Java-Quellcode des Servers SQL. Um ihn zu verstehen, sind Kenntnisse über die Datenbankverwaltung mit JDBC sowie über den Aufbau von Servern mit RMI erforderlich. Die Kommentare im Programm sollen das Verständnis erleichtern.

// importierte Pakete
import java.rmi.*;
import java.rmi.server.*;
import java.sql.*;
import java.util.*;

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

    // globale Daten der Klasse
    private Connection DB;

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

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

        // Verbindung zur Datenbank-URL über den Treiber
        // Authentifizierung mit ID und Passwort

        String resultat=null;            // Ergebnis der Methode
        try{
            // Laden des Treibers
            Class.forName(pilote);
            // Verbindungsanfrage
            DB=DriverManager.getConnection(url,id,mdp);
            // OK
            resultat="200 Connexion réussie";
        } catch (Exception e){
            // Fehler
            resultat="500 Echec de la connexion (" + e + ")";
        }
        // Ende
        return resultat;
    }            

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

        // führt eine Abfrage SQL in der Datenbank DB
        // und speichert die Ergebnisse in einem String-Array

        // Daten, die für die Ausführung der Abfrage erforderlich sind
        Statement S=null;
        ResultSet RS=null;
        String[] lignes=null;
        Vector resultats=new Vector();
        String ligne=null;

        try{
            // Erstellung des Abfragecontainers
            S=DB.createStatement();
            // Abfrage ausführen
            if (! S.execute(requete)){
                // Aktualisierungsabfrage
                // Die Anzahl der aktualisierten Zeilen wird zurückgegeben
                lignes=new String[1];
                lignes[0]="100 "+S.getUpdateCount();
                return lignes;
            }
            // Es handelte sich um eine Abfrage
            // die Ergebnisse werden abgerufen
            RS=S.getResultSet();
            // Anzahl der Felder im Resultset
            int nbChamps=RS.getMetaData().getColumnCount();
            // Auswertung der Felder
            while(RS.next()){
                // Erstellung der Ergebniszeile
                ligne="101 ";
                for (int i=1;i<nbChamps;i++)
                    ligne+=RS.getString(i)+separateur;
                ligne+=RS.getString(nbChamps);
                // Hinzufügen zum Ergebnisvektor
                resultats.addElement(ligne);
            }// while
            // Ende der Auswertung der Ergebnisse
            // Ressourcen werden freigegeben
            RS.close();
            S.close();
            // Ergebnisse werden zurückgegeben
            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){
            // Fehler
            lignes=new String[1];
            lignes[0]="500 " + e;
            return lignes;
        }// try-catch
    }// executeSQL

    // --------------- schließen
    public String close() throws RemoteException {
        // schließt die Verbindung zur Datenbank
        String resultat=null;
        try{
            DB.close();
            resultat="200 Base fermée";
        } catch (Exception e){
            resultat="500 Erreur à la fermeture de la base ("+e+")";
        }
        // Rückgabe des Ergebnisses
        return resultat;
    }

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

        // Sicherheitsmanager
        System.setSecurityManager(new RMISecurityManager());

        // Start des Dienstes
        srvSQL serveurSQL=null;
        try{
            // Erstellung
            serveurSQL=new srvSQL();
            // Speicherung
            Naming.rebind("srvSQL",serveurSQL);
            // Nachverfolgung
            System.out.println("Serveur SQL prêt");
        } catch (Exception e){
            // Fehler
            System.err.println("Erreur lors du lancement du serveur SQL ("+ e +")");
        }// try-catch
    }// main

}// Klasse

9.3.4. Schreiben des Clients RMI

Der Client des Servers RMI wird mit folgenden Parametern aufgerufen:

    urlserviceAnnuaire pilote urlBase id mdp separateur

urlserviceAnnuaire: URL RMI des Verzeichnisdienstes, der den Server SQL registriert hat

Treiber: Treiber, den der Server SQL zur Verwaltung der Datenbank verwenden soll

urlBase: URL JDBC der zu verwaltenden Datenbank

id: ID des Kunden oder „null“, falls keine ID vorhanden ist

mdp: Passwort des Kunden oder null, falls kein Passwort vorhanden ist

Trennzeichen: Zeichen, das der Server SQL verwenden muss, um die Felder in den Ergebniszeilen einer Abfrage voneinander zu trennen

Hier ein Beispiel für mögliche Parameter:

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

wobei hier:

urlserviceAnnuaire    srvSQL

RMI-Name des Servers SQL

pilote     sun.jdbc.odbc.JdbcOdbcDriver

der übliche Treiber für Datenbanken mit ODBC-Schnittstelle

urlBase    jdbc:odbc:articles

Zur Verwendung einer Artikel-Datenbank, die in der Liste der öffentlichen Datenbanken ODBC des Windows-Rechners aufgeführt ist

id    null

keine Identität

mdp    null

kein Passwort

separateur    , 

Die Felder der Ergebnisse werden durch ein Komma getrennt

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

  • Er stellt eine Verbindung zum Server RMI srvSQL her, also zu einem Server RMI auf demselben Rechner wie der Client
  • Er fordert die Verbindung zur Artikel-Datenbank an
connect(‘’sun.jdbc.odbc.JdbcOdbcDriver’’, ‘‘jdbc:odbc:articles’’, ’’’’, ’’’’)
  • er fordert den Benutzer auf, eine Abfrage SQL über die Tastatur einzugeben
  • er sendet diese an den Server SQL
executeSQL(requete, ’’,’’);
  • Er zeigt die vom Server zurückgegebenen Ergebnisse auf dem Bildschirm an
  • Er fordert den Benutzer erneut auf, eine Abfrage SQL über die Tastatur einzugeben. Er beendet den Vorgang, sobald die Abfrage beendet ist.

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

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

public class cltSQL {

    // globale Daten der Klasse
    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 Trennzeichen, Treiber, URL, ID, Passwort
        // urlServiceAnnuaire: URL des zu kontaktierenden Dienstverzeichnisses RMI
        // Treiber: Zu verwendender Treiber für die zu verarbeitende Datenbank
        // urlBase: JDBC-URL der zu verwendenden Datenbank
        // id: Benutzername
        // mdp: sein Passwort
        // Trennzeichen: Zeichenfolge, die die Felder in den Abfrageergebnissen trennt

        // Überprüfung der Anzahl der Argumente
        if(arg.length!=6)
            erreur(syntaxe,1);

        // Initialisierung der Parameter für die Datenbankverbindung
        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 eines Sicherheitsmanagers
        System.setSecurityManager(new RMISecurityManager());

        // Client-Server-Kommunikation
        String requete=null;
        String reponse=null;
        String[] lignes=null;
        String codeErreur=null;

        try{
            // Öffnen des Tastaturstroms
            in=new BufferedReader(new InputStreamReader(System.in));
            // Überwachung
            System.out.println("--> Connexion au serveur RMI en cours...");
            // Lokalisierung des Dienstes
            serveurSQL=(interSQL) Naming.lookup(urlService);
            // Nachverfolgung
            System.out.println("--> Connexion à la base de données en cours");
            // Anfrage zur ersten Verbindung mit der Datenbank
            reponse=serveurSQL.connect(pilote,urlBase,id,mdp);
            // Weiterverfolgung
            System.out.println("<-- "+reponse);
            // Antwortanalyse
            codeErreur=reponse.substring(0,3);
            if(codeErreur.equals("500")) 
                erreur("Abandon sur erreur de connexion à la base",3);
            // Schleife zum Einlesen der an den Server SQL zu sendenden Anfragen
            System.out.print("--> Requête : ");
            requete=in.readLine().toLowerCase().trim();
            while(! requete.equals("fin")){
                // Senden der Anfrage an den Server und Empfang der Antwort
                lignes=serveurSQL.executeSQL(requete,separateur);
                // Weiterverfolgung
                afficheLignes(lignes);
                // nächste Anfrage
                System.out.print("--> Requête : ");                
                requete=in.readLine().toLowerCase().trim();
            }// während
            // verfolgt
            System.out.println("--> Fermeture de la connexion à la base de données distante");
            // Verbindung wird beendet
            reponse=serveurSQL.close();
            // Weiter
            System.out.println("<-- " + reponse);
            // Ende
            System.exit(0);
        // Fehlerbehandlung        
        } 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

    // ------------ Fehler
    private static void erreur(String msg, int exitCode){
        // Anzeige einer Fehlermeldung
        System.err.println(msg);
        // Mögliche Freigabe der Ressourcen
        try{
            in.close();
            serveurSQL.close();
        } catch(Exception e){}
        // Beenden
        System.exit(exitCode);
    }// Fehler

}// Klasse    

9.3.5. Schritt 3: Erstellung der .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 Dateien „Stub“ und „Skel“ 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
  • Die Dateien interSQL.class, srvSQL_Stub.class und srvSQL_Skel.class werden in das Verzeichnis des Kunden übertragen
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
  • Der Client wird kompiliert
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: Tests 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
  • Die folgende „mypolicy“-Datei wird in den Verzeichnissen von Client und Server abgelegt
grant {
    // Vorläufig alles zulassen
    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
  • Der Client wird gestartet
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
--> Abfrage: select name, 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
--> Abfrage: update articles set stock_actu=stock_actu-1 where stock_actu<=11
<-- 100 5
--> Abfrage: select name, 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. Schritt 5: Tests mit einem Server auf einem Windows-Rechner und einem Client auf einem Linux-Rechner

  • Gegebenenfalls werden der Server und der Verzeichnisdienst angehalten
  • Die .class-Dateien des Clients werden auf einen Linux-Rechner übertragen
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 Dateien vom Server werden in ein Verzeichnis verschoben, 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
  • Der Verzeichnisdienst wird wieder in Betrieb genommen
F:\>start j:\jdk12\bin\rmiregistry
  • Der Server wird mit anderen Einstellungen als im vorherigen Test neu gestartet
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
  • Der Client wird auf dem Linux-Rechner gestartet
/usr/local/bin/jdk/bin/java cltSQL rmi://tahe.istia.univ-angers.fr/srvSQL sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,

--> Abfrage: select name,stock_actu,stock_mini from articles order by name
<-- 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
--> Abfrage: update articles set stock_actu=stock_mini where stock_mini<=7
<-- 100 4
--> Abfrage: SELECT Name, stock_actu, stock_mini FROM Artikel ORDER BY Name
<-- 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. Fazit

Hier haben wir eine interessante Anwendung, da sie den Zugriff auf eine Datenbank von jedem beliebigen Rechner im Netzwerk aus ermöglicht. Man hätte sie durchaus auf herkömmliche Weise mit Sockets schreiben können, was übrigens in einer Übung im Kapitel über Datenbanken verlangt wird. Würde man diese Anwendung auf herkömmliche Weise schreiben:

  • hätte man einen Client und einen Server, die in unterschiedlichen Sprachen geschrieben werden könnten
  • würden Client und Server über 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 folgenden vier die Verbindungsparameter zur zu verwendenden Datenbank angeben

Der Server könnte mit einer Antwort in etwa folgender Art reagieren:

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

um den Server aufzufordern, eine Abfrage SQL auf der mit dem Client verbundenen Datenbank auszuführen. „separator“ ist das Trennzeichen zwischen den Feldern in den Zeilen der Antwort.

Der Server könnte etwa wie folgt antworten

    100 n

bei einer Abfrage zur Aktualisierung der Datenbank, wobei n die Anzahl der aktualisierten Zeilen ist

    500 msg d’erreur

, wenn die Anfrage einen Fehler verursacht hat

    501 Pas de résultats

wenn die Abfrage keine Ergebnisse geliefert hat

    101 ligne1
    101 ligne2
    101 ...

wenn die Abfrage Ergebnisse geliefert hat. Die vom Server zurückgegebenen Zeilen sind die Ergebniszeilen der Abfrage.

client : close

um die Verbindung zur entfernten Datenbank zu schließen. Der Server könnte eine Zeichenkette zurückgeben, die das Ergebnis dieses Schließvorgangs angibt:

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

Hier zeigt sich: Wenn man in der Lage ist, eine herkömmliche Anwendung mit einem Protokoll der oben genannten Art zu erstellen, lässt sich daraus eine mögliche Struktur des Servers RMI ableiten. Dort, wo im Protokoll eine Nachricht vom Client an den Server wie folgt vorliegt:

    commande param1 param2 ... paramq

könnte es innerhalb des Servers RMI eine Methode geben

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

, und diese für den Client zugängliche Methode muss daher Teil der veröffentlichten Schnittstelle des Servers sein.

Abschließend sei angemerkt, dass unser Server derzeit nur einen Client verwaltet: In seiner aktuellen Form kann er nicht mehrere Clients verwalten. Wenn sich nämlich ein Client mit einer Datenbank B1 verbindet, wird vom Server ein Objekt Connection DB=DB1 erstellt. Wenn ein zweiter Client eine Verbindung zu einer Datenbank mit dem Namen B2 anfordert, vermerkt der Server dies mit Connection DB=DB2 und unterbricht damit die Verbindung des ersten Clients zur Datenbank B1.

9.4. Übungen

9.4.1. Übung 1

Erweitern Sie den oben genannten Server SQL so, dass er mehrere Clients verwalten kann.

9.4.2. Übung 2

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