5.2.20
04/02/20
Last Modified 01/11/19 by Walter Tasin
tasin/RMITutorial4 Reload Page

RMITutorial (4) - 2. Erweiterung der Serverapplikation

Aufgabenstellung

Als nächstes soll der Server dahingehend erweitert werden, dass

  • Der Entwickler der Clientapplikation auch die Möglichkeit besitzt einen Algorithmus zu programmieren und diesen auf seinem Webserver publizieren.
    Der Server soll die Möglichkeit besitzen mithilfe des Nachladens von entfernten Code auf diesen Algorithmus zurückgreifen zu können und diesen bei sich auszuführen.
  • Die Serverapplikation soll insofern erweitert werden, dass sie so lange läuft, bis der Serveradministrator die Taste [Enter] drückt.
    Bisher kann der Server nur durch Unterbrechung [Ctrl-C] der JVM beendet werden.

Serverbeendigung durch Usereingabe

Der Server exportiert - wie bereits in Teil 1 erklärt - beim rebind eine Referenz auf sein Serverobjekt (genauer gesagt auf den Server-Stub), um diese einem Client für einen RMI-Call zur Verfügung zu stellen.

/*
 * RMIDateServer.java
 *
 * Created on 24. Januar 2006, 13:03
 */
    ...

    public static void main(String args[])
    {
        try
        {
            RMIDateServer rmiServer=new RMIDateServer();
            Naming.rebind("rmi://localhost/DateCalculator", rmiServer);
    ...

Damit darf die Applikation nicht beendet werden, denn der Garbagecollector kann das Objekt nicht zerstören, so lange eine Referenz auf ein Objekt zeigt.
Es muss also ein Weg gefunden werden die "Bindung" zu lösen, also äquivalent zum export() (der implizit durch den Konstruktor des Serverobjekts aufgerufen wurde) wird ein unexport() des Serverobjekts nötig.
Vorher sollte aber auch der bereitgestellte Dienst der Registry ausgetragen werden (unbind()).
Es ergibt sich also für die main() des Servers folgender Code:

/* RMIDateServer.java
 *
 * Created on 24. Januar 2006, 13:03
 */
    ...

    public static void main(String args[])
    {
        try
        {
            RMIDateServer rmiServer=new RMIDateServer();
            Naming.rebind("DateCalculator", rmiServer);
            System.out.println("RMIServer gestartet mit nachladbaren Algorithmus");
            System.out.println("Stoppen durch [Enter] ...");
            // wait for [Enter]
            System.in.read();
            Naming.unbind("DateCalculator");
            UnicastRemoteObject.unexportObject(rmiServer, true);
        }
        catch (Exception ex)
        {
            System.out.println("EXCEPTION beendet das Programm:");
            ex.printStackTrace();
        }
    }
}


Algorithmenbereitstellung durch Client

Im nächstes Schritt soll die Serverapplikation dahingehend erweitert werden, dass auch der Client (oder ein Dritter) seinen Algorithmus zur Verfügung stellen kann.
Dazu muss der Server, die Möglichkeit haben Code von einer anderen Stelle herunterladen zu dürfen.
Dies wird von Java unterstützt, denn für Java-Versionen < 1.5 war dieses Feature sogar nötig, damit dem Client der Server-Stub zur Verfügung gestellt wird. (s. a. Tutorial Teil 2). Der Server gibt also über seine Codebase dem Client an, wo er den Server-Stub finden kann.
Der Client könnte also seine Algorithmus-Klasse ebenfalls auf einem Webserver publizieren und den Pfad zu der Klassendatei (== dem Code) über seine Codebase-Angabe dem Server mitteilen.
Es bietet sich also an, auch dafür den RMIClassLoader zu verwenden.
Hinweis: Alternativ könnte stattdessen auch der URLClassLoader verwendet werden, dies soll aber hier nicht weiter verfolgt werden.

Algorithmus-Interface für Client bereitstellen

Dazu wird wiederum ein Interface benötigt, das den gemeinsamen Funktionsaufruf zur Berechnung eines Datums festlegt.
Dieses Interface gibt es bereits. Es handelt sich dabei um DateCalc.class.
Dieses Interface muss nun - anders als in den vorhergehenden Beispielen - auch dem Client zur Verfügung stehen.

Somit wird auch dem Client (oder Dritten) ermöglicht eine Klasse zu implementieren, welche eine Datumskalkulation zur Verfügung stellt.
Hier nochmal zur Erinnerung der Inhalt von DateCalc.java:

/*
 * 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);    
}

Der neue Algorithmus gEasterCalculator

Es wird nun angenommen, dass der Client mit der Einschränkung der Osterdatumsberechnung auf den Gregorianischen Kalender nicht einverstanden ist und seinerseits einen Algorithmus bereitstellen möchte, der Server aber weiterhin die eigentliche Berechnung durchführen soll.
Clientseitig wird also folgender Algorithmus entwickelt:

/*
 * gEasterCalculator.java
 *
 * Created on 2. Februar 2006, 15:51
 */

/**
 * new client algorithm
 * easter date calculation for Julian and Gregorian calendars
 * @author Tasin
 */
public class gEasterCalculator implements DateCalc
{
    public String calculate(int year)
    {
        String answer="nicht errechenbar";

        int goldenNumber = year % 19;

        int days2fullMoon = (19*goldenNumber+15)%30;
        int weekday2fullMoon = (year + year/4 + days2fullMoon)%7;
        int day2sunday = days2fullMoon - weekday2fullMoon;

        // calculation for Gregorian
        if (year>1582)
        {
          int century = year / 100;
          int century4th = year / 400;
          int epact23Mod30 = (century-century4th-(8*century+13)/25+19*goldenNumber+15)%30;

          days2fullMoon = epact23Mod30 - (epact23Mod30/28)*(1 -  
                 (29/(epact23Mod30+1))*((21-goldenNumber)/11));
          weekday2fullMoon = (year + year/4 + days2fullMoon + 2 - century + century4th)%7;
          day2sunday = days2fullMoon - weekday2fullMoon;
        }

        // common part of the calculation
        int month = 3 + (day2sunday+40)/44;
        int day = day2sunday + 28 - 31*(month/4);        
        
        if (year>=30 && year<4000)
          answer=Integer.toString(day) + "." + Integer.toString(month)+ "." + Integer.toString(year);
        return answer;
    }
 
    // this is only for local testing purpose 
    public static void main(String[] args) 
    {
        int year=(args.length>0) ? Integer.parseInt(args[0]) : 2006;
        gEasterCalculator dc=new gEasterCalculator();
        
        System.out.println("Diese Formel funktioniert nur fuer Jahre von 30 bis 3999");
        System.out.println("Ostersonntag: "+dc.calculate(year));
    }
    
}

Erweiterung des RMI-Interfaces

Um dem Server nun mitzuteilen, dass es sich um einen Algorithmus handelt der entfernt geladen werden muss bzw. kann, muss auch die gemeinsame Kommunikationsschnittstelle RMIDateCalc entsprechend erweitert werden.
Dafür gibt es wiederum mehrere Lösungsansätze:

Übergabe des Algorithmusobjekts via RMI

Diese Methode instantiiert ein clientseitiges Algorithmusobjekt (gEasterCalculator) auf der Clientseite und übermittelt dieses an den Server. Dieser bekommt mitgeteilt, dass es sich um ein gEasterCalculator-Objekt (== gewünschte Implementierung des DateCalc-Interfaces) handelt und wo dieses zu finden ist (== Codebaseangabe des Clients).
Quasi für die Serverapplikation "unsichtbar" wird der Code dann über den RMIClassLoader nachgeladen, um somit die Berechnung serverseitig ausführen zu können.
Die RMIDateCalc-Klasse müsste also wie folgt erweitert werden:

/*
 * 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;    
       public String RMICalculate(String algo, int a) throws RemoteException;    
       public String RMICalculate(DateCalc algo, int a) throws RemoteException;    
}

Die Implementierung einer Client- und Serveranwendung - die diesem Lösungsansatz folgt - sind Gegenstand eines Praktikumsversuchs der Lehrveranstaltung Verteilte Systeme und werden hier nicht näher erläutert. Stattdessen verweise ich an dieser Stelle auf die Vorlesung Verteilte Systeme.

Explizite Übergabe der Codebase und des Algorithmus als Strings

Die hier vorgestellte Lösung soll dagegen noch einen Schritt weitergehen. Der Algorithmus wird nicht als Objekt dem Server mitgeteilt, sondern der Name der Algorithmusklasse und die entsprechende Codebase werden dem Server als Strings übermittelt.
Was also im Fall Übergabe des Algorithmusobjekts via RMI automatisch geschieht, muss die Serveranwendung explizit machen.
Diesen Lösungsansatz folgend, wird die RMIDateCalc entsprechend erweitert:

/*
 * 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;    
       public String RMICalculate(String algo, int a) throws RemoteException;    
       public String RMICalculate(String codebase, String algo, int a) 
          throws RemoteException;    
}

Serverimplementierung

Wie gehabt muss nun die neu hinzugefügte Funktion in der Serveranwendung implementiert werden. Dies kann wie folgt aussehen:

/*
 * RMIDateServer.java
 *
 * Created on 2. Februar 2006
 */
import java.rmi.*;
import java.rmi.server.*;
import java.net.*;

/**
 * RMIServer to calculate a certain date
 * @author Tasin
 */
public class RMIDateServer extends UnicastRemoteObject 
        implements RMIDateCalc 
{
    ...

    // That's the new METHOD !!!
    public String RMICalculate(String codebase, String algo, int year)
    {
        DateCalc specialDCalc=null;    
        String answer="nicht errechenbar";
        
        try
        {
          System.out.print("Client "+getClientHost()+": ");
        }
        catch (ServerNotActiveException ex)
        {
          System.out.print("lokaler Aufruf: ");
        }

        try
        {
           /* And here we are ...
            * Load a class with the RMIClassLoader and create an instance.
            *   It has to be an implementation of DateCalc.
            */
           Object dc = RMIClassLoader.loadClass(codebase, algo).newInstance();
           specialDCalc=(DateCalc) dc;
        }
        catch (Exception ex)
        {
          System.out.println("Algorithmus \'"+algo+"\' wurde nicht gefunden ...");
          answer+=" -> Algorithmus dem Server nicht zugaenglich!";
          ex.printStackTrace();
        }
 
        if (specialDCalc!=null)
        {
          answer=specialDCalc.calculate(year);
          System.out.println("Erfrage \'"+ specialDCalc.getClass().getName() +
                  "\' Jahresinfo fuer "+year);
        }
        return answer;
    }

    ...
}