Wird RMI zur Entwicklung einer verteilten Anwendung verwendet, so werden immer die folgenden Schritte durchlaufen: Entwicklung und Implementierung der Komponenten einer verteilten Anwendung Übersetzen der Quelldateien und Generierung der Stubs Herstellen der Netzwerkerreichbarkeit der Klassen Aufruf der Anwendung Diese vier Schritte werden im Folgenden detailliert betrachtet. Entwicklung und Implementierung der Komponenten einer verteilten Anwendung Zuerst muss die Architektur einer Anwendung festgelegt werden. Hierbei wird entschieden, welche Komponenten in Form von lokalen Objekten und welche in Form von entfernten Objekten realisiert werden. Dieser Schritt beinhaltet die folgenden Teilaspekte: Definition der entfernten Interfaces Implementierung der entfernten Objekte Implementierung der Clients Ein entferntes Interface spezifiziert die Methoden, die von einem Client entfernt aufgerufen werden können. Hierbei beziehen sich Clients auf entfernte Interfaces, nicht auf die Implementierungsklassen dieser Interfaces. Ein Aspekt der Entwicklung derartiger Interfaces ist unter anderem, welche lokalen Objekte als Parameter verwendet werden sollen und wie die Rückgabewerte der Methoden aussehen. Existieren diese Interfaces oder Klassen noch nicht, so müssen sie ebenfalls definiert werden. Entfernte Objekte müssen ein oder mehrere entfernte Interfaces implementieren. Die entfernte Objektklasse wiederum kann Implementierungen anderer (lokaler oder entfernter) Interfaces und andere Methoden, die nur lokal verfügbar sind, beinhalten. Wenn lokale Klassen als Parameter oder als Rückgabewerte dieser Methoden verwendet werden sollen, müssen sie ebenfalls implementiert werden. Clients, die entfernte Objekte verwenden, können zu einem beliebigen Zeitpunkt nach der Definition der entfernten Interfaces implementiert werden. Dies impliziert, dass die Implementierung auch erst nach der Realisierung der Verfügbarkeit entfernter Objekte erfolgen kann. Übersetzen der Quelldateien und Generierung der Stubs Die Übersetzung der Quelldateien und die Generierung der Stubs muss in zwei Schritten erfolgen. Zuerst wird der Compiler javac zur Übersetzung der Quelldateien verwendet, die die Implementierung der entfernten Interfaces, die Server- und die Client-Klassen enthalten. Im anschließenden Schritt wird der Compiler rmic dazu verwendet, die Stubs für entfernte Objekte zu erzeugen. RMI verwendet in Clients die Objektklasse eines entfernten Stubs als Platzhalter, damit Clients mit einem speziellen entfernten Objekt kommunizieren können. Der Compiler rmic erzeugt den Code für Stubs und für das Skeleton aus den Klassendefinitionen für das Interface und die Implementierung. Zum Aufruf von rmic muss die folgende Syntax verwendet werden: rmic [Optionen] Package.InterfaceImpl Hierbei muss immer zuerst der Package-Name, der die Klassen enthält, und anschließend der Name der Implementierung angegeben werden. Herstellen der Netzwerkerreichbarkeit der Klassen Nach der Übersetzung der Quelldateien und der Generierung der Stubs werden die Java-Klassen, die mit entfernten Interfaces, Stubs und Klassen, die vom Client geladen werden müssen, assoziiert sind, mittels eines Webservers erreichbar gemacht. Aufruf der Anwendung Zur Ausführung der Anwendung muss zuerst die RMI-Registry entfernter Objekte gestartet werden. Anschließend muss zuerst der Server, dann der Client gestartet werden. Anwendungen laufen standardmäßig ohne Security-Manager. In RMI muss allerdings immer ein Security-Manager installiert werden. Die RMI-Registry ist eine Anwendung, die Objektnamen auf Objekte abbilden kann und so externe Referenzen von Objekten auflöst. Es ist daher wichtig, die Registry vor den Programmen mittels der Anweisung rmiregistry zu starten. Beispiel Die RMI zugrunde liegenden Konzepte wurden bisher auf eine eher abstrakte Art und Weise betrachtet. Um die in diesem Unterkapitel vorgestellten Konzepte im praktischen Einsatz zu demonstrieren, wird im Folgenden die Implementierung einer Anwendung erläutert, die in folgenden Schritten abläuft: Ein Client ruft einen entfernten Dienst auf. Ein Server nimmt die Anfrage entgegen und bearbeitet den Dienst. Der Server gibt das Ergebnis an den aufrufenden Client zurück. Um die Darstellung auf die Konzepte von RMI beschränken zu können, wird im Folgenden zwar ein Dienst aufgerufen, die Methode im Server erfüllt allerdings keine Aufgabe. Aus diesem Grund ist die Methode auch lediglich als Rumpf definiert und gibt sofort nach dem Aufruf einen Rückgabewert an den Client zurück. Im Rahmen des in Kapitel 11 betrachteten Anwendungsbeispiels wird diese Methode dann mit Inhalten gefüllt. Das Beispielprogramm besteht neben der Client-Klasse aus einer Klasse, die beim Server gespeichert ist. Die Klasse DatabaseUpdateImpl beinhaltet die Funktionalität, eine Datenbank derart zu aktualisieren, dass vermerkt werden kann, ob ein bestimmter Spieler gewonnen oder verloren hat. Diese Klasse muss durch das Interface DatabaseUpdate beschrieben werden, die dann durch den Client-Stub implementiert wird. Die Stub-Klasse wird in einem späteren Schritt durch den rmic-Compiler erzeugt. rmic erfordert, dass das Interface als public deklariert wird und dass es das Interface Remote erweitert. Jede enthaltene Methode muss die Ausnahme RemoteException auswerfen. Stub- und Implementierungs-Code müssen sich in einem Package befinden. Definition des Interfaces Im Folgenden ist die Definition des Interfaces DatabaseUpdate angegeben. package server; import java.rmi.*; public interface DatabaseUpdate extends Remote { //Aktualisierung des Spielstands public int updateDatenbank(int spiel) throws RemoteException; } Nach der Definition des Interfaces muss die Implementierungsklasse erzeugt werden. Implementierungsklassen Die Implementierungsklasse wird beim Server gespeichert und implementiert das oben angegebene Interface. Die Klasse DatabaseUpdateImpl implementiert das Interface DatabaseUpdate. Diese Klasse muss alle Methoden implementieren, die im Interface deklariert sind. Weiterhin muss die Klasse die Klasse UnicastRemoteObject erweitern. Eine Unterstützung von Multicast-Objekten ist heutzutage noch nicht verfügbar. package server; import java.rmi.*; import java.rmi.server.*; import java.io.Serializable; public class DatabaseUpdateImpl extends UnicastRemoteObject implements DatabaseUpdate, Serializable { public DatabaseUpdateImpl () throws RemoteException { } public int updateDatenbank (int spiel) throws RemoteException { //Hier wird noch keine Funktion realisiert return spiel; } } Nach der Implementierung müssen alle Quelldateien übersetzt werden. Hierbei sollte mittels der Option -d [Verzeichnisname] ein Zielverzeichnis für die übersetzten Dateien angegeben werden. Innerhalb des Zielverzeichnisses legt der Compiler automatisch ein Unterverzeichnis an, dessen Name dem des Package-Namens entspricht, in dem alle Quelldateien enthalten sind. Alle übersetzten Dateien werden dann automatisch in diesem Verzeichnis abgelegt. Der Aufruf könnte daher wie folgt aussehen: javac -d . *.java Erzeugung der Stub- und Skeleton-Klassen Nach der Übersetzung der Implementierungsklassen müssen die Stub- und Skeleton-Klassen erzeugt werden, die auf die Implementierungsklassen zugreifen. Stub-Klassen werden dazu verwendet, um mit dem Skeleton-Code des Servers zu kommunizieren. Mit Hilfe des Compilers rmic werden die Stubs und Skeletons automatisch erstellt. Mittels der folgenden Anweisung wird die Übersetzung aufgerufen: rmic -d . server.DatabaseUpdateImpl Nach der Übersetzung, die im zum Unterverzeichnis server übergeordneten Verzeichnis ausgeführt werden muss, befinden sich im Unterverzeichnis server die Dateien DatabaseUpdateImpl_Skel.class und DatabaseUpdateImpl_ Stub.class. Erzeugen und Übersetzen der Server-Anwendung Nach der Erzeugung der Stub- und Skeleton-Klassen muss die Server-Anwendung erstellt werden, die diese Klassen den Clients für entfernte Aufrufe zur Verfügung stellt. Die Anwendung hat die Aufgabe, eine Instanz der Klasse DatabaseUpdate zu erzeugen. Die Anwendung sieht wie folgt aus: package server; import java.rmi.*; import java.rmi.registry.*; public class Datenbank { public static void main (String args[]) { if (args.length != 2) { System.err.println("Eingabe: java server.Datenbank <Server> <Port>"); System.exit(1); } String server = args[0]; int port = Integer.parseInt(args[1]); //Neuer Security Manager System.setSecurityManager(new RMISecurityManager()); try { //Ort der Registry LocateRegistry.createRegistry(port); System.out.println("Registry definiert"); //Instanz der Datenbankanwendung DatabaseUpdateImpl dui = new DatabaseUpdateImpl(); //Binden der Objektinstanz an entfernte Registry String urlString="//"+server+":"+port+"/DatabaseUp date"; System.out.println("Namensbindung erfolgt"); Naming.rebind(urlString, dui); }catch (Exception e) { System.out.println("Fehler aufgetreten"); e.printStackTrace(); System.out.println(e.getMessage()); } } } Anwendungen laufen standardmäßig ohne Security-Manager ab. In RMI muss allerdings mittels der Methode System.setSecurityManager eine Instanz eines RMI-Security-Managers angelegt werden. Sobald dieser Security-Manager eingerichtet ist, wird jeglicher Code, der nicht in der Variablen CLASSPATH angegeben ist, als vertrauensunwürdig angesehen. Um das Beispiel ausführen zu können, muss das Verzeichnis, unter dem die Anwendung gespeichert ist, der Datei java.policy hinzugefügt werden. Der Eintrag, der dieser Datei hinzugefügt werden muss, sieht wie folgt aus: grant codeBase "file:<Verzeichnis>" {permission java.security.AllPermissions; }; Hierbei gibt der Teil <Verzeichnis> das Verzeichnis an, in dem die Anwendung gespeichert ist. Die RMI-Registry hört einen bestimmten Port ab, um Registrierungsinformationen und Dienstanforderungen zu empfangen. Dieser Port wird mit der Anweisung LocateRegistry.createRegistry(port) angegeben. Der Server exportiert eine Objektinstanz, indem ein logischer Name an die Instanz des Objekts gebunden wird und dieser Name in der Registry registriert wird. In RMI stehen zwei Methoden zur Verfügung, mit denen eine Instanz gebunden und registriert werden kann: public static void bind (String, Remote) throws AlreadyBoundException, MalformedException, UnknownHostException, RemoteException public static void rebind(String, Remote) throws MalformedException, UnknownHostException, RemoteException Beide Methoden verlangen den Namen der Objektreferenz und die Instanz des entfernten Objekts, das an den Namen gebunden werden soll. Der Name muss hierbei in Form einer URL angegeben werden, also in der Art Protokoll://Host:port/BindeName. Als Protokoll muss hierbei rmi angegeben werden. Als Standard-Port wird immer 1099 verwendet. Die Methoden bind und rebind unterscheiden sich im Hinblick auf die Behandlung bereits vorhandener Namensbindungen. Bei der Methode bind wird eine AlreadyBoundException-Ausnahme erzeugt, wenn der Name bereits gebunden ist. Im Gegensatz dazu hebt die Methode rebind die alte Bindung auf und erstellt eine neue, wenn eine Bindung bereits besteht. Start der Registry und der Server-Anwendung Nachdem die Server-Anwendung erstellt ist, muss vor deren Ausführung die Registry mittels der Anweisung start rmiregistry gestartet werden. Anschließend kann die Server-Anwendung ausgeführt werden. Nachdem beide Komponenten laufen, kann ein Client Referenzen eines entfernten Objekts von der Registry anfordern. Im Folgenden ist die Realisierung des Clients dargestellt. Client-Programm Um die Anwendung zu testen, wird eine Client-Anwendung entwickelt, die das entfernte Objekt aufruft. Auch die Client-Anwendung muss unter Angabe des Namens des Servers und des Ports gestartet werden. Wird die Anwendung auf einem Rechner ohne Netzanschluss ausgeführt, so muss hier der Parameter localhost angegeben werden. Hieraus wird dann wiederum eine URL generiert, die an die Methode lookup() der Klasse Naming übergeben wird. Diese Methode kommuniziert mit dem Server und gibt eine Referenz in Form eines Stubs auf das entfernte Objekt zurück, das im vorangegangenen Schritt erzeugt und instantiiert wurde. Der Rückgabewert der Methode lookup ist ein Objekt vom Typ Remote, der Elternklasse aller Stub-Interfaces. Wird dieses Objekt in den Typ DatabaseUpdate umgewandelt (Casting), so können die Methoden der Klasse DatabaseUpdate aufgerufen werden. package server; import java.rmi.*; public class Klient { public static void main (String args[]) { DatabaseUpdate du = null; if (args.length != 2) { System.err.println("Eingabe: java server.Klient <Server> <Port>"); System.exit(1); } String server = args[0]; int port = Integer.parseInt(args[1]); //Neuer Security Manager System.setSecurityManager(new RMISecurityManager()); try { //Binden der Objektinstanz an entfernte Registry String url="//"+server+":"+port+"/DatabaseUpdate"; du = (DatabaseUpdate)Naming.lookup(url); } catch (Exception e) { System.err.println("Keine Verbindung"+e); } try { //Schicke 1, also Spieler hat gewonnen int spielAntwort = du.updateDatenbank(1); System.out.println("Rückgabewert des Servers: "+spielAntwort); } catch (Exception e) { System.err.println("Problem in Remote-Methode"); } } } Sicherlich ist die Funktionalität dieses Beispiels sehr eingeschränkt. Um allerdings auf einer Datenbank, die auf dem Server abläuft, navigieren zu können, muss die Verwendung von Servlets bekannt sein, die erst in einem späteren Kapitel erläutert wird. Der Leser sei daher auf Kapitel 14 vertröstet, in dem eine voll funktionsfähige Anwendung beschrieben wird.