11. RMI - programowanie rozproszone


Wykład jest poświęcony mechanizmowi Zdalnego Wywoływania Metod (Remote Method Invocation - RMI) umożliwiającemu programowanie rozproszone w Javie. Zobaczymy jak wywoływać metody z obiektów egzystujących na różnych maszynach wirtualnych, które mogą wykonywać się na różnych komputerach połączonych siecią. RMI umożliwia również łatwe tworzenie oprogramowania w technologii klient-serwer, nie wymagające wdawania się w zawiłości programowania sieciowego.

1. Wprowadzenie

Jesteśmy przyzwyczajeni do sytuacji, kiedy program napisany w Javie wykonuje się na jednej maszynie wirtualnej. Tak jednak być nie musi. Możliwe jest wykonanie fragmentów programu w obrębie różnych maszyn wirtualnych, działających na różnych komputerach (połączonych siecią), być może pod kontrolą różnych systemów operacyjnych. Typowym przykładem takiej sytuacji jest program napisany w technologii klient-serwer, gdzie część kodu (np. odpowiedzialna za interakcję z użytkownikiem) wykonywana jest po stronie klienta, natomiast część wykonująca zadania wymagające dostępu do określonego zasobu (baza danych, duża moc obliczeniowa) wykonuje się po stronie serwera, działającego z reguły na innym komputerze.

Zapewnienie właściwej komunikacji pomiędzy częściami programów wykonującymi się na różnych maszynach (wymiana danych) jest dla programisty dość uciążliwe przy wykorzystaniu tradycyjnych metod komunikacji (np. gniazdka), a jednocześnie wymaga wielokrotnego powtarzania stałych fragmentów kodu, związanych z niskopoziomową obsługą sieci. Aby ukryć przed programistą sieciowy aspekt programowania rozproszonego, zaprojektowano mechanizm RMI (Remote Method Invocation), czyli zdalne wywoływanie metod. Pozwala on na wywoływanie metod z obiektów będących pod kontrolą różnych maszyn wirtualnych Javy, mogących działać na różnych komputerach. Jak zobaczymy, samo wywołanie metody z odległego obiektu nie różni się prawie niczym od wywołania z obiektu lokalnego. Niezbędne są jednak pewne czynności przygotowawcze, które są wykonywane jednokrotnie.

1.1. Programowanie rozproszone


architektura rozproszona

Program rozproszony może wykonywać się na różnych komputerach. To, co różni go od programu typu klient-serwer, to fakt, że żadna jego część nie jest wyróżniona - nie pełni roli nadrzędnej (serwera) w stosunku do pozostałych. RMI pozwala na tworzenie programów rozproszonych, zapewniając komunikację pomiędzy obiektami składającymi się na program bez potrzeby wprowadzania centralnego kontrolera istniejących obiektów (rozproszone odśmiecanie!). Oczywiście, świetnie się nadaje również do tworzenia programów w technologii klient-serwer, gdzie jeden lub więcej obiektów pełni rolę serwera. Sposób wykorzystania zależy więc wyłącznie od programisty.

W odniesieniu do obiektów komunikujących się za pośrednictwem RMI czasem stosuje się terminy klient i serwer nawet, jeśli program jest rozproszony (tzn. nie ma wyróżnionej jednostki centralnej). Serwer oznacza wtedy obiekt, z którego wywołano metodę. Klient jest tym, który ją wywołuje.


architektura klient-serwer

Mając w pamięci powyższą uwagę, unikniemy nieporozumień związanych z przeplataniem się dwóch, wydawałoby się sprzecznych pojęć (klient-serwer, rozproszenie). Tym bardziej, że ten sam obiekt może jednocześnie pełnić rolę serwera i klienta - na przykład, kiedy w wywołanej na jego rzecz metodzie musi odwołać się do nadawcy komunikatu (obiektu wołającego metodę), w celu uzyskania dodatkowych informacji poprzez wywołanie z niego metody (tzw. wywołanie zwrotne, callback).

1.2. Elementy RMI

Oczywiście program rozproszony może składać się z wielu obiektów i mogą one się znajdować na wielu maszynach. W każdym przypadku zasady są takie same.

Typowe zastosowanie RMI sprowadza się do umożliwienia obiektowi klienta wywołania metody z obiektu serwera działającego na innej maszynie. W ramach wywołania metody należy przekazać argumenty z klienta do serwera, wstrzymać wykonywanie wątku klienta do momentu zakończenia wywołania i odebrać wynik. Jeśli argumenty lub rezultat nie są typów pierwotnych, może to wymagać przesłania obiektów poprzez sieć. Tym trudniejsze jest to w sytuacji, gdy klasa takiego obiektu nie jest zdefiniowana jednocześnie po stronie klienta i serwera (trzeba wtedy przesłać jej definicję).
Na szczęście nie musimy się martwić o takie szczegóły, ponieważ do tego zostało stworzone RMI.

Obiekt zdalny to obiekt, którego klasa implementuje interfejs Remote. Obiekty zdalne zwykle pełnią rolę serwerów udostępniając klientom jakieś usługi.

Obiektem zdalnym posługujemy się w programie rozproszonym tak samo jak każdym innym: poprzez odniesienie do niego. Zatem kod programu korzystającego ze zdalnego obiektu nie różni się niczym (poza czynnościami inicjującymi i wyjątkami) od programu wykonywanego w całości na jednej maszynie wirtualnej. Taki cel przyświecał projektantom RMI.

Pojawiają się jednak dodatkowe elementy, nieobecne w zwykłych programach, które pobieżnie tu nakreślimy, a szczegółowo omówimy dalej.

Rejestr

Rejestr RMI nie ma nic wspólnego z rejestrem w systemach Windows. Zbieżność nazw jest przypadkowa.

W jaki sposób uzyskać odniesienie do zdalnego obiektu? Trzeba go pobrać z rejestru, w którym jest zapamiętany pod określoną nazwą.

Rejestr jest bazą danych przechowującą odniesienia do zdalnych obiektów i ich nazwy, pod którymi zostały zarejestrowane. Chcąc uzyskać odniesienie do zdalnego obiektu, musimy podać jego nazwę.

rejestr przechowuje odniesienia
do obiektów zdalnych

Wynika z tego również, że chcąc udostępnić jakiś obiekt dla wywołań zdalnych, musimy go najpierw umieścić w rejestrze pod jakąś nazwą.

Interfejsy

Zdalnym obiektem posługujemy się za pomocą odniesienia. Jednak deklarowanym typem takiego odniesienia nie jest klasa tego obiektu (ani nadklasa), lecz określony interfejs, który ta klasa implementuje.

Obiektami zdalnymi posługujemy się za pośrednictwem interfejsów.

Nie może to być dowolny interfejs - musi rozszerzać java.rmi.Remote. Oczywiście musi on również deklarować metody, które chcemy wywoływać. Przy okazji ukrywa przed klientem szczegóły implementacji klasy zdalnego obiektu.

namiastka pośredniczy w odwołaniach
do obiektu zdalnego

Namiastki

Co prawda, szczegóły komunikacji sieciowej są przed programistą ukryte, jednak musi się on zatroszczyć o przygotowanie kodu, który będzie pełnić rolę łącznika (proxy) pomiędzy obiektem lokalnym i zdalnym.

Namiastka (stub) jest obiektem pośredniczącym w wywołaniach metod z obiektu zdalnego.
Do wygenerowania klasy namiastki służy program rmic.

Klasa namiastki jest przesyłana przez sieć do klienta. Obiekt namiastki jest tworzony automatycznie i reprezentuje lokalnie obiekt zdalny.

Zarządca bezpieczeństwa

Maszyna wirtualna Javy może ładować wyłącznie klasy lokalne (znajdujące się na ścieżce poszukiwań klas classpath). Aby umożliwić ładowanie klas z sieci, trzeba zainstalować zarządcę bezpieczeństwa (SecurityManager), który będzie czuwał nad wykonywaniem załadowanego kodu. Nie dotyczy to apletów, które posługują się zarządcą dostarczonym przez przeglądarkę.

Przesyłanie klas

Jeśli program rozproszony wykonuje się na maszynach wirtualnych uruchomionych na różnych komputerach, to niezbędne jest przesyłanie danych poprzez sieć. W tym celu RMI wykorzystuje protokół HTTP lub FTP.

1.3. Typowy schemat postępowania

Projektując aplikację używającą RMI należy kierować się niżej przedstawionym schematem postępowania. Poszczególne kroki zostaną szerzej opisane w dalszej części, a teraz zostanie pokazana ich kolejność.

Przygotowanie interfejsów

Dostęp do zdalnego obiektu odbywa się za pośrednictwem (odniesienia do) interfejsu, który definiuje zachowanie się obiektu. Interfejs ten musi być dostępny po stronie klienta (na jgo ścieżce poszukiwań klas), aby mógł on wywoływać metody serwera. Musi on też być znany po stronie serwera, ponieważ jego klasa implementuje go.

Zatem pierwszą czynnością jest przygotowanie interfejsów dla wszystkich zdalnych obiektów, które będą używane po stronie klienta. Trzeba przy tym pamiętać, że: Kiedy są gotowe wszystkie potrzebne interfejsy, można przystąpić do implementacji klas serwera i klienta. Te czynności są całkowicie niezależne: klasa klienta może zostać stworzona w momencie, kiedy mamy już gotowy (i nawet działający) serwer oparty na zdalnym obiekcie. A wszystko dzięki temu, że usługi udostępniane przez zdalny obiekt w postaci metod zostały zdefiniowane w interfejsie.

Przygotowanie zdalnego obiektu

Oprócz implementacji metod zadeklarowanych w interfejsie i ewentualnych metod pomocniczych - co składa się po prostu na implementację klasy serwera - w metodzie startowej main(..) trzeba umieścić instrukcje, które:

  1. zainstalują zarządcę bezpieczeństwa
  2. utworzą eksportowane obiekty
  3. zgłoszą je w rejestrze, aby były dostępne dla innych VM

Po skompilowaniu kodu serwera jest on gotowy do działania. Zanim jednak zostanie uruchomiony trzeba jeszcze:

  1. wygenerować namiastki klas obiektów zdalnych
  2. utworzyć rejestr
Po tych czynnościach serwer jest gotowy do uruchomienia.

Przygotowanie klienta

Implementując kod odwołujący się do zdalnych obiektów, w metodzie startowej main(...) programu musimy umieścić instrukcje, które:

  1. zainstalują zarządcę bezpieczeństwa
  2. pobiorą odniesienie do zdalnego obiektu z rejestru
Pozostałą część kodu tworzymy w zasadzie tak, jakby wszystko wykonywało się lokalnie.

Przygotowanie sieci

Klient musi się komunikować z serwerem poprzez sieć opartą na protokole TCP/IP. Do przesyłania obiektów służy protokół HTTP lub FTP (lokalnie dodatkowo FILE). Oznacza to, że w systemie musi być zainstalowany serwer dla którejś z tych usług. Oczywiście, musi on zostać uruchomiony zanim pierwszy klient nawiąże komunikację z obiektem zdalnym.


2. Bardzo prosty przykład

Nasz pierwszy przykład będzie rozproszoną wersją programu HelloWorld. Klient będzie wywoływał metodę ze zdalnego obiektu, która wyprowadzi na konsolę napis. Oczywiście, strumień wyjściowy będzie znajdował się w maszynie wirtualnej serwera, więc u niego pojawi się napis, a nie u wołającego.

Dla prostoty przyjmiemy, że obiekt zdalny będzie ulokowany na tym samym komputerze, co obiekt lokalny, ale będzie się wykonywał w obrębie innej maszyny wirtualnej. Pozwoli to zrezygnować z przesyłania danych poprzez sieć przy pomocy zewnętrznych serwisów.

Jeśli zarówno klient jak i serwer są uruchamiane z tego samego katalogu, to nie trzeba dostarczać serwera (HTTP lub FTP) do przesyłania plików z klasami.

A w związku z tym nie jest również konieczne używanie zarządcy bezpieczeństwa.

2.1. Interfejs

Obiekt zdalny będzie implementował interfejs Hello. Deklaruje on jedną metodę: hello(), której zadaniem jest wyprowadzenie na konsolę napisu HelloWorld.

import java.rmi.*;

public interface Hello 
  extends Remote {

  void hello() throws RemoteException;
}

Projektując zdalny interfejs należy przestrzegać następujących reguł:

Interfejs Remote nie deklaruje żadnych metod. Stanowi jedynie informację, że obiekt klasy implementującej może być obiektem zdalnym. Wyjątek RemoteException jest zgłaszany przez maszynę wirtualną, jeśli nastąpił błąd w wywołaniu związany z wewnętrznymi mechanizmami RMI.

2.2. Klasa serwera

W klasie obiektu zdalnego, która będzie implementować interfejs Hello, umieścimy definicję metody hello(). Oprócz niej i startowej metody main(...) trzeba jeszcze zdefiniować konstruktor, który będzie deklarował zgłaszanie wyjątku RemoteException. Jest to konieczne nawet, jeśli ciało konstruktora będzie puste (a więc taki sam zostałby wygenerowany przez kompilator), ponieważ istotna jest tu deklaracja zgłaszanego wyjątku (który może zostać zgłoszony na skutek operacji sieciowych).

import java.rmi.*;
import java.rmi.registry.*;
import java.rmi.server.*;

public class HelloWorld
  extends UnicastRemoteObject 
  implements Hello {

  public HelloWorld() throws RemoteException {
  }

  public void hello() throws RemoteException { 
    System.out.println("Hello World");
  }

  public static void main(String[] args){
    try {
      HelloWorld hw = new HelloWorld();
      Naming.bind("hello", hw);
    }
    catch(Exception e){
      System.err.println(e);
    }
  }
}

Jeśli nie jest możliwe, by klasa zdalna dziedziczyła UnicastRemoteObject (bo już dziedziczy inną), to jej obiekt należy wyeksportować statyczną metodą exportObject() klasy UnicastRemoteObject.

Klasa obiektu zdalnego musi dziedziczyć z java.rmi.server.UnicastRemoteObject, która dostarcza kilku niezbędnych metod. Jej konstruktor powoduje wyeksportowanie obiektu zdalnego, czyli umożliwienie mu odbierania żądań z sieci.

W metodzie main() tworzymy jeden obiekt i zapamiętujemy go w rejestrze pod nazwą podaną jako pierwszy argument metody bind() z klasy java.rmi.Naming. Ta nazwa posłuży później do uzyskania odniesienia do tego obiektu (a właściwie do jego namiastki) klientowi.

Podsumowując: implementacja klasy zdalnej musi spełniać następujące warunki:

Jeśli program ma być wykonywany na różnych komputerach i nie jest apletem, to metoda main() musi ponadto instalować zarządcę bezpieczeństwa.

Pierwsze dwa warunki są obowiązkowe. Pozostałe można zrealizować również w inny sposób (np. metoda startowa może znajdować się w innej klasie).

2.3. Klasa klienta

Klient ma jedynie za zadanie wywołać metodę hello() ze zdalnego obiektu. W tym celu trzeba pobrać z rejestru odniesienie do niego metodą lookup() z klasy Naming, podając jako argument nazwę, pod jaką został ten obiekt zarejestrowany. Posługując się tym odniesieniem wywołujemy metodę z obiektu zdalnego tak jak z lokalnego. Obie wywoływane metody muszą zostać umieszczone w bloku try-catch, ponieważ mogą zgłaszać różnego typu wyjątki.

import java.rmi.*;

public class Greet {

  public static void main(String[] args){
    try {
      Hello h = (Hello)Naming.lookup("hello");
      h.hello();
    }
    catch(Exception e){
      System.err.println(e);
    }
  }
}

Jak widać, w porównaniu z programem w całości wykonującym się na jednej maszynie wirtualnej, pojawił się tu tylko jeden nowy element - uzyskiwanie odniesienia do obiektu zdalnego. Niewidoczne na razie subtelności związane z przekazywaniem argumentów do metod będą pokazane dalej.

2.4. Uruchomienie programu

Najpierw kompilujemy pliki źródłowe z definicjami klas.

javac Hello.java
javac HelloWorld.java
javac Greet.java
Następnie generujemy namiastkę klasy zdalnej:
rmic -v1.2 HelloWorld
Spowoduje to powstanie pliku HelloWorld_Stub.class.

Konsola to wiersz poleceń trybu ms-dos lub program cmd.exe na platformie Win32, a pod Linuksem zwykły shell (powłoka).

Teraz wszystko jest gotowe do uruchomienia programu. W katalogu bieżącym powinny znajdować się pliki: Greet.class, Hello.class, HelloWorld.class i HelloWorld_Stub.class. Potrzebne będą trzy konsole, na których uruchamiamy programy w podanej kolejności:

  1. rejestr: rmiregistry
  2. serwer: java HelloWorld
  3. klient: java Greet

Każdorazowe wykonanie programu klienta powinno spowodować wyprowadzenie napisu "Hello World" na konsoli serwera.

W tym przykładzie wszystkie programy (rejestr, serwer i klient) muszą być uruchamiane z tego samego katalogu.

Możliwe błędy

Jeśli rejestr nie został zainstalowany, próba uruchomienia serwera lub klienta spowoduje następujący błąd:

java.rmi.ConnectException: Connection refused to host 127.0.0.1; nested exception is:
        java.net.ConnectException: Connection refused

Jeśli w trakcie działania programu rejestru zakończymy serwer, a następnie uruchomimy go ponownie okaże się, że nazwa naszego obiektu zdalnego jest już przydzielona:

java.rmi.AlreadyBoundException: hello
Trzeba wtedy zamknąć rejestr (zakończyć program rmiregistry) i uruchomić go ponownie. Alternatywą jest użycie metody rebind() zamiast bind() w metodzie main() serwera.

Jeśli zapomnieliśmy wygenerować klasę namiastki, albo jest ona niewidoczna (np. znajduje się w innym katalogu), uruchamiając serwer zobaczymy komunikat:

java.rmi.StubNotFoundException: Stub class not found: HelloWorld_Stub; nested exception is:
        java.lang.ClassNotfoundException: HelloWorld_Stub


3. Narzędzia RMI

Wraz z SDK Javy dostarczany jest zestaw programów narzędziowych potrzebnych do pracy z RMI. Omówimy tu najważniejsze z nich: kompilator rmic oraz program udostępniający rejestr rmiregistry. Znajdują się one w katalogu bin dystrybucji SDK, tam gdzie kompilator javac i interpreter java. Jeśli zmienna środowiskowa PATH jest ustawiona właściwie, można je uruchamiać bezpośrednio z poziomu powłoki.

3.1. Kompilator rmic

Klasa musi być widoczna na ścieżce CLASSPATH, której domyślną wartością jest katalog bieżący ".", i którą można zmienić opcją -classpath.

Program rmic służy do wygenerowania klasy namiastki obiektu zdalnego. Jako argument należy podać pełną kwalifikowaną nazwę klasy (tzn. z nazwą ew. pakietu). Dla klasy Klasa (umieszczonej w pliku Klasa.class) zostanie wygenerowana klasa namiastki o nazwie Klasa_Stub w pliku Klasa_Stub.class, który zostanie umieszczony w tym samym katalogu, co klasa źródłowa.

Klasa przekazana jako argument kompilatorowi rmic musi implementować interfejs java.rmi.Remote, a więc musi być klasą reprezentującą obiekty zdalne. Oczywiście musi ona się bezbłędnie kompilować zwykłym kompilatorem javac.

W wersji RMI 1.1 oprócz namiastki potrzebny był jeszcze szkielet klasy obiektu zdalnego, który również generuje program rmic. Ponieważ w aktualnej wersji 1.2 nie jest on już potrzebny, to aby uniknąć jego utworzenia należy podać opcję -v1.2.

Klasa namiastki implementuje te same interfejsy, co klasa obiektu zdalnego. W związku z tym można się nią posługiwać tak samo, jak obiektem zdalnym (mając w pamięci, że obiektami zdalnymi posługujemy się za pomocą interfejsów). Obiekt tej klasy, czyli namiastka, jest tworzony automatycznie i pełni rolę łącznika (proxy) pomiędzy obiektem lokalnym (klientem) a obiektem zdalnym (serwerem). Namiastka rezyduje w maszynie wirtualnej klienta - jest tam przesłana automatycznie podczas pobierania odniesienia do obiektu zdalnego z rejestru. Sam obiekt zdalny nigdy nie opuszcza maszyny wirtualnej, na której został utworzony (nawet, jeśli jest przekazywany jako argument lub wynik metody poprzez sieć - zastępuje go wtedy namiastka).

Odniesienie do obiektu zdalnego w maszynie wirtualnej klienta jest tak naprawdę odniesieniem do jego namiastki. Ponieważ klasa namiastki implementuje te same interfejsy, co obiekt zdalny, można wywoływać z niej metody obiektu zdalnego, które on udostępnił światu. Wywołanie takiej metody powoduje przygotowanie argumentów do przesłania przez sieć, przekazanie ich do obiektu zdalnego, odebranie wyniku i przekazanie go w miejsce wywołania.

3.2. Rejestr

Rejestr przechowuje odniesienia do obiektów zdalnych, czyli implementujących Remote, które żyją na (jakiejkolwiek) maszynie wirtualnej uruchomionej na tym samym komputerze, co rejestr.

Ze względów bezpieczeństwa nie można przechowywać w rejestrze odniesień do obiektów zdalnych egzystujących na innych komputerach.

Każdy obiekt zdalny, którego odniesienie znajduje się w rejestrze, jest skojarzony z jego unikalną nazwą, podaną podczas rejestracji. Nazwa ta posłuży do pobrania odniesienia przez klienta, a zatem musi on ją znać (jak również nazwę sieciową komputera, na którym się znajduje). Z reguły nazwa ta jest taka sama, jak nazwa klasy obiektu zdalnego.

Aby umieścić w rejestrze obiekt zdalny trzeba go zarejestrować. Służą do tego statyczne metody klasy java.rmi.Naming:

static void bind(String name, Remote obj);
static void rebind(String name, Remote obj);
Rejestrują one obiekt obj pod nazwą name. Jeśli podana nazwa jest już w użyciu, to pierwsza zgłasza wyjątek AlreadyBoundException, a druga zastępuje stary obiekt nowym. Metody te mogą ponadto zgłosić wyjątek RemoteException (jak zwykle przy pracy z obiektami zdalnymi), AccessException przy braku dostępu do rejestru (np. próba rejestracji z innej maszyny) lub MalformedURLException jeśli format łańcucha name jest niepoprawny. A powinien on mieć następującą postać:
"//host:port/name"
Przy czym część ":port" jest opcjonalna i określa numer portu (patrz niżej). Część "host" określa komputer, na którym jest zlokalizowany rejestr. Dla powyższych metod musi to być "localhost", więc ma ona znaczenie tylko przy pobieraniu odniesienia metodą lookup(String name) (format łańcucha identyfikującego będzie taki sam). Jeśli nie podano części "host", to domyślną jej wartością będzie "localhost". Część "name" jest właściwą nazwą obiektu.

Obiekt, który nie jest już potrzebny można usunąć z rejestru metodą

static void unbind(String name);
Jeśli podana nazwa name nie była zarejestrowana, to zostanie zgłoszony wyjątek NotBoundException. Pozostałe wyjątki są takie jak przy metodzie rebind().

Do pobierania odniesień z rejestru służy metoda klasy Naming:

static Remote lookup(String name);
Łańcuch name ma wyżej opisany format i jeśli rejestr (a tym samym zdalny obiekt) jest zlokalizowany na innej maszynie, to name musi zawierać nazwę hosta (lub jego adres IP). Zestaw zgłaszanych wyjątków jest identyczny jak przy metodzie unbind().

Rejestru z reguły używa się do pobrania odniesienia do pierwszego zdalnego obiektu w programie. Ewentualne dalsze odniesienia uzyskuje się poprzez wywołania metod z pierwszego obiektu.

Rejestr jest usługą sieciową (tak jak FTP, WWW czy SSH), która oczekuje zgłoszeń na standardowym porcie 1099 TCP. Można uruchomić usługę na innym niż domyślny porcie podając jego numer jako argument wywołania programu np.:rmiregistry 2003. W związku z tym można uruchomić kilka rejestrów na jednej maszynie - muszą tylko różnić się numerami portów. Jeśli klient chce pobrać odniesienie do obiektu zdalnego z rejestru działającego na innym porcie niż domyślny, to musi podać jego numer jawnie w metodzie lookup():

Naming.lookup("//anyhost.pjwstk.edu.pl:2003/remote_service");

Rejestr przechowuje odniesienie do obiektu zdalnego, dopóki jest on używany przez jakiegoś klienta. Kontrolowaniem aktywnych odwołań (poprzez sieć) do obiektu zdalnego zajmuje się rozproszony odśmiecacz (DGC - Distributed Garbage Collector). Zdalny obiekt stworzony w jakiejś maszynie wirtualnej nie musi być w niej specjalnie podtrzymywany przy życiu (np. w osobnym wątku) aby nie utracić odniesienia do niego - zajmuje się tym właśnie rejestr.

Rejestr jest również zwykłym zdalnym obiektem Javy implementującym interfejs java.rmi.registry.Registry. Można go utworzyć metodą

static Registry createRegistry(int port);
klasy java.rmi.registry.LocateRegistry. O ile odniesienie do rejestru może być mniej przydatne, to fakt, iż można utworzyć rejestr beż użycia zewnętrznego programu warto wykorzystać (następny przykład).

3.3. Prosty przykład

Ponieważ działamy w na jednej fizycznej maszynie, nie będziemy instalować zarządcy bezpieczeństwa. Jest on wymagany tylko przy ładowaniu kodu z sieci.

W dalszym ciągu będziemy działać w obrębie jednego komputera, ale na różnych maszynach wirtualnych. Program będzie mierzył czas dostępu do obiektu zdalnego. Mimo, iż wszystko będzie działo się na jednej fizycznej maszynie, to komunikacja pomiędzy obiektem lokalnym i zdalnym będzie się odbywać przy pomocy normalnych mechanizmów RMI. Ponieważ jednak klasy będą widoczne na ścieżce poszukiwań klas, nie będziemy korzystać z zewnętrznego serwera (np. HTTP) do przesyłania ich kodu.

Przykład ilustruje:

Struktura programu

Program klienta będzie pobierał z rejestru odniesienie do zdalnego generatora serwerów typu ServerProducer, którego zadaniem będzie dostarczanie odniesień do zdalnych serwerów czasu typu TimeResponse. Obiekt generujący odniesienia będzie zarejestrowany. Natomiast obiekty, których odniesienia będzie zwracał nie będą zarejestrowane, więc nie będzie można ich pobrać z rejestru. Będą to jednak najprawdziwsze obiekty zdalne. Różnica pomiędzy nimi będzie polegać będzie jedynie na sposobie pozyskiwania odniesień.

Pobrawszy odniesienie do generatora serwerów z rejestru, klient wywoła na jego rzecz metodę zwracającą odniesienie do nowoutworzonego serwera czasu. Z serwera czasu wywołamy metodę, zwracającą bieżący czas w jego maszynie wirtualnej.

Zdalne interfejsy

Mamy dwa zdalne interfejsy:

Jak widać są one zdefiniowane w pakiecie timing.

package timing;

import java.rmi.*;

public interface ServerProducer 
  extends Remote {

  TimeResponse getServer() throws RemoteException;
}
package timing;

import java.rmi.*;

public interface TimeResponse 
    extends Remote {
    
    long getTime(long time) throws RemoteException;
}

Oba interfejsy dziedziczą z Remote, a metody deklarują zgłaszanie wyjątku RemoteException, co jest wymagane dla obiektów zdalnych.

Klasy serwera

Po stronie serwera mamy dwie klasy implementujące zdalne interfejsy. Obiekt klasy timing.server.ObjectProducer będzie służył do generowania obiektów klasy timing.server.TimeServer.

package timing.server;

import timing.*;
import java.rmi.*;
import java.rmi.registry.*;
import java.rmi.server.*;

public class ObjectProducer
  extends UnicastRemoteObject
  implements ServerProducer {

  public ObjectProducer() throws RemoteException {
  }

  public TimeResponse getServer() throws RemoteException{
    System.out.println("TimeServer created");
    return new TimeServer();
  }

  public static void main(String[] args){
    try {
      LocateRegistry.createRegistry(2003);
      ObjectProducer op = new ObjectProducer();
      Naming.bind("//localhost:2003/TimeServer", op);
    }
    catch(Exception e){
      e.printStackTrace();
    }
  }
}
Klasa ObjectProducer dziedziczy z UnicastRemoteObject aby jej obiekt był wyeksportowany. Metoda getServer() zwraca nowy serwer czasu, który będzie się wykonywał na maszynie wirtualnej serwera, ponieważ jest to obiekt zdalny. W metodzie startowej main() tworzymy rejestr wywołaniem LocateRegistry.createRegistry(2003) na porcie 2003 (a więc innym niż domyślny). Następnie tworzymy obiekt generatora i rejestrujemy go pod nazwą "TimeServer". Jako pierwszy człon nazwy podawanej metodzie bind() podajemy też koniecznie numer portu i nazwę hosta, ponieważ rejestr będzie ustanowiony na niestandardowym porcie.

package timing.server;

import java.rmi.*;
import java.rmi.registry.*;
import java.rmi.server.*;

public class TimeServer
  extends UnicastRemoteObject
  implements timing.TimeResponse {

  public TimeServer() throws RemoteException {
  }

  public long getTime(long time) throws RemoteException {
    long here = System.currentTimeMillis();
    System.out.println("server in : " + time + "\n" + 
                       "server out: " + here);
    return here;
  }
}
Klasa serwera czasu również dziedziczy UnicastRemoteObject, ponieważ jej obiekty są obiektami zdalnymi, które komunikują się z klientem poprzez namiastkę i dlatego muszą zostać wyeksportowane.

Klasa klienta

Klient w metodzie main() pobierze z rejestru odniesienie do generatora. Następnie wywoła z niego metodę produkującą zdalny serwer czasu. Mając odniesienie do serwera czasu, można wywołać z niego metodę zwracającą czas na maszynie serwera.

package timing.client;

import timing.*;
import java.rmi.*;

public class TimeClient {

  public static void main(String[] args){
    try {
      String name = "//localhost:2003/TimeServer";
      ServerProducer sp = (ServerProducer)Naming.lookup(name);
      TimeResponse ts = sp.getServer();
      long then = System.currentTimeMillis();
      System.out.println("client before : " + then);
      long retv = ts.getTime(then);
      long now  = System.currentTimeMillis();
      System.out.println("server returns: " + retv + "\n" +
                         "client after  : " + now + "\n" +
                         "calling  time : " + (now - then));
    }
    catch(Exception e){
      e.printStackTrace();
    }
  }
}

Obiekt generatora jest zarejestrowany pod nazwą "TimeServer" na porcie 2003 w maszynie lokalnej - na podstawie tych informacji budujemy argument dla metody lookup(). Po pobraniu doniesień mierzymy czas przed wywołaniem metody serwera i po, wypisując wynik na konsoli.

Kompilacja

Klasy i interfejsy są umieszczone w różnych pakietach nazwanych, dlatego należy zachować ostrożność przy podawaniu ich położeń i nazw podczas kompilacji i uruchamiania. Zakładamy, że struktura katalogów (odpowiadająca nazwom pakietów) oraz położenie klas i interfejsów wygląda następująco:

timing/
timing/ServerProducer.java
timing/TimeResponse.java
timing/server/
timing/server/ObjectProducer.java
timing/server/TimeServer.java
timing/client/
timing/client/TimeClient.java

Najpierw kompilujemy interfejsy:

javac timing/*.java
Następnie klasy serwera i klienta (w dowolnej kolejności)
javac timing/server/*.java
javac timing/client/TimeClient.java
Należy zwrócić uwagę na inny sposób podawania nazw klas podczas kompilacji i generowania namiastek: w pierwszym przypadku podajemy nazwę pliku (ale z odpowiedniego katalogu), w drugim nazwę klasy.
Nie zapominamy o wygenerowaniu klas namiastek:
rmic -v1.2 timing.server.TimeServer
rmic -v1.2 timing.server.ObjectProducer

Uruchomienie

Uwaga: niektóre systemy (np. Windows) mierzą czas z dokładnością do 10 milisekund, więc podawane tam czasy będą niedokładne.

Najpierw uruchamiamy serwer zawarty w obiekcie klasy ObjectProducer, a następnie klienta. Nie uruchamiamy programu rejestru, ponieważ jest on tworzony w kodzie serwera. Zatem wystarczą dwie konsole - jedna dla serwera a druga dla klienta.


4. Programowanie z RMI

4.1. Klasa obiektu zdalnego

Obiekt zdalny to obiekt implementujący interfejs Remote (być może pośrednio). Z takiego obiektu można wywoływać metody zdalnie - z innej maszyny wirtualnej - ale tylko te, które zostały zadeklarowane w jakimś interfejsie zdalnym, implementowanym przez klasę obiektu.

Interfejs jest zdalny jeśli rozszerza java.rmi.Remote.

Interfejs ten nie deklaruje żadnych metod ani stałych. Jest jednynie znacznikiem informującym, że metody interfejsów dziedziczących go mogą być wywoływane zdalnie. W szczególności oznacza to, że muszą one deklarować zgłaszanie wyjątku RemoteException.

Jednak sam fakt implementowania interfejsu Remote nie sprawia, że do takiego obiektu można uzyskać zdalny dostęp. Musi on jeszcze zostać wyeksportowany, czyli stać się zdolnym do odbierania zgłoszeń od klientów (polegających na wywoływaniu metod). Obiekt zdalny, który został wyeksportowany nie będzie opuszczał maszyny wirtualnej, na której został stworzony. Komunikacja z nim dokonuje się za pośrednictwem namiastki, którą należy wygenerować.

Jeśli chcemy ponadto umożliwić dostęp do takiego obiektu przez nazwę, należy go zarejestrować. Nie jest to konieczne, co widzieliśmy w poprzednim przykładzie. Odniesienie do niego mogą zwracać również jakieś metody. Należy pamiętać, że w maszynie klienta odniesienie takie będzie związane z namiastką.

Do wyeksportowania obiektu zdalnego służy statyczna metoda klasy UnicastRemoteObject (z pakietu java.rmi.server)

public static RemoteStub exportObject(Remote obj) throws RemoteException;

Jest ona wywoływana w konstruktorze klasy UnicastRemoteObject, zatem dziedzicząc ją załatwiamy automatycznie eksportowanie naszych obiektów.

Na przykład implementacja metody hashCode() musi zapewnić, że odniesienia do różnych namiastek reprezentujących ten sam obiekt zdalny muszą mieć taką samą wartość hashCode(). Szczegóły są opisane w dokumentacji klasy RemoteObject, która jest nadklasą UnicastRemoteObject i dostarcza ich implementacji.

Co jednak zrobić w sytuacji, gdy klasa obiektu zdalnego musi dziedziczyć z jakiejś innej klasy ze względów projektowych? W Javie nie ma wielodziedziczenia, a więc funkcjonalność dostarczaną przez UnicastRemoteObject musimy zaimplementować sami. Po pierwsze będzie to wywołanie metody exportObject(), która spowoduje wyeksportowanie obiektu zdalnego. Po drugie należy odpowiednio przedefiniować metody hashCode(), equals() i toString() z klasy Object.

4.2. Przekazywanie argumentów i rezultatów

W Javie argumenty typów pierwotnych są przekazywane do metod przez wartość - metoda otrzymuje ich kopie. Obiekty są przekazywane przez odniesienie - kopiowana jest wartość samego odniesienia, lecz obiekt, do którego się odnosi pozostaje ten sam. Programowanie przy użyciu RMI trochę komplikuje ten model. Do powyższych sposobów przekazywania argumentów dochodzi trzeci: przekazywanie przez wartość obiektów.

Jak to zostało powiedziane wcześniej, obiekty zdalne nigdy nie opuszczają maszyny wirtualnej. Odniesienia do nich mogą jednak być przekazywane jako argumenty i rezultaty metod, również pomiędzy maszynami wirtualnymi. Semantyka przekazywania takich obiektów jest taka, jak zwykłych obiektów Javy.

Wyeksportowane obiekty zdalne są przekazywane przez odniesienie.

Jeśli obiekt zdalny nie został wyeksportowany, to jest traktowany jak zwykły obiekt. Przekazywanie takich obiektów pomiędzy różnymi maszynami wirtualnymi wymaga tworzenia ich kopii, które będą przesłane do innej VM. Do ich przesyłania wykorzystywany jest mechanizm serializacji.

Pozostałe obiekty są przekazywane przez wartość, o ile ich klasy implementują interfejs java.io.Serializable.

Jeśli zapomnimy wyeksportować obiekt zdalny, to przy próbie przekazania go jako rezultat bądź parametr, RMI będzie próbowało dokonać jego serializacji (aby go przekazać przez wartość). Jeśli klasa tego obiektu nie implementuje interfejsu Serializable, to zostanie zgłoszony wyjątek.

4.3. Przykład: przekazywanie obiektów

Przedstawimy teraz przykład demonstrujący omówione mechanizmy przekazywania obiektów. Jest on dość zawiły, ponieważ materia, którą ilustruje nie jest banalna, choć może się na pierwszy rzut oka taką wydawać. Mimo że problem przekazywania argumentów można sformułować w dwóch zdaniach, to jednak uruchamianie programów korzystających z RMI obfituje w rozmaite niespodzianki. Aby sobie z nimi poradzić, trzeba dogłębnie rozumieć nie tylko omawiane zasady rządzące Javą czy RMI, ale też zagadnienia związane z komunikacją w sieciach opartych na protokole TCP/IP.

Budowa

Program będzie składał się z dwóch głównych części:

Serwer jest obiektem klasy RemoteContainer implementującej zdalny interfejs Container. Interfejs będzie reprezentował obiekt serwera po stronie klienta. Klient jest obiektem klasy ParameterDemo. Serwer będzie przechowywał trzy rodzaje obiektów; każdy z nich będzie ilustrował inny sposób przekazywania argumentów: Powyższe klasy mają takie same metody (setValue() i getValue()). Są one również zadeklarowane w interfejsie Element, ale tylko pierwsza go implementuje. Obiekty powyższych klas będą przechowywać jakieś wartości, które będą modyfikowane w trakcie działania programu. Dwie pierwsze będą przechowywać jako atrybut łańcuch znakowy, trzecia - obiekty tych klas (RemoteElement i LocalElement).

Kompilacja i uruchomienie

W celu uruchomienia programu, trzeba go oczywiście

  1. skompilować: javac *.java
  2. wygenerować namiastki klas zdalnych RemoteContainer i RemoteElement:
    rmic -v1.2 RemoteContainer RemoteElement

Program klienta można uruchomić tylko raz, ponieważ po jego zakończeniu na serwerze znajduje się inny obiekt niż przed rozpoczęciem. Przed ponownym uruchomieniem klienta trzeba zakończyć i jeszcze raz uruchomić program serwera.

Następnie, z trzech różnych sesji konsolowych pracujących w tym samym katalogu uruchamiamy kolejno:
  1. rejestr: rmiregistry
  2. program serwera: java RemoteContainer
  3. program klienta: java ParameterDemo

Strona klienta - wstęp

Klasa klienta ParameterDemo jest prosta - składa się tylko z metody main(). Jej kod jest podzielony na trzy części, które zostaną omówione osobno:

  1. przekazywanie obiektów zdalnych - przez odniesienie
  2. przekazywanie obiektów lokalnych - przez wartość, czyli kopię
  3. przekazywanie obiektów złożonych - obiektów lokalnych zawierających pola zdalne i lokalne

import java.rmi.*;

public class ParameterDemo {

  public static void main(String[] args){
    Container c = null;
    try {
      c = (Container)Naming.lookup("container");
    }
    catch(Exception e){
      e.printStackTrace();
      System.exit(-1);
    }
	
   /*
    * Tu znajdują się trzy bloki kodu
    * demonstrujące techniki przekazywania
    * argumentów. Zostaną przedstawione dalej.
    */  
  }
}

Na początku pobieramy z rejestru odniesienie do zdalnego pojemnika, który został zarejestrowany pod nazwą "container". Nie podajemy nazwy hosta, ponieważ wszystko dzieje się na jednym komputerze. Jest to obiekt klasy RemoteContainer, ale dostęp do niego odbywa się poprzez odniesienie do interfejsu Container.

Strona serwera

Strona serwera jest trochę bardziej złożona. Główną rolę gra zdalny interfejs Container i implementująca go klasa RemoteContainer, która pełni rolę pojemnika na jeden obiekt. Do obiektu tej klasy będą wkładane kolejno obiekty zdalne, lokalne i złożone.

import java.rmi.*;

public interface Container
  extends Remote {

  void put(Object o) throws RemoteException;

  Object get() throws RemoteException;
}

Klasa RemoteContainer dziedziczy UnicastRemoteObject, ponieważ jej obiekt ma być wyeksportowany. Posiada ona atrybut element typu Object, którego wartość jest ustalana metodą put(), a pobierana metodą get(). Wszystkie metody oraz konstruktor deklarują zgłaszanie wyjątku RemoteException, ponieważ mogą być wywoływane zdalnie.

import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;

public class RemoteContainer
  extends UnicastRemoteObject 
  implements Container {

  private Object element;

  public RemoteContainer() throws RemoteException {
    element = new RemoteElement("aRemoteElement");
  }

  public void put(Object o) throws RemoteException {
    element = o;
  }

  public Object get() throws RemoteException {
    return element;
  }

  public static void main(String[] args){
    try {
      RemoteContainer rc = new RemoteContainer();
      Naming.rebind("container", rc);     
    } 
    catch(Exception e){
      e.printStackTrace();
    }
  }
}

W konstruktorze pole element jest inicjowane nowym obiektem klasy RemoteElement, który początkowo będzie przechowywał napis "aRemoteElement". W metodzie startowej main() odbywa się tworzenie obiektu i jego rejestracja. W przeciwieństwie do poprzednich przykładów, w celu zarejestrowania obiektu wywołuje się metodę rebind(). Jej działanie różni się od bind() tym, że jeśli pod podaną nazwą był już zarejestrowany jakiś obiekt, to zostanie on zamieniony nowym.

Zdalny element

Jednym z trzech rodzajów elementów, które będziemy przechowywać w pojemniku, są obiekty klasy RemoteElement. Implementuje ona zdalny interfejs Element, ponieważ jej obiekty będą udostępniane klientowi za pośrednictwem tego interfejsu. Metoda setValue() deklarowana w interfejsie pozwala ustalać wartość przechowywaną w obiekcie a metoda getValue() służy do jej pobrania. Wartościami tymi będą łańcuchy znakowe.

import java.rmi.*;

public interface Element 
  extends Remote {

  void setValue(String str) throws RemoteException;

  String getValue() throws RemoteException;
}

Obiekty klasy RemoteElement będą eksportowane (choć nie będą rejestrowane), więc musi ona dziedziczyć UnicastRemoteObject. Eksportowanie jest konieczne jeśli odniesienia do obiektów mają być przekazywane do innych maszyn wirtualnych, jak zwykłe obiekty Javy (w przeciwnym wypadku będą serializowane). Rejestracja nie jest konieczna, ponieważ obiekt będzie pobrany jako rezultat metody get() z pojemnika, a nie z rejestru.

import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;

public class RemoteElement
  extends UnicastRemoteObject 
  implements Element {

  private String value;

  public RemoteElement(String val) throws RemoteException {
    value = val;
  }

  public void setValue(String val) throws RemoteException {
    value = val;
  }

  public String getValue() throws RemoteException { 
    return value;
  }
}
W konstruktorze nadajemy wartość atrybutowi value typu String, który jest elementem przechowywanym w obiekcie. Będzie on zmieniany metodą setValue() przez klienta. Mimo, iż obiekty zdalne tej klasy będą przekazywane przez odniesienie, to łańcuch value będzie przekazywany jako argument metody setValue() lub rezultat metody getValue() w postaci kopii - będzie serializowany. Trzeba o tym pamiętać projektując zdalne klasy: argumenty i rezultaty metod przekazywane przez wartość muszą implementować interfejs Serializable (niespełnienie tego warunku powoduje błąd czasu wykonania). Z drugiej strony, jeśli chcemy przekazywać je przez odniesienie (jak w przykładzie klasy RemoteElement), to muszą one być obiektami zdalnymi, koniecznie wyeksportowanymi. Klasa String implementuje interfejs Serializable, ale nie implementuje Remote.

Przekazywanie obiektów zdalnych przez odniesienie

Pokażemy teraz pierwszy blok metody main() klienta demonstrujący przekazywanie rezultatu metody przez odniesienie. Oczywiście ten sam mechanizm dotyczy przekazywania argumentów metod.

Najpierw pobierzemy element przechowywany w pojemniku. Następnie zmodyfikujemy jego zawartość i jeszcze raz pobierzemy z pojemnika. Okaże się, że modyfikacja za pośrednictwem lokalnego odniesienia miała wpływ na obiekt zdalny: po ponownym pobraniu go z pojemnika zobaczymy, że obiekt został zmodyfikowany.

Oto pierwszy blok z metody main() klasy ParameterDemo:

try {
  System.out.println("\t\tTest obiektu zdalnego: przekazywanie odniesienia");
  Element e1 = (Element)c.get();
  System.out.println("pobranie elementu (e1): " + e1.getValue());
  e1.setValue("aLocalValue");
  System.out.println("lokalna modyfikacja na: " + e1.getValue());
  Element e2 = (Element)c.get();
  System.out.println("ponowne pobranie  (e2): " + e2.getValue());

  System.out.println("porównanie e1 == e2 ma wartość " + (e1 == e2));
  System.out.println("jednak e1.equals(e2) ma wartość " + (e1.equals(e2)));
}
catch(Exception e){
  e.printStackTrace();
  System.exit(-1);
}

Metoda get() wywołana na rzecz obiektu klasy RemoteContainer (reprezentowanego po stronie klienta przez odniesienie typu Container) oznaczanego zmienną c zwraca przechowywany w nim obiekt jako rezultat typu Object. Początkowa zawartość pojemnika to obiekt klasy RemoteElement zawierający napis "aRemoteElement" - tworzy go konstruktor klasy RemoteContainer. Zatem pierwsze wywołanie metody get() powinno go zwrócić. Ponieważ mamy do czynienia z obiektem zdalnym (RemoteElement), musimy posługiwać się odniesieniem do zdalnego interfejsu Element we wszystkich odwołaniach do tego obiektu. Dlatego zmienna e1 jest typu Element i rzutowanie wyniku metody get() jest bezpieczne (RemoteElement implementuje Element). Po wywołaniu metody setValue("aLocalValue") na rzecz obiektu e1 spodziewamy się, że został on (czyli obiekt przechowywany w pojemniku c) zmodyfikowany. I słusznie - potwierdza to kolejne wywołanie metody get(). Na zmiennej e2 mamy ten sam obiekt klasy RemoteElement przechowywany w pojemniku co na zmiennej e1.

Oto początek wydruku działania programu:

                Test obiektu zdalnego: przekazywanie odniesienia
pobranie elementu (e1): aRemoteElement
lokalna modyfikacja na: aLocalValue
ponowne pobranie  (e2): aLocalValue

W podkatalogu ParamPassing katalogu SAMPLES, gdzie znajdują się pliki omawianego programu, jest również plik LocalDemo.java demonstrujący efekt wykonania identycznych akcji na lokalnym pojemniku. Nie będziemy go tu omawiać, ponieważ ukazuje typowe zachowanie Javy, ale może się przydać do samodzielnego skonfrontowania z omawianym mechanizmem.

Wygląda na to, że wszystko jest jak w lokalnych wywołaniach w obrębie pojedynczej maszyny wirtualnej. Jednak nie do końca. Ostatnie dwie instrukcje mogą wprawić w zakłopotanie. Ponieważ e1 i e2 są odniesieniami do tego samego obiektu, więc wydawałoby się, że powinny być tożsame (ich porównanie da true). Tym bardziej wywołanie metody equals() w celu ich porównania powinno dać wynik true - mamy przecież jeden obiekt. W pierwszym przypadku tak jednak nie jest. Przyjrzyjmy się dalszej części wydruku:

porównanie e1 == e2 ma wartość false
jednak e1.equals(e2) ma wartość true

Porównanie odniesień daje wartość false, zatem musimy mieć do czynienia z fizycznie różnymi obiektami przy każdym wywołaniu metody get(). Tak jest w istocie: obiektami tymi są obiekty klasy RemoteElement_Stub - namiastki klasy RemoteElement, która reprezentuje ją po stronie klienta. Obiekty klasy namiastki rezydują w lokalnej maszynie wirtualnej i są pod kontrolą lokalnego odśmiecacza - jak zwykłe obiekty Javy. Jednak wywołanie metody na rzecz takiego obiektu skutkuje wywołaniem jej na rzecz obiektu, który reprezentuje (wraz z przekazaniem argumentów i rezultatu) - to właśnie zapewnia mechanizm RMI. Oba obiekty klasy namiastki zwrócone przez metodę get() reprezentują jeden obiekt zdalny klasy RemoteElement, dlatego porównanie metodą equals() będzie miało wartość true.

Jeśli nie chcemy dziedziczyć UnicastRemoteObject musimy właśnie w taki sposób przedefiniować metodę equals(). Ponadto trzeba przedefiniować metodę hashCode() tak, by była zgodna z equals(), oraz metodę toString(). Szczegóły w dokumentacji klas Object i RemoteObject.

Warto się jeszcze zastanowić nad przyczyną takiego zjawiska: przecież implementacja metody equals() w klasie Object działa tak, jak porównanie odniesień, a my jej nigdzie nie przedefiniowaliśmy. Otóż (jak wspomniano wcześniej) robi to abstrakcyjna klasa java.rmi.RemoteObject, która jest nadklasą klasy UnicastRemoteObject, którą z kolei powinny dziedziczyć eksportowane obiekty zdalne. Przedefiniowuje ona metodę equals() tak, by jej działanie było zgodne z mechanizmami RMI: porównanie różnych reprezentantów tego samego obiektu zdalnego ma dać wartość true.

Przekazywanie obiektów przez wartość

Demonstrując przekazywanie obiektów przez odniesienie, użyliśmy zdalnego obiektu, który został utworzony w maszynie wirtualnej serwera i jej nie opuszczał (po stronie klienta był on reprezentowany przez obiekty namiastek). Takie zachowanie wykazują bardzo specyficzne obiekty (np. dziedziczące UnicastRemoteObject). Większość używanych klas nie reprezentuje eksportowanych obiektów zdalnych, więc przekazywanie ich jako argumenty bądź rezultaty metod podlega innym prawom.

Klasą lokalną będziemy nazywać klasę, która nie jest zdalna, czyli nie implementuje interfejsu Remote. Obiekt lokalny to obiekt klasy lokalnej.
Klasą serializowalną nazywamy klasę implementującą interfejs java.io.Serializable. Obiekt serializowalny to obiekt klasy serializowalnej.

Obiekty lokalnych klas serializowalnych są przekazywane przez wartość. W maszynie źródłowej tworzona jest kopia takiego obiektu i przesyłana do maszyny docelowej przy użyciu mechanizmu serializacji. Zatem uzyskany efekt jest dokładnie taki jak w przypadku typów pierwotnych.

A co z pozostałymi obiektami, niepodpadającymi pod żadną z kategorii: Jeśli obiekt zdalny nie został wyeksportowany, to w momencie przekazania go w wywołaniu zdalnej metody podjęta zostanie próba serializacji. Jeśli się powiedzie (obiekt był serializowalny), to zostanie on przekazany przez wartość! W przeciwnym przypadku nastąpi błąd czasu wykonania polegający na zgłoszeniu wyjątku java.io.NotSerializableException. Podobnie będzie w przypadku obiektu lokalnego, który nie jest serializowalny.

W naszym przykładzie z pojemnikiem rolę lokalnej klasy serializowalnej pełni LocalElement. Jest ona bardzo podobna do klasy RemoteElement: również przechowuje łańcuch znakowy klasy String jako atrybut i posiada takie same metody: setValue() i getValue(). Nie są one jednak narzucone przez zdalny interfejs Element, bo klasa LocalElement go nie implementuje. Ponieważ nie jest to klasa zdalna, jej obiekt nie musi być reprezentowany przez interfejs w maszynie wirtualnej klienta (tym bardziej zdalny, dziedziczący Remote). Ponadto nie dziedziczy ona z UnicastRemoteObject, co uniemożliwia przekazywanie przez odniesienie, ale implementuje interfejs Serializable, co pozwala na przekazywanie przez wartość.

public class LocalElement
  implements java.io.Serializable {

  private String value;

  public LocalElement(String val) {
    value = val;
  }

  public void setValue(String val) {
    value = val;
  }

  public String getValue() { 
    return value;
  }
}

W drugim bloku z metody main() klasy ParameterDemo, demonstrującym przekazywanie przez wartość, będziemy wykonywać podobne akcje jak w pierwszym. Najpierw włożymy do pojemnika nowy obiekt klasy LocalElement (aktualnie znajduje się tam obiekt klasy RemoteElement). Następnie zmodyfikujemy go, a potem pobierzemy kopię włożoną do pojemnika.

try {
  System.out.println("\t\tTest obiektu lokalnego: przekazywanie kopii");
  LocalElement le1 = new LocalElement("aLocalElement");
  System.out.println("wstawiono do pojemnika : " + le1.getValue());
  c.put(le1);
  le1.setValue("newValue");
  System.out.println("lokalna modyfikacja na : " + le1.getValue());
  LocalElement le2 = (LocalElement)c.get();
  System.out.println("pobrany z pojemnika    : " + le2.getValue());
}
catch(Exception e){
  e.printStackTrace();
  System.exit(-1);
}

Obiekt le1, którego kopia zostanie wstawiona do pojemnika przechowuje napis "aLocalElement". Utworzeniem kopii (poprzez serializację) i przesyłaniem jej pomiędzy klientem a serwerem zajmuje się mechanizm RMI. Po wstawieniu do pojemnika modyfikujemy go metodą setValue("newValue") - od teraz nasz jedyny obiekt klasy LocalElement przechowuje napis "newValue". Następnie pobieramy z pojemnika uprzednio wstawioną kopię i sprawdzamy co zawiera. Okazuje się, że przechowuje napis "aLocalElement", który znajdował się w obiekcie le1 w momencie wstawiania!.

                Test obiektu lokalnego: przekazywanie kopii
wstawiono do pojemnika : aLocalElement
lokalna modyfikacja na : newValue
pobrany z pojemnika    : aLocalElement

Można by pomyśleć, że skoro został utworzony nowy obiekt, to musiał być w tym celu wywołany jakiś konstruktor. O tym, że tak nie jest można się przekonać np. umieszczając w konstruktorze instrukcję zwiększającą wartość statycznej zmiennej całkowitoliczbowej, która oznaczałaby liczbę utworzonych do tej pory obiektów danej klasy. Należy ją zapamiętać w obiekcie na niestatycznym atrybucie jako kolejny numer tego obiektu. Obiekty wprowadzone do maszyny wirtualnej przy użyciu serializacji miałyby numery, będące już w użyciu przez inne, wcześniej stworzone obiekty.
Samodzielne przekonanie się o tym pozostawiamy jako wartościowe ćwiczenie.

Zachowanie jest tu inne niż w poprzednim przypadku przekazywania przez odniesienie. Mamy faktycznie dwa różne obiekty klasy LocalElement: le1 i le2. Metoda getValue() wprowadziła do maszyny wirtualnej klienta inny obiekt (od wszystkich tam istniejących). Również następne wywołania tej metody wprowadzą nowy, inny od poprzednich choć z tą samą zawartością, obiekt do środowiska. Porównania tych obiektów operatorem == czy metodą equals() (odziedziczoną z klasy Object) dadzą wynik false. Oczywiście, jeśli przedefiniujemy metodę equals(), to wynik porównania może być inny.

Projektując klasę serializowalną, której obiekty będą przekazywane przez wartość, trzeba pamiętać, że wszystkie jej składowe obiektowe powinny również implementować interfejs Serializable. Większość standardowych klas Javy jest serializowalna, ale nie wszystkie. Jeśli atrybuty są obiektami własnych klas, to powinny one być serializowalne. Kompilator nie informuje o tym, że składowe serializowalnej klasy same takie nie są i przy próbie serializacji obiektu takiej klasy może dojść do błędu czasu wykonania. Nie zawsze musi to nastąpić. Nieserializowalny atrybut o wartości null nie sprawia problemów podczas serializacji.

Przekazywanie obiektów złożonych

Szczególną ostrożność należy zachować przy przekazywaniu obiektów klas złożonych zawierających obiektowe składowe. Jeśli lokalna klasa serializowalna zawiera pola, będące lokalnymi obiektami serializowalnymi (jak również owe pola mają takież składowe itd.), to nie ma problemu. Takie obiekty będą przekazywane przez wartość. Przykładem na to jest klasa LocalElement, która posiada serializowalny atrybut typu String. Przekazywanie wyeksportowanych obiektów zdalnych również nie nastręcza problemów, ponieważ taki obiekt nie opuszcza maszyny wirtualnej, na której został stworzony. Ciekawsze rzeczy dzieją się, gdy składową lokalnego obiektu serializowalnego jest wyeksportowany obiekt zdalny. Mimo, iż zostanie on (obiekt lokalny) przekazany przez wartość, a więc zostanie utworzona jego kopia, to jednak jego składowa zdalna nie opuści miejsca swojego pobytu. W jej imieniu zrobi to namiastka. Zatem mamy do czynienia ze sposobem przekazywania mieszanym, łączącym cechy przekazywania przez wartość jak i przez odniesienie. Ilustruje to ostatnia część przykładu.

W ostatnim bloku metody main() klienta będziemy przekazywać obiekt klasy CompoundElement na podobnych zasadach co poprzednio. Klasa ta implementuje interfejs Serializable, nie implementuje Remote ani żadnych innych. Podobnie jak klasy RemoteElement i LocalElement udostępnia metody setValue() i getValue() do ustalania wartości atrybutów. Są nimi obiekty wymienionych klas: local typu LocalElement i remote będący obiektem klasy RemoteElement. Tym ostatnim możemy jednak posługiwać się tylko za pośrednictwem interfejsu, bo jest to wyeksportowany obiekt zdalny, więc atrybut remote jest zadeklarowany jako typu Element. Metody pośredniczące w dostępie do atrybutu posługują się, jak poprzednio, typem String wywołując swoje odpowiedniki na rzecz obu tych atrybutów. W ten sposób łańcuch będący argumentem metody setValue() zostanie umieszczony w obu składowych. Podobnie rezultat metody getValue() będzie skomponowany z tego, co zostanie od nich pobrane.

import java.rmi.*;
import java.io.Serializable;

public class CompoundElement
  implements Serializable {

  private LocalElement local;
  private Element remote;      

  public CompoundElement(String val) {
    local = new LocalElement(val);
    try {
      remote = new RemoteElement(val);
    }
    catch(RemoteException e) {
      e.printStackTrace();
    }
  }

  public void setValue(String val) {
    local.setValue(val);
    try {
      remote.setValue(val);
    }
    catch(RemoteException e) {
      e.printStackTrace();
    }
  }

  public String getValue(){
    String retval = local.getValue() + "\t"; 
    try {
      retval += remote.getValue();
    }
    catch(RemoteException e){
      e.printStackTrace();
    }
    return retval;
  }
}

Po pierwsze wszystkie odwołania do zdalnego atrybutu remote są ujęte w blok przechwytujący wyjątki try{}catch(){}, ponieważ metody jak i konstruktor klasy RemoteElement mogą zgłosić wyjątek RemoteException, który musi zostać obsłużony.

Atrybut local jest serializowalny, bo klasa LocalElement implementuje Serializable. Atrybut remote również jest serializowalny, bo klasa UnicastRemoteObject implementuje ów interfejs. Jednak nie będzie to wykorzystywane do serializowania obiektu remote, bo jako wyeksportowany obiekt zdalny nie opuszcza on maszyny, na której został stworzony.

W konstruktorze inicjujemy atrybuty tym samym łańcuchem znakowym, zatem po stworzeniu obiektu składowe będą przechowywać taki sam napis. Metoda setValue() pozwala na zmianę tego łańcucha w obu atrybutach jednocześnie na inny. Metoda getValue() zwraca napis składający się z łańcuchów przechowywanych w składowych rozdzielonych znakiem tabulacji. Wydawałoby się, że metoda getValue() zawsze zwróci łańcuch składający się z tych samych części pobranych ze składowych. O tym, że tak być nie musi przekona nas ostatni blok metody main() klienta.

try {
  System.out.println(
          "\t\tTest obiektu złożonego: przekazywanie kopii i odniesienia"
  );
  CompoundElement ce = new CompoundElement("aCompoundElement");
  System.out.println("wstawiony do pojemnika : " + ce.getValue());
  c.put(ce);
  ce.setValue("newCompoundValue");
  System.out.println("lokalna modyfikacja na : " + ce.getValue());
  ce = (CompoundElement)c.get();
  System.out.println("pobrany z pojemnika    : " + ce.getValue());
  System.exit(0);
}
catch(Exception e){
  e.printStackTrace();
  System.exit(-1);
}

Najpierw tworzymy obiekt ce klasy CompoundElement i wstawiamy go do pojemnika. Następnie zmieniamy zawartość obu atrybutów tego obiektu wywołując metodę setValue("newCompoundValue"). Na końcu wyjmujemy z pojemnika włożony na początku obiekt, przypisując go na zmienną ce. Oto efekt tego działania:

                Test obiektu złożonego: przekazywanie kopii i odniesienia
wstawiony do pojemnika : aCompoundElement       aCompoundElement
lokalna modyfikacja na : newCompoundValue       newCompoundValue
pobrany z pojemnika    : aCompoundElement       newCompoundValue

Jak widać, po utworzeniu i modyfikacji zawartość obiektu była spójna: oba atrybuty zawierały ten sam napis. Jednak przy wyjmowaniu z pojemnika część obiektu zachowała się w sposób typowy dla obiektów lokalnych, a inna część jak obiekt zdalny. Dlaczego?

  1. obiekt ce jest lokalny i serializowalny, więc do pojemnika została wstawiona jego kopia (metodą put())
  2. jednak jego składowa remote jest wyeksportowanym obiektem zdalnym, więc nie opuszcza miejsca stworzenia
  3. zatem w pojemniku jest reprezentowana przez namiastkę
  4. wywołanie metody setValue() powoduje zmianę zawartości obu składowych w obiekcie znajdującym się w maszynie wirtualnej klienta, co widać na wydruku
  5. ale składowa remote występuje w jednym egzemplarzu, więc zmiana zostanie odzwierciedlona w składowej obiektu znajdującego się w pojemniku
  6. podczas gdy składowa local występuje teraz w dwóch egzemplarzach: jedna u klienta (z nową wartością "newCompoundValue"), a druga w pojemniku na serwerze (ze starą wartością "aCompoundElement").
  7. z pojemnika wyjmujemy właśnie ten rozdwojony obiekt i przypisujemy na zmienną ce
Klucz do zrozumienia zjawiska leży w tym, że składowa local została skopiowana - rozmnożyła się. Natomiast składowa remote występuje zawsze w jednym egzemplarzu, poza miejscem stworzenia będąc reprezentowana przez namiastkę.

Wyeksportowanie obiektu zdalnego wiąże się z utworzeniem usługi sieciowej, która nasłuchuje zgłoszeń na porcie TCP o numerze wybranym przez system operacyjny. Maszyna wirtualna tworzy wtedy osobny wątek podtrzymujący wyeksportowany obiekt przy życiu. Zatem po zakończeniu metody main() i tym samym głównego wątku programu, maszyna wirtualna nie zakończy działania samodzielnie ze względu na działający wątek obiektu wyeksportowanego. Trzeba to wymusić metodą System.exit() co jest zrobione w ostatniej instrukcji bloku.

4.4. Rozproszone odśmiecanie

Zdalne obiekty, które nie są używane muszą zostać usunięte z pamięci przez odśmiecacz. Ale skąd on ma wiedzieć, czy do obiektu są jakieś aktywne odniesienia, skoro mogą one pochodzić z innej maszyny wirtualnej? Kontrolowaniem odniesień do obiektów zdalnych zajmuje się rozproszony odśmiecacz (DGC - Distributed Garbage Collector). Zlicza on powstające i znikające odniesienia podobnie jak zwykły odśmiecacz. Obiekt, do którego nie ma żadnych aktywnych zdalnych odniesień może zostać usunięty z pamięci po 10 minutach. Oczywiście może się zdarzyć, że na skutek problemów z siecią nastąpiła dłuższa przerwa w komunikacji między odległymi maszynami. Niestety DGC nie jest w stanie wykryć tego faktu i może usunąć obiekt mimo iż gdzieś będą do niego używane odniesienia. Trzeba się z tym faktem liczyć projektując rozproszone aplikacje z RMI.


5. RMI w sieci

Do tej pory omawialiśmy kwestie dotyczące językowej strony RMI. Pora na omówienie zagadnień związanych z uruchamianiem programów na różnych komputerach i ich komunikacją w sieci. Fundamentalną rolę grają tu trzy elementy:

Powyższe trzy elementy muszą być określone aby możliwe było uruchomienie programu rozproszonego korzystającego z RMI w sieci (na różnych komputerach). Z punktu widzenia kodu programu wystąpią tylko dwie różnice w porównaniu z programami uruchamianymi całkowicie na jednym komputerze (choć być może na różnych maszynach wirtualnych): Poza tymi dwoma drobnymi, ale istotnymi szczegółami nie występują inne różnice od strony programistycznej. Właściwości definiuje się zwykle w argumentach wywołania maszyny wirtualnej. W ten sposób określa się również położenie pliku polityki.

5.1. Zarządca bezpieczeństwa

Zarządca bezpieczeństwa to obiekt klasy java.lang.SecurityManager lub jej podklasy (aktualnie tylko RMISecurityManager). Jego zadaniem jest zezwalanie lub nie na wykonywanie przez aplikację potencjalnie niebezpiecznych czynności. Zarządca bezpieczeństwa ma możliwość sprawdzenia niektórych skutków wykonywanej operacji przed jej wykonaniem i może ją anulować jeśli uzna, że jej wykonanie jest niebezpieczne dla systemu - zgłosi wtedy wyjątek SecurityException. W przypadku RMI niebezpieczne może być wykonywanie kodu pobranego z sieci. Przykładami operacji, które mogą znaleźć się w takim kodzie, a na które może nie zezwolić zarządca są: zakończenie maszyny wirtualnej, zapis na dysk lokalny czy też drukowanie.

Zarządcę bezpieczeństwa należy zainstalować jednokrotnie, najlepiej na początku programu metodą

System.setSecurityManager(SecurityManager sm);
sm jest nowym zarządcą bezpieczeństwa. W programach korzystających z RMI powinien to być obiekt klasy RMISecurityManager. Jeśli w maszynie wirtualnej został wcześniej zainstalowany zarządca bezpieczeństwa, może on zawetować (zgłoszeniem wyjątku) wykonanie powyższej metody. Dlatego należy najpierw sprawdzić, czy zarządca jest już zainstalowany. Typowy kod, umieszczany zwykle na początku metody main() wygląda tak:
if (System.getSecurityManager() == null) {
    System.setSecurityManager(new RMISecurityManager());
}

Zarządcę bezpieczeństwa musi instalować klient, który pobiera z sieci odniesienie do obiektu zdalnego, którego klasa nie jest znana lokalnie (nie ma jej na ścieżce poszukiwań klas). Musi zostać wtedy przesłana i wprowadzona do maszyny wirtualnej klienta klasa namiastki zdalnego obiektu. W przypadku serwera nie zawsze jest to konieczne. Jeśli klient przekazuje jako argumenty metod obiekty klas, których definicji nie ma po stronie serwera (bo są to własne klasy klienta), to serwer również musi zainstalować zarządcę i odpowiednio skonfigurowany plik polityki. Jeśli tego nie zrobi - nie będzie mógł załadować klasy argumentu (na skutek wyjątku zgłoszonego przez maszynę wirtualną).

5.2. Pliki polityki

Zachowanie zarządcy definiują pliki polityki. Są to pliki tekstowe, w których opisuje się zakres pozwoleń udzielanych aplikacji (np. umożliwienie dostępu do określonego katalogu na dysku, bądź obiór danych z określonego komputera w sieci). Składnia tych plików jest opisana w dokumentacji klasy java.lang.SecurityManager oraz w dokumentach docs/guide/security/permissions.html i docs/guide/security/PolicyFiles.html dostarczanych wraz z dokumentacją SDK Javy. Ograniczymy się tu jednynie do podania najprostszych przykładów. Oto zawartość pliku polityki, zezwalającego na wszystko:

grant {
    permission java.security.AllPermission;
};
Do generowania plików polityki można wykorzystać program policytool dostarczany wraz z SDK Javy.
Kolejne przykłady poznamy dalej, w trakcie omawiania programów.

Jeśli nie instalujemy zarządcy bezpieczeństwa, bo nie jest potrzebny, to nie musimy dostarczać pliku polityki.
Położenie pliku polityki określa właściwość java.security.policy maszyny wirtualnej. Jej wartość podaje się jako argument wywołania interpretera Javy opcją -D:

java -Djava.security.policy=plik.polityki    .....
Jeśli plik polityki znajduje się w innym katalogu niż ten, z którego został uruchomiony interpreter, to należy podać pełną lub relatywną nazwę ścieżkową pliku.

5.3. Właściwości

Oprócz właściwości określającej położenie pliku polityki bardzo ważna dla RMI jest właściwość java.rmi.server.codebase (w skrócie codebase) określającą położenie klas, które muszą być przesyłane do klientów. Należą do nich przede wszystkim klasy namiastek obiektów zdalnych, jak również zdalne interfejsy.

Jeśli klasa przesyłanego obiektu jest widoczna u odbiorcy na jego ścieżce poszukiwań klas classpath, to przesyłanie klasy nie jest konieczne. Maszyna wirtualna załaduje klasę, którą widzi na ścieżce. Takie zachowanie może być przyczyną subtelnych błędów. Np. klasa dostawcy przesyłana przez sieć może nazywać się tak samo jak inna klasa widoczna na ścieżce classpath odbiorcy, albo - co gorsza - inna wersja tej samej klasy (!).

Położenie klas jest podawane w formacie URL, który oprócz lokalizacji w systemie plików specyfikuje też protokół służący do transportowania pliku. RMI obsługuje 3 protokoły: FILE, FTP i HTTP. Pierwszy z nich ma zastosowanie tylko w przypadku, gdy maszyny wirtualne serwera i klienta pracują na tym samym systemie plików. Pozostałe wykorzystują zewnętrzne aplikacje w celu przesłania plików z klasami - serwer WWW lub FTP.

Klasy mogą być umieszczone w katalogu, jak również w archiwum jar.

W pierwszym przypadku URL koniecznie musi być zakończony ukośnikiem '/' - inaczej zostanie potraktowany jak plik zawierający archiwum.

Oto kilka przykładów URLi:

file:/home/user/classes/
file:/c:/java/
http://host/~user/classes.jar
http://host/server/classes/

Właściwość określającą położenie klas podaje się podobnie jak właściwość określającą położenie pliku polityki - przy użyciu opcji -D programu java:

java -Djava.rmi.server.codebase=URL   ......

Maszyna wirtualna Javy poszukuje klas dostarczonych przez użytkownika na ścieżce poszukiwań klas classpath. Można ją zdefiniować zmienną środowiskową CLASSPATH albo opcją -classpath lub -cp programu java. Jej domyślną wartością jest katalog bieżący - ten, z którego został uruchomiony interpreter.

Właściwość codebase jest przesyłana najpierw do rejestru, który używa jej do pobrania klas wyeksportowanych obiektów zdalnych i ich namiastek. Klient pobierający odniesienie do obiektu z rejestru również ją otrzymuje, aby mógł pobrać definicje klas z określonej przez nią lokalizacji.

Rejestr będzie poszukiwał klas obiektów, które są w nim rejestrowane na ścieżce poszukiwań klas classpath. Jeśli ich tam nie znajdzie, to wykorzysta URL zawarty we właściwości codebase przesłanej podczas rejestracji obiektu do odnalezienia jego klasy. Podobnie zachowuje się klient pobierający odniesienie do obiektu z rejestru.
Takie zachowanie może prowadzić do subtelnych błędów, spowodowanych nieostrożnością uruchamiającego program rozproszony. Należy zadbać o to, by zmienna CLASSPATH, nie była zdefiniowana w środowisku uruchomieniowym rejestru, ani też nie uruchamiać go z katalogu zawierającego te klasy.

Omówione wcześniej przykłady były uruchamiane w taki sposób, że klient odszukiwał klasy namiastek na ścieżce poszukiwań klas, dzięki temu nie musieliśmy podawać ich położenia definiując właściwość codebase. Podobnie zachowywał się rejestr. Jeśli rozdzielimy część serwera i część klienta umieszczając ich klasy w różnych katalogach, to klient (ani rejestr) nie odnajdzie już tych klas na ścieżce i niezbędne będzie określenie ich położenia właściwością codebase. URL będzie oparty wtedy na protokole FILE.

Jeśli występują problemy ze zlokalizowaniem serwera po jego nazwie, należy zdefiniować właściwość java.rmi.server.hostname po stronie serwera:

java -Djava.rmi.server.hostname=host.domain  .....
Opcja ta jest szczególnie użyteczna gdy komputer, na którym działa serwer ma wiele nazw.

5.4. Prosty przykład

Do nauki uruchamiania programów w środowisku sieciowym wykorzystamy bardzo prosty przykład, podobny do programu HelloWorld. Serwer będzie obliczał silnię liczby przekazanej jako argument typu long i zwracał wynik jako obiekt klasy BigInteger.

Kod źródłowy

Program składa się ze zdalnego interfejsu MathOps, klasy implementującej MathServer oraz klasy klienta MathClient.

import java.rmi.*;
import java.math.BigInteger;

public interface MathOps
  extends Remote {

  BigInteger factorial(long arg) throws RemoteException;
}
Jedyną metodą deklarowaną w interfejsie MathOps jest metoda factorial() obliczająca silnię argumentu.

Klasa serwera implementująca powyższy interfejs zawiera metodę startową main(), w której tworzy nowy obiekt serwera i zgłasza go do rejestru.

import java.rmi.*;
import java.rmi.registry.*;
import java.rmi.server.*;
import java.math.BigInteger;

public class MathServer
  extends UnicastRemoteObject 
  implements MathOps {

  public MathServer() throws RemoteException {
    super();
  }

  public BigInteger factorial(long val) throws RemoteException { 
    BigInteger fact = BigInteger.ONE;
    for (long i = 2; i <= val; i++){
      fact = fact.multiply(BigInteger.valueOf(i));
    }
    return fact;
  }

  public static void main(String[] args){
    try {
      MathServer ms = new MathServer();
      Naming.rebind("mathser", ms);
    }
    catch(Exception e){
      System.err.println(e);
    }
  }
}

Klient pobiera odniesienie do serwera i wywołuje metodę factorial(). Jeśli podano jakieś argumenty wywołania, to pierwszy jest liczbą, której silnię będziemy liczyć, a drugi jest nazwą (lub adresem IP) komputera, na którym jest uruchomiony serwer.

import java.rmi.*;
import java.math.BigInteger;

public class MathClient {

  public static void main(String[] args){
    try {
      System.setSecurityManager(new RMISecurityManager());
      String host = "localhost";
      if (args.length > 1)
        host = args[1]; 
      MathOps mo = 
      mo = (MathOps)Naming.lookup("//" + host + "/mathser");

      long number = 100;
      if (args.length > 0)
        number = Long.parseLong(args[0]); 
      BigInteger fact = mo.factorial(number);
      System.out.println(number + "! = " + fact);
    }
    catch(Exception e){
      System.err.println(e);
    }
  }
}
Pierwszą różnicą w stosunku do poprzednich przykładów, jaką napotykamy jest instrukcja instalująca zarządcę bezpieczeństwa na początku metody main(). Druga różnica kryje się w nazwie, pod którą chcemy wydobyć odniesienie do serwera z rejestru. Musi ona zawierać nazwę komputera, na którym jest uruchomiony rejestr (i tym samym serwer). Zwróćmy uwagę, że nazwa hosta nie była podawana podczas rejestracji serwera, a domyślną jej wartością było localhost.

Zwróćmy uwagę, że zarządca bezpieczeństwa jest zainstalowany tylko po stronie klienta. Będzie on pobierał z sieci namiastkę klasy serwera, aby utworzyć jej obiekt. Po stronie serwera zarządca nie jest potrzebny, bo nie używa on odniesień do obiektów zewnętrznych klas.

Przygotowanie do uruchomienia programu

Pokażemy teraz kilka sposobów uruchomienia programu. Oczywiście najpierw trzeba go skompilować i wygenerować namiastkę klasy serwera. Aby uniemożliwić maszynie wirtualnej załadowanie klas ze ścieżki classpath umieścimy klasy klienta i serwera w osobnych katalogach a ich programy, jak również rejestr będziemy uruchamiać z różnych katalogów.
W katalogu server umieścimy następujące pliki:

MathOps.class
MathServer.class
MathServer_Stub.class

Oprócz klasy serwera i jej namiastki musimy w nim umieścić skompilowany interfejs.
W katalogu client umieścimy pliki:

MathClient.class
MathOps.class

Maszyna klienta również musi mieć dostęp do skompilowanego interfejsu za pośrednictwem ścieżki poszukiwań klas classpath, w przeciwnym wypadku klient się nie uruchomi.

Zarówno po stronie serwera jak i klienta muszą być widoczne wszystkie używane zdalne interfejsy.

Klasy implementujące te interfejsy muszą być widoczne tylko po stronie serwera. Do klienta zostaną przesłane ich namiastki przy użyciu mechanizmów RMI, co za chwilę zobaczymy. We wcześniej omówionych przykładach namiastki były dostępne na ścieżce classpath zarówno po stronie klienta jak i serwera, bo oba programy były uruchamiane z tego samego katalogu.

Zastosowanie protokołu FILE

Najpierw uruchomimy program na jednym komputerze korzystając z protokołu FILE. Będzie do tego potrzebny plik polityki zezwalający klientowi na dostęp (tylko do odczytu) do katalogu server zawierającego klasy serwera.

grant {
    permission java.net.SocketPermission "*:1024-65535", "connect";
    permission java.io.FilePermission "/home/rmi/MathServer/server/-", "read";
};

Na platformie Win32, ze względu na inny sposób zapisu katalogów, ten plik polityki będzie miał postać:

grant {
    permission java.net.SocketPermission "*:1024-65535", "connect";
    permission java.io.FilePermission "c:\\home\\rmi\\MathServer\\server\\-", "read";
};

Pierwszy wiersz w tym pliku zezwala na łączenie się za pomocą gniazd z portami o numerach od 1024 wzwyż. W szczególności umożliwia to połączenie się z rejestrem nasłuchującym na porcie 1099 oraz wyeksportowanymi obiektami zdalnymi, które przeważnie nasłuchują na portach o numerach powyżej 32767. Drugi wiersz zezwala na odczyt z podanego katalogu wraz z jego podkatalogami.

Najpierw uruchamiamy rejestr pamiętając, by w środowisku nie była zdefiniowana zmienna CLASSPATH, a w katalogu uruchomieniowym nie było klasy serwera ani namiastki.

Nie wolno zapomnieć o ukośniku kończącym nazwę katalogu przy podawaniu właściwości!
Następnie, z katalogu server uruchamiamy program serwera podając właściwość codebase (zakładamy, że katalog server znajduje się w katalogu /home/rmi/MathServer/):
java -Djava.rmi.server.codebase=file:/home/rmi/MathServer/server/ MathServer
Następnie z katalogu client uruchamiamy program klienta:
java -Djava.security.policy=java.policy.file  MathClient 50
Zakładamy, że katalog ten jest umiejscowiony tak jak i server w /home/rmi/MathServer/ i zawiera przedstawiony wyżej plik polityki java.policy.file.

Oto wynik działania programu:

50! = 30414093201713378043612608166064768844377641568960512000000000000

Zastosowanie protokołu HTTP

Teraz wykorzystamy do przesyłania klas serwer WWW (np. apache). W katalogu domowym użytkownika user (/home/user/) utworzymy podkatalog public_html. Zgodnie z konwencją respektowaną przez większość serwerów HTTP, URL postaci http://host/~user/ będzie odnosił się do plików zawartych w tym podkatalogu (jeśli nie podamy nazwy, będzie to oczywiście domyślny index.html). Umieścimy w nim katalog MathServer zawierający klasy serwera. Dzięki takiemu umiejscowieniu będą one widoczne dla serwera WWW, a jednocześnie zwykły użytkownik (tu: user) będzie miał do nich dostęp z prawem do zapisu.

Do współpracy z serwerem WWW potrzebujemy innego pliku polityki. Musi on zezwalać na połączenia z serwerem, który nasłuchuje na porcie 80.

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

Zakładamy, że program serwera będzie uruchomiony na komputerze o nazwie server.domain, a pliki serwera będą zlokalizowane w katalogu /home/user/public_html/MathServer/. Najpierw jak zwykle uruchamiamy rejestr mając w pamięci uwagi dotyczące środowiska wyjaśnione w poprzednim punkcie.

Następnie uruchamiamy serwer z katalogu /home/user/public_html/MathServer/ lub innego, zawierającego klasy serwera:

java -Djava.rmi.server.codebase=http://server.domain/~user/MathServer/ 
     -Djava.rmi.server.hostname=server.domain 
     MathServer
W opcjach maszyny wirtualnej pojawiła się definicja właściwości hostname. Nie jest ona konieczna, ale pozwala uniknąć problemów związanych z różnymi nazwami hostów i dlatego jest zalecana. URL podany we właściwości codebase może być relatywny (jak w tym przykładzie), ale może też być absolutny (np. http://server.domain/www/user/server/), jednak wtedy rzeczywiste położenie podanego katalogu zależy od konfiguracji serwera WWW.

Tera możemy uruchamiać program klienta z innego komputera w sieci:

java -Djava.security.policy=java.policy.http  MathClient 50 server.domain
Jako drugi argument wywołania programu musimy podać koniecznie taką samą nazwę hosta, jaka została nadana właściwości hostname serwera podczas jego uruchamiania.

Użycie RMI w Intranetach nie powinno nastręczać problemów. Natomiast jeśli serwer i klient są oddzielone zaporami firewall, może być konieczne wykorzystanie po stronie serwera skryptu java-rmi.cgi znajdującego się w katalogu bin SDK Javy. Należy go umieścić w katalogu skryptów cgi serwera WWW.

Użycie serwera klas

Jeśli nie jest możliwe skorzystanie z serwera WWW, a chcemy wykorzystać protokół HTTP do transportu klas, możemy użyć serwera klas, dostępnego pod adresem: ftp://ftp.javasoft.com/pub/jdk1.1/rmi/class-server.zip Jest to prosty serwer HTTP, napisany w Javie. Jego funkcjonalność jest ograniczona wyłącznie do współpracy z RMI, czyli do przesyłania klas. Jego zaletą jest fakt, że może być wykorzystany w programach "od wewnątrz" - poprzez utworzenie obiektu klasy ClassFileServer. Spowoduje to zainstalowanie usługi HTTP na podanym porcie. Program jest dostępny w wersji źródłowej.

Standardowym numerem portu dla serwera HTTP jest 80. Co zrobić jeśli korzystamy z serwera, który jest zainstalowany na innym porcie (powyżej 1024)? Tak jest z omawianym serwerem klas, ale nie tylko - różne środowiska programistyczne Javy udostępniają własne serwery HTTP. Działają one na porcie o numerze, który może im przydzielić zwykły użytkownik (nie administrator), a więc co najmniej 1024. W takiej sytuacji trzeba podawać numer portu wewnątrz URLa:

http://host:8080/~user/dir/

Dla przykładu uruchomimy nasz program korzystając z serwera klas. Zakładamy, że struktura katalogów jest taka, jak w przypadku protokołu FILE: pliki serwera znajdują się w katalogu /home/rmi/MathServer/server/, a pliki klienta w katalogu /home/rmi/MathServer/client/.

Serwer klas po skompilowaniu, uruchamiamy poleceniem:

java examples.classServer.ClassFileServer 2003
Korzystając z serwera klas podajemy pełną nazwę ścieżkową katalogów.
2003 jest numerem portu, na którym będzie on oczekiwał zgłoszeń.
Po odpaleniu rejestru uruchamiamy program serwera MathServer:
java -Djava.rmi.server.codebase=http://localhost:2003/home/rmi/MathServer/server/
     MathServer
Numer portu 2003 podajemy w URLu. Po nim następuje pełna nazwa katalogu z klasami. Klienta odpalamy tak, jak poprzednio.
java -Djava.security.policy=java.policy.sock MathClient 50 localhost
Rejestr przekazując klientowi właściwość codebase poinformuje go, na jaki port należy się połączyć.
Użyliśmy tu innego pliku polityki. Oto jego zawartość:
grant {
    permission java.net.SocketPermission "*:1024-65535", "connect";
};
Jak widać, w porównaniu z poprzednim, nie zezwala on na łączenie się ze standardowym portem usługi WWW, bo nie jest to potrzebne.


6. Wywołania zwrotne

W poprzednim przykładzie rozważyliśmy najprostszą sytuację, w której klasy obiektów zdalnych są przesyłane tylko w jednym kierunku: od serwera do klienta. W rzeczywistych zastosowaniach mogą one być przesyłane w obu kierunkach. Ze szczególnym przypadkiem takiej sytuacji mamy do czynienia, gdy serwer wywołuje metodę z obiektu klienta (callback).Do serwera musi wtedy być przesłana klasa namiastki klienta (o ile nie jest dostępna lokalnie). Po stronie klienta nie jest konieczne w takich przypadkach stosowanie całej procedury, związanej z rejestrowaniem obiektu zdalnego.

6.1. Przykład - Giełda

Ilustrację wywołań zwrotnych zobaczymy na przykładzie serwera notowań giełdowych. Program będzie składał się z jednego serwera i dowolnej liczby klientów. Serwer będzie cyklicznie rozsyłał do każdego zarejestrowanego klienta informację o kursie akcji interesującej go spółki. Klient podczas rejestracji będzie podawał nazwę spółki. Zestaw spółek notowanych na giełdzie jest ustalany podczas tworzenia serwera. Klient może się wyrejestrować w dowolnej chwili - wtedy przestanie otrzymywać informacje od serwera. Nasza implementacja klienta będzie się wyrejestrowywać, jeśli kurs akcji wzrośnie powyżej pewnej kwoty.

Budowa programu

Tym razem zarówno klasa klienta jak i serwera będą implementować zdalne interfejsy. Ich obiekty będą wyeksportowane, jednak tylko serwer będzie rejestrowany. Klient będzie pobierał odniesienie do serwera poprzez rejestr, natomiast serwerowi zostanie przekazane odniesienie do klienta jako argument metody. Serwer będzie wywoływał metodę z obiektu klienta, za pośrednictwem uzyskanego odniesienia - dlatego klient musi być wyeksportowanym obiektem zdalnym. Pełni on tym samym również rolę serwera.

Zdalne interfejsy

Interfejs Client jest zdalnym interfejsem definiującym klientów giełdowych.

import java.rmi.*;

public interface Client
  extends Remote {

  void report(String com, double val) throws RemoteException;
}
Zawiera tylko jedną metodę - wywoływaną przez serwer. Jej zadaniem jest dostarczenie informacji o wartości val kursu spółki com. Klient implementując metodę report() może podejmować rozmaite akcje, w zależności od kursu: np. dokonać zakupu lub sprzedaży.

Interfejs Stock deklaruje dwie metody służące do rejestracji klientów.

import java.rmi.*;

public interface Stock 
  extends Remote {

  boolean register(Client cli, String com) throws RemoteException;

  boolean unregister(Client cli) throws RemoteException;
}
Metodę register() klient wywołuje w celu zarejestrowania się na serwerze. Jako argumenty podaje się odniesienie do klienta cli oraz nazwę spółki com. Jeśli spółka o podanej nazwie nie jest notowana na giełdzie zostanie zwrócona wartość false, w przeciwnym przypadku true - co będzie oznaczało, że klient został zarejestrowany. Od tej pory będzie otrzymywał dane o kursie do momentu wyrejestrowania metodą unregister(). Jeśli podany klient cli nie był zarejestrowany, to metoda zwróci jako wynik wartość false.

Klasa serwera

Serwer jest obiektem klasy StockServer, która implementuje interfejs Stock. Zadaniem serwera jest przyjmowanie zgłoszeń od klientów określonych przez ten interfejs oraz rozsyłanie do nich informacji o kursach. Dzieje się to w osobnym wątku opartym o obiekt serwera (klasa serwera implementuje interfejs Runnable). Klasa serwera zawiera również metodę startową main().

import java.rmi.*;
import java.rmi.server.*;
import java.util.*;


public class StockServer
  extends UnicastRemoteObject
  implements Stock, Runnable {

  private static final int FREQ = 1000;

  private Random rand = new Random();
  private Map rates;
  private Map clients;


  public StockServer() throws RemoteException {
    rates = Collections.synchronizedMap(new HashMap());
    clients = Collections.synchronizedMap(new HashMap());
    rates.put("PJWSTK", new Double(0));
    rates.put("UW", new Double(0));
    rates.put("PW", new Double(0));
    new Thread(this).start();
  }
Stała FREQ określa częstość (w milisekundach) obliczania notowań. Atrybut rand jest generatorem liczb losowych wykorzystywanym do obliczania kursów akcji. Są one przechowywane w kolekcji mieszającej rates, będącej prywatną składową tej klasy. Jej kluczami będą nazwy spółek, a wartościami - ich kursy jako obiekty klasy Double. Zarejestrowani klienci są przechowywani w kolekcji mieszającej clients. Tu kluczami będą klienci jako obiekty typu Client, a wartościami nazwy spółek, którymi są oni zainteresowani. Obie kolekcje są tworzone w konstruktorze jako synchronizowane wersje kolekcji typu HashMap, którą otrzymujemy metodą synchronizedMap() z klasy Collections. Jako argument podajemy w obu przypadkach kolekcję typu HashMap. Musimy tu użyć synchronizowanych wersji, ponieważ dostęp do tych kolekcji będzie odbywał się z dwóch wątków. W wątku głównym klienci będą się rejestrować i wyrejestrowywać, natomiast obliczanie kursów i rozsyłanie ich do klientów będzie wykonywane w dodatkowym wątku opartym na obiekcie serwera. Wątek ten jest tworzony i uruchamiany na zakończenie konstruktora. Zestaw spółek notowanych na giełdzie jest określony w konstruktorze i obejmuje spółki o nazwach "PJWSTK", "UW" i "PW".

Metody interfejsu Stock:

  public boolean register(Client cli, String com) throws RemoteException {
    if (rates.keySet().contains(com)){
      clients.put(cli, com);
      System.out.println("Client registered: " + com);
      return true;
    } 
    else
      return false;
  }

  public boolean unregister(Client cli) throws RemoteException {
    String com = (String)clients.get(cli);
    if (clients.remove(cli) != null) {
      System.out.println("Client unregistered: "  + com);
      return true;
    }
    else
      return false;
  }
Metoda register() służy do rejestracji klienta. Jeśli podana nazwa spółki com nie jest notowana, to klient nie jest rejestrowany i zostaje zwrócona wartość false. W przeciwnym wypadku klient jest dodawany do kolekcji clients. Jeśli był już wcześniej zarejestrowany, to poprzedni wpis zostanie anulowany, tak by każdy klient mógł być zarejestrowany tylko dla jednej spółki. Wyrejestrowanie klienta metodą unregister() powiedzie się, jeśli był on zarejestrowany - zostanie wtedy zwrócona wartość true.

Metoda run() jest wykonywana przez osobny wątek zajmujący się generowaniem informacji o wynikach spółek i propagowaniem ich do klientów. Na początku wątek jest usypiany na czas określony stałą FREQ. Następnie wywołujemy metodę getQuotations() obliczającą kursy i sendReports() rozsyłającą je do klientów.

  public void run(){
    while (true){
      try {
        Thread.sleep(FREQ);
      }
      catch(InterruptedException e){
        System.err.println("Interrupted! : " + e);
      }
      getQuotations();
      sendReports();
    }
  }

  private void getQuotations(){
    Iterator it = rates.entrySet().iterator();
    while (it.hasNext()){
      Map.Entry me = (Map.Entry)it.next();
      me.setValue(new Double(rand.nextDouble()));
    }
  }

  private void sendReports(){
    synchronized(clients){            
      Iterator it = clients.keySet().iterator();
      while (it.hasNext()){
        Client cli = (Client)it.next();
        String com = (String)clients.get(cli);
        Double val = (Double)rates.get(com);
        try {
            cli.report(com, val.doubleValue());
        }
        catch (RemoteException e){
          System.err.println("RemoteException at: " + cli);
          e.printStackTrace();
        }
      }
    }
  }
Metoda getQuotations() przegląda kolekcję, a właściwie zbiór asocjacji, które przechowuje, przy pomocy iteratora it. Asocjacja typu Map.Entry przechowuje klucz - nazwę spółki - i wartość - jej cenę. metodą setValue() zmieniamy cenę na losową liczbę typu double, zapamiętując ją jako wartość skojarzoną z kluczem w obiekcie klasy Double.
Metoda sendReports() będzie przeglądać kolekcję clients. Ponieważ może ona zostać zmodyfikowana w innym wątku poprzez dodanie lub usunięcie klienta, całość musi być ujęta w bloku synchronizowanym ryglem clients. Iterator it będzie przeglądał zbiór kluczy, którymi są zarejestrowani klienci. Od każdego klienta pobieramy nazwę spółki, którą jest zainteresowany, a następnie z kolekcji rates pobieramy jej aktualny kurs. Metodą report() klienta (z interfejsu Client) rozsyłamy informację o cenie spółki com. Wywołanie tej metody musi być ujęte w blok przechwytywania wyjątków, ponieważ jest to metoda wywoływana z obiektu zdalnego. Jej wykonanie będzie przebiegać w maszynie wirtualnej klienta. W tym miejscu klient zachowuje się jak serwer w stosunku do obiektu klasy StockServer.

W metodzie main() instalujemy zarządcę bezpieczeństwa. Jest to nowy element, nieobecny w poprzednim przykładzie (klasa MathServer), w którym serwer nie ładował kodu z sieci. W tym przypadku będzie ładowana klasa namiastki klienta StockClient_Stub. Po utworzeniu obiektu serwera rejestrujemy go pod nazwą "exchange" z ewentualnym prefiksem określającym nazwę hosta.

  public static void main(String[] args){
    try {
      System.setSecurityManager(new RMISecurityManager());
      StockServer exchange = new StockServer();
      String host = (args.length > 0 ? args[0] : "localhost");
      Naming.rebind("//" + host + "/exchange", exchange);
    }
    catch(Exception e){
      e.printStackTrace();
    }
  }
}

Klasa klienta

Klient jest obiektem klasy StockClient, która implementuje interfejs Client. W odróżnieniu od poprzednich przykładów, klient jest wyeksportowanym obiektem zdalnym (dlatego dziedziczy z UnicastRemoteObject). Jednak obiekt klienta nie będzie dostępny poprzez rejestr. Serwer dostanie odniesienie do niego jako argument metody register() serwera. Po utworzeniu obiektu klienta będzie on nasłuchiwał zgłoszeń na porcie TCP przydzielonym przez system operacyjny. Dlatego też nie jest potrzebne tworzenie osobnego wątku podtrzymującego klienta przy życiu. W wątku prowadzącym nasłuch będzie wykonywana metoda report() interfejsu Client. Do zakończenia pracy programu klienta stworzymy specjalny wątek, który będzie sprawdzał cenę akcji i jeśli przekroczy ona 0.9. to spowoduje zakończenie programu po uprzednim wyrejestrowaniu klienta z serwera. Wątek kończący jest oparty na obiekcie klienta, którego klasa implementuje interfejs Runnable.

Atrybutami klasy Stock są: odniesienie do serwisu giełdowego typu Stock oraz bieżąca cena price akcji obserwowanej spółki. Konstruktor próbuje zarejestrować tworzony obiekt klienta na serwerze. Jeśli się to nie powiedzie z powodu podania niewłaściwej nazwy spółki company, zostanie zgłoszony wyjątek IllegalArgumentException. Ostatnia instrukcja konstruktora tworzy i uruchamia nowy wątek oparty na obiekcie klienta, którego zadaniem będzie kontrola składowej price i zakończenie programu.

import java.rmi.*;
import java.rmi.server.*;

public class StockClient
  extends UnicastRemoteObject
  implements Client, Runnable {

  private Stock stock;
  private double price = 0;

  public StockClient(Stock stock, String company) throws RemoteException {
    this.stock = stock;
    if (!stock.register(this, company))
      throw new IllegalArgumentException(company);
    new Thread(this).start();
  }

  public void report(String com, double val) throws RemoteException {
    System.out.println(com + " : " + val);
    synchronized (this){
      price = val;
      if (price > 0.9)
	notify();
    }	
  }

Dlaczego nie można wywołać unregister() z metody report(), lecz trzeba przeznaczać do tego osobny wątek?
Prowadzi to do blokady: metoda report() jest wywoływana w metodzie sendReports() klasy StockServer, w bloku synchronizowanym obiektem clients. Metoda unregister() wywołuje na rzecz obiektu clients metody, które są na nim synchronizowane. Próba wywołania takiej metody (np. clients.get()) prowadzi do zajęcia rygla, który jest już zajęty w innym wątku (wykonującym sendReports()). Zatem żadne wywołanie metody na rzecz clients nie dojdzie do skutku, a tym samym nie zakończy się wywołanie unregister(), co z kolei zablokuje wątek oczekujący na powrót z wywołania report() w metodzie sendReports().

Metoda report() wyprowadza na konsolę nazwę spółki i jej notowanie oraz zapamiętuje aktualną jej cenę. Jest ona wywoływana przez serwer i wykonywana w specjalnym wątku maszyny wirtualnej klienta. Dostęp do atrybutu price odbywa się również w wątku nadzorującym uruchomionym w konstruktorze. Aby zagwarantować poprawną wartość tego atrybutu, dostęp do niego musi odbywać się w bloku synchronizowanym (synchronizatorem jest this). Wewnątrz tego bloku, po zapamiętaniu bieżącej ceny akcji, sprawdzamy czy przekroczyła ona 0.9. Jeśli tak, to wywołaniem metody notify() jest uwalniany wątek nadzorujący, który jest uśpiony na tym samym synchronizatorze (this).

Metoda run() jest wykonywana przez wątek nadzorujący uruchomiony w konstruktorze. W bloku synchronizowanym obiektem klienta (a więc tym samym, co w poprzedniej metodzie) sprawdzamy cenę akcji i jeśli jest ona mniejsza, to wątek jest zawieszany na synchronizatorze this w oczekiwaniu na wywołanie notify(). Początkowa wartość atrybutu price jest 0, więc do zawieszenia dojdzie od razu po rozpoczęciu wykonywania tego wątku. Po wywołaniu notify() w metodzie report() nastąpi opuszczenie pętli, a po zapamiętaniu wartości atrybutu price - wyjście z bloku synchronizowanego. Teraz można bezpiecznie się wyrejestrować i zakończyć działanie programu. Wywołanie metody exit() na końcu jest konieczne, ponieważ maszyna wirtualna przeznaczyła specjalny wątek podtrzymujący wyeksportowany obiekt klienta, w którym są obsługiwane zgłoszenia do tego obiektu (m.in. jest wywoływana metoda report()).

  public void run(){
    double p;
    try {
      synchronized (this){
        while (price <= 0.9){
          System.out.println("Waiting...");
          wait();
        }
        p = price;
      }
      stock.unregister(this);
      System.out.println("Done, price = " + p);
      System.exit(0);
    }
    catch(Exception e){
      e.printStackTrace();
    }
  }

  public static void main(String[] args){
    try {
      System.setSecurityManager(new RMISecurityManager());
      String host = (args.length > 0 ? args[0] : "localhost");
      String com = (args.length > 1 ? args[1] : "PJWSTK");
      Stock stock = (Stock)Naming.lookup("//" + host + "/exchange");
      StockClient sc = new StockClient(stock, com);
    }
    catch(Exception e){
      e.printStackTrace();
      System.exit(-1);
    }
  }
}
W metodzie main() najpierw instalujemy zarządcę bezpieczeństwa. Następnie przygotowujemy nazwę, pod jaką będziemy poszukiwać serwera (musi ona zawierać nazwę hosta, na którym jest uruchomiony rejestr) oraz nazwę spółki. Kiedy wszystko jest gotowe, pobieramy odniesienie do serwera i tworzymy obiekt klienta.

Kompilacja i uruchomienie

Kompilujemy wszystkie pliki, a dla klas serwera i klienta generujemy namiastki:

javac *.java
rmic -v1.2 StockServer StockClient

Przesyłanie klas zlecimy standardowemu serwerowi HTTP. Potrzebny będzie do tego następujący plik polityki (przyjmijmy, że będzie się on nazywał java.policy.http zarówno po stronie serwera jak i klienta):

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

Zwróćmy uwagę, że w odróżnieniu od poprzedniego przykładu, tym razem udzielamy zezwoleń "connect,accept" na portach od 1024 wzwyż (poprzednio tylko "connect"). Oznacza to, że pozwalamy dodatkowo na akceptowanie połączeń wykonywanych z dowolnych adresów i portów niezastrzeżonych. W szczególności akceptujemy połączenia zainicjowane przez obiekt serwera.

W celu uruchomienia programu przyjmiemy następującą konfigurację:

Na obu komputerach jest uruchomiony serwer HTTP na standardowym porcie 80.

Najpierw uruchamiamy po stronie serwera rejestr. Należy pamiętać, aby w katalogu uruchomieniowym nie były widoczne klasy programu a zmienna środowiskowa CLASSPATH nie była zdefiniowana. Następnie z katalogu /home/broker/public_html/Stock/server/ uruchamiamy serwer poleceniem:

java -Djava.security.policy=java.policy.http                              
     -Djava.rmi.hostname=server                                      
     -Djava.rmi.server.codebase=http://server/~broker/Stock/server/  
      StockServer server
Program klienta uruchamiamy z katalogu /home/user/public_html/Stock/client/ poleceniem:
java -Djava.security.policy=java.policy.http                              
     -Djava.rmi.hostname=client                                     
     -Djava.rmi.server.codebase=http://client/~user/Stock/client/  
     StockClient server
Oto wynik działania programu klienta:
Waiting...
PJWSTK : 0.4564939772829154
PJWSTK : 0.5487154295406457
PJWSTK : 0.29823304970619735
PJWSTK : 0.9484500628817595
Done, price = 0.9484500628817595
Po stronie serwera zobaczymy natomiast:
Client registered: PJWSTK
Client unregistered: PJWSTK

Serwer będzie oczekiwał na dalsze zgłoszenia dopóki go ręcznie nie zakończymy.

6.2. Uwagi

Serwer HTTP nie musi być uruchomiony na obu maszynach. Może nie być uruchomiony nawet na żadnej z nich. Musi jedynie działać na jakimś komputerze i mieć dostęp do wszystkich klas. Właściwość codebase określa położenie serwera i klas.

Czasem bardzo ważne jest, by argument określający nazwę hosta podawany serwerowi i klientowi był taki sam. Każdy komputer oprócz standardowej nazwy localhost może mieć jeszcze wiele innych. RMI może nie być w stanie poprawnie posługiwać się jednocześnie wieloma nazwami hosta. Aby ułatwić to zadanie należy zdefiniować właściwość hostname oraz zadbać o to, by nazwa hosta zawarta na początku łańcucha znakowego identyfikującego usługę w rejestrze była identyczna po stronie serwera i klienta (i taka, jak określona we właściwości hostname). Nieprzestrzeganie tego prowadzi do trudnych do wytropienia problemów związanych z konfiguracją środowiska nazw hostów.

Oczywiście jako serwer HTTP można wykorzystać serwer klas opisany w poprzednim przykładzie.


7. Podsumowanie

Uruchamianie programów wykorzystujących mechanizm RMI wymaga dużego doświadczenia. Bardzo łatwo jest popełnić drobny błąd, który będzie bardzo trudno zidentyfikować. Przyczynia się do tego beznadziejnie zła diagnostyka występujących błędów. Komunikaty generowane przez wyjątki są całkowicie nieczytelne i nie wskazują przyczyny błędu. Z drugiej strony mamy mnóstwo okazji do popełniania błędów. Szczególną uwagę należy zwrócić na:


Dokumentacja i literatura

Java Tutorial
Rozdział poświęcony RMI w podręczniku on-line Java Tutorial (wersja html).
Dokumentacja
Specyfikacja on-line (wersja html), jest również dostępna w dokumentacji Javy w katalogu docs/guide/rmi.