RMI - ćwiczenie pierwsze

Implementacja klienta i serwera Echo

Celem tego podstawowego ćwiczenia jest zrozumienie sposobu projektowania i implementacji prostych aplikacji działających w oparciu o technologię RMI. Poszczególne punkty tłumaczą jak zaprojektować interfejs obiektu wywoływanego zdalnie, jak zaimplementować serwer tego obiektu oraz w jaki sposób łączyć sie z tym serwerem z programu klienta RMI.

Definicja interfejsów

Na początku definiujemy interfejs jakiego będzie używał serwer RMI. Będzie to bardzo prosta aplikacja odbijająca napis do nadawcy.

Metody, które mogą być wywoływane zdalnie, należy zdefiniować w interfejsie dziedziczącym z interfejsu (rozszerzającym interfejs) java.rmi.Remote. Co więcej, każda z tych metod musi zgłaszać wyjątek java.rmi.RemoteException z uwagi na fakt, że jej wywołanie następuje zdalnie.

public interface Echo extends Remote {
  String reply(String s) throws RemoteException;
}

Kompletny plik z definicją interfejsu: Echo.java.

Implementacja serwera Echo

W czasie implementacji serwera w technologii RMI należy pamiętać o następujących warunkach:

  1. Klasa serwera musi implementować zdefiniowany wyżej interfejs oraz dziedziczyć z klasy java.rmi.server.UnicastRemoteObject

    public class EchoServer extends UnicastRemoteObject implements Echo

  2. Konieczny jest konstruktor, nawet jeżeli nie wykonuje on niczego, ze względu na wspomnianą deklarację zgłaszania wyjątku

    public EchoServer() throws RemoteException {
    }

  3. Następnie należy zaimplementować metodę zadeklarowaną w interfejsie. Ponieważ ma być to echo, dlatego metoda po prostu zwraca bez zmian to, co otrzymała jako argument. Aby potwierdzic fakt wywołania metody wypisujemy komunikat

    public String reply(String s) {
      System.out.println("EchoServer: odebrana wiadomosc "+s);   return s;
    }

  4. Na początku metody main() musimy utworzyć obiekt SecurityManager, który będzie decydował, jakie uprawnienia będzie miała aplikacja, która jest dostępna z zewnątrz. W przypadku RMI dobrze jest skorzystać z poręcznego java.rmi.RMISecurityManager

    public static void main(String[] args) {
      System.setSecurityManager(new RMISecurityManager());
    ...

  5. Po utworzeniu obiektu serwera, musimy go wpisac do rejestru (w Javie nazywanego Naming Service). Obiekt w rejestrze zawsze figuruje pod pewną nazwą - podajemy ją w tym wypadku w linii poleceń. Pod taką nazwą klient będzie mógł uzyskać referencję do obiektu serwera. Obiekt java.rmi.Naming w poniższym przykładzie jest obiektem statycznym i oznacza rejestr znajdujący się na tym samym komputerze, co aplikacja.

    String name = args[0];
    try {
      EchoServer server = new EchoServer();
      Naming.rebind(name, server);
      System.out.println("EchoServer: zarejestrowalem sie");
    } catch (Exception e) {
      System.err.println("EchoServer wyjatek: " + e.getMessage());
      e.printStackTrace();
    }

Kompletny plik z implementacją serwera Echo: EchoServer.java.

Implementacja klienta Echo

W implementacji klienta trzeba wykonać następujące czynności:

  1. Podobnie jak w przypadku serwera zaraz na początku metody main() powołujemy do życia obiekt RMISecurityManager
  2. Wywołujemy metodę lookup aby uzyskać z rejestru referencję do obiektu serwera na podstawie nazwy (nazwę tę przekazujemy do programu klienta także z linii komend). Metoda ta zwraca nam zdalną referencję do obiektu serwera, którą możemy zrzutować na interfejs Echo (gdyż wiemy że serwer implementuje ten interfejs). Termin zdalna referencja fizycznie oznacza zwykłą, lokalną referencję, ale nie do samego obiektu serwera (który wszak znajduje sie na odległej maszynie) a do instancji stub serwera. Jak wiemy, stub serwera musi implementować ten sam interfejs Echo (przez co rzutowanie jest możliwe).

    String name = args[0];
    Echo echo = (Echo) Naming.lookup(name);

  3. Następnie możemy już wywołać metodę reply tak, jak zwykłą metodę obiektu

    System.out.println(echo.reply(args[1]));

Kompletny plik z implementacją klienta Echo: EchoClient.java.

Kompilacja

Zakładamy, że wszystkie pliki źródłowe mamy w jednym katalogu, np. /home/user/rmi/echo.

  1. Kompilujemy wszystkie trzy pliki źródłowe w zwykły sposób

    javac *.java

  2. Używamy polecenia rmic do wygenerowania stubu i skeletonu. Polecenie to automatycznie wygeneruje pliki EchoServer_Stub.java oraz EchoServer_Skeleton.java w tym samym katalogu a następnie pliki te skompiluje. Otrzymujemy EchoServer_Stub.class i EchoServer_Skeleton.class. Jeżeli chcemy oglądnąć pliki źródłowe generowane przez ten kompilator należy użyć opcji -keep. Uwaga! Należy pamiętać aby użyć narzędzia z tej samej dystrybucji Java SDK z jakiej pochodzi użyty kompilator javac.

    rmic EchoServer
    which javac
        /usr/java/j2sdk1.4.0_02/bin/javac
    /usr/java/j2sdk1.4.0_02/bin/rmic EchoServer

Uruchamianie

  1. Przed uruchmieniem serwera należy zdefiniować uprawnienia, które będą przysługiwały aplikacji. Ponieważ aplikacja ta ma korzystać z sieci, dlatego musi miec prawo do otwierania portów (w szczególności portu 1099 którego domyślnie używa RMI). Dodatkowo dostępny port 80 (web server) będzie potrzebny przy kolejnych ćwiczeniach dotyczących dynamicznego pobierania klas z sieci. Zapisujemy to w pliku java.policy w następujący sposób

    grant {
      permission java.net.SocketPermission "*:1024-65535", "connect,accept";
      permission java.net.SocketPermission "*:80", "connect";
    };

  2. W osobnym shellu uruchamiamy Naming Service (rejestr), poleceniem

    rmiregistry
    which javac
        /usr/java/j2sdk1.4.0_02/bin/javac
    /usr/java/j2sdk1.4.0_02/bin/rmiregistry

    Uwaga! Jezeli otrzymamy błąd w rodzaju: java.rmi.server.ExportException: Port already in use: 1099 oznacza to, że rejestr jest już uruchomiony na tym komputerze i nie trzeba go powtórnie uruchamiać.
  3. Uruchomienie serwera wykonujemy jako aplikację javy, podając dodatkowe opcje Jako parametr wywołania podajemy również nazwę, pod jaką chcemy zarejestrować nasz obiekt w Name Service (np. echo123)

    java -Djava.rmi.server.codebase=file:`pwd`/ -Djava.security.policy=java.policy EchoServer echo123

  4. W osobnym terminalu (konsoli) wykonujemy aplikację klienta. Przekazujemy jej dwa parametry: nazwę, pod jaką zarejestrowany jest serwer oraz napis, który ma być przekazany do serwera Echo

    java -Djava.security.policy=java.policy EchoClient echo123 HelloWorld!

    Odpowiedź powinna brzmieć

    HelloWorld!

Zadanie

Proszę dodać do serwera drugą metodę (np. dodawanie dwóch liczb) i zaimplementować odpowiedniego klienta.


Tomasz Gubała