Zurück zum Inhaltsverzeichnis des Manuskripts verteilte Systeme

2.3.2 Remote Method Invocation

Architektur

Remote Method Invocation (RMI) ist die Java-Implementierung der Remote Procedure Calls (RPC). Als Transportprotokoll wird TCP verwendet. RMI arbeitet wie RPC synchron, aber während RPC dem klassischen Client/Server-Modell folgt, weicht RMI davon ab. Hier wird auf jedem Rechner, auf dem wenigstens ein RMI-Server installiert ist, zusätzlich ein Verzeichnisdienst benötigt, bei dem der oder die auf dem Rechner vorhandenen Server ihre Dienstleistungen anmelden. Verzeichnisdienste werden durch eigenständige Prozesse realisiert und oft auch als Namensdienste oder Registrierungsdienste bezeichnet.

Ein Client, der eine (für ihn ferne) Methode von einem der Server ausführen lassen möchte, stellt eine Verbindung zu dem Verzeichnisdienst her, der ihm daraufhin einen geeigneten Server vermittelt oder mit einer Fehlermeldung reagiert. RMI arbeitet demnach mit einem Client/Registry/Server-Modell, das im Abschnitt 1.4.2 (Kommunikationstechniken) bereits vorgestellt worden ist. Mit ihm soll eine größere Flexibilität als mit dem klassischen Client/Server-Modell erreicht werden.

Die Java-Entwicklungsumgebung enthält einen Verzeichnisdienst in Form eines Programms namens rmiregistry, das explizit über die Kommandozeile einer Rechnerkonsole oder implizit als Java-Befehl vom Serverprogramm gestartet werden kann.

Beispiel Zeitserving

Bei dem folgenden Beispiel wird das praktische Vorgehen beim Arbeiten mit RMI gezeigt. Auch hier wird, wie bei allen Programmbeispielen des Manuskripts, der Schwerpunkt auf das prinzipielle Vorgehen gelegt und dazu eine sehr einfach gehaltene Programmierung durchgeführt. Da auch die Programmierumgebung so einfach wie möglich gehalten werden soll, wird bei den Programmbeispielen davon ausgegangen, dass weder die beteiligten Rechner, noch Ports auf diesen Rechnern durch eine Firewall gesperrt werden. Dieser Hinweis ist für RMI-Programme besonders bedeutsam, denn ein RMI-Server lässt sich beim Start voreingestellt einen zufällig gewählten freien Port geben. Das heißt, dass auch dann, wenn für eine Übungsveranstaltung ein bestimmter Bereich an Ports freigegeben worden ist, durch die zufällige Portvergabe dem Server ein Port außerhalb des freien Bereichs zugewiesen werden kann.

Für die Programmentwicklung ist es sinnvoll, zunächst sowohl die Server- als auch die Clientprogramme auf ein und demselben Rechner zu entwickeln. Rechnername ist dann localhost. Erst in einem zweiten Schritt sollten die Programme auf zwei Rechner verteilt werden. Im Quellkode der Programme sind die Stellen angegeben, an denen localhost durch den Namen des Rechners, auf dem der Server implementiert ist, zu ersetzen ist.

Das Beispiel realisiert ein Zeitserving. Dazu wurde auf dem Rechner sun71.bht-berlin.de unseres Übungsraums ein RMI-Server eingerichtet, der den Verzeichnisdienst rmiregistry startet und die Ausführung einer Methode gibZeit() zur Verfügung stellt.

Der Client wurde auf einem anderen Rechner des Übungsraums implementiert, ruft die für ihn ferne Methode gibZeit() unmittelbar nacheinander zweimal auf und erhält bei jedem Aufruf die aktuelle Uhrzeit des Servers, die er mit der getTime()-Methode der java.util.Date-Klasse in Millisekunden umsetzt. Er bildet die Differenz der beiden Zeiten und erhält damit ein grobes Maß dafür, wie lange ein RMI in der gegebenen Situation dauert. Das folgende Bild zeigt die zeitlichen und geometrischen Zusammenhänge:

RMI-Zeitserving, Ablauf

Serverseitige Programmierung

Der Zeitserver besteht aus

Alle drei Dateien liegen in demselben Verzeichnis des Rechners sun71.bht-berlin.de. Sie werden jetzt einzeln vorgestellt.

(1) Das Interface

Bei RMI sind alle Methoden, deren Ausführung von einem Client angefordert werden kann, in einem Interface zu deklarieren. Im Beispiel gibt es nur eine einzige solche Methode, nämlich gibZeit(). Das Interface für das Beispiel heißt ZeitServInter und muss public sein. Es muss das Interface java.rmi.Remote ableiten und alle aufgeführten Methoden müssen java.rmi.RemoteException werfen können.

ZeitServInter.java
(2) Die Methodenimplementierung

Die in dem Interface ZeitServInter aufgeführten Methoden müssen implementiert (ausprogrammiert) werden. Dies geschieht in einer Klasse namens ZeitServImpl. Die Methodenimplementierung könnte programmiertechnisch Teil des Serverquellkodes sein. Die Verwendung einer eigenen Klasse hat didaktische Gründe: Sie soll die Implementierung der Methoden hervorheben. ZeitServImpl muss die Klasse java.rmi.Server.UnicastRemoteObject ableiten und das zuvor erstellte Interface ZeitServInter implementieren.

Verbindungen zwischen Kommunikationspartnern können jederzeit gestört oder unterbrochen werden. Deshalb müssen alle Methoden, und damit auch die Konstruktoren, die Ausnahme java.rmi.RemoteException werfen können. Daher, und weil er public sein muss, ist der voreingestellte Konstruktor der Klasse ZeitServImpl entsprechend überschrieben worden.

Die Implementierung der Methode gibZeit() besteht darin, eine Konsolenausgabe zu tätigen, um anzuzeigen, dass ein Aufruf erfolgt ist, und dann mit dem Konstruktor der java.util.Date()-Klasse die aktuelle Uhrzeit zurückzugeben.

ZeitServImpl.java
(4) Der Server

Die Klasse ZeitServServer realisiert den Kern des Zeitservers. Sie muss public sein, und auch hier muss, aus den gleichen Gründen wie in der Klasse ZeitServImpl, der voreingestellte Konstruktor überschrieben werden. Die main()-Methode kann durch Verbindungsprobleme unterbrochen werden, weshalb auch sie java.rmi.RemoteException werfen können muss. Zur Identifizierung der zu erbringenden Dienstleistung ist ein besonders geformter String namens RMI-URL zu bilden. Auf ihn wird in wenigen Sätzen eingegangen. Dieser String könnte falsch aufgebaut sein. Deshalb muss main() auch java.net.MalformedURLException werfen können.

Die Stringvariable serverHost wird mit dem Namen des Rechners belegt, auf dem der Server implementiert worden ist. Dieser String wird etwas später Teil des bereits erwähnten RMI-URL und darf keine Domänenangabe enthalten.

Nach einer eröffnenden Konsolenausgabe wird der Verzeichnisdienst rmiregistry an seinem Standard-Port 1099 gestartet und eine Instanz impl der Methodenimplementierungsklasse ZeitServImpl gebildet. Diese wird beim Verzeichnisdienst angemeldet, wobei dazu ein String namens RMI-URL (RMI Uniform Ressource Locator) mit folgender Syntax zu verwenden ist:

rmi://hostname:port/remoteObjectName

Als hostname wurde im Beispiel sun71 angegeben. Da der Standard-Port verwendet wird, konnte hier auf eine explizite Port-Angabe verzichtet werden. Der remoteObjectName ist frei wählbar und beschreibt den Dienst, den die implementierten Methoden erbringen können. Im Beispiel wurde der Name MyService gewählt. Exakt diese, im Serverprogramm festgelegte RMI-Adresse, muss auch von den Clients verwendet werden. Die Anmeldung beim Verzeichnisdienst erfolgt im Beispiel mit der Methode java.rmi.Naming.rebind(), wobei auch java.rmi.Naming.bind() hätte verwendet werden können. Letztere Methode löst eine Exception aus, wenn die Anmeldung bereits stattgefunden hat, während erstere eine alte Anmeldung überschreibt. Für die Programmentwicklung ist Naming.rebind() vorzuziehen.

Am Ende des Programms wird eine Besonderheit der RMI-Server sichtbar: Nach dem Ausgabebefehl erzeugt der Java-Compiler eine Endlosschleife, in der der Server auf RMI-Anforderungen durch Clients wartet. Er beendet sich nicht naiv. Nach der Vorstellung der clientseitigen Programmierung werden mehrere Möglichkeiten beschrieben, um mit dieser Situation umzugehen.

ZeitServServer.java

Clientseitige Programmierung

Jeder RMI-Client benutzt das Interface mit den fern ausführbaren Methoden. Deshalb muss im Beispiel die Datei ZeitServInter auch clientseitig vorhanden sein. Für das Zeitserving-Beispiel befanden sich die beiden Dateien ZeitServClient.java und eine Kopie von ZeitServInter.java im selben Verzeichnis des Clientrechners.

Die main()-Methode muss java.rmi.RemoteException werfen können, denn es könnten Verbindungsprobleme auftreten. Weiterhin ist es möglich, dass der Verzeichnisdienst auf dem Serverrechner nicht erreicht werden kann. Das heißt, main() muss zusätzlich auch java.rmi.NotBoundException werfen können. Und schließlich muss der Client exakt die gleiche RMI-Adresse benutzen wie der Server, und diese könnte falsch aufgebaut sein. Deshalb muss auch die main()-Methode des Clients java.net.MalformedURLException werfen können.

Nach einer eröffnenden Ausgabe wird der Name des Serverrechners in der Stringvariablen serverHost vermerkt und mit der Methode java.rmi.Naming.lookup() der Verzeichnisdienst auf dem Serverrechner angesprochen. Dabei muss eine RMI-Adresse verwendet werden, die identisch zur serverseitigen ist. Naming.lookup() liefert eine Referenz auf die Implementierung der fernaufrufbaren Methoden. Der Client startet die Methode gibZeit() zweimal direkt hintereinander, wandelt jeweils die erhaltene Zeitangabe in Millisekunden um und gibt die Differenz aus.

ZeitServClient.java

Bei den folgenden beiden Testläufen befanden sich Client und Server im selben Laborraum und wurden unter Linux betrieben. Der Client machte folgende Ausgaben:

$ java ZeitServClient Dauer = 16 ms $ java ZeitServClient Dauer = 17 ms

Beenden des Servers

Es gibt mehrere Methoden, einen RMI-Server zu beenden. In manchen Fällen ist es ausreichend, von außerhalb des Programms mit Hilfe des Kommandointerpreters den entsprechenden Prozess zu beenden. Unter Linux kann dies mit

kill -9 <PID> # <PID> ist die Process-Id

und unter Windows mit

taskkill /PID <PID> # <PID> ist die Process-Id

erfolgen. Unter Windows führt auch die Benutzung des Taskmanagers zum Ziel. Und in beiden Fällen beendet ein Schließen des zugehörigen Konsolenfensters den RMI-Server.

Soll jedoch ein RMI-Server durch eine Aktion eines RMI-Clients beendet werden, dann kann dafür eine fern ausführbare Methode deklariert und implementiert werden, deren Inhalt darin besteht, einen System.exit()-Aufruf zu starten. Nachteilig an diesem Vorgehen ist, dass der Server zwar beendet wird, der Client aber vergeblich auf eine Serverantwort wartet, und deshalb eine Fehlermeldung erzeugt. Diese Fehlermeldung könnte abgefangen und ignoriert werden, was allerdings programmierstilistisch unbefriedigend wäre.

Eine elegantere Lösung besteht darin, den System.exit()-Befehl durch einen Thread ausführen zu lassen, der zunächst einige Sekunden schläft bevor er den Server beendet. Das gibt dem RMI-Programm Zeit, um sich zu beenden, so dass der wartende Client eine Antwort erhält und nicht mehr mit einer Fehlermeldung reagiert.

Alternativ zum Erzeugen eines Threads, der einen System.exit()-Aufruf enthält, kann die fern ausfühbare Methode, mit der der Server beendet werden soll, die Methode

static boolean UnicastRemoteObject.unexportObject(Remote obj, boolean force))

aufrufen. Sie entfernt ein fernes Objekt aus der Laufzeitumgebung. Wird der Parameter Remote obj als this angegeben, wird auf diese Art der Server beendet ohne dass eine Fehlermeldung erzeugt wird. Hat der zweite Parameter den Wert false, dann wirkt der Aufruf nur, falls es keine wartenden RMI-Aufrufe zu dem fernen Objekt gibt, bei true wird darauf keine Rücksicht genommen.



Zurück zum Inhaltsverzeichnis des Manuskripts verteilte Systeme