gniazdka, RMI 14.1 Pakiet java.net 14.2 Mechanizm gniazdek TCP 14.3 Przykład aplikacji klient-serwer 14.4 RMI model DOA 14.5 Przykład aplikacji rozproszonej W. Kasprzak: Programowanie zdarzeniowe 14-1 14.1 Pakiet java.net - wprowadzenie 1) Elementy programowania sieciowego Aplikacje programowe w Javie korzystają z protokółów warstwy transportowej sieci: Transmission Control Protocol (TCP) lub User Datagram Protocol (UDP). TCP to połączeniowy, niezawodny protokół typu point-to-point dane nadchodzą w kolejności wysłania. Z TCP korzystają takie aplikacje, jak Hypertext Transfer Protocol (HTTP), File Transfer Protocol (FTP), Telnet. UDP to bezpołączeniowy protokół niezaleŝnego przesyłania pakietów danych (tzw. datagramy). Korzystają z niego programy nie wymagające gwarantowanych połączeń, np. serwer zegara, program ping. Porty - logiczne punkty wejścia do aplikacji pracujących na danym komputerze, dołączonym do sieci. W. Kasprzak: Programowanie zdarzeniowe 14-2
Dane przekazywane przez Internet posiadają adres swojego przeznaczenia: adres komputera - 32-bitowy adres IP, adres portu - 16-bitowy numer portu. W komunikacji według protokółu TCP, serwer związuje swoje gniazdko z określonym portem rejestruje się pod określonym portem. RównieŜ w protokóle UDP pakiety kierowane są do zadanego portu, przydzielonego pewnej aplikacji. Numery portów: od 0 do 65535. Numery zastrzeŝone portów: od 0 do 1023 zarezerwowane na usługi takie, jak HTTP, FTP, itd. W. Kasprzak: Programowanie zdarzeniowe 14-3 Klasy w java.net Komunikację zgodną z tymi protokółami TCP i UDP realizują klasy pakietu java.net: URL, URLConnection, Socket, ServerSocket (dla TCP); DatagramPacket, DatagramSocket, MulticastSocket (dla UDP). 2) Klasa URL URL (Uniform Resource Locator) to adres zasobu w Internecie. URL ma postać napisu specyfikującego: protokół dla dostępu do zasobu i lokalizację zasobu. URL jest teŝ nazwą klasy w java.net. Obiekt klasy URL reprezentuje adres URL. Konstruktory klasy URL 1. Z parametrem typu String reprezentującym adres bezwzględny. Np.: URL adrespw = new URL("http://www.pw.edu.pl/"); W. Kasprzak: Programowanie zdarzeniowe 14-4
2. Z dwoma parametrami obiektem URL reprezentującym adres bazowy i parametrem typu String reprezentującym adres względny. Np.: dane są dwa adresy URL http://www.ia.pw.edu.pl/dydaktyka/proz.html http://www.ia.pw.edu.pl/dydaktyka/probe.html MoŜna utworzyć obiekty klasy URL dla obu stron na podstawie adresów względem jednej bazy - http://www.ia.pw.edu.pl/dydaktyka/ : URL dydaktyka = new URL("http://www.ia.pw.edu.pl/dydaktyka/"); URL dydaktykaproz = new URL(dydaktyka, "proz.html"); URL dydaktykaprobe = new URL(dydaktyka, "probe.html"); korzystając z konstruktora: URL(URL bazowyurl, String wzglednyurl) MoŜna teŝ utworzyć referencję do pozycji w dokumencie. Np. URL dydaktykaprozprojekt = new URL(dydaktykaProz, "#PROJEKT"); 3. Trzy-argumentowy konstruktor URL: new URL("http", "www.ia.pw.edu.pl", "/dydaktyka/proz.html"); W. Kasprzak: Programowanie zdarzeniowe 14-5 4. Cztero-argumentowy konstruktor URL uwzględniający numer portu. Np. URL dydaktykaproz = new URL("http", "www.ia.pw.edu.pl,80, "dydaktyka/proz.html"); utworzy obiekt klasy URL dla następującego adresu URL: http://www.ia.pw.edu.pl:80/dydaktyka/proz.html KaŜdy z kontruktorów klasy URL moŝe zgłosić wyjątek MalformedURLException (gdy argumenty referują null lub nieznany protokół). try { URL myurl = new URL(...) catch (MalformedURLException e) { // obsługa wyjątku... Obiekty klasy URL nie mogą być zmieniane po pierwszej inicjalizacji. W. Kasprzak: Programowanie zdarzeniowe 14-6
Podstawowe metody klasy URL Metody zwracające pełny adres URL dla danego obiektu klasy URL: String tostring() String toexternalform(). Metody podające informacje o stanie obiektu URL: getprotocol podaje część URL dotyczącą protokółu; gethost - podaje część URL dotyczącą adresu hosta; getport - podaje część URL dotyczącą numeru portu (liczba integer lub wartość -1 gdy port nie jest ustawiony); getfile - podaje część URL dotyczącą nazwy pliku; getref - podaje część URL dotyczącą referencji w pliku dokumentu. Przykład 14.1. Tworzenie i korzystanie z obiektu klasy URL. import java.net.*; import java.io.*; public class ParseURL { W. Kasprzak: Programowanie zdarzeniowe 14-7 public static void main(string[] args) throws Exception { URL aurl = new URL("http://java.sun.com:80/docs/books/" + "tutorial/index.html#downloading"); System.out.println("protocol = " + aurl.getprotocol()); System.out.println("host = " + aurl.gethost()); System.out.println("filename = " + aurl.getfile()); System.out.println("port = " + aurl.getport()); System.out.println("ref = " + aurl.getref()); Wynik pracy programu: protocol = http host = java.sun.com filename = /docs/books/tutorial/index.html port = 80 ref = DOWNLOADING W. Kasprzak: Programowanie zdarzeniowe 14-8
Metody klasy URL do połączeń w sieci Metoda openstream() Metoda zwraca obiekt strumieniowy klasy java.io.inputstream związany z adresem URL. Przykład 14.2. Wykorzystanie metody openstream() do połączenia w sieci. Odczyt informacji - poprzez obiekt klasy BufferedReader i zapis na standardowym wyjściu. import java.net.*; import java.io.*; public class URLReader { public static void main(string[] args) throws Exception { URL yahoo = new URL("http://www.yahoo.com/"); BufferedReader in = new BufferedReader( new InputStreamReader( yahoo.openstream())); String inputline; W. Kasprzak: Programowanie zdarzeniowe 14-9 while ((inputline = in.readline())!= null) System.out.println(inputLine); in.close(); Metoda openconnection() Metoda zwraca obiekt klasy URLConnection, który moŝe być wykorzystany do szczegółowej współpracy z adresem URL. Np. try { URL yahoo = new URL("http://www.yahoo.com/"); URLConnection yahooconnection = yahoo.openconnection(); catch (MalformedURLException e) { // new URL() nie powiodło się... catch (IOException e) { // openconnection() nie powiodło się... W. Kasprzak: Programowanie zdarzeniowe 14-10
3) Metody klasy URLConnection Przykład 14.3. Odczyt z URL za pomocą obiektu klasy URLConnection. import java.net.*; import java.io.*; public class URLConnectionReader { public static void main(string[] args) throws Exception { URL yahoo = new URL("http://www.yahoo.com/"); URLConnection yc = yahoo.openconnection(); // Połącz BufferedReader in = new BufferedReader( new InputStreamReader( yc.getinputstream())); // Pobierz strumień String inputline; while ((inputline = in.readline())!= null) System.out.println(inputLine); in.close(); W. Kasprzak: Programowanie zdarzeniowe 14-11 Adresy URL do których dołączono skrypty cgi-bin wymagają, aby zapisywać informację do URL. Typowy przykład zapisu do URL polega na wypełnieniu przez uŝytkownika zapytania podanego w postaci formatki na stronie HTML i wysłaniu go do serwera pod adres URL. Przetwarza on zwykle skrypt cgi-bin po stronie serwera i odpowiada w postaci strony HTML. Współpraca programu Javy ze skryptami cgi-bin po stronie serwera wymaga moŝliwości zapisu do URL przez ten program. Czyli wymaga to realizacji następujących kroków w rogramie: 1. Utworzyć obiekt typu URL. 2. Otworzyć połączenie z tym URL. 3. Ustawić moŝliwości wyjściowe dla URLConnection. 4. Pobrać strumień wyjściowy z połączenia bedzie on połączony ze standardowym strumieniem wejściowym skryptu cgi-bin po stronie serwera. 5. Zapisywać dane do strumienia wyjściowego. 6. Zamknąć strumień. W. Kasprzak: Programowanie zdarzeniowe 14-12
Przykład 14.4. Skrypt na naszej stronie sieci odczytuje napis ze standardowego wejścia, odwraca kolejność znaków napisu i zapisuje wynik do standardowego wyjścia. Skrypt wymaga danych wejściowych w formacie string=napis, gdzie napis jest napisem do odwrócenia. import java.io.*; import java.net.*; public class Reverse { public static void main(string[] args) throws Exception { if (args.length!= 1) { System.err.println("Wywołanie: java Reverse " + "napis"); System.exit(1); String stringtoreverse = URLEncoder.encode(args[0]); URL url = new URL("http://java.sun.com/cgi-bin/backwards"); URLConnection connection = url.openconnection(); connection.setdooutput(true); // UmoŜliwić wyjście do URL PrintWriter out = new PrintWriter( connection.getoutputstream()); W. Kasprzak: Programowanie zdarzeniowe 14-13 out.println("string=" + stringtoreverse); out.close(); BufferedReader in = new BufferedReader( new InputStreamReader( connection.getinputstream())); String inputline; while ((inputline = in.readline())!= null) System.out.println(inputLine); in.close(); Komentarz do programu: 1) Przetwarzanie parametrów programu z linii komend: if (args.length!= 1) { System.err.println("Wywołanie: java Reverse " + "napis"); System.exit(-1); String stringtoreverse = URLEncoder.encode(args[0]); W. Kasprzak: Programowanie zdarzeniowe 14-14
Ma być tylko jeden parametr programu i kodowany on jest dla przesłania (w tym spacje i znaki przestankowe) go do skryptu cgi-bin. 2) Utworzenie obiektu klasy URL dla skryptu na java.sun.com, otwarcie URLConnection i ustawienie połączenia do zapisu: URL url = new URL("http://java.sun.com/cgi-bin/backwards"); URLConnection c = url.openconnection(); c.setdooutput(true); // Ustawienie do zapisu 3) Utworzenie strumienia wyjściowego dla połączenia i utworzenie na nim obiektu PrintWriter : PrintWriter out = new PrintWriter(c.getOutputStream()); Jeśli URL nie zezwala nam na zapis (brak wyjścia) to metoda getoutputstream zgłosi wyjątek UnknownServiceException. 4) Zapis danych do strumienia wyjściowego i zamknięcie strumienia: out.println("string=" + stringtoreverse); out.close(); Strumien wyjściowy klienta jest strumieniem wejściowym serwera skryptu realizującego odrócenie kolejności liter. W. Kasprzak: Programowanie zdarzeniowe 14-15 5) Odczytujemy informację zwrotną ze skryptu: BufferReader in = new BufferedReader( new InputStreamReader(c.getInputStream())); String inputline; while ((inputline = in.readline())!= null) System.out.println(inputLine); in.close(); Przykład wyniku działania: Reverse Me reversed is: em esrever W. Kasprzak: Programowanie zdarzeniowe 14-16
14.2 Mechanizm gniazdek TCP Do komunikacji sieciowej z wykorzystaniem protokółu TCP wykorzystywane są mechanizmy: strumieni i gniazdek (ang. socket) - udostępniany w pakiecie java.net.*. Programista moŝe skupić się na tym jakie dane chce przesłać i pod jaki adres albo co chce odebrać i skąd. 1) Gniazdka sieciowe dla TCP: Socket i ServerSocket Do bezpośredniego komunikowania uŝywany jest obiekt klasy Socket, a do prowadzenia nasłuchu - ServerSocket. Tworząc gniazdko do komunikacji typu Socket podajemy adres komputera, z którym chcemy się połączyć oraz numer portu, na którym ma zostać nawiązane połączenie. Np.: Socket s = new Socket("localhost", 4444); - gdy chcemy połączyć się z komputerem lokalnym na porcie 4444; Socket s = new Socket("SOLARIS", 4444); W. Kasprzak: Programowanie zdarzeniowe 14-17 - gdy chcemy połączyć się z komputerem mającym nazwę SOLARIS w sieci lokalnej LAN na porcie 4444; Socket s = new Socket("194.29.130.225", 4444); - gdy chcemy połączyć się z komputerem o danym adresie IP na porcie 4444; Socket s = new Socket("www.ia.pw.edu.pl", 4444); - gdy chcemy połączyć się z danym serwerem internetowym na porcie 4444. Tworząc gniazdko do nasłuchu typu ServerSocket, podajemy tylko numer portu, na którym ma być prowadzony nasłuch, np.: ServerSocket ss = new ServerSocket(4444); Numer portu musi zawierać się w przedziale [0,...,65535]. JednakŜe niektóre numery portów są zajmowane przez usługi sieciowe takie, jak standardowe protokoły transmisji, np.: 21 przez FTP, 80 przez HTTP. Ponadto pewnych portów uŝywają same systemy operacyjne. W rezultacie uŝywanie numerów portów z przedziału [0,...,1023] jest zabronione. W. Kasprzak: Programowanie zdarzeniowe 14-18
2) Realizacja prostej komunikacji Przykład 14.5.a. Nawiązanie komunikacji pomiędzy dwoma programami. Najpierw przedstawiamy program serwera nasłuchujący na gniazdku i odbierający pojawiające się dane. //Serwer.java import java.io.*; import java.net.*; public class Serwer { public static void main(string args[]) throws IOException { ServerSocket ss = new ServerSocket(4444); // Gniazdko nasłuchu while(true) { // Praca w pętli Socket s = ss.accept(); // Oczekiwanie na połączenie DataInputStream dis = new DataInputStream( s.getinputstream()); // Pobierz strumień String msg = dis.readutf(); // Odczyt danych ze strumienia System.out.println(msg); // Przetwarzaj dane tu : wyprowadź dis.close(); // Zamknięcie strumienia W. Kasprzak: Programowanie zdarzeniowe 14-19 s.close(); // Zamknięcie połączenia gniazdka PowyŜszy program działa, jak praktycznie kaŝdy serwer, w nieskończonej pętli. Nasłuchuje on na porcie 4444, korzystając z gniazdka ServerSocket. Sam proces oczekiwania na połączenie realizowany jest w metodzie accept. Gdy na wybranym porcie pojawi się informacja o próbie uzyskania połączenia, następuje zakończenie nasłuchu i utworzenia gniazdka Socket. Dalej następuje odczytanie strumienia podłączonego do gniazdka, zawartość jest wypisywana na ekran, a strumień i gniazdo są zamykane. Następnie program serwera znowu czeka na próbę nawiązania połączenia. Proste uŝycie pętli nieskończonej powoduje, Ŝe aby go zakończyć, musimy uŝyć kombinacji klawiszy Ctrl+C. W. Kasprzak: Programowanie zdarzeniowe 14-20
Przykład 14.5.b. Wysłanie danych przez klienta. Program klienta tworzy gniazdko Socket w celu nawiązania połączenia z lokalnym komputerem na porcie 4444. Po zaakceptowaniu połączenia tworzony jest strumień, przez który przesyłany jest obiekt typu String. Następnie strumień i gniazdko są zamykane, a program kończy działanie. //Klient.java import java.io.*; import java.net.*; public class Klient { public static void main(string args[]) throws IOException { Socket s = new Socket("localhost", 4444); DataOutputStream dos = new DataOutputStream(s.getOutputStream()); dos.writeutf("witaj w sieci!"); // Prześlij napis do serwera dos.close(); // Zamknij strumień s.close(); // Zamknij gniazdko W. Kasprzak: Programowanie zdarzeniowe 14-21 Podsumowanie przykładu 14.5 Musimy najpierw skompilować oba programy i uruchomić program serwerowy. Następnie w drugim okienku-konsoli uruchamiamy program kliencki. KaŜde kolejne uruchomienie programu-klienta spowoduje wyświetlenie jednego napisu w programie-serwerze, co oznacza, Ŝe połączenie jest skutecznie nawiązywane. W. Kasprzak: Programowanie zdarzeniowe 14-22
14.3 Przykład aplikacji klient - serwer Aplikacje sieciowe są tworzone w celu zdalnego wykonywania jakichś konkretnych czynności bądź udostępniania usług, czy zasobów. Zobaczmy przykład aplikacji klient-serwer, która - poza nawiązaniem łączności - będzie realizowała zdalną obsługę trzech poleceń systemowych: list - wyświetlenie zawartości bieŝącego katalogu na serwerze; get - pobranie pliku z serwera i wyświetlenie go na konsoli klienta; exit - zakończenie pracy klienta. 1) Program serwera Przykład 14.6. Program serwera pracuje w pętli nieskończonej nasłuchując na porcie 4444 i oczekując na Ŝądania połączeń od klientów. Gdy takie nadchodzą - kaŝdemu przydzielany jest osobny wątek do obsługi. //PlikSerwer.java import java.io.*; W. Kasprzak: Programowanie zdarzeniowe 14-23 import java.net.*; /* Wątek obsługujący połączenie pracuje w pętli nieskończonej. Czyta on ze strumienia podłączonego do gniazdka obiekt serializowalnej klasy programisty Command, a dokładniej - najpierw czyta obiekt ze strumienia, a następnie zmienia jego typ na Command. */ public class PlikSerwer implements Runnable { Socket socket; public PlikSerwer(Socket s) { socket = s; public void run() { // Metoda dla wątku try { ObjectInputStream ois = new ObjectInputStream(socket.getInputStream()); // Strumień wejściowy ObjectOutputStream oos = // i wyjściowy new ObjectOutputStream(socket.getOutputStream()); while(true) { // Pętla nieskończona Command cmd = (Command)ois.readObject(); // Odczyt obiektu W. Kasprzak: Programowanie zdarzeniowe 14-24
/* Następnie sprawdza zawartość odczytanego obiektu i w zaleŝności od niej tworzy odpowiedni obiekt klasy programisty Response. Tworzonemu obiektowi przekazywany jest efekt obsługi polecenia, tzn. napis na zakończenie pracy, lista plików w katalogu bieŝącym serwera, czy zawartość konkretnego pliku. */ // Obsługa komendy zakończenia pracy klienta if (cmd.getcommand() == Command.EXIT) { send(oos, new Response(cmd, "Do zobaczenia")); break; // Obsługa wyświetlania zawartości bieŝącego katalogu serwera else if (cmd.getcommand() == Command.LIST) { File file = new File("."); // Plik - aktualny katalog send(oos, new Response(cmd, file.list())); // Lista plików else if (cmd.getcommand() == Command.GET) { // Obsługa wyświetlenia zawartości danego pliku na serwerze String filename = (String) cmd.getcommandarg(); W. Kasprzak: Programowanie zdarzeniowe 14-25 File file = new File(filename); byte buff [] = new byte[(int) file.length()]; FileInputStream fis = new FileInputStream(file); fis.read(buff); // Wczytaj dane z podanego pliku fis.close(); send(oos, new Response(cmd, buff)); // Prześlij zawartość pliku else { send(oos, new Response(cmd, "Nieznana komenda")); ois.close(); // Zamknij strumienie wejściowy oos.close(); // - - wyjściowy socket.close(); // Zamknij gniazdko catch(exception e) { System.out.println(e); W. Kasprzak: Programowanie zdarzeniowe 14-26
/* Obiekt zapisywany jest do strumienia wyjściowego czyli resultat przesyłany jest do klienta w obiekcie klasy Response. */ private void send(objectoutputstream oos, Response response) throws IOException { oos.writeobject(response); oos.flush(); /* Program serwera pracuje w pętli nieskończonej nasłuchując na porcie 4444 i oczekując na Ŝądania połączeń od klientów. Gdy takie nadchodzą - kaŝdemu przydzielany jest osobny wątek do obsługi. */ public static void main(string args[]) throws IOException { ServerSocket ss = new ServerSocket(4444); // Utwórz gniazdko System.out.println("Nasłuchiwanie na porcie 4444..."); while(true) { Socket s = ss.accept(); // Oczekuje na Ŝądanie od klienta System.out.println("Akceptuję połączenie z: "+s.getinetaddress()); (new Thread(new PlikSerwer(s))).start(); // Uruchom wątek obsługi W. Kasprzak: Programowanie zdarzeniowe 14-27 2) Klasy pomocnicze w tym przykładzie Przykład 14.7. Opiszemy tu klasę pomocniczą serwera Command. Klasa pomocnicza zawiera deklaracje stałych identyfikujących polecenia wydawane serwerowi przez klienta. Zawiera teŝ zmienną określającą bieŝące polecenie wydane serwerowi oraz metody ją obsługujące. Cała klasa jest serializowana, aby jej obiekty mogły być zapisywane do strumieni. //Command.java import java.io.*; public class Command implements Serializable { public final static int LIST = 0; public final static int GET = 1; public final static int EXIT = 2; private int cmd; private Object arg; public Command (int cmd) { W. Kasprzak: Programowanie zdarzeniowe 14-28
this.cmd = cmd; public Command (int cmd, Object arg) { this.cmd = cmd; this.arg = arg; public int getcommand() { return cmd; public Object getcommandarg() { return arg; Przykład 14.8. Opiszemy teraz klasę Response. Klasa zawiera prywatne pole do przechowywania obiektu będącego rezultatem realizacji polecenia klienta. Dzięki serializacji obiekty tej klasy mogą być przesyłane za pomocą strumieni, tak samo jak obiekty Command. // Response.java W. Kasprzak: Programowanie zdarzeniowe 14-29 import java.io.*; public class Response implements Serializable { private Object data; private Command cmd; public Response (Command cmd, Object data) { this.cmd = cmd; this.data = data; public Object getdata() { return data; public Command getcommand() { return cmd; W. Kasprzak: Programowanie zdarzeniowe 14-30
3) Program klienta w tym przykładzie Przykład 14.9. Program klienta. // PlikKlient.java import java.io.*; import java.net.*; public class PlikKlient { Socket socket; ObjectOutputStream oos; ObjectInputStream ois; public PlikKlient(String host, int port) throws IOException { socket = new Socket(host, port); oos = new ObjectOutputStream(socket.getOutputStream()); ois = new ObjectInputStream(socket.getInputStream()); public void go() { try { BufferedReader br = new BufferedReader( W. Kasprzak: Programowanie zdarzeniowe 14-31 new InputStreamReader(System.in)); while(true) { System.out.print("Zdalny>"); System.out.flush(); String line = br.readline(); // Komenda końca pracy klienta if (line.startswith("exit")) { Command cmd = new Command(Command.EXIT); Response response = send(cmd); System.out.println(response.getData()); System.exit(0); //Komenda wyświetlenia zawartości bieŝącego katalogu serwera else if (line.startswith("list")) { Command cmd = new Command(Command.LIST); Response response = send(cmd); String list [] = (String []) response.getdata(); for (int i=0; i<list.length; i++) { W. Kasprzak: Programowanie zdarzeniowe 14-32
System.out.println(list[i]); //Komenda wyświetlenia zawartości danego pliku na serwerze else if (line.startswith("get")) { Command cmd = new Command(Command.GET, line.substring(4)); Response response = send(cmd); byte content [] = (byte []) response.getdata(); System.out.println(new String(content)); else { System.out.println("Nieznana komenda: "+line); catch (Exception e) { System.out.println(e); W. Kasprzak: Programowanie zdarzeniowe 14-33 private Response send(command cmd) throws Exception { oos.writeobject(cmd); oos.flush(); return (Response) ois.readobject(); public static void main(string args[]) throws IOException { (new PlikKlient("localhost", 4444)).go(); Program klienta odczytuje z konsoli polecenia od uŝytkownika, zapisuje je w postaci obiektów do strumienia podłączonego do gniazdka pracującego na porcie 4444, odczytuje ze strumienia przychodzące z serwera odpowiedzi i wyświetla je na konsoli. W. Kasprzak: Programowanie zdarzeniowe 14-34
4) Uruchomienie całej aplikacji Po skompilowaniu wszystkich klas uruchamiamy serwer, a następnie klienta (lub wielu klientów) - oczywiście kaŝdy z programów uruchamiany jest w osobnym okienku konsoli. Gdy oba programy znajdują się na jednym komputerze (tj. testujemy połączenie na komputerze lokalnym), nie muszą być umieszczone w jednym katalogu. JednakŜe wtedy zarówno w katalogu z serwerem - jak i z klientem - muszą znajdować się skompilowane klasy pomocnicze. Po uruchomieniu obu programów moŝemy wydać w konsoli klienta któreś z trzech obsługiwanych poleceń i zobaczyć efekt jego działania. W. Kasprzak: Programowanie zdarzeniowe 14-35 14.4 RMI (Remote Method Invocation) model DOA 1) Model DOA Mechanizm RMI wykorzystuje model komunikacji klient-serwer i realizuje paradygmat programowania obiektowego - rozproszonego (DOA distributed object application): W. Kasprzak: Programowanie zdarzeniowe 14-36
program serwera tworzy obiekty, udostępnia referencje do nich (dzięki ich zarejestrowaniu poprzez rmiregistry) i oczekuje na wywołania metod na tych obiektach inicjowane przez klientów; programy klientów uzyskują referencję do zdalnego obiektu i wywołują na nim metody. 2) Typowy schemat współpracy klient - serwer w RMI (A) Zdalny interfejs i zdalny obiekt Aplikacja z uŝyciem Java RMI składa się z interfejsów i klas, przy czym spodziewamy się, Ŝe implementacje metod mogą być rozproszone po róŝnych maszynach. Zdalnym obiektem nazywamy obiekt, którego metody mogą być wywoływane z róŝnych maszyn wirtualnych Javy. Takie metody deklarowane są w tzw. zdalnych interfejsach: zdalny interfejs dziedziczy po interfejsie java.rmi.remote, w liście wyjątków kaŝdej metody tego interfejsu istnieje deklaracja wyjątku java.rmi.remoteexception. W. Kasprzak: Programowanie zdarzeniowe 14-37 Serwer związuje nazwę w rejestrze ze swoim zdalnym obiektem, który moŝe być wołany zdalnie. (B) Pobranie obiektu i wywołanie metody Klient odnajduje zdalny obiekt w rejestrze poprzez jego nazwę i wywołuje na nim metodę interfejsu. RMI nie kopiuje zdalnego obiektu do programu odbiorcy lecz przekazuje namiastkę (stub) zdalnego obiektu reprezentuje ona referencję do zdalnego obiektu. Ta referencja moŝe być konwertowana na dowolny zdalny interfejs implementowany przez klasę zdalnego obiektu. Klasa zdalnego obiektu pełni w programie klienta rolę pośrednika (proxy) w przekazie danych. Zdalnie moŝna wywołać wyłącznie metody zdalnych interfejsów danej klasy. (C) Dynamiczne ładowanie definicji klasy W. Kasprzak: Programowanie zdarzeniowe 14-38
Jeśli klasa obiektu nie jest zdefiniowana na maszynie odbiorcy obiektu to RMI pobierze od nadawcy i prześle ten kod. Mechanizm RMI korzysta z istniejącego serwera sieciowego dla przekazywania bajtkodu klas pomiędzy klientem a serwerem w obie strony. 3) Podstawowe klasy i interfejsy w java.rmi W. Kasprzak: Programowanie zdarzeniowe 14-39 4) Tworzenie aplikacji rozproszonej w RMI NaleŜy: zdefiniować komponenty aplikacji (lokalne i zdalne obiekty, zdalne interfejsy, implementacje metod); skompilować program (kompilator javac) i wygenerować namiastki zdalnych obiektów (kompilator rmic); udostępnić klasy i namiastki obiektów w sieci, uruchomić aplikację (dokonać rejestracji w rejestrze zdalnych obiektów RMI, uruchomić serwer i klientów). W. Kasprzak: Programowanie zdarzeniowe 14-40
14.5 Przykład aplikacji rozproszonej o dynamicznie ładowanym kodzie Przykład 14.10. Projekt serwera obliczeń w oparciu o mechanizm RMI Serwer akceptuje zadania pochodzące od klienta, wykonuje te zadania i zwraca wyniki. Serwer składa sie ze zdalnego interfejsu Compute i klasy ComputeEngine implementującej ten interfejs, przeznaczonej dla utworzenia zdalnego obiektu. W metodzie main tej klasy następuje utworzenie tego obiektu, jego zarejestrowanie i ustawienie menadŝera zabezpieczeń (security manager). Interfejsy typu Remote Interface W przykładzie występują 2 interfejsy zdalne deklarujące po jednej metodzie. W. Kasprzak: Programowanie zdarzeniowe 14-41 Interfejs Compute serwera ComputeEngine pozwala na dostarczanie zadania do wykonania. package compute; import java.rmi.remote; import java.rmi.remoteexception; public interface Compute extends Remote { Object executetask(task t) throws RemoteException; Dziedziczenie z interfejsu java.rmi.remote oznacza, Ŝe: metody dziedziczonego interfejsu mogą być wołane z dowolnej maszyny wirtualnej Javy; obiekt z implementacją tego interfejsu staje się zdalnym obiektem. Zdalna metoda musi deklarować zgłaszanie wyjątku java.rmi.remoteexception. Jest to sprawdzalny wyjątek. W. Kasprzak: Programowanie zdarzeniowe 14-42
Interfejs klienta Task wyznacza sposób wykonania przez serwer powierzonego zadania. package compute; import java.io.serializable; public interface Task extends Serializable { Object execute(); Dziedziczenie interfejsu po java.io.serializable oznacza, Ŝe obiekt klasy implementującej ten interfejs moŝe zostać zamieniony na strumień bajtów i jednoznacznie zrekonstruowany z niego. Obiekty róŝnych klas mogą zostać przekazane do obiektu Compute, jeśli tylko klasy implementują interfejs Task. W. Kasprzak: Programowanie zdarzeniowe 14-43 Implementacja zdalnego interfejsu Klasa uŝytkownika engine.computeengine implementuje interfejs zdalny Compute, a w jej metodzie main inicjalizowany jest obiekt tej klasy. package engine; import java.rmi.*; import java.rmi.server.*; import compute.*; public class ComputeEngine extends UnicastRemoteObject implements Compute { // Konstruktor public ComputeEngine() throws RemoteException { super(); // Wywoła metodę exportobject // Konstruktor nadklasy został by wywołany nawet przy braku super(); // Metoda dla pobrania obiektu klienta do wykonania public Object executetask(task t) { return t.execute(); // W. Kasprzak: Programowanie zdarzeniowe 14-44
// Metoda main dla ustawienia obiektu public static void main(string[] args) { if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); String name = "//host/compute"; // Określa nazwę obiektu // zdalnego serwera try { Compute engine = new ComputeEngine(); // Utworzy obiekt Naming.rebind(name, engine); // Rejestracja obiektu zdalnego System.out.println("Compute: związany"); catch (Exception e) { System.err.println("Compute: wyjątek " + e.getmessage()); e.printstacktrace(); // Koniec metody main! W. Kasprzak: Programowanie zdarzeniowe 14-45 Komentarz 1) Klasa serwera implementuje zdalny interfejs: public class ComputeEngine extends UnicastRemoteObject implements Compute i dziedziczy po klasie bazowej java.rmi.server.unicastremoteobject. Ta klasa zapewnia podstawową funkcjonalność zdalnego obiektu: implementuje metody klasy java.lang.object (equals, hashcode, tostring) odpowiednio dla zdalnego obiektu; zawiera konstruktory i metody statyczne przewidziane do wyeksportowania zdalnego obiektu (exportobject()) tzn. aby zdalny obiekt mógł odbierać wezwania od klientów; zapewnia połączenia point-to-point w oparciu o gniazdka. Inna klasa dla zdalnych obiektów java.rmi.activation.activatable (dla zdalnych obiektów wywoływalnych na Ŝądanie). W. Kasprzak: Programowanie zdarzeniowe 14-46
2) Konstruktor klasy ComputeEngine : public ComputeEngine() throws RemoteException { super(); // Następuje tu teŝ eksportowanie obiektu NaleŜy zadeklarować zgłaszalność wątku RemoteException, który zostanie zgłoszony, gdy nie powiedzie się eksport obiektu. 3) Implementacje metod interfejsów zdalnych Klasa implementuje jedną metodę interfejsu zdalnego Compute metodę executetask: public Object executetask(task t) { return t.execute(); Metoda wyznacza sposób komunikacji pomiędzy obiektem klasy ComputeEngine a jego klientami. Klienci przekazują serwerowi obiekt klasy implementującej interfejs Task i posiadającej implementację metody execute. W. Kasprzak: Programowanie zdarzeniowe 14-47 4) Klasa klienta Do serwera moŝe być przekazany obiekt dowolnego typu o ile jest to: zmienna typu prostego, obiekt zdalny lub serializowalny obiekt (tzn. jego klasa implementuje interfejs java.io.serializable). Sposoby przekazywania obiektów zdalnych, parametrów i wyników. Zdalne obiekty przekazywane są przez referencję. Po stronie klienta tworzona jest namiastka obiektu (stub) pełniąca rolę proxy (przekazującą odwołania do rzeczywistego obiektu serwera). Zwykłe obiekty przekazywane są przez wartość poprzez ich serializację. W zdalnym wywołaniu metody zwykłe obiekty (parametry, zwracany wynik, wyjątki) są przekazywane przez wartość tworzona jest ich kopia w zdalnej wirtualnej maszynie Javy. 5) Metoda main Metoda main() klasy ComputeEngine nie jest elementem interfejsu, czyli nie jest metodą wywoływalną zdalnie. W. Kasprzak: Programowanie zdarzeniowe 14-48
W pierwszej kolejności metoda main() powinna utworzyć i zainstalować zarządcę polityki bezpieczeństwa (Security Manager), w przeciwnym razie mechanizm RMI nie zezwoli na ładowanie zdalnych klas. W RMI dostępna jest klasa RMISecurityManager, która realizuje pdobną politykę bezpieczeństwa, jak ta stosowana dla apletów. if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); Następnie utworzony zostaje obiekt klasy ComputeEngine i udostępniony klientom na anonimowym porcie: Compute engine = new ComputeEngine(); // Utworzenie obiektu Mechanizm RMI zarządza rejestrem zdalnych obiektów, z którego moŝna pobrać referencję do zdalnego obiektu na podstawie jego nazwy. Rejestr ten moŝe być wspólny dla wszystkich serwerów na W. Kasprzak: Programowanie zdarzeniowe 14-49 danej maszynie albo teŝ proces serwera moŝe posiadać odrębny taki rejestr. Metody dostępu do rejestru zdalnych obiektów wyznacza interfejs java.rmi.naming (związanie, rejestracja, pobranie). W metodzie main klasy ComputeEngine nadajemy nazwę String name = "//host/compute"; // host to nazwa maszyny // Compute to nazwa identyfikująca obiekt zdalny w rejestrze. i rejestrujemy obiekt engine klasy ComputeEngine w rejestrze: Naming.rebind(name, engine); Podczas wywołania rebind() moŝe powstać wyjątek: RemoteException. Parametry wywołania metody Naming.rebind : pierwszy parametr jest typu java.lang.string o postaci adresu URL ; Np. //host:1234/objectname Jeśli brak jest numeru portu to przyjmuje się domyślny port: 1099. W. Kasprzak: Programowanie zdarzeniowe 14-50
Po zarejestrowaniu obiektu zdalnego i wydruku informacji o gotowości do pracy metoda main() kończy się wykonywać (wątek główny kończy swoje wykonanie!). Jednak istnieje referencja do obiektu przekazana innemu obiektowi bedącemu rejestrem zdalnych obiektów. Mechanizm RMI pilnuje, aby proces dla obiektu ComputeEngine nadal istniał. W. Kasprzak: Programowanie zdarzeniowe 14-51 Przykład 14.11 Program klienta musi definiować zadanie, które ma wykonać w jego imieniu serwer. W tym przykładzie klient składa się z 2 klas: ComputePi w celu pobrania obiektu serwera Compute ; Pi - klasa implementująca interfejs Task i definiująca zadanie. Definicja Task: package compute; public interface Task extends java.io.serializable { Object execute(); Klasa klienta client.computepi package client; import java.rmi.*; import java.math.*; import compute.*; public class ComputePi { W. Kasprzak: Programowanie zdarzeniowe 14-52
public static void main(string args[]) { if (System.getSecurityManager() == null) { System.setSecurityManager(new RMISecurityManager()); try { // Nazwa serwera w formacie URL String name = "//" + args[0] + "/Compute"; // Pobierz referencje do obiektu zdalnego serwera: Compute comp = (Compute) Naming.lookup(name); Pi task = new Pi(Integer.parseInt(args[1])); // Wywołaj metodę executetask obiektu zdalnego comp // przekazując zadanie do wykonania task : BigDecimal pi = (BigDecimal) (comp.executetask(task)); System.out.println(pi); // Wydrukuj obliczony wynik catch (Exception e) { System.err.println("ComputePi exception: " + e.getmessage()); e.printstacktrace(); W. Kasprzak: Programowanie zdarzeniowe 14-53 Przepływ informacji pomiędzy obiektem klienta klasy ComputePi, rejestrem obiektów zdalnych rmiregistry i obiektem serwera ComputeEngine. Komentarz 1) Klasa klienta instaluje menadŝera bezpieczeństwa Jest to niezbędne, gdyŝ RMI moŝe ładować kod do procesu klienta w tym przypadku namiastkę obiektu ComputeEngine. W. Kasprzak: Programowanie zdarzeniowe 14-54
Podobnie jak serwer równieŝ klient korzysta tu z menadŝera bezpieczeństwa dostępnego w systemie RMI. 2) Klient tworzy nazwę do nadzoru obiektu zdalnego typu Compute Pierwszy parametr z linii wywołania programu, args[0], stanowi nazwę zdalnej maszyny, na której wykonuje się object typu Compute. Klient odwołuje się do metody Naming.lookup w celu odszukania obiektu po jego nazwie w rejestrze zdalnych obiektów zdalnego hosta. Parametr metody (typu String) ma tę samą składnię adresu URL jak parametr metody Naming.rebind w przypadku rejestracji obiektu przez serwer. 3) Klient tworzy nowy obiekt klasy Pi Argumentem wywołania konstruktora Pi jest drugi paramer z inii wywołania programu klienta, args[1], podający liczbę pozycji dziesiętnych dla obliczeń. 4) Klient wywołuje metodę executetask W. Kasprzak: Programowanie zdarzeniowe 14-55 Klient wywołuje metodę executetask zdalnego obiektu typu Compute. Defnicja klasy client.pi, która implementuje inerfejs Task: package client; import compute.*; import java.math.*; public class Pi implements Task { /** Stałe potrzebne dla obliczenia watości pi */ private static final BigDecimal ZERO = BigDecimal.valueOf(0); private static final BigDecimal ONE = BigDecimal.valueOf(1); private static final BigDecimal FOUR = BigDecimal.valueOf(4); /** Tryb zaokrąglania */ private static final int roundingmode = BigDecimal.ROUND_HALF_EVEN; /** Precyzja - liczba pozycji po kropce dziesiętnej */ private int digits; /** Konstruktor */ public Pi(int digits) { W. Kasprzak: Programowanie zdarzeniowe 14-56
this.digits = digits; /** Oblicz pi */ public Object execute() { return computepi(digits); /** według formuły * pi/4 = 4*arctan(1/5) - arctan(1/239) * i po rozwinięciu arctan(x) w szereg. */ public static BigDecimal computepi(int digits) { int scale = digits + 5; BigDecimal arctan1_5 = arctan(5, scale); BigDecimal arctan1_239 = arctan(239, scale); BigDecimal pi = arctan1_5.multiply(four).subtract( arctan1_239).multiply(four); return pi.setscale(digits, W. Kasprzak: Programowanie zdarzeniowe 14-57 BigDecimal.ROUND_HALF_UP); /** * Oblicza wartość arct tangens w radianach według wzoru: * arctan(x) = x - (x^3)/3 + (x^5)/5 - (x^7)/7 + (x^9)/9... */ public static BigDecimal arctan(int inversex, int scale) { BigDecimal result, numer, term; BigDecimal invx = BigDecimal.valueOf(inverseX); BigDecimal invx2 = BigDecimal.valueOf(inverseX * inversex); numer = ONE.divide(invX, scale, roundingmode); result = numer; int i = 1; do { numer = numer.divide(invx2, scale, roundingmode); W. Kasprzak: Programowanie zdarzeniowe 14-58
int denom = 2 * i + 1; term = numer.divide(bigdecimal.valueof(denom), scale, roundingmode); if ((i % 2)!= 0) { result = result.subtract(term); else { result = result.add(term); i++; while (term.compareto(zero)!= 0); return result; Uwaga Obiekt klasy Compute nie wymaga definicji klasy Pi dopóki obiekt klasy Pi nie zostanie przekazany jako argument metody executetask. W tym momencie kod klasy ładowany jest przez RMI do maszyny wirtualnej właściwej dla obiektu typu Compute, wołana jest metoda execute klasy Pi i wykonuje się kod tej metody. Wynikowy obiekt typu Object, w tym W. Kasprzak: Programowanie zdarzeniowe 14-59 przypadku jest on typu java.math.bigdecimal, jest przekazywany zwrotnie do klienta, w którym następuje wydruk tego obiektu. Z punktu widzenia serwera obiektu typu ComputeEngine jest bez znaczenia, co oblicza zlecona metoda. Jedyne co musi wiedzieć obiekt klasy implementującej Compute o przekazywanym obiekcie to to, Ŝe jego klasa posiada metodę execute. Dynamiczne ładowanie klas PoniŜszy rysunek ilustruje skąd ładowane są definicje klas potrzebnych programom: rmiregistry, serwerowi ComputeEngine, i klientowi ComputePi podczas wykonania rozproszonej aplikacji Javy. Gdy serwer ComputeEngine rejestruje (bind) swój zdalny obiekt w rejestrze, rejestr pobiera klasę ComputeEngine_Stub i interfejsy Compute i Task, od której ta klasa zaleŝy. Te klasy pobierane są z serwera sieciowego właściwego dla ComputeEngine lub z systemu plików. W. Kasprzak: Programowanie zdarzeniowe 14-60
Klient typu ComputePi ładuje klasę ComputeEngine_Stub z serwera sieci właściwego dla ComputeEngine w wyniku wykonania metody Naming.lookup. Klient ComputePi dysponuje opisami interfejsów Compute i Task, dlatego teŝ ładowane są one z jego lokalnego katalogu klas. Na koniec, do maszyny wirtualnej wykonującej serwer ComputeEngine ładowany jest kod klasy Pi, gdy obiekt klasy Pi przekazywany jest w zdalnym wywołaniu metody executetask na rzecz obiektu typu ComputeEngine klasa Pi ładowana jest z serwera sieciowego właściwego dla klienta. W. Kasprzak: Programowanie zdarzeniowe 14-61 Kompilacja i uruchomienie aplikacji (serwera i klientów) oraz obliczenie wartości π. 1) Tworzymy pliki typu JAR (Java ARchive) zawierający interfejsy Compute i Task dla implementacji ich przez klasy serwera i z których korzysta program klienta. 2) Tworzymy implementację interfejsu Compute i instalujemy tę usługę na maszynie udostępniając ją klientom. 3) Twórcy programów klientów, korzystając z interfejsów Compute i Task, zawartych w pliku JAR, mogą niezaleŝnie od siebie utworzyć zadanie (Task) i program klienta korzystającego z usługi Compute. Klasa klienta Pi ładowana jest na serwer podczas wykonania. Podobnie zdalna namiastka dla ComputeEngine ładowana jest z serwera na maszynie klienta podczas wykonania. Mamy trzy pakiety: compute ( interfejsy Compute i Task) engine (klasa implementująca ComputeEngine i jej namiastka) client (kod klienta ComputePi i implementacja zadania Pi). W. Kasprzak: Programowanie zdarzeniowe 14-62