5.2.20
04/05/20
Last Modified 01/11/19 by Walter Tasin
tasin/RMITutorial Reload Page

RMITutorial

Einleitung

Der hier aufgeführte Text zeigt den Entwicklungsprozess einer Java-Beispielapplikation von einer rein lokalen Applikation bis zur Client-/Serverapplikation unter Verwendung der RMI (Remote Method Invocation).
WICHTIG: Diese Beschreibung der RMI setzt die Verwendung eine Java-Version >=1.5 voraus!

Aufgabenstellung

Es soll eine Applikation geschrieben werden, die für ein bestimmtes Jahr das Osterdatum errechnet.
Übergeben wird ein int-Wert (Jahreszahl) und als Resultat erhält man ein String-Objekt, auf welchen Tag Ostersonntag in dem Jahr fällt.
Der dafür verwendete Algorithmus findet sich auf folgender Website www.tondering.dk.


Das erste Programm

Interface

Als erstes wird ein Interface definiert, um oben genannte Aufgabenstellung zu errechnen:

/*
 * DateCalc.java
 * Created on 19. Januar 2006, 22:33
 */
import java.io.*;

/**
 * Interface to calculate a certain date
 * @author Tasin
 */
public interface DateCalc extends Serializable
{
   public String calculate(int a);    
}

Bemerkung: Die Ableitung von Serializable ist in diesem Fall nicht nötig, da in diesem Fall nur serialisierbare Objekte vorkommen, d. h. die verwendeten Typen String und int
sind bereits serialisierbar.

Implementierung

Die Implementierung zur Errechnung des Osterdatums sieht dann wie folgt aus:

/*
 * EasterCalculator.java
 *
 * Created on 23. Januar 2006
 */

import java.util.*;

public class EasterCalculator implements DateCalc
{
    public String calculate(int year)
    {
        String answer=null;

        int goldenNumber = year % 19;
        int century = year / 100;
        int century4th = year / 400;
        int epact23Mod30 = (century-century4th-(8*century+13)/25+19*goldenNumber+15)%30;
        int days2fullMoon = epact23Mod30 - (epact23Mod30/28)*(1 -  
                (29/(epact23Mod30+1))*((21-goldenNumber)/11));
        int weekday2fullMoon = (year + year/4 + days2fullMoon + 2 - century + century4th)%7;
        int day2sunday = days2fullMoon - weekday2fullMoon;

        int month = 3 + (day2sunday+40)/44;
        int day = day2sunday + 28 - 31*(month/4);        
        
        answer=Integer.toString(day) + "." + Integer.toString(month)+ "." + Integer.toString(year);
        return answer;
    }
 
    public static void main(String[] args) 
    {
        int year=(args.length>0) ? Integer.parseInt(args[0]) : 2006;
        EasterCalculator dc=new EasterCalculator();
        
        if (year<1583 || year>3999)
        {
            System.out.println("Diese Formel funktioniert nur sicher fuer Jahre von 1583 bis 3999");
            System.exit(1);
        }
        System.out.println("Ostersonntag: "+dc.calculate(year));
    }
}

Beispielaufruf

Bereits jetzt ist die Aufgabenstellung als lokale Anwendung gelöst.

C:\Java-Test>java EasterCalculator 1984
Ostersonntag ist am 22.4.1984

C:\Java-Test>java EasterCalculator 
Ostersonntag ist am 16.4.2006

C:\Java-Test>


Die Client-/Serverapplikation mithilfe der RMI

Als nächsten Schritt soll diese Applikation zu einer Serverapplikation werden, um entfernten Rechnern auch den Dienst der Ostersonntag-Errechnung zu ermöglichen.

Serverapplikation

Interface

Es wird dazu wiederum ein Interface entwickelt, das die nötige Funktionalität als Remote-Aufruf definiert.

/*
 * RMIDateCalc.java
 *
 * Created on 20. Januar 2006, 22:28
 *
 */
import java.rmi.*;

/**
 * Like DateCalc (only for Remote Method Invocation)
 * @author Tasin
 */
public interface RMIDateCalc extends Remote
{
       public String RMICalculate(int a) throws RemoteException;    
}

Implementierung

/*
 * RMIDateServer.java
 *
 * Created on 23. Januar 2006, 22:38
 */
import java.rmi.*;
import java.rmi.server.*;

/**
 * RMIServer to calculate a certain date
 * @author Tasin
 */
public class RMIDateServer extends UnicastRemoteObject 
        implements RMIDateCalc 
{
    DateCalc dcalculator=null;    

    /** Creates a new instance of RMIDateServer */
    public RMIDateServer(DateCalc dc) throws RemoteException 
    {
       dcalculator=dc;
    }
    
    public String RMICalculate(int year)
    {
        String answer="nicht errechenbar";
        try
        {
          System.out.print("Client "+getClientHost()+": ");
        }
        catch (ServerNotActiveException ex)
        {
          System.out.print("lokaler Aufruf: ");
        }

        System.out.println("Erfrage Jahresinfo fuer "+year);
        // Fehler abfangen
        if (dcalculator!=null && year>1582)
          answer=dcalculator.calculate(year);
        return answer;
    }
    
    public static void main(String args[])
    {
        try
        {
            RMIDateServer rmiServer=new RMIDateServer(new EasterCalculator());
            Naming.rebind("EasterCalculator", rmiServer);
            System.out.println("RMIServer gestartet mit Algorithmus \'EasterCalculator\'");
        }
        catch (Exception ex)
        {
            System.out.println("EXCEPTION beendet das Programm:");
            ex.printStackTrace();
        }
    }
}

Aufruf des Servers

Als erstes muss ein Namensdienst gestartet werden, der es ermöglicht den eben erzeugten Serverdienst EasterCalculator für den Client bekannt zu machen.
Diese Aufgabe erledigt die rmiregistry.

Nach dem Starten der Registry wird nun der Serverapplikation ermöglicht seinen Dienst an den Namensdienst zu binden und diesen somit unter dem Namen EasterCalculator zu veröffentlichen.

/*
 * RMIDateServer.java
 *
 * Created on 20. Januar 2006, 22:38
 */

  ...

public class RMIDateServer extends UnicastRemoteObject 
        implements RMIDateCalc 
{
  ...

            RMIDateServer rmiServer=new RMIDateServer(new EasterCalculator());
            Naming.rebind("EasterCalculator", rmiServer);
  ...

}

Für ein erfolgreiches Binden an die rmiregistry muss dieser eine Stellvertreterklasse (Stub-Klasse) zur Verfügung stehen, sollten diese nicht erreichbar sein (z. B. explizit mit rmic erzeugt und über Codebase oder CLASSPATH der Registry zur Verfügung gestellt worden sein), so kann die rmiregistry (wie auch die Clientapplikation) seit Java 1.5 eine dynamische Stub-Klasse aus der Funktionsbeschreibung erzeugen. Aus diesem Grund muss der Registry zumindest die Funktionsbeschreibung (Remote-Interface) RMIDateCalc bekannt sein, dementsprechend muss sie Zugriff auf RMIDateCalc.class haben.

Eine Untersuchung mit Filemon und dem Debugger unter Netbeans ergab folgendes:
rmiregistry erhält die Information, wie der Stub erzeugt wurde, durch die Serverapplikation.

  • Die Serverapplikation versucht die Stubklasse bei der Konstruktion eines Serverobjekts zu laden.
  • Kann diese nicht geladen werden, dann wird eine Stellvertreterklasse (s. a. java.lang.reflect.Proxy) für den Server-Stub dynamisch erzeugt (z. B. Class $Proxy0).
  • Die Serverapplikation erzeugt nun ein Stub-Objekt entweder aus der geladenen Stub-Klasse oder der dynamisch erzeugten Proxy-Klasse.
  • Eine Referenz dieses Objekts wird nun an die Registry "exportiert".
    (s. a. java.rmi.server.RemoteRef und java.rmi.server.UnicastRemoteObject)
  • Sollte dieses Objekt vom Server aus einer Proxy-Klasse erzeugt worden sein, verlangt auch die Registry nicht mehr nach einer expliziten Server-Stub-Klasse.
    --> Es reicht also der Zugriff auf RMIDateCalc.class.

Die rmiregistry (JDK 1.5) und der Server kann also auf folgende Arten gestartet werden:

  • Kopieren der Datei RMIDateCalc.class in das Startverzeichnis von rmiregistry (NICHT empfohlen!!)

C:\ganzwoanders>copy C:\Java-Test\RMIDateCalc.class .
C:\ganzwoanders>rmiregistry

C:\Java-Test>java RMIDateServer
RMIServer gestartet mit Algorithmus 'EasterCalculator'

  • Zugriff über CLASSPATH (ungünstig, falls mehrere unterschiedl. Serverdienste über eine Registry gebunden werden sollen)
    • rmiregistry wird in einem Pfad gestartet, über den auch der Zugriff auf RMIDateCalc.class möglich ist.
    • Vor dem Start von rmiregistry wird die CLASSPATH-Umgebungsvariable entsprechend gesetzt.
C:\ganzwoanders>set CLASSPATH=C:\Java-Test
C:\ganzwoanders>rmiregistry

C:\Java-Test>java RMIDateServer
RMIServer gestartet mit Algorithmus 'EasterCalculator'

Bemerkung:
Die CLASSPATH-Variable - wie oben angegeben - läßt die JVM nur noch in C:\Java-Test suchen; Das defaultmäßig eingestellte Suchen im aktuellen Verzeichnis wurde dadurch deaktiviert.
  • Zugriff über Angabe der Codebase (1)
Man kann auch der RMI-Registry beim Start mitteilen, wo sich die nötigen Dateien (hier RMIDateCalc.class) für die Server-Client-Kommunikation befinden.
Der Client erhält dann den vollqualifizierenden Pfad über die Codebase-Angabe der RMI-Registry.
C:\ganzwoanders>set CLASSPATH=
C:\ganzwoanders>rmiregistry -J-Djava.rmi.server.codebase=file:///C:\Java-Test/

C:\Java-Test>java RMIDateServer
RMIServer gestartet mit Algorithmus 'EasterCalculator'


  • Zugriff über Angabe der Codebase (2)
Alternativ dazu kann der Registry durch den Serverstart mitgeteilt werden, woher diese die nötige RMIDateCalc.class "herunterladen" kann. Dazu erhält der Serverstart die entsprechenden Codebase-Angabe.
Anders gesagt: Die Registry erhält von der Serverapplikation einen Zugriffspfad auf RMIDateCalc.class.
ACHTUNG: Änderung seit Version 1.7u21, 1.6u45 und 1.5u45:
Der RMI-Registry wird standardmäßig nicht mehr erlaubt entfernte Codebaseangaben zu akzeptieren, damit dieses wieder möglich gemacht wird, muss der Schalter java.rmi.server.useCodebaseOnly auf false gesetzt werden.
Wieter Informationen dazu finden Sie hier.

C:\ganzwoanders>set CLASSPATH=
C:\ganzwoanders>rmiregistry -J-Djava.rmi.server.useCodebaseOnly=false
</geshi>
<geshi type="dos">C:\Java-Test>java -Djava.rmi.server.codebase=file:///C:\Java-Test/ RMIDateServer
RMIServer gestartet mit Algorithmus 'EasterCalculator'

Clientapplikation

Der Client dient als Benutzerschnittstelle, um dem Server die gewünschten Parameter zu übermitteln und das erhaltene Resultat in geeigneter Form auszugeben.

Interface

Wie bereits oben in "Aufruf des Servers" erwähnt wird als gemeinsame Kommunikationsschnittstelle zwischen Server und Client die Datei RMIDateCalc.class verwendet.
Diese muss dem Client lokal zur Verfügung stehen.

Implementierung

/*
 * RMIDateClient.java
 *
 * Created on 21. Januar 2006
 */
import java.rmi.*;

/**
 * RMIClient to calculate a certain date
 * @author Tasin
 */
public class RMIDateClient 
{
   public static void main(String args[])
   {
       if (args.length==0)
       {
           System.out.println("Aufruf:\n"+
                   "RMIDateClient Jahr [<Serveradresse>]");
           System.exit(1);
       }
       int year=Integer.parseInt(args[0]);
       String serverURL="rmi://"+
               ((args.length>1) ? args[1] : "localhost") + "/EasterCalculator";
       
       try
       {
         RMIDateCalc dcalc=(RMIDateCalc) Naming.lookup(serverURL);
         System.out.println("Resultat: "+dcalc.RMICalculate(year));
       }
       catch (Exception ex)
       {
         System.out.println("EXCEPTION beendet das Programm:");
         ex.printStackTrace();
       }
   }
}

Aufruf des Clients

Der Client muss mit mindestens einem Kommandozeilenparameter aufgerufen werden, der den Parameter für die Serverberechnung festlegt (Jahreszahl).
Der zweite Parameter ist optional und legt die IP-Adresse des Servers, der die Berechnung durchführen soll, fest. Fehlt der zweite Parameter, dann wird localhost angenommen.

C:\Java-Test>java RMIDateClient 2003
Resultat: 20.4.2003

C:\Java-Test>java RMIDateClient 1500
Resultat: nicht errechenbar

C:\Java-Test>java RMIDateClient 2500 server.example.com
Resultat: 18.4.2500

C:\Java-Test>

Ergänzende Erläuterung
Unmittelbar nach dem Starten des Clients wird mit der Registry Kontakt aufgenommen:
RMIDateCalc dcalc=(RMIDateCalc) Naming.lookup(serverURL);
Dieser Aufruf erhält falls erfolgreich als Resultat ein Stub-Objekt des Servers. Dieses Objekt ist - wie oben bereits beschrieben - entweder vom Typ RMIDateServer_Stub oder von $Proxy0 (mithilfe von RMIDateCalc auf der Clientseite dynamisch erzeugten Klasse).

Somit existiert für die Applikation ein geeignetes "Stellvertreterobjekt", dass den entfernten Methodenaufruf ermöglicht.

Damit ist bereits die erste RMI-Applikation fertig!

Bevor wir nun zum Thema Erweiterung dieser RMI-Applikation kommen, noch etwas zu Änderungen gegenüber JDK 1.4 --> Wo ist der Stub und der Securitymanager?.