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.
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.
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).
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.
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.
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.
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.
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.
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ę.
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.
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ść.
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:
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:
Po skompilowaniu kodu serwera jest on gotowy do działania. Zanim jednak zostanie uruchomiony trzeba jeszcze:
Implementując kod odwołujący się do zdalnych obiektów, w metodzie startowej
main(...)
programu musimy umieścić instrukcje, które:
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.
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.
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ł:
java.rmi.Remote
java.rmi.RemoteException
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.
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); } } }
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:
main()
musi ponadto instalować zarządcę bezpieczeństwa.
Remote
RemoteException
UnicastRemoteObject
main()
tworzy obiekt i zapamiętuje go w rejestrze
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.
Najpierw kompilujemy pliki źródłowe z definicjami klas.
javac Hello.java javac HelloWorld.java javac Greet.javaNastępnie generujemy namiastkę klasy zdalnej:
rmic -v1.2 HelloWorldSpowoduje to powstanie pliku HelloWorld_Stub.class.
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:
Każdorazowe wykonanie programu klienta powinno spowodować wyprowadzenie napisu
"Hello World"
na konsoli serwera.
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: helloTrzeba 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
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.
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.
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.
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.
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.
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()
.
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).
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:
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.
Mamy dwa zdalne interfejsy:
timing.TimeResponse
z metodą long getTime(long l)
zwracającą aktualny czas (w miejscu wykonania). Parametr l
służy
do przekazania czasu z klienta.
timing.ServerProducer
z metodą TimeResponse getServer()
,
która służy do utworzenia serwera czasu.
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.
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.
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.
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/*.javaNastępnie klasy serwera i klienta (w dowolnej kolejności)
javac timing/server/*.java javac timing/client/TimeClient.java
rmic -v1.2 timing.server.TimeServer rmic -v1.2 timing.server.ObjectProducer
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.
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.
java.rmi.Remote
.
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.
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
.
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.
java.io.Serializable
.
Serializable
, to zostanie zgłoszony wyjątek.
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.
Program będzie składał się z dwóch głównych części:
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:
RemoteElement
implementującej zdalny interfejs Element
będą przekazywane przez odniesienie.
LocalElement
implementującej interfejs Serializable
będą przekazywane przez wartość.
CompoundElement
również implementującej Serializable
będą przekazywane przez wartość. Jej dwie składowe będą obiektami powyższych klas -
zatem jeden z nich będzie przekazywany przez wartość, drugi przez odniesienie.
CompoundElement
) ma zwrócić uwagę na fakt,
że semantyka przekazywania argumentów może być różna dla obiektu i jego składowych.
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
).
W celu uruchomienia programu, trzeba go oczywiście
RemoteContainer
i RemoteElement
:
rmic -v1.2 RemoteContainer RemoteElement
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:
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 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.
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
.
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
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
.
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
.
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
.
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.
Remote
.
Obiekt lokalny to obiekt klasy lokalnej.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.
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
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.
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.
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.
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?
ce
jest lokalny i serializowalny, więc do pojemnika
została wstawiona jego kopia (metodą put()
)
remote
jest wyeksportowanym obiektem zdalnym,
więc nie opuszcza miejsca stworzenia
setValue()
powoduje zmianę zawartości obu składowych
w obiekcie znajdującym się w maszynie wirtualnej klienta, co widać na wydruku
remote
występuje w jednym egzemplarzu, więc zmiana zostanie
odzwierciedlona w składowej obiektu znajdującego się w pojemniku
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"
).
ce
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ę.
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.
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.
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:
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ą).
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; };
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.
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.
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.
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 ......
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.
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.
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
.
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.
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.classOprócz klasy serwera i jej namiastki musimy w nim umieścić skompilowany interfejs.
MathClient.class MathOps.classMaszyna 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.
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.
CLASSPATH
, a w katalogu uruchomieniowym nie było klasy serwera ani namiastki.
java -Djava.rmi.server.codebase=file:/home/rmi/MathServer/server/ MathServerNastępnie z katalogu
client
uruchamiamy program klienta:
java -Djava.security.policy=java.policy.file MathClient 50Zakł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
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 MathServerW 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.domainJako 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.
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
MathServer
:
java -Djava.rmi.server.codebase=http://localhost:2003/home/rmi/MathServer/server/ MathServerNumer 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 localhostRejestr przekazując klientowi właściwość codebase poinformuje go, na jaki port należy się połączyć.
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.
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.
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.
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.
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
.
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
.
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(); } } }
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(); } }
unregister()
z metody report()
,
lecz trzeba przeznaczać do tego osobny wątek?
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()
.
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.
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"; };
"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ę:
Client.class Stock.class StockServer.class StockServer_Stub.class java.policy.http
Client.class Stock.class StockClient.class StockClient_Stub.class java.policy.http
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 serverProgram 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 serverOto wynik działania programu klienta:
Waiting... PJWSTK : 0.4564939772829154 PJWSTK : 0.5487154295406457 PJWSTK : 0.29823304970619735 PJWSTK : 0.9484500628817595 Done, price = 0.9484500628817595Po stronie serwera zobaczymy natomiast:
Client registered: PJWSTK Client unregistered: PJWSTKSerwer będzie oczekiwał na dalsze zgłoszenia dopóki go ręcznie nie zakończymy.
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.
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:
CLASSPATH
może przesłaniać znaczenie właściwości
codebase. Podobnie opcja -classpath lub -cp
maszyny wirtualnej.