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.

Poniższe ćwiczenie wykorzystuje JDK 1.6. Wersja dla JDK 1.4 jest dostępna tutaj

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() tworzymy 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 RMI. 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. Statyczna metoda LocateRegistry.getRegistry() obiektu java.rmi.registry.LocateRegistry pozwala uzystać referencje do rejestru RMI (obiekt java.rmi.registry.Registry).

    String name = args[0];
    try {
      EchoServer server = new EchoServer();
      Registry registry = LocateRegistry.getRegistry();
      registry.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 ze zdefiniowaniem uprawnien aplikacji.

Implementacja serwera Echo bez dziedziczenia z UnicastRemoteObject

Pytanie: jakie sa wady rozwiania z dziedziczeniem z UnicastRemoteObject?

  1. W odroznieniu od poprzedniego przykladu konstruktor klasy serwera nie musi rzucać wyjątkiem RemoteException
  2. Jeśli klasa nie rozszerza UnicastRemoteObject instancja serwera musi zostać zarejestrowana Java RMI runtime

    EchoServer server = new EchoServer();
    Echo stub = (Echo) UnicastRemoteObject.exportObject(server, 0);

  3. Pozostałe kroki są identyczne jak w przykładzie dziedziczenia z UnicastRemoteObject

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 host = args[0];
    String name = args[1];
    Registry registry = LocateRegistry.getRegistry(host);
    Echo echo = (Echo) registry.lookup(name);

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

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

Kompletny plik z implementacją klienta Echo: EchoClient.java ze zdefiniowaniem uprawnien aplikacji.

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

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

    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 trzy parametry: host oraz nazwę, pod jaką zarejestrowany jest serwer oraz napis, który ma być przekazany do serwera Echo

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

    Odpowiedź powinna brzmieć

    HelloWorld!

Zadanie

Prosze przerobić przyklad serwera dziedziczącego po UnicastRemoteObject na taki ktore nie potrzebuje tego dziedziczenia.

!! Ćwiczenie prosze wykonać dwójkami !! Proszę dodać do serwera drugą metodę (np. dodawanie dwóch liczb) i zaimplementować odpowiedniego klienta.


Tomasz Gubała, Marek Kasztelnik