Seweryn Hejnowicz

Instytut Informatyki, Uwniwersytet Wrocławski, I rok studiów uzupełniających
e-mail: azaghal@skynet.com.pl
 www: www.skynet.com.pl/~azaghal

Wprowadzenie do RMI w Javie 2

dnia 9 marca 2005 roku.

W ramach seminarium "Java dla wtajemniczonych", prowadzonym przez mgra Pawła Rzechonka w instytucie informatyki Uniwersytetu Wrocławskiego, w semestrze letnim roku akademickiego 2004/2005.

Plan referatu:

  1. Wprowadzenie do RMI i techniki rozproszonych obiektów
  2. Schemat wywołania metody poprzez RMI. Rola klienta, serwera i namiastek obiektów
  3. Implementacja zdalnego obiektu i generowanie namiastek
  4. Eksport zdalnego obiektu, usługa rejestru nazw i sposób uzyskiwania referencji przez obiekt klienta.
  5. Implementacja programów klienta i serwera
  6. Wdrożenie:
  7. Problemy przekazywania parametrów metod
  8. Literatura 
1. Wprowadzenie do RMI i techniki rozproszonych obiektów

        Najkrócej mówiąc, RMI (ang. Remote method Invocation) jest techniką pozwalającą na zorganizowanie komunikacji między obiektami, zaprogramowanymi w języku Java, działającymi na odrębnych, rozproszonych w środowisku sieciowym, maszynach wirtualnych, w sposób zbliżony do tego, jaki stosuje się dla obiektów działających lokalnie - za pomocą wywołań metod, ukrywając przed programistą większość niskopoziomowych szczegółów związanych z obsługą protokołów sieciowych. Takie podejście, obok wygody i łatwości implementacji, daje możliwość zaprojektowania systemu rozproszonego w sposób obiektowy, tak dalece, jak to tylko możliwe. 
      Aby unaocznić istotę powyższych stwierdzeń, rozpatrzymy przykład. Niech problemem do rozwiązania będzie zaprojektowanie w sposób obiektowy komunikacji sieciowej między serwerem, świadczącym usługi czasu (tj. podającego aktualną datę i czas na żądanie), a klientem korzystającym z jego usług. Jako pierwszy rozpatrzymy projekt aplikacji opartej o niskopoziomowe mechanizmy gniazdek sieciowych (ang. network sockets). Gniazdka sieciowe stanowią podstawową abstrakcję protokołu komunikacji sieciowej w warstwach: sieci i transportu modelu odniesienia OSI. Używając w dalszej części tego tekstu pojęcia protokół sieciowy będziemy mieli na myśli protokół TCP/IP lub jego zawodną odmianę UDP/IP - najszerzej wykorzystywane. Abstrakcja gniazdek sieciowych będzie odnosić się do końcówek połączenia sieciowego między dwoma komputerami. Schemat komunikacji między klientem a serwerem jest następujący:

  1. Podczas inicjalizacji, serwer tworzy gniazdo nasłuchujące na ustalonym porcie połączeń ze strony klientów;

  2. po stronie klienta jest tworzone gniazdo sprzężone z adresem ip i numerem portu, na jakich działa wspomniane gniazdo nasłuchujące serwera;

  3. kiedy połączenie zostanie zestawione, serwer po swojej stronie tworzy odrębne gniazdo (na innym porcie), poprzez które będzie przebiegać właściwa komunikacja;

  4. Po zestawieniu połączenia klient wymienia dane z serwerem.

W Javie istnieją dwa rodzaje obiektów pozwalające zaimplementować przedstawiony schemat. Są to Socket i ServerSocket. Po stronie klienta będzie tworzony obiekt typu Socket, którego pomyślne stworzenie będzie oznaczało nawiązanie połączenia z serwerem. Obiekt typu ServerSocket będzie z kolei wykorzystywany po stronie serwera - jego zadaniem będzie nasłuchiwanie klientów i zestawianie połączeń. Zestawienie połączenia będzie polegało na stworzeniu właściwego gniazda - obiektu typu Socket, dedykowanego konkretnemu klientowi. kiedy połączenie zostanie zestawione, serwer i klient będą mogli wymieniać dane w sposób zbliżony do tego, w jaki korzysta się ze strumieni wejścia/wyjścia w Javie (obiekt typu Socket może zwrócić odpowiednio: obiekt InputStream lub OutputStream).

Wracając do zadania, schemat klas w uwzględnieniem przedstawionych założeń dotyczących komunikacji może być następujący (bez zbędnych i nie mających znaczenia w ogólnym kontekście szczegółów):

Podstawową klasą serwera jest SimpleDateTimeServer, wyposażona w obiekt typu ServerSocket. W nieskończonej pętli serwer będzie nasłuchiwał poprzez obiekt ServerSocket nowych klientów. Kiedy pojawi się klient, zostanie dla niego stworzony obiekt DedicatedDatetimeServer, który w osobnym wątku będzie poprzez odrębne gniazdo komunikował się z tym klientem. Taka wielowątkowa architektura pozwoli na obsługę wielu klientów równocześnie.

Na diagramie poniżej spróbujemy przedstawić komunikację między klientem a serwerem.

Używane w modelowaniu obiektowym diagramy przepływu komunikatów zakładają, że jedynymi podmiotami podlegającymi interakcjom są obiekty, a sposób tej interakcji to wywołanie metody. Z tego wynika, że wszelkie interakcje, polegające np. na tym, że dany obiekt wysyła dane do sieci, a inny je stamtąd odbiera, muszą być modelowane w zupełnie nienaturalny sposób, tak jak to widać na rysunku (wywołanie "metod" z adnotacją "???"). Dodatkowo diagram jest niepotrzebnie skomplikowany z uwagi na potrzebę uwzględnienia na nim klas odpowiedzialnych za komunikację sieciową. Oba czynniki powodują, że obiektowe modelowanie aplikacji sieciowych, których projekty muszą obejmować modele interakcji inne niż pomiędzy obiektami, staje się trudne i wymaga innych notacji, niż diagramy UML'owe.

Te i inne niedogodności sprawiły, że powstał wysokopoziomowy protokół komunikacji międzyobiektowej RMI, pozwalający zorganizować komunikację sieciową na poziomie obiektów i wywoływania metod, w sposób dalece zbliżony do komunikacji, jaka zachodzi między obiektami działającymi na tej samej maszynie wirtualnej. 

Używając RMI, jeszcze raz zamodelujemy opisaną wyżej aplikację sieciową. 

Tym razem na diagramie nie ma żadnych klas nie pochodzących wprost z logiki aplikacji. Jest klasa klienta SimpleRMIDatetimeClient oraz klasa serwera SimpleRMIDatetimeServerImpl, która implementuje specjalny interfejs określający jakie metody będą zdalnie dostępne dla obiektów będącymi klientami RMI. Taki interfejs będzie rozszerzał interfejs Remote (na razie znaczenie tych interfejsów pozostawimy bez komentarza).

Zrealizowanie usługi serwera będzie nie bardziej skomplikowane od zwykłego wywołania metody normalnego obiektu:

Diagramy przedstawione w tej postaci bronią się same. Wszystkie szczegóły związane z niskopoziomową komunikacją sieciową są ukryte przez protokół RMI tak dalece, że programista RMI w większości przypadków może nie mieć większego pojęcia o tym, że dane jego obiektów przesyłane są protokołem TCP z wykorzystaniem gniazdek sieciowych.

2. Schemat wywołania metody poprzez RMI. Rola klienta, serwera i namiastek obiektów

        W tym punkcie skupimy się na próbie zaprezentowania wewnętrznej struktury i architektury RMI. Tak prosta czynność jak wywołanie metody, jest  z jednej strony czynnością trywialną w odniesieniu do obiektów lokalnych, a z drugiej może okazać przedsięwzięciem bardzo skomplikowanym, gdy chodzi o obiekty zdalne. 
   
     W każdej interakcji (wywołanie metody) biorą udział dwa obiekty. Obiekty wywołujący zdalną metodę nazywać będziemy klientem, a obiekt świadczący usługi w postaci osiągalnych zdalnie metod, serwerem. Z każdym zdalnym obiektem jest związany obiekt namiastki (ang. Stub), posiadający identyczny interfejs jak interfejs właściwego zdalnego obiektu. Obiekt klienta wywołując zdalną metodę, wywołuje w rzeczywistości odpowiednią metodę namiastki zdalnego obiektu (wzorzec projektowy Proxy). Namiastka tworzy pakiet informacji zawierający: identyfikator zdalnego obiektu, nazwę metody oraz zaszeregowane parametry, który następnie przesyłany jest za pomocą gniazdek sieciowych do maszyny, na której działa zdalny obiekt (dla każdego wywołania metody tworzone jest odrębne połączenie sieciowe). Do nadejścia odpowiedzi, namiastka blokuje działanie obiektu klienta. Po stronie serwera, mechanizm RMI odbiera żądania wywołania metody i dla każdego z nich odpowiednio: rozszeregowuje parametry, lokalizuje zdalny obiekt, wywołuje metodę, pobiera wynik wywołania oraz odsyła wynik lub wyjątek do namiastki. Po tej operacji sterowanie z namiastki wraca do obiektu klienta. Cały proces przedstawimy na odpowiednim diagramie przepływu danych:

 

3. Implementacja zdalnego obiektu i generowanie namiastek

        Aby schemat, przedstawiony w punkcie poprzednim, był możliwy do zrealizowania, projekt RMI musiał zostać oparty na abstrakcyjnym podejściu w projektowaniu klas przy użyciu interfejsów. Miało to szczególne znaczenie przy implementacji wzorca projektowego Proxy dla zorganizowania interakcji miedzy obiektem klienta, a obiektem zdalnym, za pośrednictwem namiastki. Z punktu widzenia programu klienta, referencja do zdalnego obiektu, sprawia wrażenie zwykłej referencji. W rzeczywistości jest to referencja do obiektu namiastki. Aby taka iluzja miała miejsce, interfejsy zdalnego obiektu oraz namiastki muszą być zgodne. Tylko te metody, które są zdefiniowane w tym interfejsie będą dostępne zdalnie. Pozostałe metody zdalnego obiektu, będą dostępne jedynie lokalnie. 

przykład zdalnego obiektu:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.rmi.Remote;
import java.rmi.RemoteException;

//interfejs
interface SimpleDatetimeServer extends Remote{
       public String getDescription() throws RemoteException;
       public long getTime() throws RemoteException;
}

//implementacja
public class SimpleDatetimeServerImpl implements SimpleDatetimeServer{
       private String description;

       SimpleDatetimeServerImpl(String desc){
             description = desc;
       }

       //implementacja metod ze zdalnego interfejsu
       public String getDescription() throws RemoteException{
             return description;
       }

       public long getTime() throws RemoteException{
             return System.currentTimeMillis();
       }

       //implementacja metod lokalnych
      
public void setDescription(String desc){
             description = desc;
       }
}

    Proces tworzenia zdalnego obiektu można przedstawić w kilku punktach:

  1. Tworzymy interfejs zdalnych metod. W naszym przypadku będzie to SimpleDatetimeServer, definiujący metody dostępu dla opisanego wcześniej serwera świadczącego usługi czasu. Te metody to: getDescription() - podająca informację o serwerze oraz getTime() - zwracająca aktualny czas na serwerze. Interfejs ten musi spełniać dwa wymogi: musi rozszerzać interfejs Remote z pakietu java.rmi oraz deklarować możliwość wyrzucenia wyjątku RemoteException dla wszystkich swoich metod (dlatego, że istnieje wiele różnorakich przyczyn z powodu których wywołanie metody może się nie powieść).

  2. Implementujemy interfejs z punktu [1]. W naszym przypadku implementacją będzie klasa SimpleDatetimeServerImpl, która oprócz swoich lokalnych metod, będzie zawierała implementację wszystkich metod z tego interfejsu  - tylko te metody będą dostępne zdalnie.

  3. Kod klasy i interfejsu rozdzielamy do odpowiednich plików:
        SimpleDatetimeServer.java
        SimpleDatetimeServerImpl.java;

  4. Pliki w zadanej kolejności kompilujemy poleceniami:

        javac SimpleDatetimeServer.java
        javac SimpleDatetimeServerImpl.java

    lub wszystkie na raz: javac *.java;

  5. Generujemy namiastki dla zdalnego obiektu*. Dokonujemy tego kompilatorem rmic (dostepnym standardowo razem z kompilatorem javac w katalogu bin instalacji Javy) wywołując polecenie:

        rmic -v1.2 SimpleDatetimeServerImpl 

    (uwaga: w przeciwieństwie do javac, dla rmic podajemy jedynie nazwę klasy - bez rozszerzenia .java).


    Po wykonaniu tych czynności, nasz zdalny obiekt jest już prawie gotowy do użycia. Aby faktycznie stał się zdalnym obiektem, musimy sprawić, że będzie on dostępny poprzez sieć dla klienta. W tym celu musimy: dostarczyć mechanizm, który będzie odbierał komunikaty od klientów, lokalizował odpowiedni obiekt, wywoływał metodę lokalną i zwracał dane lub wyjątek oraz mechanizm, który pozwoli klientowi zlokalizować potrzebny obiekt. Oba te aspekty będą omówione w punkcie następnym.

Podsumowując:

  1. zdefiniowaliśmy interfejs zdalnego obiektu
  2. dostarczyliśmy jego implementację
  3. wygenerowaliśmy namiastkę zdalnego obiektu

*W javie 1.5 namiastki są generowane dynamicznie w czasie wykonania przez mechanizm RMI

4. Eksport zdalnego obiektu, usługa rejestru nazw i sposób uzyskiwania referencji przez obiekt klienta.

        Aby zdalny obiekt był osiągalny zdalnie, musimy dostarczyć mechanizm, który to umożliwi. Programista może taki mechanizm zaimplementować samodzielnie, korzystając z klas znajdujących się w pakiecie java.rmi.*, jednakże w większości przypadków nie będzie to konieczne. Typowe podejście, to użycie już istniejącej implementacji, np. klasy UnicastRemoteObject, dostarczonej przez producenta. Wspomniana klasa, jak i inne klasy świadczące usługi serwera dla zdalnego obiektu, muszą rozszerzać klasę RemoteServer. UnicastRemoteObject będzie dostarczać usługi serwera dla zdalnego obiektu, rezydującego na pojedynczej maszynie, poprzez protokół sieciowy TCP (można sobie wyobrazić inne podejścia, np. klasę MulticastRemoteObject - replikowanych zdalnych obiektów lub klasy obsługujące inne protokoły - np. UDP). 
        W jaki sposób możemy wykorzystać klasę UnicastRemoteObject? - dwojako. Pierwsza możliwość, to sprawienie, aby klasa będąca implementacją naszego zdalnego obiektu, oprócz implementowania swojego "zdalnego" interfejsu, rozszerzała klasę UnicastRemoteObject. Wybierając taką opcję, odpowiedni serwer obsługi zdalnych wywołań metod dla tego obiektu będzie tworzony w czasie tworzenia naszego obiektu (z tego też powodu konstruktor naszej klasy musi wtedy deklarować możliwość wyrzucenia wyjątku RemoteException). Jeżeli jednakże chcielibyśmy aby nasza klasa rozszerzała jakąś inną klasę, np. pochodzącą z warstwy logiki naszej aplikacji, to nie możemy expicite rozszerzyć klasy UnicastRemoteObject - możemy natomiast stworzyć odpowiedni serwer ręcznie. dokonujemy tego za pomocą statycznej metody exportObject() klasy UnicastRemoteObject, której wywołanie sprawi, że nasz nasz zdalny obiekt będzie dostępny (istnieje kilka wariantów tej metody - w jednej z nich możemy podać numer portu TCP, na jakim serwer ma nasłuchiwać żądań klientów).

Kolejna sprawa, to sposób uzyskania dostępu do zdalnych obiektów przez program klienta. Jak wiemy, aby klient mógł korzystać ze zdalnego obiektu, potrzebuje jego namiastki. Jedna z możliwości jest taka, że otrzyma ją poprzez wywołanie metody jakiegoś innego zdalnego obiektu, którego namiastkę już posiada np. jako rezultat wywołania tej metody (o przekazywaniu referencji do zdalnych obiektów jako parametrów wywołań metod innych zdalnych obiektów lub wyników wywołań tych metod, będzie jeszcze mowa w dalszej części). Zawsze jednakże trzeba zlokalizować pierwszy ze zdalnych obiektów, a nie możemy tego dokonać w opisany sposób, bo dostępu do innych zdalnych obiektów wtedy jeszcze nie mamy. Aby rozwiązać tą sytuację musimy użyć tzw. usługę rejestru początkowego. Usługa rejestru początkowego będzie pewnym rezpozytorium zdalnych obiektów, której adres sieciowy będzie ogólnie znany. Aby zarejestrować zdalny obiekt w rejestrze, w programie serwera, przed wyeksportowaniem obiektu, napiszemy:

        Naming.bind("nazwa_obiektu", ref);

gdzie nazwa_obiektu, to identyfikator znany klientowi (jeżeli usługa rejestru początkowego znajduje się na innej maszynie niż obiekt, który chcemy zarejestrować, to wtedy  nazwa_obiektu = "//host:port/"+nazwa_obiektu), a ref, to referencja do naszego obiektu (Oprócz metody bind(), istnieją metody: rebind() oraz unbind()). Samą  usługę rejestru początkowego uruchamiamy, albo wpisując w programie:

LocateRegistry.createRegistry(port);

albo uruchamiamy ją z zewnątrz, wpisując w linii poleceń interpretatora poleceń:

[unix] rmiregistry & 
[dos,win] start rmiregistry

gdzie rmiregistry, to narzędzie dostarczane razem z instalacją javy (& oraz start spowodują, że rmiregistry uruchomi się w tle)

Natomiast klient chcący uzyskać zdalną referencję (namiastkę) do zdalnego obiektu napisze:

Typ o = (Typ)Naming.lookup(url);

gdzie Typ będzie oznaczać typ zdalnego obiektu (jego "zdalny" interfejs), a url, to nazwa obiektu wraz z informacją o położeniu rejestru nazw w formacie URL (przykładowo url = "rmi://twojserwer.pl/nazwa_obiektu"). Po wywołaniu metody lookup(), zostanie ściągnięta namiastka zdalnego obiektu (metoda lookup() jest uniwersalna, więc musimy rzutować typ ogólny Remote do interesującego nas typu).

Wiedząc już w jaki sposób sprawić, że zdalny obiekt będzie dostępny w sieci i w jaki sposób klient może uzyskać do niego dostęp, przystąpimy teraz do napisania prostej aplikacji z wykorzystaniem naszego obiektu SimpleDatetimeServerImpl.

5. Implementacja programów klienta i serwera

Program serwera:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import java.rmi.*;
import java.rmi.server.*;
import java.rmi.registry.*;
import java.util.Date;
import java.net.*;
import java.io.*;
import java.security.*;

public class SimpleDatetimeServerApp{
       public static void main(String[] s){
              boolean err = false;
              int registryPort = 8000;
              try{
                     SimpleDatetimeServer server = 
                                   new SimpleDatetimeServerImpl("Simple datetime server: v. 1.0.0");
                     LocateRegistry.createRegistry(registryPort);
                     UnicastRemoteObject.exportObject(server);
                     Naming.rebind("//localhost:"+registryPort+"/dServer", server);
              }
              catch(java.rmi.UnknownHostException uhe){
                     System.out.println("[error] podana nazwa hosta nie
                                                         jest \n identyfikatorem tego komputera\n"+uhe+"\n");
                     err = true;
              }
              catch(AccessControlException ace){
                     System.out.println("[error] nie masz uprawnien aby 
                                   uruchomic serwer\n na tym porcie dla podanej nazwy hosta\n"+ace+"\n");
                     err = true;
              }
              catch(RemoteException re){
                     System.out.println("[error] nie udało się zarejestrować \n
                                                                       zdalnego obiektu serwera\n"+re+"\n");
                     err = true;
              }
              catch(MalformedURLException mURLe){
                     System.out.println("[error] wewnętrzny błąd" + mURLe+"\n");
                     err = true;
              }
              catch(Exception ee){
                     System.out.println("[error] cccc"+ee.getMessage()+"\n");
                     err = true;
              }
              if(!err)
                     System.out.println("\n[OK] Simple datetime server running...\n");

       }
}

Przedstawiony kod programu jest najprostszym z możliwych. Jedynym jego zadaniem jest: poprawne stworzenie zdalnego obiektu (wiersze 14,15), uruchomienie usługi rejestru początkowego (wiersz 16), uczynienie zdalnego obiektu osiągalnym dla klienta (wiersz 17) oraz dodanie go do rejestru (wiersz 18). Wszystkie te metody należy wywołać w bloku try{}catch(), gdzie należy zorganizować obsługę wyjątków. Należy zauważyć, że program po opuszczeniu metody main() nie kończy swojego działania, gdyż poza wątkiem głównym, został stworzony wątek w którym mechanizm RMI nasłuchuje połączeń do klientów. Aby zakończyć działanie serwera należy wywołać UnicastRemoteObject.unexportObject() dla wszystkich zdalnych obiektów, które stworzyliśmy.

program klienta:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.rmi.registry.*;
import java.rmi.*;
import java.text.*;
import java.util.*;

public class SimpleDatetimeClientApp{
        public static void main(String[] s){
        int registryPort = 8000;
        SimpleDatetimeServer server = null;
        try{
                System.setSecurityManager(new RMISecurityManager());
                server = (SimpleDatetimeServer)Naming.lookup("//localhost:"+registryPort+"/dServer");
                String desc = server.getDescription();
                System.out.println("\n\npolaczenie z serwerem "+desc+" przebieglo pomyslnie");
                long time = server.getTime();
                SimpleDateFormat df = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
                StringBuffer data = new StringBuffer();
                System.out.println("czas na serwerze: "+df.format(new Date(time), data, 
                                                                                new FieldPosition(0))+"\n\n");                 
        }
        catch(Exception ex){
                System.out.println(ex);
        }
}

Program klienta również jest bardzo prosty, a jego działanie można opisać w kilku zdaniach: w wierszu 11 instalujemy specjalnego menedżera bezpieczeństwa, który będzie nadzorować m. in. poprawność procesu ściągania i ładowania namiastek (instalacja tego menedżera po stronie jest wymagana); w wierszu 12 pobieramy referencję do namiastki zdalnego obiektu poprzez usługę rejestru początkowego, którą przypisujemy zmiennej server; w wierszach 13 oraz 15 wywołujemy zdalne metody: getDescription(), która zwróci nam informacje na temat serwera oraz getTime() - zwracająca czas na serwerze. W liniach 16-18 formatujemy otrzymany znacznik czasu i wyświetlamy wynik na konsoli. Po wyjściu z metody main() program klienta kończy działanie.


Należy sobie zdawać sprawę z tego, ze przedstawiona aplikacja prezentuje jedynie absolutne minimum jeśli chodzi o zastosowania RMI. Istnieje wiele złożonych aspektów, które projektant złożonych aplikacji sieciowych musi rozważyć. Do najważniejszych można zaliczyć: problemy dostępu do danych w środowisku rozproszonym, synchronizacja czy najbardziej zaawansowane takie jak migracja zdalnych obiektów. Niektóre z tych aspektów zostaną przybliżone w dalszej części tego opracowania.

Spakowaną aplikację można ściągnąć tutaj


6. Wdrożenie:

Aby nie komplikować sprawy, całą aplikację zawierającą zarówno klasy serwera jak i klienta umieścimy w jednym katalogu. W rzeczywistej sytuacji jednakże być może chcielibyśmy te dwa odrębne programy od siebie oddzielić. Jeżeli decydujemy się na takie rozwiązanie, to musimy upewnić się, że skompilowane klasy namiastek umieścimy zarówno po stronie aplikacji klienta jak i serwera.

        a) konfiguracja polityki bezpieczeństwa maszyny wirtualnej Javy

Zanim uruchomimy obie aplikacje, należy się upewnić czy polityka bezpieczeństwa Javy jest odpowiednio skonfigurowana. W szczególności, czy program serwera może nasłuchiwać na wskazanym porcie, a program klienta wykonywać połączenia skierowane na wskazany adres i port. Możemy albo zmodyfikować (jeżeli mamy odpowiednie prawa) plik java.policy znajdujący się w katalogach z instalacją Javy, lub przygotować wersję tego pliku specjalnie dla nasze aplikacji. Taki plik może mieć następującą postać:

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

         b) uruchomienie programu serwera

serwer uruchamiamy poleceniem:

java -Djava.security.policy=java.policy SimpleDatetimeServerApp
(parametrem -D wskazujemy nasz plik polityki bezpieczeństwa)

        c) uruchomienie programu klienta

program klienta uruchamiamy poleceniem:

java -Djava.security.policy=java.policy SimpleDatetimeClientApp

7. Problemy przekazywania parametrów metod

Z problemem przekazywania parametrów (tudzież zwracania wyników) przez zdalnie wywoływane metody wiąże się szereg różnych mniej lub bardziej złożonych aspektów, o których programista RMI powinien mieć świadomość. W tym punkcie omówimy najważniejsze z nich. Będą to: przekazywanie parametrów przez wartość, przekazywanie referencji do zdalnych obiektów jako parametrów wywołań metod innych zdalnych obiektów oraz problem migracji zdalnego obiektu.

Jedynym sposobem na przekazanie parametrów metodom lokalnych obiektów jest przekazanie ich przez tzn. uchwyt, często utożsamiany z referencją (w rzeczywistości jest to kopia referencji). Wydaje się oczywiste, że takiego mechanizmu nie można zastosować przy wywołaniu metody obiektu zdalnego, działającego na innej maszynie wirtualnej. W szczególności, przesyłając dowolny obiekt lokalny w wywołaniu metody, musielibyśmy dbać o to, aby, wszelkie odwołania do takiego obiektu na maszynie wirtualnej serwera, odnosiły się w rzeczywistości do maszyny wirtualnej klienta (tzn. tam, gdzie ten obiekt znajduje się fizycznie). Doszlibyśmy wtedy do sytuacji, w której obiekty dowolnego typu, musiałyby być obiektami zdalnymi. Projektanci RMI rozwiązali ten problem w sposób następujący: wszystkie obiekty lokalne, tzn. takie, których typ nie posiada w swojej hierarchii dziedziczenia interfejsu Remote, są przesyłane przez wartość, przy pomocy standardowego mechanizmu serializacji w Javie. Implikuje to od razu fakt, że tylko obiekty klas serializowalnych mogą być tą drogą przesłane. Obiekty klas nieserializowalnych nie mogą być w żaden sposób przesyłane poprzez zdalne wywołanie metody. 

Z drugiej strony, wszystkie obiekty zdalne, tzn. takie, które implementują interfejs Remote (oczywiście przy spełnieniu tych wszystkich wymagań, o których pisaliśmy wcześniej) są przesyłane poprzez swoją zdalną referencję. Przesyłanie przez zdalną referencję oznacza, że przesyłana jest w rzeczywistości namiastka zdalnego obiektu, za pomocą której zdalny obiekt, do którego taki parametr trafi, będzie mógł się z nadawcą komunikować. Oznacza to, że praktycznie po pierwszym użyciu usługi rejestru początkowego, i zlokalizowaniu pierwszego zdalnego obiektu, wszystkie obiekty zdalne mogą uzyskać referencję do innych zdalnych obiektów za pomocą wywołania zdalnej metody obiektu, do którego referencję już posiadają. Jednym z zastosowań tego mechanizmu jest możliwość zorganizowania obustronnej komunikacji między zdalnymi obiektami - zwanej komunikacją ze sprzężeniem zwrotnym (ang. callback communication). 

przykład: serwer rozsyłający cyklicznie pewną informację do klientów. Taki serwer możemy zaprojektować następująco. Niech obiektami zdalnymi będą: CallbackServer oraz CallbackClient. CallbackServer będzie posiadał przynajmniej jedną zdalną metodę nazwaną register(CallbackClient client), a klient metodę sendMessage(String msg). Schemat działania takiej aplikacji będzie następujący. Każdy klient, na początku swojego działania, będzie uzyskiwał referencję do zdalnego obiektu serwera poprzez usługę rejestru początkowego. Po otrzymaniu takiej referencji wywoła metodę obiektu serwera register(this), co oznaczać będzie przekazanie serwerowi, poprzez parametr metody, swojej własnej namiastki. W ciele metody register() zwyczajowo referencja do namiastki klienta zostanie dodana do listy lub zbioru, o synchronizowanym dostępie (wywołania zdalnych metod działają podobnie jak wywołania metod lokalnych w środowisku wielowątkowym. Z tego powodu programista musi wziąć pod uwagę wszelkie następstwa wynikające z tego faktu. W szczególności - musi zsynchronizować dostęp do współdzielonych obiektów np. przy użyciu standardowego mechanizmu w postaci słowa kluczowego synchronize). Serwer, jeżeli zajdą odpowiednie okoliczności, będzie informował o nich wszystkich zarejestrowanych klientów, wywołując metodę sendMessage(msg) obiektów namiastek.

Mechanizm sprzężeń zwrotnych ma jednakże tą wadę (lub zaletę, jeżeli by popatrzeć na to z innej strony), że zaciera się granica pomiędzy serwerem, a klientem. Serwer, jeżeli wywołuje metody zdalnego obiektu klienta, sam staje się klientem, a klient serwerem. implikuje to niekorzystne w pewnych zastosowaniach zjawisko, że fizycznie podzielona aplikacja na program klienta i program serwera, staje się w rezultacie jednym wielkim serwerem. Problem polega na tym, że zwyczajowo aplikacje klientów działają na systemach zabezpieczonych np. zaporami sieciowymi skonfigurowanymi dla poprawnego działania aplikacji klientów, tzn. takich, które nawiązują połączenia, a nie je odbierają. Jeżeli użyjemy mechanizmu sprzężeń zwrotnych, może się okazać, że system zablokuje przychodzące połączenia od serwera i program nie zadziała. Przypominamy, że dla każdego wywołania zdalnej metody tworzone jest odrębne połączenie sieciowe, a standardowe fabryki gniazdek sieciowych, używane przez RMI, nie pozwalają na wtórne wykorzystanie połączenia - np. tego, które zostało otwarte w momencie kiedy klient rejestrował swój obiekt u serwera. Rozwiązaniem tej niekorzystnej sytuacji jest dostarczenie mechanizmowi RMI specjalnie przygotowanych fabryk gniazdek sieciowych, które pozwolą na wtórne wykorzystanie połączeń (niestety, autorowi znane są jedynie komercyjne biblioteki tego typu), lub zastąpienie mechanizmu sprzężeń zwrotnych innym mechanizmem, który będzie symulował komunikację obustronną przy użyciu komunikacji asynchronicznej, przy której tylko fizycznie wyodrębniony klient łączy się z fizycznie wyodrębnionym serwerem. Prosty algorytm tego typu jest przedstawiony poniżej:

Należy sobie zdawać sprawę z tego, że przedstawione rozwiązanie działa tylko wtedy, gdy komunikacja obustronna polega na tym, że serwer jedynie informuje o czymś klienta. Jeżeli natomiast serwer potrzebuje informacji od klienta, tzn. wywołałby zdalną metodę dla jej wyniku, to musimy zastanowić się nad innym rozwiązaniem, czego nie zrobimy w tym opracowaniu.

Innym aspektem problemu przekazywania parametrów w zdalnych metodach jest migracja zdalnego obiektu, czyli zmiana jego środowiska działania z jednej maszyny wirtualnej na inną, co jest istotne w obliczeniowych systemach rozproszonych wykorzystujących dynamiczne równoważenie narzutu obliczeniowego na węzłach. Jeżeli ziarno obliczeń utożsamiamy ze zdalnym obiektem, to równoważenie narzutu będziemy utożsamiać z migracją zdalnego obiektu z jednej maszyny na inną. Niestety, RMI nie dostarcza żadnych standardowych mechanizmów, za pomocą których moglibyśmy taką operację przeprowadzić. Jeżeli zdalnej metodzie przekażemy zdalny obiekt, to zostanie przesłana jego namiastka, tak jak to opisywaliśmy. Aby fizycznie przemieścić zdalny obiekt, musimy ponownie posiłkować się pomysłowością i inwencją twórczą. Przykładowe rozwiązanie może być takie:

  1. Przy konstrukcji zdalnego obiektu zastosujmy model delegacji. Tzn. oddzielmy od zdalnego obiektu wszystkie dane (pola obiektu), które są niezbędne w odtworzeniu jego stanu, i umieśćmy je w innym, lokalnym, serializowalnym obiekcie. Obiekt ten będzie posiadać odpowiedniki wszystkich metod obiektu zdalnego, które manipulują na tych danych.

  2. Związek pomiędzy obojgiem obiektów zaprojektujmy jako agregacja całkowita obiektu lokalnego, w obiekcie zdalnym. 

  3. Wywołania wszystkich zdalnych metod wyszczególnionych w [1] będziemy delegować do agregowanego obiektu lokalnego.

  4. Jeżeli będziemy chcieli przeprowadzić migracje zdalnego obiektu, to prześlemy, nie zdalny obiekt, ale jego lokalny odpowiednik. Oczywiście, po drugiej stronie musimy dla niego stworzyć nowy zdalny obiekt i uaktualnić referencje do niego w całym systemie, co jest operacją najbardziej skomplikowaną;

8. Literatura 

[1] Specyfikacja RMI - http://java.sun.com/j2se/1.3/docs/guide/rmi/spec/rmiTOC.html;
[2] B. Eckel; Thinking in Java; wydanie polskie - 2001 Helion (dostępna w internecie na stronie www.bruceeckel.com);
[3] C. S. Horstmann, G. Cornell; Core Java 2 - techniki zaawansowane; wydanie polskie - 2002 Helion.
[4] http://java.sun.com