RMI - ćwiczenie pierwsze
Implementacja klienta i serwera Echo
Celem tego podstawowego ćwiczenia jest zrozumienie sposobu
projektowania i implementacji prostych aplikacji działających w oparciu
o technologię RMI. Poszczególne punkty tłumaczą jak zaprojektować
interfejs obiektu wywoływanego zdalnie, jak zaimplementować
serwer tego obiektu oraz w jaki sposób łączyć sie z tym serwerem z programu
klienta RMI.
Definicja interfejsów
Na początku definiujemy interfejs jakiego będzie używał serwer RMI.
Będzie to bardzo prosta aplikacja odbijająca napis do nadawcy.
Metody, które mogą być wywoływane zdalnie, należy zdefiniować
w interfejsie dziedziczącym z interfejsu (rozszerzającym
interfejs) java.rmi.Remote. Co więcej, każda z tych metod
musi zgłaszać wyjątek java.rmi.RemoteException
z uwagi na fakt, że jej wywołanie następuje zdalnie.
public interface Echo extends Remote {
String reply(String s) throws RemoteException;
}
Kompletny plik z definicją interfejsu:
Echo.java.
Implementacja serwera Echo
W czasie implementacji serwera w technologii RMI należy pamiętać
o następujących warunkach:
- Klasa serwera musi implementować zdefiniowany wyżej interfejs
oraz dziedziczyć z klasy
java.rmi.server.UnicastRemoteObject
public class EchoServer extends UnicastRemoteObject implements Echo
- Konieczny jest konstruktor, nawet jeżeli nie wykonuje on niczego,
ze względu na wspomnianą deklarację zgłaszania wyjątku
public EchoServer() throws RemoteException {
}
- Następnie należy zaimplementować metodę zadeklarowaną w interfejsie.
Ponieważ ma być to echo, dlatego metoda po prostu zwraca bez zmian
to, co otrzymała jako argument. Aby potwierdzic fakt wywołania metody
wypisujemy komunikat
public String reply(String s) {
System.out.println("EchoServer: odebrana wiadomosc "+s);
return s;
}
- Na początku metody main() musimy utworzyć obiekt
SecurityManager, który będzie decydował, jakie uprawnienia
będzie miała aplikacja, która jest dostępna z zewnątrz. W przypadku
RMI dobrze jest skorzystać z poręcznego
java.rmi.RMISecurityManager
public static void main(String[] args) {
System.setSecurityManager(new RMISecurityManager());
...
- Po utworzeniu obiektu serwera, musimy go wpisac do rejestru
(w Javie nazywanego Naming Service). Obiekt w rejestrze
zawsze figuruje pod pewną nazwą - podajemy ją w tym wypadku w linii
poleceń. Pod taką nazwą klient będzie mógł uzyskać referencję do
obiektu serwera. Obiekt java.rmi.Naming
w poniższym przykładzie jest obiektem statycznym i oznacza rejestr
znajdujący się na tym samym komputerze, co aplikacja.
String name = args[0];
try {
EchoServer server = new EchoServer();
Naming.rebind(name, server);
System.out.println("EchoServer: zarejestrowalem sie");
} catch (Exception e) {
System.err.println("EchoServer wyjatek: " + e.getMessage());
e.printStackTrace();
}
Kompletny plik z implementacją serwera Echo:
EchoServer.java.
Implementacja klienta Echo
W implementacji klienta trzeba wykonać następujące czynności:
- Podobnie jak w przypadku serwera zaraz na początku metody main()
powołujemy do życia obiekt RMISecurityManager
- Wywołujemy metodę lookup aby uzyskać z rejestru referencję
do obiektu serwera na podstawie nazwy (nazwę tę przekazujemy do programu
klienta także z linii komend). Metoda ta zwraca nam zdalną referencję
do obiektu serwera, którą możemy zrzutować na interfejs Echo (gdyż
wiemy że serwer implementuje ten interfejs). Termin zdalna referencja
fizycznie oznacza zwykłą, lokalną referencję, ale nie do samego obiektu
serwera (który wszak znajduje sie na odległej maszynie) a do instancji stub
serwera. Jak wiemy, stub serwera musi implementować ten sam interfejs Echo
(przez co rzutowanie jest możliwe).
String name = args[0];
Echo echo = (Echo) Naming.lookup(name);
- Następnie możemy już wywołać metodę reply tak, jak zwykłą
metodę obiektu
System.out.println(echo.reply(args[1]));
Kompletny plik z implementacją klienta Echo:
EchoClient.java.
Kompilacja
Zakładamy, że wszystkie pliki źródłowe mamy w jednym katalogu,
np. /home/user/rmi/echo.
- Kompilujemy wszystkie trzy pliki źródłowe w zwykły sposób
javac *.java
- Używamy polecenia rmic do wygenerowania stubu i skeletonu.
Polecenie to automatycznie wygeneruje pliki EchoServer_Stub.java oraz
EchoServer_Skeleton.java w tym samym katalogu a następnie pliki te
skompiluje. Otrzymujemy EchoServer_Stub.class i
EchoServer_Skeleton.class. Jeżeli chcemy oglądnąć pliki źródłowe
generowane przez ten kompilator należy użyć opcji -keep.
Uwaga! Należy pamiętać aby użyć narzędzia z tej
samej dystrybucji Java SDK z jakiej pochodzi użyty kompilator javac.
rmic EchoServer
which javac
/usr/java/j2sdk1.4.0_02/bin/javac
/usr/java/j2sdk1.4.0_02/bin/rmic EchoServer
Uruchamianie
- Przed uruchmieniem serwera należy zdefiniować uprawnienia,
które będą przysługiwały aplikacji. Ponieważ aplikacja ta ma
korzystać z sieci, dlatego musi miec prawo do otwierania portów
(w szczególności portu 1099 którego domyślnie używa RMI).
Dodatkowo dostępny port 80 (web server) będzie potrzebny przy kolejnych
ćwiczeniach dotyczących dynamicznego pobierania klas z sieci.
Zapisujemy to w pliku java.policy
w następujący sposób
grant {
permission java.net.SocketPermission "*:1024-65535", "connect,accept";
permission java.net.SocketPermission "*:80", "connect";
};
- W osobnym shellu uruchamiamy Naming Service (rejestr), poleceniem
rmiregistry
which javac
/usr/java/j2sdk1.4.0_02/bin/javac
/usr/java/j2sdk1.4.0_02/bin/rmiregistry
Uwaga! Jezeli otrzymamy błąd w rodzaju:
java.rmi.server.ExportException: Port already in use: 1099
oznacza to, że rejestr jest już uruchomiony na tym komputerze
i nie trzeba go powtórnie uruchamiać.
- Uruchomienie serwera wykonujemy jako aplikację javy,
podając dodatkowe opcje
- -Djava.rmi.server.codebase="URL" wskazuje adres URL do miejsca,
gdzie znajduje się stub obiektu serwera. Jest to informacja potrzebna
dla rejestru, aby mógł przekazać klientom referencje do serwera.
URL może mieć postać "file: ...". W naszym wypadku powinien on wskazywać
na katalog bieżący (pwd). W szczególności podawanie tej informacji
może nie być potrzebne, jeżeli rejestr RMI uruchomimy z tego samego
katalogu w którym znajduje się klasa EchoServer_Stub
(lub katalog w którym ta klasa się znajduje będzie wymieniony
w CLASSPATH). Uwaga! Jeżeli URL
wskazuje na katalog, wówczas musi być zakończony znakiem /
- -Djava.security.policy=java.policy wskazuje plik z uprawnieniami.
Jako parametr wywołania podajemy również nazwę, pod jaką chcemy
zarejestrować nasz obiekt w Name Service (np. echo123)
java -Djava.rmi.server.codebase=file:`pwd`/ -Djava.security.policy=java.policy EchoServer echo123
- W osobnym terminalu (konsoli) wykonujemy aplikację klienta.
Przekazujemy jej dwa parametry: nazwę, pod jaką zarejestrowany jest
serwer oraz napis, który ma być przekazany do serwera Echo
java -Djava.security.policy=java.policy EchoClient echo123 HelloWorld!
Odpowiedź powinna brzmieć
HelloWorld!
Zadanie
Proszę dodać do serwera drugą metodę (np. dodawanie dwóch liczb)
i zaimplementować odpowiedniego klienta.
Tomasz Gubała