c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
1
Java RMI – Remote Method Invocation
RMI
to podstawowy mechanizm typu RPC w języku
Java
. W przeciwieństwie do Sun RPC jest to
mechanizm w pełni obiektowy i nowoczesny. Nie jest to przy tym jedyne narzędzie tego typu dostępne w
języku Java – można używać także np.
CORBA
.
W przeciwieństwie do RPC czy CORBA, RMI nie wprowadza żadnych nowych elementów wykraczających
poza sam język. Językiem specyfikacji interfejsu jest tu sama Java. Szeregowanie danych odbywa się
przez interfejs, który musi być implementowany przez przekazywane obiekty. W efekcie cały mechanizm
doskonale wplata się w program, nie ma potrzeby sprawdzać, jak zostały przetłumaczone deklaracje typów
z interfejsu, czy inne temu podobne elementy. Jeśli program jest napisany w przemyślany sposób, to poza
inicjalizacją, tzn. pobraniem referencji do pierwszego zdalnego obiektu, w zasadzie nie widać w kodzie, że
używa się zdalnych obiektów.
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
2
Definicja interfejsu w RMI
Interfejs w RMI jest po prostu interfejsem w sensie języka Java. W efekcie niemożliwy jest bezpośredni
zdalny dostęp do pól obiektu, można jedynie wołać jego metody. Jest to zresztą zgodne z duchem zarówno
Javy, jak i mechanizmów RPC.
Każdy interfejs zdalny musi spełniać dwa warunki:
• Musi rozszerzać interfejs
java.rmi.Remote
przez dziedziczenie pośrednie lub bezpośrednie.
• Wszystkie
jego metody muszą deklarować klauzulą
throws
możliwość rzucenia wyjątku
java.rmi.RemoteException
.
Interfejs nie wymaga żadnej nietypowej obróbki w celu uzyskania właściwego kodu źródłowego, jak w
przypadku IDL (CORBA), czy RPC – wystarczy, że właściwa klasa-serwer implementuje go.
Do obiektów zdalnych odwołuje się przez referencję do odpowiedniego interfejsu. Można też realizować
różne wersje interfejsów i, mając odpowiednią referencję, sprawdzać, którą wersję implementuje operatorem
instanceof
. W rzeczywistości referencja do obiektu zdalnego jest po prostu referencją na skojarzoną z
nim lokalną namiastkę.
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
3
Implementacja klasy serwera
Aby metody obiektu mogły być zdalnie dostępne, musi on spełniać dwa warunki:
• Implementować jakiś interfejs zdalny.
• Być wyeksportowany.
Eksport obiektu oznacza otwarcie odpowiedniego portu i uruchomienie nasłuchu. Najprościej uzyskać
ten efekt jest dziedzicząc po którejś z podklas klasy
java.rmi.server.RemoteServer
, najczęściej
java.rmi.server.UnicastRemoteObject
. Jeśli nie chce się, lub nie można dziedziczyć po tej kla-
sie, można to zastąpić „owinięciem” obiektu – wyeksportować obiekt, najczęściej w konstruktorze, metodą
static RemoteStub exportObject(Remote obj)
klasy
java.rmi.server.UnicastRemoteObject
(lub odpowiedniej innej). W obu wariantach można wymusić numer portu, lub pozostawić domyślny.
Usługi typu
UnicastRemoteObject
, najstarsze i najczęstsze, są dostępne od momentu ich wyeksporto-
wania do końca programu, lub odeksportowania metodą
static boolean unexportObject(Remote
obj, boolean force)
i przez cały czas muszą być aktywne, tj. rezydować w pamięci. Nie jest to jedyny
typ serwera, wrócimy jeszcze do tego tematu.
Gotowe klasy kompiluje się normalnie, po czym, dla gotowych już klas, wywołuje się generator namiastek
rmic
. Jego argumentem jest nie nazwa pliku, a nazwa klasy, łącznie z pakietem, jeśli jest określony. Dla
klasy
Klasa
z pliku
Klasa.class
zostaną wtedy wygenerowane klasy
Klasa_Stub
i
Klasa_Skel
. Klasa
_Stub
to namiastka dla klienta, będzie mu potrzebna, jak również plik ze skompilowanym interfejsem.
Klient nie musi natomiast mieć plików z klasami samego obiektu zdalnego, ani
_Skel
.
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
4
java.io.Serializable – szeregowanie obiektów
Podczas, gdy obiekty zdalne przekazywane są zawsze przez referencję, czy to jako argumenty, czy
wyniki funkcji w wywołaniach RMI, to przesyłanie obiektów lokalnych w RMI odbywa się przez wartość,
z zastosowaniem operacji szeregowania.
Każdy obiekt, który ma być przekazywany przez RMI powinien implementować (
implements
) interfejs
java.io.Serializable
. Interfejs ten nie zawiera żadnych metod, nie trzeba więc robić nic więcej. Jego
implementacja to jedynie zgoda na szeregowanie danego obiektu. Oczywiście klasy pochodne również
będą szeregowalne. Istotne jest natomiast ograniczenie w drugą stronę: klasę można zadeklarować jako
Serializable
tylko, jeśli jej nadklasy bez tej własności mają nieprywatny konstruktor bezparametrowy.
W tym przypadku szeregowane będą dostępne pola tej nadklasy, a więc nie prywatne – te zostaną
zrekonstruowane po drugiej stronie przez konstruktor.
Jeśli potrzebne jest nietypowe szeregowanie, można je osiągnąć implementując ten interfejs oraz definiując
następujące metody:
private void writeObject(java.io.ObjectOutputStream out)
throws IOException
private void readObject(java.io.ObjectInputStream in)
throws IOException, ClassNotFoundException;
w praktyce taka potrzeba nie powinna jednak wystąpić.
W wypadku napotkania przy szeregowaniu dynamicznej struktury danych na nieszeregowalny obiekt,
rzucany jest wyjątek
java.io.NotSerializableException
.
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
5
rmiregistry – rejestracja serwerów. Fabryki obiektów.
Ostatnim nierozwiązanym problemem jest nawiązywanie łączności między klientem a serwerem. RMI
dysponuje w tym celu rejestrem serwerów, uruchamianym poleceniem
rmiregistry
. Rejestr ten zapewnia
bardzo proste wyszukiwanie serwisów na podstawie nazwy.
Serwis identyfikowany jest na podstawie URL o postaci
[rmi:]//<komputer>[:<port>]/<nazwa
usługi>
, np.
//komputer.w.domenie.pl/MojSerwer
, czy
rmi://127.0.0.1/Zegar
. Pominięcie
nazwy komputera oznacza
localhost
, a port domyślny to 1099.
Uwaga:
to rzeczywiście URL, w efekcie
metody wyszukiwania i rejestrowania nazw mogą rzucić
MalformedURLException
.
Do obsługi rejestracji w
rmiregistry
służą statyczne metody klasy
java.rmi.Naming
(uwaga na
pominięte klauzule
throws
!):
1.
public static void bind(String name, Remote obj)
rejestruje nazwę
name
, o ile jest
wolna, kojarząc ją z obiektem
obj
;
2.
public static void unbind(String name)
wyrejestrowuje nazwę;
3.
public static void rebind(String name, Remote obj)
zmienia obiekt skojarzony z nazwą
na nowy;
4.
public static String[] list(String name)
podaje tablicę zarejestrowanych nazw (
name
jest w tym przypadku tylko URLem rejestru, bez nazwy serwisu);
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
6
5.
public static Remote lookup(String name)
wyszukuje serwis o podanej nazwie i zwraca
referencję do niego. Uwaga: tylko dlatego, że to wywołanie nie zawiodło, nie należy zakładać, że
otrzymana referencja jest tą poszukiwaną! Pod daną nazwą może być zarejestrowany zupełnie inny
serwis. Pomocny tu jest operator
instanceof
.
Rejestr RMI jest bardzo prosty, a jednocześnie rzadko wykorzystywany. Typowy program RMI korzysta z
niego tylko raz, w celu początkowego nawiązania łączności. Stosuje się tu mechanizm
fabryki
: rejestrowany
jest jeden obiekt, którego metody tworzą na żądanie odpowiednie obiekty – serwery różnych usług
– i zwracają referencje do nich. Te obiekty mogą potem tworzyć następne, itp. Referencja zdalna
może być przekazywana między komputerami, więc dowolne dwa obiekty mogą wymienić informację o
położeniu trzeciego. Ponieważ składniowo nie różni się to niczym od zwykłego przekazywania referencji do
obiektów lokalnych, od momentu uzyskania pierwszej zdalnej referencji RMI staje się właściwie przejrzyste
dla programisty, o ile pominąć konieczność łapania
RemoteException
i jeśli wszystkie zdalne obiekty
eksportują się już w konstruktorze.
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
7
RMI a zarządzanie pamięcią
Choć RMI jest proste w użyciu, jego protokół jest bardzo złożony. Główną tego przyczyną jest rozproszone
zwalnianie pamięci (distributed garbage collection).
Dla każdego obiektu zdalnego utrzymywany jest licznik zdalnych referencji. Protokół RMI zapewnia
odpowiednią jego aktualizację. Obiekty zdalne mogą być zniszczone dopiero, kiedy nie ma do nich ani
zdalnych, ani lokalnych referencji.
Aby serwer np. kończył sam pracę, kiedy nie jest już potrzebny, przydatna może być informacja o zniknięciu
ostatniej zdalnej referencji. W takiej sytuacji serwer powinien implementować interfejs
Unreferenced
,
zawierający tylko jedną, bez parametrową metodę
unreferenced()
, którą można wypełnić wedle uznania,
a która jest wywoływana przez RMI w momencie stwierdzenia opróżnienia listy referencji do danego obiektu.
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
8
Zaawansowane możliwości RMI
Tunelowanie przez HTTP
– RMI jest w stanie zapewnić łączność nawet w przypadku zablokowania łączności
bezpośredniej przez np. firewall. W tym przypadku można skorzystać z protokołu HTTP.
Ściąganie namiastek
– RMI może automatycznie ściągać namiastki serwerów, z których chce korzystać.
Wymaga to co prawda nieco więcej pracy (trzeba m.in. utworzyć menedżera bezpieczeństwa), ale może
się przydać, choć rzadko w obliczeniach równoległych.
Uruchamianie serwisów na żądanie
– już od wersji 1.2 Javy dostępny jest obok
java.rmi.server.Unicast-
RemoteObject
także serwer
java.rmi.activation.Activatable
. Klasa ta, wraz z zestawem klas
pomocniczych, zapewniają w dość skomplikowany sposób uruchamianie serwisów na żądanie. W szcze-
gólności oznacza to, że referencje zdalne mogą zachowywać ważność po zniszczeniu obiektu, do którego
się odwoływały, a nawet po restarcie maszyny wirtualnej, na której się znajdował, co nie ma miejsca dla
UnicastRemoteObject
.
Użycie CORBA
– RMI korzysta z własnego protokołu JRMP (Java Remote Method Protocol). Od wersji
1.2 Java zawiera jednak ORB i może używać oprócz RMI także CORBA. W wersji 1.3 pojawiło się nowe
narzędzie – RMI over IIOP (Internet Inter-ORB Protocol). Dzięki nowym opcjom
rmic
można napisać
interfejs RMI, a następnie wygenerować namiastki i szkielety używające CORBA, a także odpowiedni plik
w języku IDL.
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
9
Prosty przykład programu
interfaces.java
interface Factory extends java.rmi.Remote {
Server get() throws java.rmi.RemoteException;
}
interface Server extends java.rmi.Remote {
void hello(Client me) throws java.rmi.RemoteException;
void forget() throws java.rmi.RemoteException;
String say() throws java.rmi.RemoteException;
}
interface Client extends java.rmi.Remote {
String what() throws java.rmi.RemoteException;
}
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
10
implementations.java
import java.lang.*;
class theFactory implements Factory {
theFactory() throws java.rmi.RemoteException
{
java.rmi.server.UnicastRemoteObject.exportObject(this);
}
public Server get() throws java.rmi.RemoteException
{
return new theServer();
}
public static void main(String args[])
{
try {
theFactory maker = new theFactory();
java.rmi.Naming.rebind("//127.0.0.1/Fabryka",maker);
System.out.println("Fabryka zarejestrowana");
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
11
}
catch (Exception e) {
System.out.println("Nie zarejestrowano fabryki! Wyjątek: "
+ e.getMessage() );
}
}
}
class theServer implements Server {
Client he;
theServer() throws java.rmi.RemoteException
{
he = null;
System.out.println("Nowy serwer!");
java.rmi.server.UnicastRemoteObject.exportObject(this);
}
public void hello(Client me) throws java.rmi.RemoteException
{
System.out.println("Rejestracja klienta");
he = me;
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
12
}
public void forget() throws java.rmi.RemoteException
{
System.out.println("Wyrejestrowanie klienta");
he = null;
}
public String say() throws java.rmi.RemoteException
{
try {
String text = he.what(); // Callback! To tak proste w RMI.
// Tak naprawdę he mogłoby
// być argumentem!
System.out.println(text);
return "Witaj! " + text;
}
catch (Exception e) {
System.out.println("Blad komunikacji:" + e.getMessage());
}
return null;
}
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
13
}
class theClient implements Client {
Server textmaster;
String tekst;
theClient() throws java.rmi.RemoteException
{
tekst = null;
textmaster = null;
java.rmi.server.UnicastRemoteObject.exportObject(this);
}
public String what() throws java.rmi.RemoteException
{
System.out.println("-- Pytanie! Wysylam tekst...");
return tekst;
}
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
14
public static void main(String args[])
{
try {
theClient me = new theClient();
me.tekst = "Test RMIkrofonu, 1, 2, 3...";
Factory dom = (Factory)java.rmi.Naming.lookup(
"//localhost/Fabryka");
System.out.println("Jest fabryka.");
me.textmaster = dom.get();
System.out.println("Jest serwer. Podanie referencji...");
me.textmaster.hello(me);
System.out.println("Praca...");
System.out.println( me.textmaster.say() );
System.out.println("Kasowanie referencji...");
me.textmaster.forget();
System.out.println("Koniec!");
me.textmaster = null;
me = null;
System.exit(0);
}
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
15
catch (Exception e) {
// Nieładnie tak ogólnie, ale to tylko mały
// program. Tu może być java.rmi.RemoteException
// lub java.rmi.NotBoundException.
System.out.println("Niepowodzenie!: " + e.getMessage() );
}
}
}
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
16
Wyniki
Kompilacja:
$ javac *.java
$ rmic theFactory theClient theServer
Serwer:
$ rmiregistry &
[1] 21708
$ java theFactory
Fabryka zarejestrowana
Nowy serwer!
Rejestracja klienta
Test RMIkrofonu, 1, 2, 3...
Wyrejestrowanie klienta
Klient:
$ java theClient
Jest fabryka.
Jest serwer. Podanie referencji...
Praca...
-- Pytanie! Wysylam tekst...
Witaj! Test RMIkrofonu, 1, 2, 3...
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
17
Kasowanie referencji...
Koniec!
c
Karbowski, Kozakiewicz, Szynkiewiczowie, 2003
18
Podsumowanie
RMI jest wyjątkowo udanym, choć nieco powolnym, narzędziem zdalnego wywoływania procedur. Jego
podstawową wadą jest przywiązanie do jednego języka (choć istnieje RMI over IIOP), w zamian jednak
integracja RMI w Javie jest wyjątkowo głęboka i cały mechanizm jest bardzo intuicyjny i wygodny.
Ewenementem jest użycie języka macierzystego do definicji interfejsu, jak również generowanie namiastek
i szkieletów z gotowych, skompilowanych klas. W praktyce RMI okazuje się jednym z najlepszych narzędzi
do szybkiego budowania prostych aplikacji rozproszonych.
Zastosowanie RMI w obliczeniach rozproszonych jest dość ograniczone, nie tyle nawet z powodu powolności
samego mechanizmu – jest wyraźnie wolniejszy od nieobiektowych, a nawet od CORBA – co z powodu
wad samego języka, który przystosowany jest raczej do innych celów.
Wywołania RMI są oczywiście blokujące, jak zwykle w RPC. W Javie wielowątkowość jest jednak
wbudowana w język, więc nie ma problemu z faktycznym zrównolegleniem obliczeń. Jedyną barierą
jest rozbudowana, obiektowa składnia, nie pasująca do większości matematycznych zastosowań, oraz
stosunkowo niska wydajność (choć nieporównanie lepsza, niż w początkach języka).