9. JAVA RMI
9.1. Wprowadzenie
Przyjrzeliśmy się, jak tworzyć aplikacje sieciowe przy użyciu narzędzi komunikacyjnych o nazwie sockets. W aplikacji typu klient/serwer opartej na tych narzędziach połączeniem między klientem a serwerem jest protokół komunikacyjny, który przyjęły one do komunikacji. Obie aplikacje mogą być napisane w różnych językach: na przykład w Javie dla klienta, w Perlu dla serwera lub w dowolnej innej kombinacji. Mamy tu do czynienia z dwiema odrębnymi aplikacjami połączonymi protokołem komunikacyjnym znanym obu stronom. Ponadto dostęp do sieci za pośrednictwem gniazd (sockets) nie jest przezroczysty dla aplikacji Java: musi ona korzystać z klasy Socket, stworzonej specjalnie w celu obsługi narzędzi komunikacyjnych, jakimi są sockets.
JAVA RMI (Remote Method Invocation) umożliwia tworzenie aplikacji sieciowych o następujących cechach:
- Aplikacje typu klient/serwer to aplikacje Java po obu stronach komunikacji
- Klient może korzystać z obiektów znajdujących się na serwerze tak, jakby były one lokalne
- Warstwa sieciowa staje się przezroczysta: aplikacje nie muszą martwić się o to, w jaki sposób informacje są przesyłane z jednego punktu do drugiego.
Ostatni punkt stanowi czynnik zapewniający przenośność: gdyby warstwa sieciowa aplikacji RMI uległa zmianie, sama aplikacja nie musiałaby być przepisywana. To klasy RMI w języku Java będą musiały zostać dostosowane do nowej warstwy sieciowej.
Zasada komunikacji RMI jest następująca:
- Klasyczna aplikacja Java jest napisana na komputerze A. Będzie ona pełnić rolę serwera. W tym celu niektóre z jej obiektów zostaną „opublikowane” na komputerze A, na którym aplikacja jest uruchomiona, i staną się wówczas usługami.
- Klasyczna aplikacja Java jest tworzona na komputerze B. Będzie ona pełnić rolę klienta. Będzie miała dostęp do obiektów/usług opublikowanych na komputerze A, co oznacza, że za pośrednictwem odległego odwołania będzie mogła nimi manipulować tak, jakby były lokalne. W tym celu będzie musiała znać strukturę obiektu zdalnego, do którego chce uzyskać dostęp (metody i właściwości).
9.2. Nauczmy się na przykładzie
Teoria leżąca u podstaw interfejsu RMI nie jest prosta. Aby lepiej to zrozumieć, prześledzimy krok po kroku proces tworzenia aplikacji klient-serwer wykorzystującej pakiet RMI języka Java. Wybieramy aplikację, którą można znaleźć w wielu publikacjach dotyczących RMI: klient wywołuje jedną metodę obiektu zdalnego, który następnie zwraca mu ciąg znaków. Przedstawiamy tutaj niewielką odmianę: serwer powtarza to, co wysyła mu klient. W niniejszej publikacji przedstawiliśmy już podobną aplikację, opartą jednak na gniazdach (sockets).
9.2.1. Aplikacja serwerowa
9.2.1.1. Krok 1: interfejs obiektu/serwera
Obiekt zdalny to instancja klasy, która musi implementować interfejs Remote zdefiniowany w pakiecie java.rmi. Metody obiektu, które będą dostępne zdalnie, to te zadeklarowane w interfejsie wywodzącym się z interfejsu Remote:
import java.rmi.*;
// interfejs zdalny
public interface interEcho extends Remote{
public String echo(String msg) throws java.rmi.RemoteException;
}
W tym przypadku deklarujemy zatem interfejs interEcho, który określa metodę echo jako dostępną zdalnie. Metoda ta może wygenerować wyjątek klasy RemoteException, która obejmuje wszystkie błędy związane z siecią.
9.2.1.2. Krok 2: tworzenie obiektu serwera
W kolejnym etapie definiujemy klasę, która implementuje powyższy interfejs zdalny. Klasa ta musi być pochodną klasy UnicastRemoteObject, która posiada metody umożliwiające zdalne wywoływanie metod.
import java.rmi.*;
import java.rmi.server.*;
import java.net.*;
// klasa implementująca echo zdalne
public class srvEcho extends UnicastRemoteObject implements interEcho{
// konstruktor
public srvEcho() throws RemoteException{
super();
}// koniec konstruktora
// metoda realizująca echo
public String echo(String msg) throws RemoteException{
return "[" + msg + "]";
}// koniec funkcji echo
}// koniec klasy
W powyższej klasie znajduje się:
- metodę, która wyświetla komunikat
- konstruktor, który nie wykonuje żadnych czynności poza wywołaniem konstruktora klasy nadrzędnej. Znajduje się on tam w celu zadeklarowania, że może wygenerować wyjątek typu RemoteException.
Utworzymy instancję tej klasy za pomocą metody main. Aby obiekt/usługa była dostępna z zewnątrz, musi zostać utworzona i zarejestrowana w katalogu obiektów dostępnych z zewnątrz. Klient pragnący uzyskać dostęp do obiektu zdalnego postępuje bowiem w następujący sposób:
- zwraca się do usługi katalogowej na komputerze, na którym znajduje się żądany obiekt. Usługa ta działa na porcie, który klient musi znać (domyślnie 1099). Klient zwraca się do katalogu z prośbą o odnośnik do obiektu lub usługi, podając jej nazwę. Jeśli nazwa ta odpowiada nazwie obiektu lub usługi znajdującej się w katalogu, katalog zwraca klientowi odnośnik, za pomocą którego klient będzie mógł komunikować się ze zdalnym obiektem lub usługą.
- Od tego momentu klient może korzystać z tego zdalnego obiektu tak, jakby był on lokalny
Wracając do naszego serwera, musimy utworzyć obiekt typu srvEcho i zarejestrować go w katalogu obiektów dostępnych z zewnątrz. Rejestracja ta odbywa się za pomocą metody klasy rebind należącej do klasy Naming:
z
nazwa: nazwa, która zostanie przypisana do obiektu zdalnego
obj: obiekt zdalny
Nasza klasa srvEcho przyjmuje zatem następujący kształt:
import java.rmi.*;
import java.rmi.server.*;
import java.net.*;
// klasa implementująca echo zdalne
public class srvEcho extends UnicastRemoteObject implements interEcho{
// konstruktor
public srvEcho() throws RemoteException{
super();
}// koniec konstruktora
// metoda realizująca echo
public String echo(String msg) throws RemoteException{
return "[" + msg + "]";
}// koniec funkcji echo
// utworzenie usługi
public static void main (String arg[]){
try{
srvEcho serveurEcho=new srvEcho();
Naming.rebind("srvEcho",serveurEcho);
System.out.println("Serveur d’écho prêt");
} catch (Exception e){
System.err.println(" Erreur " + e + " lors du lancement du serveur d’écho ");
}
}// main
}// koniec klasy
Czytając poprzedni program, można odnieść wrażenie, że zatrzyma się on natychmiast po utworzeniu i zapisaniu usługi echo. Tak jednak nie jest. Ponieważ klasa srvEcho wywodzi się z klasy UnicastRemoteObject, utworzony obiekt działa w nieskończoność: nasłuchuje żądań klientów na porcie anonimowym, czyli wybranym przez system w zależności od okoliczności. Utworzenie usługi przebiega asynchronicznie: w przykładzie metoda main tworzy usługę i kontynuuje swoje działanie: wyświetli komunikat „Serwer echo gotowy”.
9.2.1.3. Krok 3: kompilacja aplikacji serwerowej
Na tym etapie możemy skompilować nasz serwer. Kompilujemy plik interEcho.java z interfejsu interEcho oraz plik srvEcho.java z klasy srvEcho. Otrzymujemy odpowiednie pliki .class: interEcho.class i srvEcho.class.
9.2.1.4. Krok 4: napisanie klienta
Piszemy klienta, któremu przekazujemy jako parametr plik URL z serwera echo i który
- odczytuje wiersz wpisany z klawiatury
- wysyła ją do serwera echo
- wyświetla odpowiedź, którą ten wysyła
- powraca do kroku 1 i zatrzymuje się, gdy wpisanym wierszem jest „koniec”.
W rezultacie otrzymujemy następujący klient:
import java.rmi.*;
import java.io.*;
public class cltEcho {
public static void main(String arg[]){
// składnia: cltEcho URLService
// weryfikacja argumentów
if(arg.length!=1){
System.err.println("Syntaxe : pg url_service_rmi");
System.exit(1);
}
// komunikacja klient-serwer
String urlService=arg[0];
BufferedReader in=null;
String msg=null;
String reponse=null;
interEcho serveur=null;
try{
// otwarcie strumienia klawiatury
in=new BufferedReader(new InputStreamReader(System.in));
// lokalizacja usługi
serveur=(interEcho) Naming.lookup(urlService);
// pętla odczytu wiadomości do wysłania na serwer echa
System.out.print("Message : ");
msg=in.readLine().toLowerCase().trim();
while(! msg.equals("fin")){
// wysyłanie wiadomości do serwera i odbieranie odpowiedzi
reponse=serveur.echo(msg);
// monitorowanie
System.out.println("Réponse serveur : " + reponse);
// następna wiadomość
System.out.print("Message : ");
msg=in.readLine().toLowerCase().trim();
}// while
// koniec
System.exit(0);
// obsługa błędów
} catch (Exception e){
System.err.println("Erreur : " + e);
System.exit(2);
}// try
}// main
}// klasa
W tym kliencie nie ma nic szczególnego, poza instrukcją żądającą referencji serwera:
Przypomnijmy, że nasza usługa echo została zarejestrowana w katalogu usług komputera, na którym się znajduje, za pomocą polecenia:
Naming.rebind("srvEcho",serveurEcho);
Klient również korzysta więc z metody klasy Naming, aby uzyskać identyfikator serwera, z którego chce skorzystać. Wykorzystywana metoda lookup przyjmuje jako parametr adres URL żądanego serwisu. Ma on postać klasycznego adresu URL:
gdzie
rmi: opcjonalne – protokół RMI
machine: nazwa lub adres IP komputera, na którym działa serwer echo – opcjonalne, domyślnie localhost.
port: port nasłuchowy usługi katalogowej na tym komputerze – opcjonalny, domyślnie 1099
nom_service: nazwa, pod którą zarejestrowano żądaną usługę (w naszym przykładzie srvEcho)
Pobierana jest instancja zdalnego interfejsu interEcho. Zakładając, że klient i serwer nie znajdują się na tym samym komputerze, podczas kompilacji klienta cltEcho.java w tym samym katalogu musi znajdować się plik interEcho.class, będący wynikiem kompilacji interfejsu zdalnego interEcho; w przeciwnym razie wystąpi błąd kompilacji w wierszach odwołujących się do tego interfejsu.
9.2.1.5. Krok 5: generowanie plików .class niezbędnych dla aplikacji klient-serwer
Aby dobrze zrozumieć, co znajduje się po stronie serwera, a co po stronie klienta, umieścimy serwer w katalogu echo\serveur, a klienta w katalogu echo\client.
Katalog serwera zawiera następujące pliki źródłowe:
E:\data\java\RMI\echo\serveur>dir *.java
INTERE~1 JAV 158 09/03/99 15:06 interEcho.java
SRVECH~1 JAV 759 09/03/99 15:07 srvEcho.java
Po skompilowaniu tych dwóch plików źródłowych otrzymujemy następujące pliki .class:
E:\data\java\RMI\echo\serveur>dir *.class
SRVECH~1 CLA 1 129 09/03/99 15:58 srvEcho.class
INTERE~1 CLA 256 09/03/99 15:58 interEcho.class
W katalogu klienta znajduje się następujący plik źródłowy:
a także plik interEcho.class, który został wygenerowany podczas kompilacji serwera:
Po kompilacji pliku źródłowego otrzymujemy następujące pliki .class:
E:\data\java\RMI\echo\client>dir *.class
CLTECH~1 CLA 1 506 09/03/99 16:08 cltEcho.class
INTERE~1 CLA 256 09/03/99 15:59 interEcho.class
Jeśli spróbujemy uruchomić klienta cltEcho, pojawi się następujący błąd:
E:\data\java\RMI\echo\client>j:\jdk12\bin\java cltEcho rmi://localhost/srvEcho
Erreur : java.rmi.UnmarshalException: error unmarshalling return; nested exception is:
java.lang.ClassNotFoundException: srvEcho_Stub
Jeśli spróbujemy uruchomić serwer srvEcho, pojawia się następujący błąd:
E:\data\java\RMI\echo\serveur>j:\jdk12\bin\java srvEcho
Erreur java.rmi.StubNotFoundException: Stub class not found: srvEcho_Stub; nested exception is:
java.lang.ClassNotFoundException: srvEcho_Stub lors du lancement du serveur d’écho
W obu przypadkach wirtualna maszyna Java zgłasza, że nie znalazła klasy srvEcho_stub. Rzeczywiście, nigdy wcześniej nie słyszeliśmy o tej klasie. W kliencie lokalizacja serwera została przeprowadzona za pomocą następującej instrukcji:
W tym przypadku urlservice to ciąg rmi://localhost/srvEcho z
Rmi: protokół RMI
Localhost: komputer, na którym działa serwer – w tym przypadku ten sam komputer, na którym działa klient. Składnia to zazwyczaj komputer:port. W przypadku braku portu domyślnie używany jest port 1099. Na tym porcie nasłuchuje usługa katalogowa serwera.
srvEcho: jest to nazwa konkretnej usługi, o którą się zwraca
Podczas kompilacji nie zgłoszono żadnych błędów. Wystarczyło jedynie, aby plik interEcho.class z interfejsu zdalnego był dostępny.
Podczas uruchamiania maszyna wirtualna wymaga obecności pliku srvEcho_stub.class, jeśli żądaną usługą jest usługa srvEcho; ogólnie rzecz biorąc, pliku X_stub.class dla usługi X. Plik ten jest potrzebny wyłącznie podczas wykonywania, a nie podczas kompilacji klienta. To samo dotyczy serwera. Czym więc jest ten plik?
Na serwerze znajduje się klasa srvEcho.class, która jest naszym obiektem/usługą zdalną. Klient, nawet jeśli nie potrzebuje tej klasy, potrzebuje jednak jej swego rodzaju obrazu, aby móc się z nią komunikować. W rzeczywistości klient nie kieruje swoich żądań bezpośrednio do obiektu zdalnego: kieruje je do swojego lokalnego obrazu srvEcho_stub.class znajdującego się na tej samej maszynie co on. Ten lokalny obraz srvEcho_stub.class komunikuje się z obrazem tego samego rodzaju (srvEcho_stub.class), znajdującym się tym razem na serwerze. Obraz ten jest tworzony na podstawie pliku .class znajdującego się na serwerze za pomocą narzędzia Java o nazwie rmic. W systemie Windows polecenie:
spowoduje, że na podstawie pliku srvEcho.class zostaną utworzone dwa kolejne pliki .class:
E:\data\java\RMI\echo\serveur>dir *.class
SRVECH~2 CLA 3 264 09/03/99 16:57 srvEcho_Stub.class
SRVECH~3 CLA 1 736 09/03/99 16:57 srvEcho_Skel.class
Znajduje się tam plik srvEcho_stub.class, który jest potrzebny zarówno klientowi, jak i serwerowi do działania. Znajduje się tam również plik srvEcho_Skel.class, którego rola na razie nie jest znana. Tworzymy kopię pliku srvEcho_stub.class w katalogu klienta i serwera, a następnie usuwamy plik srvEcho_Skel.class. Mamy zatem następujące pliki:
po stronie serwera:
E:\data\java\RMI\echo\serveur>dir *.class
SRVECH~1 CLA 1 129 09/03/99 15:58 srvEcho.class
INTERE~1 CLA 256 09/03/99 15:58 interEcho.class
SRVECH~1 CLA 3 264 09/03/99 16:01 srvEcho_Stub.class
po stronie klienta:
E:\data\java\RMI\echo\client>dir *.class
CLTECH~1 CLA 1 506 09/03/99 16:08 cltEcho.class
INTERE~1 CLA 256 09/03/99 15:59 interEcho.class
SRVECH~1 CLA 3 264 09/03/99 16:01 srvEcho_Stub.class
9.2.1.6. Krok 6: Uruchomienie aplikacji klient-serwer typu echo
Jesteśmy gotowi do uruchomienia naszej aplikacji klient-serwer. Na początku klient i serwer będą działać na tym samym komputerze. Najpierw należy uruchomić naszą aplikację serwerową. Przypomnijmy, że aplikacja ta:
- tworzy usługę
- rejestruje ją w katalogu usług komputera, na którym działa serwer echo
Ten ostatni punkt wymaga obecności usługi katalogowej. Uruchamia się ją za pomocą polecenia:
rmiregistry to usługa katalogowa. Jest ona tutaj uruchamiana w tle w oknie wiersza poleceń systemu Windows za pomocą polecenia start. Gdy katalog jest aktywny, można utworzyć usługę echo i zarejestrować ją w katalogu usług. Również w tym przypadku jest ona uruchamiana w tle za pomocą polecenia start:
Serwer echo działa w nowym oknie o nazwie DOS i wyświetla zgodnie z poleceniem:
Pozostaje nam tylko uruchomić i przetestować naszego klienta:
E:\data\java\RMI\echo\client>j:\jdk12\bin\java cltEcho rmi://localhost/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin
9.2.1.7. Klient i serwer na dwóch różnych komputerach
W poprzednim przykładzie klient i serwer znajdowały się na tym samym komputerze. Teraz umieścimy je na różnych komputerach:
- serwer na komputerze z systemem Windows
- klient na komputerze z systemem Linux
Serwer uruchamia się tak samo jak poprzednio na komputerze z systemem Windows. Na komputerze z systemem Linux przeniesiono pliki .class klienta:
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir
total 9
drwxr-xr-x 2 serge admin 1024 Mar 10 10:02 .
drwxr-xr-x 4 serge admin 1024 Mar 10 10:01 ..
-rw-r--r-- 1 serge admin 1506 Mar 10 10:02 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
-rw-r--r-- 1 serge admin 3264 Mar 10 10:02 srvEcho_Stub.class
Uruchomiono klienta:
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Erreur : java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
java.rmi.UnmarshalException: error unmarshalling call header; nested exception is:
java.rmi.UnmarshalException: skeleton class not found but required for client version
Mamy więc błąd: maszyna wirtualna Java najwyraźniej wymaga pliku srvEcho_skel.class, który został wygenerowany przez narzędzie rmic, ale do tej pory nie był używany. Tworzymy go ponownie i przenosimy również na maszynę z systemem Linux:
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir
total 11
drwxr-xr-x 2 serge admin 1024 Mar 10 10:17 .
drwxr-xr-x 4 serge admin 1024 Mar 10 10:01 ..
-rw-r--r-- 1 serge admin 1506 Mar 10 10:02 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
-rw-r--r-- 1 serge admin 1736 Mar 10 10:17 srvEcho_Skel.class
-rw-r--r-- 1 serge admin 3264 Mar 10 10:02 srvEcho_Stub.class
Pojawia się ten sam błąd, co poprzednio... Zastanawiamy się więc i ponownie przeglądamy dokumentację dotyczącą pliku RMI. W końcu dochodzimy do wniosku, że być może to sam serwer potrzebuje wspomnianego pliku srvEcho_Skel.class. Następnie na komputerze z systemem Windows ponownie uruchamiamy serwer z dwoma plikami: srvEcho_Stub.class i srvEcho_Skel.class, przy czym plik : jest już obecny
E:\data\java\RMI\echo\serveur>dir *.class
SRVECH~1 CLA 1 129 09/03/99 15:58 srvEcho.class
INTERE~1 CLA 256 09/03/99 15:58 interEcho.class
SRVECH~2 CLA 3 264 10/03/99 9:05 srvEcho_Stub.class
SRVECH~3 CLA 1 736 10/03/99 9:05 srvEcho_Skel.class
E:\data\java\RMI\echo\serveur>start j:\jdk12\bin\java srvEcho
następnie na komputerze z systemem Linux ponownie testujemy klienta i tym razem wszystko działa:
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r-- 1 serge admin 1506 Mar 10 10:02 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
-rw-r--r-- 1 serge admin 3264 Mar 10 10:02 srvEcho_Stub.class
shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin
Można zatem wywnioskować, że po stronie serwera muszą znajdować się dwa pliki: srvEcho_Stub.class oraz srvEcho_Skel.class. Po stronie klienta do tej pory potrzebny był tylko plik srvEcho_Stub.class. Okazał się on niezbędny, gdy klient i serwer znajdowały się na tym samym komputerze z systemem Windows. W systemie Linux usuwamy go, aby sprawdzić...
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r-- 1 serge admin 1506 Mar 10 10:02 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
*** Security Exception: No security manager, stub class loader disabled ***
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
at sun.rmi.server.RMIClassLoader.getClassLoader(RMIClassLoader.java:84)
at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:88)
at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
at java.rmi.Naming.lookup(Naming.java:60)
at cltEcho.main(cltEcho.java:28)
Erreur : java.rmi.UnexpectedException: Unexpected exception; nested exception is:
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
Mamy tu ciekawy błąd, który wydaje się sugerować, że maszyna wirtualna Java próbowała załadować słynną klasę stub, ale nie udało się to z powodu braku „menedżera bezpieczeństwa”. Przypomina nam się, że widzieliśmy coś na ten temat w dokumentacji. Zaglądamy do niej ponownie… i dowiadujemy się, że serwer musi utworzyć i zainstalować menedżera bezpieczeństwa (security manager), który zagwarantuje klientom żądającym załadowania klas, że są one bezpieczne. W przypadku braku tego menedżera bezpieczeństwa załadowanie klas jest niemożliwe. Wydaje się to pasować: nasz klient z systemem Linux zażądał od serwera potrzebnej mu klasy srvEcho_stub.class, a serwer odmówił, informując, że nie zainstalowano żadnego menedżera bezpieczeństwa. Modyfikujemy więc kod funkcji main na serwerze w następujący sposób:
// utworzenie usługi
public static void main (String arg[]){
// instalacja menedżera bezpieczeństwa
System.setSecurityManager(new RMISecurityManager());
// uruchomienie i rejestracja usługi
try{
srvEcho serveurEcho=new srvEcho();
Naming.rebind("srvEcho",serveurEcho);
System.out.println("Serveur d’écho prêt");
} catch (Exception e){
System.err.println(" Erreur " + e + " lors du lancement du serveur d’écho ");
}
}// główna
Kompilujemy i generujemy pliki srvEcho_stub.class oraz srvEcho_Skel.class za pomocą narzędzia rmic. Uruchamiamy usługę katalogową (rmiregistry), a następnie serwer i pojawia się błąd, którego wcześniej nie było!
Erreur java.security.AccessControlException: access denied (java.net.SocketPermission 127.0.0.1:1099 connect,resolve) lors du lancement du serveur d’écho
Wygląda na to, że menedżer bezpieczeństwa zadziałał zbyt skutecznie. Ponownie zapoznajemy się z dokumentacją... Okazuje się, że gdy menedżer bezpieczeństwa jest aktywny, podczas uruchamiania programu należy określić jego uprawnienia. Robi się to za pomocą następującej opcji:
start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho
gdzie
java.security.policy to słowo kluczowe
mypolicy to plik tekstowy definiujący uprawnienia programu. W tym przypadku wygląda on następująco:
Program posiada tutaj pełne uprawnienia.
Zaczynamy od nowa. Przechodzimy do katalogu serwera i wykonujemy kolejno następujące czynności:
- uruchomienie usługi katalogowej: start j:\jdk12\bin\rmiregistry
- uruchomienie serwera: start j:\jdk12\bin\java -Djava.security.policy=mypolicy srvEcho
I tym razem serwer echo (klient jeszcze nie) uruchamia się poprawnie. Teraz możesz przeprowadzić następujący eksperyment:
- zatrzymaj serwer echo, a następnie usługę katalogową
- uruchom ponownie usługę katalogową, znajdując się w innym katalogu niż ten, w którym znajduje się serwer
- wróć do katalogu serwera uruchom serwer echo – pojawi się następujący błąd:
Erreur java.rmi.ServerException: RemoteException occurred in server thread; nes
ted exception is:
java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
java.lang.ClassNotFoundException: srvEcho_Stub lors du lancement du serveur d’écho
Można z tego wywnioskować, że katalog, z którego uruchamiana jest usługa katalogowa, ma znaczenie. W tym przypadku Java nie znalazła klasy srvEcho_stub.class, ponieważ usługa katalogowa nie została uruchomiona z katalogu serwera. Podczas uruchamiania serwera można określić, w którym katalogu znajdują się klasy niezbędne dla serwera:
start j:\jdk12\bin\java
-Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/
srvEcho
Polecenie znajduje się w jednym wierszu. Słowo kluczowe java.rmi.server.codebase służy do wskazania ścieżki URL do katalogu zawierającego klasy niezbędne dla serwera. W tym przypadku URL określa protokół file, który służy do uzyskiwania dostępu do plików lokalnych, oraz katalog zawierający pliki serwera o nazwie .class. Jeśli więc wykonamy następujące czynności:
- zatrzymanie usługi katalogowej
- uruchom ponownie usługę katalogową z katalogu innego niż ten na serwerze
- w katalogu serwera uruchomimy go za pomocą polecenia (jedna linia):
start j:\jdk12\bin\java
-Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/
srvEcho
Serwer został pomyślnie uruchomiony. Można zatem przejść do klienta. Testujemy go:
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r-- 1 serge admin 1506 Mar 10 14:28 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
*** Security Exception: No security manager, stub class loader disabled ***
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
at sun.rmi.server.RMIClassLoader.getClassLoader(RMIClassLoader.java:84)
at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:88)
at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
at java.rmi.Naming.lookup(Naming.java:60)
at cltEcho.main(cltEcho.java:31)
Erreur : java.rmi.UnexpectedException: Unexpected exception; nested exception is:
java.rmi.RMISecurityException: security.No security manager, stub class loader disabled
Pojawia się ten sam błąd sygnalizujący brak menedżera bezpieczeństwa. Zastanawiamy się, czy nie popełniliśmy błędu i czy to klient powinien utworzyć swój menedżer bezpieczeństwa. Pozostawiamy serwer z jego menedżerem bezpieczeństwa, ale tworzymy również menedżera dla klienta. Funkcja main klienta cltEcho.java wygląda wówczas następująco:
public static void main(String arg[]){
// składnia: cltEcho port maszyny
// maszyna: maszyna, na której działa serwer echo
// port: port, na którym działa katalog usług na komputerze serwera echo
// weryfikacja argumentów
if(arg.length!=1){
System.err.println("Syntaxe : pg url_service_rmi");
System.exit(1);
}
// instalacja menedżera bezpieczeństwa
System.setSecurityManager(new RMISecurityManager());
// komunikacja klient-serwer
String urlService=arg[0];
BufferedReader in=null;
String msg=null;
String reponse=null;
interEcho serveur=null;
try{
....
} catch (Exception e){
....
}// spróbuj
}// main
Następnie postępujemy w następujący sposób:
- kompilujemy ponownie plik cltEcho.java
- przesyła się pliki .class na komputer z systemem Linux
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r-- 1 serge admin 1506 Mar 10 14:28 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
- uruchamia się klienta
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
java.io.FileNotFoundException: /e:/data/java/rmi/echo/serveur/srvEcho_Stub.class
at java.io.FileInputStream.<init>(FileInputStream.java)
at sun.net.www.protocol.file.FileURLConnection.connect(FileURLConnection.java:150)
at sun.net.www.protocol.file.FileURLConnection.getInputStream(FileURLConnection.java:170)
at sun.applet.AppletClassLoader.loadClass(AppletClassLoader.java:119)
at sun.applet.AppletClassLoader.findClass(AppletClassLoader.java:496)
at sun.applet.AppletClassLoader.loadClass(AppletClassLoader.java:199)
at sun.rmi.server.RMIClassLoader.loadClass(RMIClassLoader.java:159)
at sun.rmi.server.MarshalInputStream.resolveClass(MarshalInputStream.java:97)
at java.io.ObjectInputStream.inputClassDescriptor(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at java.io.ObjectInputStream.inputObject(ObjectInputStream.java)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java)
at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:105)
at java.rmi.Naming.lookup(Naming.java:60)
at cltEcho.main(cltEcho.java:31)
File not found when looking for: srvEcho_Stub
Erreur : java.rmi.UnmarshalException: Return value class not found; nested exception is:
java.lang.ClassNotFoundException: srvEcho_Stub
Wbrew pozorom robimy postępy: błąd nie jest już ten sam. Widzimy, że klient mógł zwrócić się do serwera z żądaniem klasy srvEcho_Stub.class, ale serwer jej nie znalazł. Zatem klient musi posiadać menedżera bezpieczeństwa, jeśli chce móc żądać klas od serwera.
Jeśli przyjrzymy się poprzedniemu błędowi, widzimy, że plik srvEcho_Stub.class był szukany w katalogu e:/data/java/rmi/echo/serwer/ i nie został znaleziony. A przecież właśnie tam się znajduje. Jeśli przyjrzymy się dokładniej liście metod związanych z tym błędem, znajdziemy następującą: sun.net.www.protocol.file.FileURLConnection.getInputStream. Wygląda na to, że klient otworzył strumień z obiektem typu FileURLConnection. Można przypuszczać, że wszystko to ma związek ze sposobem, w jaki uruchomiliśmy nasz serwer:
start j:\jdk12\bin\java
-Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/echo/serveur/
srvEcho
Komunikat o błędzie wydaje się odnosić do wartości słowa kluczowego java.rmi.server.codebase. Gdy ponownie przejrzymy dokumentację, zauważymy, że wartość tego słowa kluczowego w podanych przykładach zawsze brzmi: http://.., c.a.d, a używanym protokołem jest http. Nie jest jasne, w jaki sposób klient żąda i otrzymuje klasy od serwera. Być może żąda ich przy użyciu słowa kluczowego URL, którego wartością jest java.rmi.server.codebase lub URL, określone podczas uruchamiania serwera. Postanawiamy zatem uruchomić serwer za pomocą następującego nowego polecenia:
start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
Protokół to teraz http. Konieczne jest przeniesienie plików .class do lokalizacji dostępnej dla serwera http na komputerze, na którym będą przechowywane klasy. W naszym przykładzie serwer działa na komputerze z systemem Windows, wyposażonym w serwer HTTP firmy Microsoft o nazwie PWS. Katalog główny tego serwera to d:\Inetpub\wwwroot. Postępujemy zatem w następujący sposób:
- tworzymy katalog d:\Inetpub\wwwroot\rmi\echo
- umieszczamy w nim pliki serwera o nazwie .class oraz plik mypolicy
- uruchamiamy serwer WWW, jeśli jeszcze tego nie zrobiono
- ponownie uruchamia się usługę katalogową (rmiregistry)
- uruchamia się serwer ponownie za pomocą polecenia
start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
- na komputerze z systemem Linux uruchamia się klienta:
shiva[serge]:/home/admin/serge/java/rmi/client#
$ dir *.class
-rw-r--r-- 1 serge admin 1622 Mar 10 14:37 cltEcho.class
-rw-r--r-- 1 serge admin 256 Mar 10 10:02 interEcho.class
shiva[serge]:/home/admin/serge/java/rmi/client#
$ java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin
Uff! Działa. Klientowi udało się pobrać ten słynny plik srvEcho_Stub.class.
To wszystko podsunęło nam pewne pomysły i zastanawiamy się, czy klient działający na serwerze z systemem Windows również działałby bez pliku srvEcho_Stub.class. Przechodzimy do katalogu klienta, usuwamy plik srvEcho_Stub.class, jeśli się tam znajduje, i uruchamiamy klienta w taki sam sposób jak w systemie Linux:
E:\data\java\RMI\echo\client>dir *.class
CLTECH~1 CLA 1 622 10/03/99 14:12 cltEcho.class
INTERE~1 CLA 256 09/03/99 15:59 interEcho.class
E:\data\java\RMI\echo\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
Message : nouveau message
Réponse serveur : [nouveau message]
Message : fin
9.2.1.8. Résumé
Po stronie serwera Windows:
- serwer posiada menedżera bezpieczeństwa
- został uruchomiony z następującymi opcjami: start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/echo/ srvEcho
Po stronie klienta (Linux lub Windows)
- klient posiada menedżera bezpieczeństwa
- w systemie Linux został uruchomiony przez java cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
- w systemie Windows został uruchomiony za pomocą j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://tahe.istia.univ-angers.fr/srvEcho
9.2.1.9. Serwer echo w systemie Linux, klienci w systemach Windows i Linux
Teraz przenosimy serwer na komputer z systemem Linux i testujemy klientów z systemów Linux i Windows. Należy postępować w następujący sposób:
- przenosimy pliki .class z serwera na komputer z systemem Linux
shiva[serge]:/home/admin/serge/WWW/rmi/echo/serveur#
$ dir
total 11
drwxr-xr-x 2 serge admin 1024 Mar 10 16:15 .
drwxr-xr-x 3 serge admin 1024 Mar 10 16:09 ..
-rw-r--r-- 1 serge admin 256 Mar 10 16:09 interEcho.class
-rw-r--r-- 1 serge admin 1245 Mar 10 16:09 srvEcho.class
-rw-r--r-- 1 serge admin 1736 Mar 10 16:09 srvEcho_Skel.class
-rw-r--r-- 1 serge admin 3264 Mar 10 16:09 srvEcho_Stub.class
- ponieważ klienci będą żądać klasy srvEcho_Stub.class, katalog wybrany dla klas serwera jest katalogiem dostępnym dla serwera HTTP na komputerze z systemem Linux. W tym przypadku klasa URL z tego katalogu to http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serveur
- usługa katalogowa jest uruchamiana w tle: /usr/local/bin/jdk/rmiregistry &
- serwer jest uruchamiany w tle: /usr/local/bin/jdk/bin/java
-Djava.rmi.server.codebase=http://shiva.istia.univ-angers.fr/~serge/rmi/echo/serwer/
srvEcho &
Można przetestować klientów. Najpierw klienta Windows.
- Przechodzimy do katalogu klienta na komputerze z systemem Windows
- uruchamiamy klienta za pomocą polecenia:
E:\data\java\RMI\echo\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltEcho rmi://shiva.istia.univ-angers.fr/srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : fin
Testujemy klienta w systemie Linux:
shiva[serge]:/home/admin/serge/java/rmi/echo/client#
$ java cltEcho srvEcho
Message : msg1
Réponse serveur : [msg1]
Message : msg2
Réponse serveur : [msg2]
Message : fin
Należy zauważyć, że w przypadku klienta Linux działającego na tej samej maszynie co serwer echo nie było konieczne określanie maszyny w URL żądanej usługi.
9.3. Drugi przykład: Serwer SQL na komputerze z systemem Windows
9.3.1. Problem
W rozdziale JDBC omówiliśmy obsługę relacyjnych baz danych. W przedstawionych przykładach aplikacje i wykorzystywana baza danych znajdowały się na tym samym komputerze z systemem Windows. W niniejszym rozdziale zamierzamy stworzyć serwer RMI na komputerze z systemem Windows, który umożliwi zdalnym klientom korzystanie z publicznych baz danych ODBC znajdujących się na komputerze, na którym zainstalowano serwer.

Klient RMI mógłby wykonać 3 operacje:
- połączyć się z wybraną bazą danych
- wysyłać zapytania SQL
- zamknąć połączenie
Serwer wykonuje zapytania SQL od klienta i wysyła mu wyniki. Jest to jego podstawowe zadanie i dlatego nazwiemy go serwerem SQL.
Stosujemy różne etapy omówione wcześniej w przypadku serwera echo.
9.3.2. Krok 1: interfejs zdalny
Interfejs zdalny to interfejs, który zawiera listę metod serwera RMI, które będą dostępne dla klientów RMI. Wykorzystamy następujący interfejs:
import java.rmi.*;
// interfejs zdalny
public interface interSQL extends Remote{
public String connect(String pilote, String url, String id, String mdp)
throws java.rmi.RemoteException;
public String[] executeSQL(String requete, String separateur)
throws java.rmi.RemoteException;
public String close()
throws java.rmi.RemoteException;
}
Rola poszczególnych metod jest następująca:
Connect: klient łączy się ze zdalną bazą danych, podając jej identyfikator pilote, url, JDBC, a także swoją tożsamość id i hasło mdp, aby uzyskać dostęp do tej bazy. Serwer zwraca mu ciąg znaków wskazujący wynik połączenia:
executeSQL: klient żąda wykonania zapytania SQL w bazie, z którą jest połączony. Określa znak, który ma oddzielać pola w zwracanych wynikach. Serwer zwraca tablicę ciągów znaków:
dla zapytania o aktualizację bazy, gdzie n oznacza liczbę zaktualizowanych wierszy
jeśli zapytanie spowodowało błąd
jeśli zapytanie nie zwróciło żadnych wyników
jeśli zapytanie zwróciło wyniki. Wiersze zwrócone w ten sposób przez serwer stanowią wyniki zapytania.
close: klient zamyka połączenie ze zdalną bazą danych. Serwer zwraca ciąg znaków wskazujący wynik tego zamknięcia:
9.3.3. Krok 2: Kod serwera
Poniżej znajduje się kod źródłowy serwera SQL w języku Java. Jego zrozumienie wymaga opanowania zarządzania bazami danych JDBC oraz budowy serwerów RMI. Komentarze w programie powinny ułatwić jego zrozumienie.
// importowane pakiety
import java.rmi.*;
import java.rmi.server.*;
import java.sql.*;
import java.util.*;
// klasa srvSQL
public class srvSQL extends UnicastRemoteObject implements interSQL{
// dane globalne klasy
private Connection DB;
// ------------- konstruktor
public srvSQL() throws RemoteException{
super();
}
// --------------- połączenie
public String connect(String pilote, String url, String id,
String mdp) throws RemoteException{
// połączenie z bazą danych poprzez sterownik
// autoryzacja przy użyciu identyfikatora id i hasła mdp
String resultat=null; // wynik metody
try{
// ładowanie sterownika
Class.forName(pilote);
// żądanie połączenia
DB=DriverManager.getConnection(url,id,mdp);
// ok
resultat="200 Connexion réussie";
} catch (Exception e){
// błąd
resultat="500 Echec de la connexion (" + e + ")";
}
// koniec
return resultat;
}
// ------------- executeSQL
public String[] executeSQL(String requete, String separateur)
throws RemoteException{
// wykonuje zapytanie SQL w bazie DB
// i umieszcza wyniki w tablicy ciągów znaków
// dane niezbędne do wykonania zapytania
Statement S=null;
ResultSet RS=null;
String[] lignes=null;
Vector resultats=new Vector();
String ligne=null;
try{
// utworzenie kontenera zapytania
S=DB.createStatement();
// wykonanie zapytania
if (! S.execute(requete)){
// zapytanie o aktualizację
// zwracana jest liczba zaktualizowanych wierszy
lignes=new String[1];
lignes[0]="100 "+S.getUpdateCount();
return lignes;
}
// było to zapytanie
// pobieranie wyników
RS=S.getResultSet();
// liczba pól w zestawie wyników
int nbChamps=RS.getMetaData().getColumnCount();
// przetwarzamy je
while(RS.next()){
// tworzenie wiersza wyników
ligne="101 ";
for (int i=1;i<nbChamps;i++)
ligne+=RS.getString(i)+separateur;
ligne+=RS.getString(nbChamps);
// dodanie do wektora wyników
resultats.addElement(ligne);
}// while
// koniec przetwarzania wyników
// zwolnienie zasobów
RS.close();
S.close();
// zwracanie wyników
int nbLignes=resultats.size();
if (nbLignes==0){
lignes=new String[1];
lignes[0]="501 Pas de résultats";
} else {
lignes=new String[resultats.size()];
for(int i=0;i<lignes.length;i++)
lignes[i]=(String) resultats.elementAt(i);
}//if
return lignes;
} catch (Exception e){
// błąd
lignes=new String[1];
lignes[0]="500 " + e;
return lignes;
}// try-catch
}// executeSQL
// --------------- zamknij
public String close() throws RemoteException {
// zamyka połączenie z bazą danych
String resultat=null;
try{
DB.close();
resultat="200 Base fermée";
} catch (Exception e){
resultat="500 Erreur à la fermeture de la base ("+e+")";
}
// zwraca wynik
return resultat;
}
// ----------- main
public static void main (String[] args){
// moduł bezpieczeństwa
System.setSecurityManager(new RMISecurityManager());
// uruchomienie usługi
srvSQL serveurSQL=null;
try{
// tworzenie
serveurSQL=new srvSQL();
// rejestracja
Naming.rebind("srvSQL",serveurSQL);
// monitorowanie
System.out.println("Serveur SQL prêt");
} catch (Exception e){
// błąd
System.err.println("Erreur lors du lancement du serveur SQL ("+ e +")");
}// try-catch
}// main
}// klasa
9.3.4. Kod klienta RMI
Klient serwera RMI jest wywoływany z następującymi parametrami:
urlserviceAnnuaire: adres URL RMI usługi katalogowej, która zarejestrowała serwer SQL
sterownik: sterownik, którego serwer SQL ma używać do zarządzania bazą danych
urlBase: adres URL JDBC bazy danych, którą ma zarządzać
id: identyfikator klienta lub null, jeśli nie ma identyfikatora
mdp: hasło klienta lub null, jeśli nie ma hasła
separator: znak, którego serwer SQL musi używać do oddzielania pól w wierszach wyników zapytania
Oto przykład możliwych parametrów:
gdzie:
nazwa rmi serwera SQL
standardowy sterownik baz danych z interfejsem ODBC
aby korzystać z bazy artykułów zgłoszonej na liście baz publicznych ODBC na komputerze z systemem Windows
brak identyfikatora
brak hasła
pola wyników będą oddzielone przecinkami
Po uruchomieniu z powyższymi parametrami klient wykonuje następujące kroki:
- łączy się z serwerem RMI srvSQL, a więc z serwerem RMI znajdującym się na tej samej maszynie co klient
- wysyła żądanie połączenia z bazą danych artykułów
- prosi użytkownika o wpisanie zapytania SQL na klawiaturze
- wysyła ją do serwera SQL
- wyświetla na ekranie wyniki zwrócone przez serwer
- ponownie prosi użytkownika o wpisanie zapytania SQL na klawiaturze. Zakończy działanie po zakończeniu wprowadzania zapytania.
Poniżej znajduje się kod Java klienta. Komentarze powinny wystarczyć do jego zrozumienia.
import java.rmi.*;
import java.io.*;
public class cltSQL {
// dane globalne klasy
private static String syntaxe =
"syntaxe : cltSQL urlServiceAnnuaire pilote urlBase id mdp separateur";
private static BufferedReader in=null;
private static interSQL serveurSQL=null;
public static void main(String arg[]){
// składnia: cltSQL urlServiceAnnuaire separator kierowca adres URL identyfikator hasło
// urlServiceAnnuaire: adres URL katalogu usług RMI, z którym należy się skontaktować
// sterownik: sterownik, którego należy użyć dla bazy danych, z której będą pobierane dane
// urlBase: adres URL JDBC bazy danych, z której ma być pobierane dane
// id: identyfikator użytkownika
// mdp: hasło użytkownika
// separator: ciąg znaków oddzielający pola w wynikach zapytania
// sprawdzanie liczby argumentów
if(arg.length!=6)
erreur(syntaxe,1);
// inicjalizacja parametrów połączenia z bazą danych
String urlService=arg[0];
String pilote=arg[1];
String urlBase=arg[2];
String id, mdp, separateur;
if(arg[3].equals("null")) id=""; else id=arg[3];
if(arg[4].equals("null")) mdp=""; else mdp=arg[4];
if(arg[5].equals("null")) separateur=" "; else separateur=arg[5];
// instalacja menedżera bezpieczeństwa
System.setSecurityManager(new RMISecurityManager());
// komunikacja klient-serwer
String requete=null;
String reponse=null;
String[] lignes=null;
String codeErreur=null;
try{
// otwarcie strumienia klawiatury
in=new BufferedReader(new InputStreamReader(System.in));
// monitorowanie
System.out.println("--> Connexion au serveur RMI en cours...");
// lokalizacja usługi
serveurSQL=(interSQL) Naming.lookup(urlService);
// monitorowanie
System.out.println("--> Connexion à la base de données en cours");
// wstępne żądanie połączenia z bazą danych
reponse=serveurSQL.connect(pilote,urlBase,id,mdp);
// monitorowanie
System.out.println("<-- "+reponse);
// analiza odpowiedzi
codeErreur=reponse.substring(0,3);
if(codeErreur.equals("500"))
erreur("Abandon sur erreur de connexion à la base",3);
// pętla odczytu zapytań do wysłania na serwer SQL
System.out.print("--> Requête : ");
requete=in.readLine().toLowerCase().trim();
while(! requete.equals("fin")){
// wysyłanie zapytania do serwera i odbieranie odpowiedzi
lignes=serveurSQL.executeSQL(requete,separateur);
// monitorowanie
afficheLignes(lignes);
// kolejne żądanie
System.out.print("--> Requête : ");
requete=in.readLine().toLowerCase().trim();
}// podczas gdy
// następnie
System.out.println("--> Fermeture de la connexion à la base de données distante");
// zamyka się połączenie
reponse=serveurSQL.close();
// kontynuacja
System.out.println("<-- " + reponse);
// koniec
System.exit(0);
// obsługa błędów
} catch (Exception e){
erreur("Abandon sur erreur : " + e,2);
}// próba
}// main
// ----------- AfficheLignes
private static void afficheLignes(String[] lignes){
for (int i=0;i<lignes.length;i++)
System.out.println("<-- " + lignes[i]);
}// afficheLignes
// ------------ błąd
private static void erreur(String msg, int exitCode){
// wyświetlenie komunikatu o błędzie
System.err.println(msg);
// ewentualne zwolnienie zasobów
try{
in.close();
serveurSQL.close();
} catch(Exception e){}
// zakończenie działania
System.exit(exitCode);
}// błąd
}// klasa
9.3.5. Krok 3: tworzenie plików .class
- serwer został skompilowany
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\javac interSQL.java
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\javac srvSQL.java
E:\data\java\RMI\sql\serveur>dir *.class
INTERS~1 CLA 451 12/03/99 17:54 interSQL.class
SRVSQL~1 CLA 3 238 12/03/99 17:54 srvSQL.class
- utworzono pliki Stub i Skel
E:\data\java\RMI\sql\serveur>j:\jdk12\bin\rmic srvSQL
E:\data\java\RMI\sql\serveur>dir *.class
INTERS~1 CLA 451 12/03/99 17:54 interSQL.class
SRVSQL~1 CLA 3 238 12/03/99 17:54 srvSQL.class
SRVSQL~2 CLA 4 491 12/03/99 17:56 srvSQL_Stub.class
SRVSQL~3 CLA 2 414 12/03/99 17:56 srvSQL_Skel.class
- przenosimy pliki interSQL.class, srvSQL_Stub.class, srvSQL_Skel.class do katalogu klienta
E:\data\java\RMI\sql\client>dir
CLTSQL~1 JAV 3 486 11/03/99 11:39 cltSQL.java
INTERS~1 CLA 451 11/03/99 10:55 interSQL.class
SRVSQL~1 CLA 4 491 11/03/99 13:19 srvSQL_Stub.class
SRVSQL~2 CLA 2 414 11/03/99 13:19 srvSQL_Skel.class
- kompilujemy klienta
E:\data\java\RMI\sql\client>j:\jdk12\bin\javac cltSQL.java
E:\data\java\RMI\sql\client>dir *.class
INTERS~1 CLA 451 11/03/99 10:55 interSQL.class
CLTSQL~1 CLA 2 839 12/03/99 18:00 cltSQL.class
SRVSQL~1 CLA 4 491 11/03/99 13:19 srvSQL_Stub.class
SRVSQL~2 CLA 2 414 11/03/99 13:19 srvSQL_Skel.class
9.3.6. Krok 4: Testy z serwerem i klientem na tym samym komputerze z systemem Windows
- usługa katalogowa jest uruchamiana w katalogu innym niż ten, w którym znajdują się serwer i klient
- Umieszczamy poniższy plik mypolicy w katalogach klienta i serwera
- uruchamia się serwer
E:\data\java\RMI\sql\serveur>start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=file:/e:/data/java/rmi/sql/serveur/ srvSQL
- uruchamiamy klienta
E:\data\java\RMI\sql\client>j:\jdk12\bin\java -Djava.security.policy=mypolicy cltSQL srvSQL
sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,
--> Connexion au serveur RMI en cours...
--> Connexion à la base de données en cours
<-- 200 Connexion réussie
--> Zapytanie: select nazwa, stock_actu from artykuły order by stock_actu desc
<-- 101 vélo,31
<-- 101 essai3,13
<-- 101 skis nautiques,13
<-- 101 canoé,13
<-- 101 panthère,11
<-- 101 léopard,11
<-- 101 cachalot,10
<-- 101 fusil,10
<-- 101 arc,10
--> Zapytanie: update articles set stock_actu=stock_actu-1 where stock_actu<=11
<-- 100 5
--> Zapytanie: select nazwa, stock_actu from articles order by stock_actu asc
<-- 101 cachalot,9
<-- 101 fusil,9
<-- 101 arc,9
<-- 101 panthère,10
<-- 101 léopard,10
<-- 101 essai3,13
<-- 101 skis nautiques,13
<-- 101 canoé,13
<-- 101 vélo,31
--> Requête : fin
--> Fermeture de la connexion à la base de données distante
<-- 200 Base fermée
9.3.7. Krok 5: Testy z serwerem na komputerze z systemem Windows i klientem na komputerze z systemem Linux
- w razie potrzeby zatrzymujemy serwer i usługę katalogową
- przenosimy pliki .class klienta na komputer z systemem Linux
shiva[serge]:/home/admin/serge/java/rmi/sql/client#
$ dir *.class
-rw-r--r-- 1 serge admin 2839 Mar 11 14:37 cltSQL.class
-rw-r--r-- 1 serge admin 451 Mar 11 14:37 interSQL.class
- Pliki z serwera umieszcza się w katalogu dostępnym dla serwera HTTP na komputerze z systemem Windows
D:\Inetpub\wwwroot\rmi\sql>dir
INTERS~1 CLA 451 11/03/99 10:55 interSQL.class
SRVSQL~1 CLA 3 238 11/03/99 13:19 srvSQL.class
SRVSQL~2 CLA 4 491 11/03/99 13:19 srvSQL_Stub.class
SRVSQL~3 CLA 2 414 11/03/99 13:19 srvSQL_Skel.class
MYPOLICY 81 08/06/98 15:01 mypolicy
- Wznowiono działanie serwisu katalogowego
- uruchamiamy serwer ponownie z innymi ustawieniami niż te użyte w poprzednim teście
D:\Inetpub\wwwroot\rmi\sql>start j:\jdk12\bin\java -Djava.security.policy=mypolicy
-Djava.rmi.server.codebase=http://tahe.istia.univ-angers.fr/rmi/sql/ srvSQL
- uruchamiamy klienta na komputerze z systemem Linux
/usr/local/bin/jdk/bin/java cltSQL rmi://tahe.istia.univ-angers.fr/srvSQL sun.jdbc.odbc.JdbcOdbcDriver jdbc:odbc:articles null null ,
--> Zapytanie: select nazwa,stock_actu,stock_mini from articles order by nazwa
<-- 101 arc,9,8
<-- 101 cachalot,9,6
<-- 101 canoé,13,7
<-- 101 essai3,13,9
<-- 101 fusil,9,8
<-- 101 léopard,10,7
<-- 101 panthère,10,7
<-- 101 skis nautiques,13,8
<-- 101 vélo,31,8
--> Zapytanie: update articles set stock_actu=stock_mini where stock_mini<=7
<-- 100 4
--> Zapytanie: select nazwa, stock_actu, stock_mini z artykułów, uporządkowane według nazwy
<-- 101 arc,9,8
<-- 101 cachalot,6,6
<-- 101 canoé,7,7
<-- 101 essai3,13,9
<-- 101 fusil,9,8
<-- 101 léopard,7,7
<-- 101 panthère,7,7
<-- 101 skis nautiques,13,8
<-- 101 vélo,31,8
--> Requête : fin
--> Fermeture de la connexion à la base de données distante
<-- 200 Base fermée
9.3.8. Wnioski
Mamy tu do czynienia z interesującą aplikacją, ponieważ umożliwia ona dostęp do bazy danych z dowolnego komputera w sieci. Można by ją było równie dobrze napisać w tradycyjny sposób, wykorzystując gniazda (sockets), co zresztą jest wymagane w jednym z ćwiczeń z rozdziału poświęconego bazom danych. Gdybyśmy napisali tę aplikację w tradycyjny sposób:
- mielibyśmy klienta i serwer, które mogłyby być napisane w różnych językach
- klient i serwer komunikowałyby się poprzez wymianę wierszy tekstu, a ich dialog mógłby wyglądać mniej więcej tak:
gdzie dwa pierwsze parametry określają lokalizację serwera, a cztery kolejne wskazują parametry połączenia z bazą danych, z której ma być korzystano
Serwer mógłby odpowiedzieć czymś w rodzaju:
aby poprosić serwer o wykonanie zapytania SQL w bazie danych połączonej z klientem. separator to znak oddzielający pola w wierszach odpowiedzi.
Serwer mógłby odpowiedzieć na przykład tak:
w przypadku zapytania o aktualizację bazy, gdzie n oznacza liczbę zaktualizowanych wierszy
jeśli żądanie spowodowało błąd
jeśli zapytanie nie dało żadnych wyników
jeśli zapytanie zwróciło wyniki. Wiersze zwrócone w ten sposób przez serwer stanowią wyniki zapytania.
w celu zamknięcia połączenia ze zdalną bazą danych. Serwer może zwrócić ciąg znaków wskazujący wynik tego zamknięcia:
Widać tutaj, że jeśli potrafimy zbudować tradycyjną aplikację z wykorzystaniem protokołu podobnego do powyższego, możemy na tej podstawie wywnioskować możliwą strukturę serwera RMI. Tam, gdzie w protokole mamy komunikat od klienta do serwera w postaci:
to w serwerze RMI może istnieć metoda
, a ta metoda, dostępna dla klienta, powinna zatem stanowić część opublikowanego interfejsu serwera.
Na zakończenie zauważmy, że nasz serwer obsługuje obecnie tylko jednego klienta: w obecnej postaci kodu nie jest w stanie obsługiwać wielu klientów. W rzeczywistości, jeśli klient łączy się z bazą B1, serwer tworzy obiekt Connection o relacji DB=DB1. Jeśli drugi klient zażąda połączenia z bazą B2, serwer odnotowuje to, tworząc obiekt Connection DB=DB2, co powoduje przerwanie połączenia pierwszego klienta z bazą B1.
9.4. Ćwiczenia
9.4.1. Ćwiczenie 1
Rozbuduj poprzedni serwer SQL tak, aby obsługiwał wielu klientów.
9.4.2. Ćwiczenie 2
Napisz aplet Java do handlu elektronicznego przedstawiony w ćwiczeniach z rozdziału JDBC tak, aby współpracował z serwerem RMI z poprzedniego ćwiczenia.