Advanced Java Services | RMI remote method invocation |
RMI, remote method invocation, ermöglicht die Kommunikation zweier JavaApplikationen auf verschiedenen Rechnern über ein Netzwerk. Die ClientApplikation erhält ein JavaObjekt, das die ServerApplikation liefert und der Client ruft Methoden zu diesem Objekt. Er ruft diese genauso so auf wie bei lokalewn Objekten, tatsächlich aber findet der Methodenaufruf auf der Serverseite statt und der Server übermittelt das Ergebnis. Die API stellt dafür Hilfsklassen bereit, sodaß man sich um den Verbindungsaufbau nicht selbst kümmern muß. So erhält etwa der Client durch einen einzigen statischen Methodenaufruf ( Naming.lookup() ) das Remoteobjekt zur Verfügung gestellt. Auch auf der Serverseite ist der Aufwand kaum größer. Voraussetzung für einen erfolgreichen Methodenaufruf ist allerdings, daß die Parameter und der Returnwert der Methode serialisierbar sind, denn im Hintergrund werden ja die Parameter vom Client zum Server und der Returnwert vom Server zum Client übertragen.
Es ist nicht schwer eine Kommunikation zwischen zwei JavaApplikationen auf einem Rechner herzustellen. Es gibt mehrere Möglichkeiten dafür. Wir wählen hier einen Ansatz, der uns in Richtung RMI führen wird. Wir entwerfen eine "ServerApplikation", die ein Objekt deponiert. Die "ClientApplikation" greift dann auf dieses Objekt zu und ruft zu diesem Objekt dann Methoden auf (dies entspricht natürlich nicht genau dem Vorgang bei der RMI, kommt ihm aber nahe). Sowohl Server als auch Client müssen den Typ dieses Objektes kennen. Dazu verwenden wir ein Interface, das beiden bekannt ist und die Methoden enthält, die der Client aufrufen kann. Wir nennen es Kommunikationsinterface. Weiter muß es eine Klasse geben, die dieses Interface implementiert. Diese implementierende Klasse ist nur für den Server wichtig, der Client braucht sie nicht zu kennen. Der Server legt ein Objekt dieser Klasse an und "deponiert" es. Dazu verwendet er eine Hilfsklasse, die das Objekt in serialisierter Form speichern kann. Anschließend verwendet der Client dieselbe Hilfsklasse, um das Objekt zu deserialisieren. Er castet das Objekt zum Typ des Interfaces und kann so die Methoden aufrufen, die das Interface zur Verfügung stellt. Damit das Objekt serialisierbar wird, schreiben wir vor, daß die Implementierung des KommunikationsInterfaces zusätzlich das Interface Serializable implementieren muß. In unserem Beispiel gehen wir von einer einfachen Methode getDate() aus. Sie soll das aktuelle Datum auf dem Server liefern.
Das KommunikationsInterface müssen beide Parteien kennen, sowohl Client als auch Server.
import java.util.Date; public interface CommunicationInterface { Date getDate(); }
Sie wird auf der Serverseite realisiert.
import java.io.*; import java.util.*; public class ServerInterfaceImpl implements CommunicationInterface, Serializable { public KommunikationInterfaceImpl() { } // ------ vom Interface CommunicationInterface geforderte Methode ------ \\ public Date getDate() { return new Date() ; } }
Diese beiden Schritte sind keine Schwierigkeit. Wir wollen nun annehmen, es gäbe bereits eine Hilfsklasse, die Objekte serialisieren und wieder deserialisieren kann. Die Klasse heiße Register. Sie bietet die zwei folgenden statische Methoden an:
public static void bind(String key, Object value) throws IOException public static Object lookup(String key) throws IOException, ClassNotFoundException
Man sieht natürlich sofort, daß hier u.a. eine (key,value)-Tabelle (etwa eine HashMap) dahintersteckt. Wir geben dem zu deponierenden Objekt einen eindeutigen Namen, mit dem wir es wieder restaurieren können. Mit Hilfe dieser Klasse, die wir am Schluß dieser Einführung vorstellen, können wir nun problemlos Server und Client schreiben.
Der Server legt ein Kommunikationsobjekt an und deponiert es.
import java.io.*; import java.util.*; public class Server { // setting up the server public static void main(String args[]) { try { System.out.println("-- start server at " + new Date() + " --" ) ; // Objekt vom Typ ServerInterfaceImpl anlegen ServerInterfaceImpl ob = new ServerInterfaceImpl(); // Objekt deponieren Register.bind("DateServer", ob); System.out.println("-- server ready --") ; } catch(IOException ex) { System.out.println("Exception " + ex) ; } } }
Wir lassen uns die Uhrzeit ausgeben, wann das Objekt deponiert wurde.
Mit unserer Hilfsklasse Register wird die Clientklasse nahezu trivial...
import java.io.*; import java.util.*; public class Client { public static void main(String args[]) { try { System.out.println("Objekt restaurieren") ; KommunikationInterface remote = (KommunikationInterface)Register.lookup("DateServer"); System.out.println("Typ des Objekts : " + remote.getClass().getName() ) ; // Datum ausgeben System.out.println("date = " + remote.getDate() ) ; } catch(Exception ex) { System.out.println("Exception " + ex) ; } } }
Um das Beispiel laufen lassen, starten Sie in einer main()-Methode zuerst den Server und warten dann einige Sekunden und starten dann den Client im selben main(). Sie sehen dann, daß der Client die aktuelle Uhrzeit des Servers ausgibt. Damit haben wir das Prinzip von RMI nachempfunden. Über einen Übertragungskanal erhält ein Client ein Objekt, das ein Server angelegt hat und kann dazu Methoden aufrufen. Jetzt fehlt nur noch die Klasse Register.
Die Klasse Register hat zwei zentrale Aufgaben. Zum einen kann über sie das Objekt mit einem eindeutigen Namen
versehen werden. Über diese ID kann der Client dann kundgeben welches Objekt er möchte und der Server kann mit
diesem Namen das Objekt suchen. Zum anderen muß das gefundene Objekt übermittelt werden. In unserer Hilfsklasse
geschieht das dadurch, daß das die (key, value)-Tabelle serialisiert wird und vom Client wieder eingelesen wird.
Das entspricht nur näherungsweise dem tatsächlichen Vorgang, aber das Prinzip ist sehr ähnlich.
Tatsächlich gibt es aber bei der echten RMI zum einen zwei Hilfsklassen, die die API zur Verfügung stellt
und zum anderen ein Konsoltool, das zwei weitere Hilfklassen generiert. Man braucht also in der Praxis
das Innenleben dieser Klassen nicht zu kennen.
import java.io.*; import java.util.*; public class Register { private static Hashtable ht = new Hashtable(); public static void bind(String key, Object value) throws IOException { ht.put(key, value); save(ht); } public static Object lookup(String key) throws IOException, ClassNotFoundException { Hashtable ht = read(); return ht.get(key); } private static void save(Hashtable ht) throws IOException { FileOutputStream fos = new FileOutputStream("table.ser"); ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(ht); oos.close(); System.out.println("Hashtable saved") ; } private static Hashtable read() throws IOException, ClassNotFoundException { FileInputStream fis = new FileInputStream("table.ser"); ObjectInputStream ois = new ObjectInputStream(fis); Hashtable table = (Hashtable)ois.readObject(); ois.close(); System.out.println("Hashtable loaded") ; return table; } }
Was passiert hier. Die Klasse Register arbeitet mit einer statischen Hashtable. Die Methode save() speichert die ganze Hashtable, die Methode read() liest sie wieder ein. Für diesen Vorgang müssen sämtliche Inhalte der Hashtable serialisierbar sein. Die Methode bind() trägt ein (key,value)-Paar ein und speichert anschließend die Hashtable mit save(). Die Methode lookup() liest die Hashtable ein, holt über den Schlüssel das gespeicherte Objekt und gibt es zurück.