Advanced   Java   Services RMI   remote method invocation


Idee von RMI

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.





Idee von RMI an einem lokalen Beispiel

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

Das KommunikationsInterface müssen beide Parteien kennen, sowohl Client als auch Server.

import java.util.Date;

public interface CommunicationInterface
{
   Date getDate();
}

Die implementierende Klasse

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.


Die Serverklasse

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.


Die Clientklasse

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 Hilfsklasse 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.