Pokazywanie postów oznaczonych etykietą RMI. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą RMI. Pokaż wszystkie posty

wtorek, 5 października 2010

Mechanizm RMI (Remote Method Invocation) w praktyce cz. 3

Dotychczas w poprzednich artykułach (część 1 i część 2) przedstawiłem jak zbudować prostą aplikację przy użyciu RMI, gdzie klient wywołuje metody serwera i serwer wywoluje metody klienta. Dotychczas wszystko uruchamialiśmy na jednej maszynie (localhost). Spróbujmy czy zadziała wcześniej napisana przez nas aplikacja przy próbie uruchomienia na dwóch komputerach.

Moje komputery to laptop o adresie 192.168.0.2 i PC 192.168.0.1. Na PCie odpalę serwer, a na laptopie klienta. Kopiujemy projekty na odpowiednie maszyny i pierwsze co to należy zmienić ścieżki dostępowe w skryptach uruchomieniowych. Bez tego otrzymamy zapewne jeden z błędów opisywanych w poprzednich artykułach.

No dobra ruszamy. Odpalamy serwer na PCie - wszystko działa (bo na nim przygotowywalem poprzednie artykuły). Odpalamy klienta na laptopie i:

javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: 192.168.0.2; nested exception is:
        java.net.ConnectException: Connection refused: connect]


Czyli aplikacja kliencka próbuje się połączyć z serwerem działającym na laptopie. Trzeba więc zmodyfikować adres pod który aplikacja próbuje się łączyć. Otwieramy więc MyClientMain.java w Eclipse i modyfikujemy linię definiującą adres URL:

String url = "rmi://192.168.0.1";

Uruchamiamy ponownie klienta i znów ten sam błąd. Jednak to nie tu szukaliśmy. Przyglądajac się bardziej stosowi wywołań, można zauważyć że wyjątek został rzucony przez MyClientMain.java:19. Zobaczmy co się tam dzieje:

context.bind("rmi:MyClientObject", myClientImpl);

Aaaa, czyli aplikacja kliencka nie może zarejestrować obiektu, gdyż rmiregistry zostało uruchomione na innej maszynie. Poświęciłem sporo czasu aby rozwikłać ten problem. Z tego co wyczytałem nie można zarejestrować obiektu, który ma być zdalny na maszynie innej niż localhost. (Moge się mylić w tym momencie, wiec jak ktoś wie lepiej niech pisze!) Po co więc ten argument w metodzie bind() do podania adresu? A no po to, że na jednej maszynie możemy mieć uruchomionych kilka rmiregistry działających na innych portach.

Wracając do aplikacji to jeśli chcemy klientem wywoływać metody serwera i serwer chce wywoływać metody klienta i ma to działać na osobnych maszynach, to musimy troche przerobić aplikację. Przede wszystkim klient musi posiadać uruchomienione własne rmiregistry, gdzie będzie wystawiał swoje zdalne obiekty. Dodatkowo serwer musi jakoś poznać IP z jakim ma się połaczyć, więc warto go mu przekazać (np. jako wywołanie zdalnej metody).

Ok najpierw zmiany u klienta. W skrypcie uruchomieniowym programu klienta należy dopisac linie uruchamiającą rmiregistry zanim uruchomimy właściwą aplikację:

start rmiregistry

Następnie w klasie MyClientMain należy zmodyfikować wywołanie zdalnej metody:

String clientAdres = InetAddress.getLocalHost().getHostAddress();
String str = myRemoteObject.getDescription(clientAdres);

Teraz argument przekazywany do metody będziemy traktować jak adres IP klienta, aby serwer wiedział z kim się połaczyć.

Teraz czas na serwer. Zmieniamy wywołanie metody szukającej zdalny obiekt klienta na następujący kod:

MyClientInt myClientInt = (MyClientInt) 
        context2.lookup("rmi://" + text + "/MyClientObject");

gdzie text jest argumentem przekazywanym do metody.

Uruchamiamy i wszystko działa poprawnie :) Możemy cieszyc się aplikacją RMI działającą na osobnych maszynach. Tym samym kończę cykl artykułów o RMI (chyba że jeszcze coś ciekawego przyjdzie mi do głowy, lub pojawi się zapotrzebowanie na rozwiązanie jakiegoś problemu). Wszelkie uwagi mile widziane.

Więcej informacji:
Mechanizm RMI (Remote Method Invocation) w praktyce cz. 1
Mechanizm RMI (Remote Method Invocation) w praktyce cz. 2

niedziela, 3 października 2010

Mechanizm RMI (Remote Method Invocation) w praktyce cz. 2

W poprzedniej części tego artykułu zatytułowanej Mechanizm RMI (Remote Method Invocation) w praktyce cz. 1 pokaząłem jak napisać prostą aplikację typu klient / serwer. Teraz spróbujemy rozbudować tamten projekt tak, aby serwer wywoływał metody klienta. Nie jest to typowa sytuacja wykorzystania tego mechanizmu, ale jest to dobra sytuacja dydaktyczna. Jako kod bazowy będziemy korzystać z projektu przygotowanego w poprzedniej części kursu.

W projekcie klienta dodajmy zdalny interfejs:

package com.blogspot.mstachniuk.example.rmiclient;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface MyClientInt extends Remote {

    void showInfo(String info) throws RemoteException;
}

Nalezy pamiętać aby metody interfejsu deklarowały wyjątek RemoteException. W przeciwnym wypadku podczas tworzenia obiektu dostaniemy wyjatek: ExportException powodowany przez: IllegalArgumentException.

Stwórzmy dalej implementację tego interfejsu, analogicznie jak w poprzednim wpisie:

package com.blogspot.mstachniuk.example.rmiclient;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class MyClientImpl extends UnicastRemoteObject
        implements MyClientInt {

    private static final long serialVersionUID = 1L;

    public MyClientImpl() throws RemoteException {
        super();
    }

    @Override
    public void showInfo(String info) {
        System.out.println("Info: " + info);
    }
}

i dodajmy do MyClientMain linijki rejestrujące obiekt (zaraz po utworzeniu context'u):

MyClientImpl myClientImpl = new MyClientImpl();
context.bind("rmi:MyClientObject", myClientImpl);

Teraz należało by po stronie serwera napisać kod, który by wyołał metodę klienta. Dodajmy poniższy kod w metodzie getDescription(String text) klasy MyServerImpl tuż przed instrukcją return:

String url = "rmi://localhost/";
try {
    Context context2 = new InitialContext();
    MyClientInt myClientInt = (MyClientInt) 
            context2.lookup(url + "MyClientObject");
    myClientInt.showInfo("Info od serwera");
} catch(Exception e) {
    e.printStackTrace();
}

Wówczas gdy klient wywoła zdalną metodę getDescription() serwer wywoła metodę klienta i dopiero zwróci wynik do klienta. Nie jest to może najszczęśliwszy przykład, al chodzi tu o zaprezentowanie działania mechanizmu. Jako że serwer jest uruchamiany wcześniej niż kod klienta, to serwer musi poczekać na moment w którym zdalny obiekt klienta będzie już dostępny. Dlatego próbę wywołania zdalnej metody klienta umieściłem w metodzie getDescription().

No dobra, importujemy to czego potrzebuje nasz wklejony kod (swoją drogą Eclipse kilkakrotnie w tym momencie mi się zawiesiło) i kopiujemy odpowiednie interfejsy (tj. MyClientInt). Następnie uruchamiamy i otrzymujemy masę wyjątków po stronie klienta. Trzeba dodać do skryptu uruchamiającego informację o codebase:

-Djava.rmi.server.codebase=file:/D:/Java_programy/RMIClient/bin/

Teraz otrzymujemy dwa wyjątki - po stronie klienta AccessControlException, a po stronie serwera UnmarshalException, spowodowany przez EOFException.

Zmodyfikujmy więc spowrotem plik polityki client.policy w projekcie klienta, aby zezwolić na wszystko:

grant {
 permission java.security.AllPermission;
};

Uruchamiamy i działa:) Czyli zabezpieczenia nie pozwalały na odpowiednią komunikację. Chcąc ustawić tylko tyle ile musimy, powyższy plik musimy zmodyfikować w następujący sposób:

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

Napiszę jeszcze trochę o wyjątkach. W przypadku jabyśmy podali zły adres zdalnego obiektu otrzymalibyśmy  wyjątek javax.naming.NoInitialContextException. W przypadku gdy nazwa na zdalny obiekt jest zajęta (np. gdy odpalimy dwie instancje klienta), otrzymamy javax.naming.NameAlreadyBoundException. Istnieje jeszcze pewnie wiele sposobów aby coś zepsuć.

Jak widać technologia RMI daje możliwość łatwego wywoływania zdalnych metod w kodzie, tylko że trzeba się trochę namęczyć z całą otoczką uruchamiania tych aplikacji. Przedstawione tutaj rozwiązanie nie jest idealne, o czym napiszę w kolejnej części kursu.

Więcej informacji:
Mechanizm RMI (Remote Method Invocation) w praktyce cz. 1

sobota, 2 października 2010

Mechanizm RMI (Remote Method Invocation) w praktyce cz. 1

Dzisiaj chciałbym Wam przedstawić mechanizm wywoływania zdalnych metod (ang. Remote Method Invocation, RMI). Spotkałem się z nim podczas studiów na jednym z przedmiotów. Pamiętam, że znajomi męczyli się bardzo, aby napisać prostą aplikację wykorzystującą RMI. Narzekali przy tym, że to niewykorzystywane jest, że są nowsze, lepsze metody realizacji podobnej funkcjonalności, że .NET itd. Jako że zaczął się niedawno rok akademicki mam nadzieję, że moje wywody pomogą niektórym w ogarnięciu tematu.

No dobra, ale przejdźmy do rzeczy. Mechanizm RMI dodano do Javy w wersji 1.1. W późniejszych wersjach pojawiały się jakieś modyfikacje tego mechanizmu, ułatwiające trochę pisanie. My napiszemy prostą aplikację typu klient / serwer. Klient będzie miał za zadanie wywołać metodę serwera. Projekt wykonam w środowisku Eclipse (choć nie jest to moje ulubione środowisko).

Tworzymy nowy (Ctrl+N) Java Project w Eclipse, jako Project name podajemy np. RMIServer. Dodatkowo zaznaczę, że projekt trzymam w katalogu (czy też mam tak ustawione Workspace): D:\Java_programy\ - przyda nam się to na później. Zaczniemy od zdefiniowania interfejsu który nasz serwer będzie udostępniać na zewnątrz. Klikamy na src w projekcie prawym przyciskiem myszy i New -> Interface. Wpisujemy nazwę pakietu (w moim przypadku: com.blogspot.mstachniuk.example.rmiserver) i nazwę interfejsu - ja dałem: MyServerInt. Przyrostek Int pochodzi od Interface.

W utworzonym interfejsie definiujemy metody, które będziemy udostępniać na naszym serwerze. Ja utworzyłem jedną metodę, która jako argument przyjmuje String i również go zwraca. W przypadku obiektów sprawa się trochę komplikuje, więc nie będę jej opisywał. Nasz interfejs musi rozszerzać inny interfejs: java.rmi.Remote. Dodatkowo każda metoda musi mieć zadeklarowane, że rzuca wyjątek java.rmi.RemoteException. Jest on rzucany, przy niepowodzeniu operacji wywołania zdalnej metody, przy zerwaniu połączenia itp. Poniżej kod mojego opisanego interfejsu:



No dobra, to teraz napiszmy kod, który będzie implementował przedstawioną powyżej funkcjonalność. Utwórzmy klasę MyServerImpl (przyrostek Impl od Implementation). Będzie on rozszerzał klasę java.rmi.server.UnicastRemoteObject - dla wygody. Można też bez tego - tylko wtedy sami musimy utworzyć obiekt serwera. Nasza klasa dodatkowo będzie implementować nasz wcześniej zdefiniowany interfejs. Przykładowy kod poniżej:



Linia definująca pole serialVersionUID jest do tego aby środowisko Eclipse dało nam spokój (tzn. aby nie wyswietlał sie żółty wykrzyknik przy nazwie klasy). Bez tego też zadziała. Wujek Bob w książce "Czysty kod. Podręcznik dobrego programisty" zaleca jednak aby samemu nie deklarować pola serialVersionUID, a pozwolić kompilatorowi na jego automatyczne wygenerowanie. Ma to znaczenie przy ewentualnej deserializacji różnych wersji klas.

Konstruktor jest wymagany i musi deklarować wyjatek RemoteException, gdyż konstruktory UnicastRemoteObject również deklarują ten wyjątek i może się zdarzyć, że będzie problem z utworzeniem zdalnego obiektu.

Chcąc wywołać zdalną metodę, musimy zarejestrować naszą klasę w rejestrze RMI. Posłuży nam do tego klasa MyServerMain. Utwórzmy więc taką klasę w naszym projekcie. To co musimy w niej zrobić to zarejestrować nasz obiekt pod jakąś nazwą. W tym przypadku będzie to nawa MyRemoteObject i obiekt klasy MyServerImpl. Kod poniżej:



Teraz już możemy spróbować uruchomić nasz serwer. Wciśnięcie Run (Ctrl+F11) w Eclipse powoduje wyjątki. Trzeba skorzystać z linii poleceń. Będę posługiwał się bezpośrednio komendami (Windows) aby lepiej można było zrozumieć co się w aplikacji dzieje.

Na początek utówrzmy sobie plik runServer.bat w katalogu projektu (u mnie: D:\Java_programy\RMIServer). Umieśćmy w nim taką zawartość:



Na początek należy odpalić rmiregistry, czyli rejestr początkowy RMI. Poprzedzmay go komendą start, aby otworzył nam się w osobnym oknie i kolejna komenda mogła się wykonać. Następnie uruchamiamy już naszą aplikację. Podajemy flagę -cp aby wskazać gdzie nasze skompilowane klasy leżą i następnie nazwę klasy (wraz z pakietem).

I tu zaczyuna sie pierwszy problem. Dostajemy spory stos wyjątków. Pośród nich można dostrzeć:



Rozwiązanie jakie kiedyś znalazłem, to można ustawić właściwość (ang. Property) java.rmi.server.codebase. Chodzi o to, że rmiregistry nie wie gdzie jest bytecode, z którego ma korzystać i trzeba mu to jakoś powiedzieć.

Wspomniane property możemy ustawić na 2 sposoby. Pierwszy z nich to wywołanie System.setProperty(key, value), a drugi to odpowiednie wywołanie z lini poleceń:
-Dkey=value

Zmodyfikujmy więc nasz skryp uruchomienowy:



Dobra, działa (na razie). Teraz sie zajmijmy klientem. Musi on korzystać z Menadzera bezpieczeństwa RMI, jeśli chcemy ładować kod ze zdalnego serwera. Dla projektu klienta tworzymy osobny projekt o nazwie RMIClient. Tworzymy w nim pakiet (w moim przypadku com.blogspot.mstachniuk.example.rmiclient) i umieszczamy w nim nową klasę MyClientMain. Po zainicjowaniu menażera bezpieczeństwa, musimy się dostać do zdalnego obiektu. Przykładowy kod klasy klienta poniżej:




Aby Eclipse nam nie krzyczało, że nie wie co to MyServerInt, skopiujmy więc ten plik z projektu serwera do projektu klienta, pamiętając aby umieścić go w tym samym pakiecie (nazwa pakietu jest nierozłączną nazwą klasy). Gdybyśmy plik umiescili w innym pakiecie lub zmienili nazwę klasy otrzymalibyśmy wyjątek ClassCastException.

Sprawdzamy czy coś działa. Uruchamiamy najpierw serwer (za pomocą naszego skryptu), a następnie klienta (z poziomu Eclipse). Dostajemy wyjątek java.security.AccessControlException, rzucany przez metdę: lookup(). Metoda ta próbuje odnaleść zdalny obiekt. Okzuje się bowiem, że RMISecurityManager zabrania nawiązywania połączenia w sieci. Musimy więc utworzyć plik polityki bezpieczeństwa. W tym celu tworzymy plik o nazwie client.policy w katalogu bin projektu naszego klienta. W pliku tym umieszczamy następującą zawartość, dającą nam nieograniczony dostęp do wszystkiego:



Podczas wdrożenia aplikacji trzeba bedzie plik ten zmodyfikować, w myśl zasady nie dawać więcej niż trzeba. Czas napisać skrypt uruchomieniowy dla klienta (katalog projektu klienta: D:\Java_programy\RMIClient):



Niestety dalej ten sam błąd. Rozwiązaniem tego problemu moze być dodanie pełnej (bezwzglednej) ścieżki do pliku cient.policy. Modyfikujemy nasz skrypt:



Warto jednak plik policy przenieść do głównego katalogu projektu. Po co? A no podczas czyszczenia projektu w Eclipse zawartość bin jest usuwana i możemy stracić nasz plik. Modyfikujemy więc odpowiednio skrypt uruchomieniowy:



Warto też czasem sprawdzić w kodzie, czy udało nam się uzyskać odpowiednie pozwolenia:



Gdyby druga metoda rzuciła java.security.AccessControlException oznaczało by to, że nie udało się ustawić odpowiedniej polityki bezpieczeństwa, czyli pewnie ścieżka jest błędna.


Oczywiście u Was ścieżki mogą byc trochę inne i musicie je dopasować do tego gdzie trzymacie projekt. Odpalamy skrypt i... udało się. Nie ma żadnego wyjątku:) Tyle że nasz kod klienta za wiele nie robi. Dodajmy więc poniższe 2 liniki do kodu klienta (za wywołaniem lookup()):



W tym momencie ładnie widać, że operowanie na zdalnych obiektach jest tak samo proste jak operowanie na lokalnych. Wywołanie metody niczym się nie różni.
Odpalmy klienta. Na konsoli powiązanej z programem klienta powinniśmy zobaczyć wynik działania:

Wynik: getDescription: Ala ma kota

a na konsoli serwerowej:

MyServerImpl.getDescription Ala ma kota

Oznacza to, że zadziałało. Zmieńmy tylko zawartość pliku client.policy, na następującą:



Powyższy plik oznacza zezwolenie na uzyskanie połaczenia z dowlną maszyną na portach 1024 - 65535. Domyślnie RMI siedzi na porcie 1099, a obiekty serwera mogą korzystać z wyższych portów. Chcąc być bardziej restrykcyjnym zamiast gwiazdki * możemy podać localhost.

Podsumowując, stworzyliśmy prosty serwer i klienta RMI. Przedstawiłem klika wyjątków, na które można się natknąć i podałem sposób radzenia sobie z nimi. Po więcej informacji odsyłam do ksiazki Core Java 2 Techniki Zaawansowane, a także do dokumentacji i tutoriali na stronie Sun'a. Z tego co mi wiadomo istnieje jeszcze wtyczka do Eclipse'a ułatwiająca pisanie aplikacji typu RMI. Ja osobiście nie korzystałem, więc nie wiem jak ona działa.

Więcej informacji:
[1] Tutorial na stronie Oracle: http://download.oracle.com/javase/tutorial/rmi/index.html
[2] Wprowadzenie do RMI w Javie 2 by Seweryn Hejnowicz:
http://www.ii.uni.wroc.pl/~prz/200405/2005lato/java/rmi/referat_rmi.htm
[3] Tworzenia aplikacji rozproszonej RMI by dr inż. Tomasz Kubik:
http://tomasz.kubik.staff.iiar.pwr.wroc.pl/dydaktyka/Java/JavaWyk06-RMI-TK.pdf
[4] Java. Techniki zaawansowane. Wydanie VIII
http://helion.pl/ksiazki/java_techniki_zaawansowane_wydanie_viii_cay_s_horstmann_gary_cornell,javtz8.htm (ja czytałem starsze wydanie i bardzo ładnie było wszystko opisane)
[5] Czysty kod. Podręcznik dobrego programisty:
http://helion.pl/ksiazki/czysty_kod_podrecznik_dobrego_programisty_robert_c_martin,czykod.htm (odnośnie serialVersionUID)