Java Standard: RMI


Einleitung, GrundbegriffeBearbeiten

  • RMI

RMI ist die Abkürzung von 'Remote Method Invocation' und bedeutet 'Aufruf einer entfernten Methode' (i.a. über ein Netzwerk). Der Aufruf selbst ist transparent, d.h. der Nutzer merkt nichts davon, dass die Methode auf einem entfernten Rechner implementiert ist und dort abgearbeitet wird. Parameter und Ergebnis werden transparent zwischen Client und Server ausgetauscht. Im Hintergrund übernehmen 2 Klassen (genannt Skeleton/Stub) stellvertretend für Server/Client die Kommunikation.

Das hier beschriebene Beispiel setzt Java 1.5 oder höher voraus, damit entfällt die zuvor erforderliche Nutzung der 'Java-RMI-Tools'.

  • Anforderungen an die Applikation

Definiere ein Interface für das 'remote object' (im Beispiel 'Hello'), die von Server und Client gemeinsam benutzte Methode.

  • RMI-Server
1. Implementiere eine Instanz des 'remote object' ('HelloImpl', Name beliebig)
2. Exportiere die Instanz

das geschieht mit 'UnicastRemoteObject.exportObject' oder durch Erweiterung der Instanz mit 'UnicastRemoteObject'. Dadurch wird automatisch der Konstruktor von 'UnicastRemoteObject' aufgerufen, der den Export durchführt.

Durch den Export entsteht die Skeleton-Klasse, die Kommunikation und Datenaustausch mit dem Client durchführt.

3. Registry

Erzeuge eine Registry in der das 'remote object' mit der Methode 'registry.[re]bind' angemeldet wird. Dabei ist ein eindeutiger Name und ein Kommunikationsport (Standard 1099) festzulegen. Diesen Namen benutzt später die Stub-Klasse des Client (der Partner der Skeleton-Klasse des Servers), um die RMI durchzuführen.

Nach dieser Anmeldung ist der Server bereit, Client-Anfragen entgegen zu nehmen. Die Anfragen werden in der implementierten Klasse 'HelloImpl' behandelt.

  • RMI-Client

Zuerst verschafft man sich Zugang zur Registry des Servers, um dort nach dem gewünschten 'remote object' zu fragen (Naming.lookup). Steht dieses zur Verfügung wird automatisch eine Stub-Klasse bereit gestellt, die die Kommunikation und den Datenaustausch mit dem Server durchführt.

  • Parameter/Ergebnis der aufzurufenden Methode des 'remote object'
  1. Es wird in jedem Falle 'call by value' durchgeführt.
  2. Einfache Variable
  3. Objekte

Werden Objekte benutzt müssen diese das Interface 'Serializable' implementieren (Datenübertragung als serieller Bytestrom).

Die meisten Standardklassen von Java erfüllen diese Bedingung, so dass für den Anwender in diesem Falle kein weiterer Aufwand entsteht.

Im Beispiel wird für das Ergebnis des Aufrufs der entfernten Methode eine eigene Klasse (RmiResult) verwendet, die auf Client und Server bekannt sein muss.

  • Dynamisches Laden von Objekten

Dieser Punkt ist für das Funktionieren des Beispiels unerheblich. Hier wird nur eine Zusatzmöglichkeit für die verteilte Verarbeitung beschrieben.

Unter RMI ist es möglich, Objekte über das Netz zu laden. Dabei können sowohl Server als auch Client Quelle oder Ziel sein. Für das Laden kann u.a. das http-Protokoll verwendet werden, wenn auf Quellseite ein Webserver vorhanden ist.

Hier wurde dafür ein Beispiel mit der Klasse 'LoadClient' erstellt, die auf Client-Seite die Klasse 'TClient' vom RMI-Server (TServer) lädt und abarbeiten lässt.

Das zuvor beschriebene Beispiel mit RMI-Client und RMI-Server kann natürlich ohne 'LoadClient' verwendet werden. 'TClient' ist allerdings so programmiert, dass es mit oder ohne 'LoadClient'-Klasse funktioniert.

Beim dynamischen Laden von externem Bytecode ist ein eigener Sicherheitsmanager erforderlich und in der Datei '.java.policy' sind die entsprechenden Zugriffsrechte zu erteilen (das Ziel braucht Connect-Recht bei der Quelle und für den Bytecode Zugriffsrecht auf dem eigenen Rechner). Im Beispiel wurde für den RMI-Client folgende Policy benutzt:

grant {
  permission java.net.SocketPermission "rechner_name:80", "connect";
  permission java.net.SocketPermission "rechner_name:1024-", "connect, resolve";
};

'rechner_name' ist die Webadresse des Servers oder einfacher aber risikoreicher:

grant {
  permission java.security.AllPermission;
};

Die Policy-Datei ist im Home-Verzeichnis des Nutzers, unter Windows bspw.:
"c:\Dokumente und Einstellungen\nutzername\"

Mit der Folgeanweisung kann eine andere Policy-Datei benutzt werden:
java -Djava.security.policy=rmi/client.policy rmi/LoadClient

Das Interface der 'remote method' und die ErgebnisklasseBearbeiten

Müssen bei Server und Client verfügbar sein.

package rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Hello extends Remote {
  //String-Parameter, Ergebnis ist eigene Klasse mit serialisierbaren Objekten
  public RmiResult sayHello(String txt) throws RemoteException;
}

package rmi;
import java.util.GregorianCalendar;

public class RmiResult implements java.io.Serializable {
  String name;
  String adresse;
  GregorianCalendar birthday;
}

Der RMI-ServerBearbeiten

package rmi;

import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.RemoteServer;
import java.io.*;

public class TServer {
    int port;

    public TServer(int port) {
      this.port = port;
      worker();
    }

  void worker() {
    try {
      //Server-Logging in eine Datei
      PrintStream logFile = new PrintStream(
        new FileOutputStream("rmi/TServerLog.txt",true));
      RemoteServer.setLog(logFile);
      //Instanz mit der implementierten Methode
      HelloImpl hello = new HelloImpl();

      //Export des Objekts in 'HelloImpl' selbst, wo automatisch der Konstruktor
      //von UnicastRemoteObject aufgerufen wird, der das Objekt exportiert.

      //Registry anlegen, Methode anmelden, Generierung der Skeleton-Klasse
      Registry registry = LocateRegistry.createRegistry(port);
      registry.rebind("Hello", hello);

      String strtSrv = Thread.currentThread() + ", Server ready...";
      System.err.println(strtSrv);
      logFile.println(strtSrv);
      logFile.println("---------------");
      logFile.flush();
    } catch (Exception e) {
      System.err.println("Server exception: " + e.getMessage());
      e.printStackTrace();
    }
  }
  public static void main(String args[]) {
    int port = 1099;
    TServer ts = new TServer(port);
  }
}

Die Implementierung der 'remote method'Bearbeiten

package rmi;

import java.rmi.server.UnicastRemoteObject;
import java.util.GregorianCalendar;

//die Erweiterung sorgt für den automatischen Export des Objekts
public class HelloImpl extends UnicastRemoteObject implements Hello {
  int cnt = 0;

  public HelloImpl() throws java.rmi.RemoteException {
  }
  
  //synchronized verhindert Unterbrechung durh andere Anfragen, das wäre
  //kritisch in Bezug auf die Variable 'cnt'.
  public synchronized RmiResult sayHello(String txt) {
    System.err.println("sayHello-Thread: this=" +
      Integer.toHexString(this.hashCode()) + ", " + Thread.currentThread());
    cnt++;
    RmiResult rr = new RmiResult();
    rr.name = "Hallo, mein Name ist: ..., alias Sabbat, alias Wallenstein !";
    rr.adresse = "Laufende Nr.:" + cnt + ", " + txt;  //Parameter der Methode
    rr.birthday = new GregorianCalendar(1941,11,24);
    return rr;
  }
}

Der RMI-ClientBearbeiten

Der vom Schalter 'loadclient' abhängige Code ist nur für den Fall bedeutsam, dass der RMI-Client über die Klasse 'LoadClient' und nicht als eigenständige Applikation über die Shell aufgerufen wird.

package rmi;

import java.rmi.*;
import java.util.Date;
import java.text.SimpleDateFormat;

public class TClient {
  public static void main( String[] args ) throws Exception {
    //Aufruf aus der Shell als eigenständige Applikation
    TClient tc = new TClient(false);
  }

  Hello stub;
  String par = "Adresse: Mitteldeutschland";

  //Aufruf über 'cl.newInstance()' in LoadClient
  public TClient() throws Exception {
    this(true);
  }
  public TClient(boolean loadclient) throws Exception {
    String FEHLER_TEXT = "\n*** Moegliche Ursachen:\n" +
           "*** Server nicht aktiv oder 'remote object' nicht registriert!";
    System.err.println("*** loadclient=" + loadclient);

    try {
      //kontaktiere Server bezüglich Port 1099
      //ist auf dem Server eine Methode 'Hello' registriert ?
      //generiere eine Stub-Klasse für den Client
//      stub = (Hello) Naming.lookup("Hello");  //Server lokal
      //Server entfernt (siehe Doku von 'Naming.lookup')
      stub = (Hello) Naming.lookup("//rechner_name:1099/Hello");
    } catch (Exception ce) {
      System.err.println(ce.getMessage() + FEHLER_TEXT);
      System.exit(1);
    }

    if (!loadclient)
      worker2();
  }

  //Diese Methode wird von LoadClient (dynamisches Laden erforderlicher Objekte)
  //aufgerufen.
  public RmiResult worker1() throws RemoteException {
    RmiResult erg = stub.sayHello(par);  //remote method
    return erg;
  }
  //Diese Methode wird durch Direktruf von TClient aufgerufen und hier beginnt
  //die eigentliche Applikation mit dem transparenten Aufruf der
  //entfernten Methode.
  public void worker2() throws RemoteException {
    RmiResult erg = stub.sayHello(par);  //remote method
    System.out.println(erg.name );
    System.out.println( erg.adresse );
    SimpleDateFormat sdf = new SimpleDateFormat("EEEE, dd.MM.yyyy");
    Date bd = erg.birthday.getTime();  //(Gregorian)Calendar-Objekt -> Date
    System.out.println(sdf.format(bd));
  }
}

Klasse LoadClientBearbeiten

Zusatzmöglichkeit für die verteilte Verarbeitung.

package rmi;

import java.rmi.RMISecurityManager;
import java.rmi.server.RMIClassLoader;
import java.lang.reflect.*;
import java.text.SimpleDateFormat;
import java.util.Date;

//Hier wird die TClient-Klasse vom Webserver des RMI-Servers dynamisch
//geladen, instanziert und ihre Methode 'worker1' aufgerufen, die ein Ergebnis
//vom Typ 'RmiResult' zurückliefert, das dann bearbeitet werden kann.
public class LoadClient {

  public static void main(String[] args) {
    new LoadClient();
  }
  Method clMethod;
  Object[] obargs;
  Object obj;
  public LoadClient() {
    //setze einen SecurityManager ein, in der Policy-Datei '.java.policy'
    //muss das Laden von externem Bytecode erlaubt sein
    System.setSecurityManager(new RMISecurityManager());
    try {
      // 1. Lade die 'rmi.TClient'-Klasse von der angegebenen Web-Adresse.
      Class cl = RMIClassLoader.loadClass("http://rechner_name/wicki/","rmi.TClient");
      // 2. Bilde eine Instanz der Klasse und suche deren Methode 'worker1'.
      //    Durch 'cl.newInstance()' wird auch der Konstruktor von TClient
      //    abgearbeitet, der den Schalter 'loadclient' setzt.
      obj = cl.newInstance();
      Method[] methods = cl.getMethods();  //alle Methoden der Klasse
      //Argumentliste für aufzurufende Methode(0: keine Param.)
      obargs = new Object[0];
      for (int i = 0; i < methods.length; i++) {
        //suche die Methode in 'TClient', die die entfernte Methode aufruft
        if (methods[i].getName().compareTo("worker1") == 0) {
          clMethod = methods[i];
          System.err.println("In LoadClient, ReturnType:" +
                             clMethod.getReturnType().getName());
          worker();  //zur Abarbeitung, dort Aufruf entfernte Methode
          break;
        }
      }
    } catch (Exception e) {
      System.err.println("Exception:" + e.getMessage());
      e.printStackTrace();
    }
  }

  //Hier kann die eigentliche Abarbeitung mit dem Aufruf der entfernten Methode
  //und der Auswertung des Ergebnisses beginnen.
  public void worker() throws Exception {
    // 3. Rufe die unter 2. gefundene Methode 'clMethod' der Instanz 'obj' auf,
    //    'obargs' ist die in diesem Falle leere Argumentliste für die Methode.
    RmiResult erg = (RmiResult)(clMethod.invoke(obj,obargs));
    System.out.println("In worker ... ");
    System.out.println( erg.name );
    System.out.println( erg.adresse );
    SimpleDateFormat sdf = new SimpleDateFormat("EEEE, dd.MM.yyyy");
    Date bd = erg.birthday.getTime();  //(Gregorian)Calendar-Objekt -> Date
    System.out.println(sdf.format(bd));
  }

}

Abarbeitung des BeispielsBearbeiten

Beispiel TServer/TClient
Rechner A Rechner B
Dateien in 'basispfad/rmi': TServer,Hello,HelloImpl,RmiResult TClient,Hello,RmiResult
Übersetzen cd basispfad
javac rmi/*.java
cd basispfad
javac rmi/*.java
Abarbeiten cd basispfad
java rmi/TServer
cd basispfad
java rmi/TClient
Beispiel LoadClient
Rechner A Rechner B
Dateien in 'basispfad/rmi': TServer,Hello,HelloImpl,RmiResult LoadClient,RmiResult,client.policy
Dateien in 'pfad_webserver/wicki/rmi': class-Dateien: TClient,Hello,RmiResult
Übersetzen wie Beispiel 1 cd basispfad
javac rmi/*.java
Abarbeiten wie Beispiel 1 cd basispfad
java -Djava.security.policy=rmi/client.policy rmi/LoadClient