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:
- Client-Server-Anwendungen sind Java-Anwendungen an beiden Enden der Kommunikation
- Der Client kann Objekte auf dem Server so nutzen, als befänden sie sich lokal
- 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:
- 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.
- 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:
- die Methode, die das Echo ausgibt
- 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:
- 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.
- 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:
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
- eine über die Tastatur eingegebene Zeile liest
- an den Echo-Server sendet
- die vom Echo-Server gesendete Antwort anzeigt
- 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:
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:
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:
sowie die Datei interEcho.class, die bei der Kompilierung des Servers generiert wurde:
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:
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:
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:
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:
Der Echo-Server läuft in einem neuen Fenster unter dem Namen DOS und zeigt, wie gewünscht, Folgendes an:
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:
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.

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:
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:
bei einer Abfrage zur Aktualisierung der Datenbank, wobei n die Anzahl der aktualisierten Zeilen ist
, wenn die Abfrage einen Fehler verursacht hat
wenn die Abfrage kein Ergebnis geliefert hat
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:
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: 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:
wobei hier:
RMI-Name des Servers SQL
der übliche Treiber für Datenbanken mit ODBC-Schnittstelle
Zur Verwendung einer Artikel-Datenbank, die in der Liste der öffentlichen Datenbanken ODBC des Windows-Rechners aufgeführt ist
keine Identität
kein Passwort
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
- er fordert den Benutzer auf, eine Abfrage SQL über die Tastatur einzugeben
- er sendet diese an den Server SQL
- 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
- Die folgende „mypolicy“-Datei wird in den Verzeichnissen von Client und Server abgelegt
- 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
- 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:
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:
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
bei einer Abfrage zur Aktualisierung der Datenbank, wobei n die Anzahl der aktualisierten Zeilen ist
, wenn die Anfrage einen Fehler verursacht hat
wenn die Abfrage keine Ergebnisse geliefert hat
wenn die Abfrage Ergebnisse geliefert hat. Die vom Server zurückgegebenen Zeilen sind die Ergebniszeilen der Abfrage.
um die Verbindung zur entfernten Datenbank zu schließen. Der Server könnte eine Zeichenkette zurückgeben, die das Ergebnis dieses Schließvorgangs angibt:
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:
könnte es innerhalb des Servers RMI eine Methode geben
, 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.