5.2.20
04/06/20
Last Modified 01/11/19 by Walter Tasin
tasin/RMITutorial3 Reload Page

RMITutorial (3) - 1. Erweiterung der Serverapplikation

Aufgabenstellung

Als nächstes soll der Server insofern erweitert werden, so dass er auch andere Datumsberechnungen durchführen kann.

  • Der Client soll dazu dem Server zusätzlich übermitteln, welche Aufgabe auf dem Server durchgeführt werden soll.
  • Da Java ein Objekt anhand des Klassennamens erzeugen kann, soll dem Server als zusätzlichen Parameter den Klassennamen in Form eines String-Objekts übermittelt werden.
  • Der Server soll diesen Dienst ab sofort nicht mehr als EasterCalculator sondern als DateCalculator registrieren.
  • Der Server soll kein EasterCalculator-Objekt mehr im Konstruktur erzeugen, sondern durch den Client mitgeteilt bekommen, welcher Algorithmus verwendet wird.
  • Die zusätzlichen Algorithmen werden serverseitig als Klassen implementiert, die - wie gehabt - durch das Interface DateCalc beschrieben sind.
  • Im vorigen Beispiel übernahm der Server die Parameterüberprüfung (= ist die Jahresangabe gültig). Dies muss jetzt der entsprechende Algorithmus übernehmen.
  • Die Funktion String RMICalculate(int a) soll weiterhin erhalten bleiben und den Algorithmus EasterCalculator verwenden.
  • Als zusätzl. "Beispiel"-Algorithmus soll eine Klasse WeekdayFoYCalculator implementiert werden, die ermittelt an welchen Wochentag Neujahr in dem als Parameter übergebenen Jahr fällt/fiel/fallen wird.

Ziel ist es also serverseitig folgenden Abstraktionsgrad zu erhalten:

  • nur noch Klassendateien dem Package hinzufügen, um weitere Funktionalität zu implementieren
    (natürlich festgelegt durch die Spezifikation im Interface DateCalc).
  • die Server- und Clientimplementierungen unter dieser Voraussetzung nicht mehr modifizieren zu müssen.

Änderungen

Algorithmen

EasterCalculator

Da jetzt die Überprüfung der Jahreszahl im Algorithmus statt finden sollen, wird EasterCalculator wie folgt geändert:

/*
 * EasterCalculator.java
 *
 * Created on 19. Januar 2006, 22:19
 */

import java.util.*;

public class EasterCalculator implements DateCalc
{
    public String calculate(int year)
    {
        String answer="nicht errechenbar";

        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);        
        
        if (year>1582 && year<4000)
          answer="Ostersonntag: " + 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();
        
        System.out.println("Achtung: Diese Formel funktioniert nur fuer Jahre von 1583 bis 3999");
        System.out.println(dc.calculate(year));
    }
}

WeekdayFoYCalculator

Auch für diese Berechnung existiert ebenfalls ein Algoritmus auf www.tondering.dk.
Für den Fall des festgelegten Datums 1. Januar des gewünschten Datums, läßt sich die Berechnung wesentlich vereinfachen.

/*
 * WeekdayFoYCalculator.java
 *
 * Created on 24. Januar 2006, 16:38
 */

/**
 * Returns the German weekday of 1st Jan of a certain year
 * @author Tasin
 */
public class WeekdayFoYCalculator implements DateCalc
{
    private static final String wday[] = { "Sonntag", "Montag", "Dienstag", "Mittwoch", 
            "Donnerstag", "Freitag", "Samstag" };

    public String calculate(int year)
    {
        String answer="nicht errechenbar";
        int a = year-1;
        int b = a/4 - a/100 + a/400; 
        int wd = (a + b + 1) % 7;
        
        if (year>1582 && year<4000)
          answer=wday[wd] + ", 1.1." + Integer.toString(year);
        return answer;
    }
 
    public static void main(String[] args) 
    {
        int year=(args.length>0) ? Integer.parseInt(args[0]) : 2006;
        WeekdayFoYCalculator dc=new WeekdayFoYCalculator();

        System.out.println("Achtung: Diese Formel funktioniert nur fuer Jahre von 1583 bis 3999");
        System.out.println(dc.calculate(year));
    }    
}


RMI-Interface

Da vom Client ein weiterer Parameter benötigt wird, muss auch die Kommunikationsschnittstelle erweitert werden. Deshalb wird
public String RMICalculate(String algo, int a) throws RemoteException;
dem Interface hinzugefügt.
/*
 * 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;    
}

Wird eine Änderung in einem Interface nötig, so muss sich auch die Implementierung entsprechend ändern.
In diesem Fall ist das die Serverimplementierung.

Serverimplementierung

Für die oben erwähnte Aufgabenstellung ergibt sich folgende Implementierung:

/*
 * RMIDateServer.java
 *
 * Created on 24. Januar 2006, 13:03
 */
import java.rmi.*;
import java.rmi.server.*;

/**
 * RMIServer to calculate a certain date
 * @author Tasin
 */
public class RMIDateServer extends UnicastRemoteObject 
        implements RMIDateCalc 
{
    /** Creates a new instance w/o any parameter */
    public RMIDateServer() throws  RemoteException
    {
    }
    
    public String RMICalculate(String algo, int year)
    {
        /* default string for the client in case of an exception ... */
        String answer="Kann Berechnung fuer \'"+algo+"\' nicht durchgefuehren.";
        try
        {
          System.out.print("Client "+getClientHost()+": ");
        }
        catch (ServerNotActiveException ex)
        {
          System.out.print("lokaler Aufruf: ");
        }

        try
        {
          /* create an object with the class name ... great language feature */
          Object dc=Class.forName(algo).newInstance();
          DateCalc dcalculator=(DateCalc) dc;

          System.out.println("Erfrage \'"+algo+"\' Jahresinfo fuer "+year);
          answer=dcalculator.calculate(year);
        }
        catch (Exception ex)
        {
          /* report to stdout any exception concerning 
           *   - the creation of the algorithm object
           *   - getting the answer from the algorithm
           */
          System.out.println("EXCEPTION bei Erzeugung des Antwortstrings: ");
          ex.printStackTrace();
          System.out.println("Warte weiterhin auf Anfragen ...");
        }
        return answer;
    }
    
    // The first interface function, done with the recent one ...
    public String RMICalculate(int year)
    {
        return RMICalculate("EasterCalculator", year);
    } 
    
    public static void main(String args[])
    {
        try
        {
            RMIDateServer rmiServer=new RMIDateServer();
            /* 
             * change the service name to 'DateCalculator' 
             */
            Naming.rebind("DateCalculator", rmiServer);
            System.out.println("RMIServer gestartet mit Server-Algorithmen");
        }
        catch (Exception ex)
        {
            System.out.println("EXCEPTION beendet das Programm:");
            ex.printStackTrace();
        }
    }
}


Clientimplementierung

Natürlich soll nun auch die Clientapplikation etwas geändert werden, da jetzt auch nach dem Algorithmus gefragt werden soll.
Dieser soll nun als 3. Kommandozeilenparameter angegeben werden.

Nötig wäre diese Änderung jedoch nicht, wenn die alte Funktionalität dem Anwender ausreicht.
Es sollte dem Client nur die neue RMIDateCalc.class zur Verfügung gestellt werden.

Da die Funktion String RMICalculate(int a) throws RemoteException immer noch im RMI-Interface zur Verfügung steht, kann der "alte" Client weiterhin verwendet werden.

/*
 * RMIDateClient.java
 *
 * Created on 24. Januar 2006, 13:34
 */
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> [<Datumsalgorithmus>]]");
           System.exit(1);
       }
       int year=Integer.parseInt(args[0]);
       String serverURL="rmi://"+
               ((args.length>1) ? args[1] : "localhost") + "/DateCalculator";
       
       try
       {
         RMIDateCalc dcalc=(RMIDateCalc) Naming.lookup(serverURL);
         if (args.length>2) 
           System.out.println("Resultat: "+dcalc.RMICalculate(args[2], year));
         else
           System.out.println("Resultat: "+dcalc.RMICalculate(year));
       }
       catch (Exception ex)
       {
         System.out.println("EXCEPTION beendet das Programm:");
         ex.printStackTrace();
       }
   }
}


Beispielaufrufe

Um die Beispielapplikation geeignet zu testen, werden nun die unter Windows kompilierten Client-Klassendateien auf den Linux-Rechner client.example.com kopiert.
Die entsprechenden Server-Klassendateien ebenfalls auf einen Linux-Rechner (server.example.com).

Serverstart

tasin@server:~> rmiregistry &
[1] 6054
tasin@server:~> cd Server/
tasin@server:~/Server> ls
DateCalc.class          RMIDateCalc.class    WeekdayFoYCalculator.class
EasterCalculator.class  RMIDateServer.class
tasin@server:~/Server> java -Djava.rmi.server.codebase=file:///home/tasin/Server/ RMIDateServer
RMIServer gestartet mit Server-Algorithmen

Clientstart

tasin@client:~> cd Client/
tasin@client:~/Client> ls
RMIDateCalc.class  RMIDateClient.class
tasin@client:~/Client>tasin@ti1:~/Client> java RMIDateClient 2003 server
Resultat: 20.4.2003
tasin@client:~/Client> java RMIDateClient 2003 server WeekdayFoYCalculator
Resultat: Mittwoch, 1.1.2003
tasin@client:~/Client> java RMIDateClient 1500 server EasterCalculator
Resultat: nicht errechenbar
tasin@client:~/Client> java RMIDateClient 1700 server Easter
Resultat: Kann Berechnung fuer 'Easter' nicht durchgefuehren.
tasin@client:~/Client> java RMIDateClient 2006 server WeekdayFoYCalculator
Resultat: Sonntag, 1.1.2006
tasin@client:~/Client>

Serveroutput

Die oben gezeigten Clientaufrufe zeigen folgenden Output serverseitig:

tasin@server:~/Server> java -Djava.rmi.server.codebase=file:///home/tasin/Server/ RMIDateServer
RMIServer gestartet mit Server-Algorithmen
Client 10.87.21.131: Erfrage 'EasterCalculator' Jahresinfo fuer 2003
Client 10.87.21.131: Erfrage 'WeekdayFoYCalculator' Jahresinfo fuer 2003
Client 10.87.21.131: Erfrage 'EasterCalculator' Jahresinfo fuer 1500
Client 10.87.21.131: EXCEPTION bei Erzeugung des Antwortstrings:
java.lang.ClassNotFoundException: Easter
        at java.net.URLClassLoader$1.run(URLClassLoader.java:200)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:188)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:268)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:251)
        at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:319)
        at java.lang.Class.forName0(Native Method)
        at java.lang.Class.forName(Class.java:164)
        at RMIDateServer.RMICalculate(RMIDateServer.java:36)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
        at java.lang.reflect.Method.invoke(Method.java:585)
        at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:294)
        at sun.rmi.transport.Transport$1.run(Transport.java:153)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.Transport.serviceCall(Transport.java:149)
        at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:460)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:701)
        at java.lang.Thread.run(Thread.java:595)
Warte weiterhin auf Anfragen ...
Client 129.187.206.131: Erfrage 'WeekdayFoYCalculator' Jahresinfo fuer 2006

Hinweis: Sollte sich beim Algorithmus etwas ändern und dieser neukompiliert worden sein, der Server aber bereits die alte Algorithmus-Klasse durch einen Testaufruf geladen haben, so muss der Server erneut gestartet werden, denn die "alte" Klassendatei wird von der JVM zwischengespeichert.

Im nächsten Kapitel wird der Abstraktionsgrad des Server nochmals erhöht und eine Methode gezeigt den Server ordnungsgemäß abzuschalten.