Programowanie w języku JAVA (część II) 

Mechanizm gniazd (ang. socket)

Pojęcie gniazda pojawiło się pierwszy raz w latach osiemdziesiątych w środowisku Unix jako Berkley Sockets Interface (ich odpowiednikiem w Windows jest WinSock). Jego zadaniem jest umożliwienie komunikacji pomiędzy dwoma oddalonymi od siebie komputerami. Z punktu widzenia aplikacji działającej w systemie operacyjnym gniazdo można traktować jako końcowy punkt komunikacji. Unikalne gniazdo oznacza się adresem IP i numerem portu. Numery portów mogą przyjmować wartość od 1 do 65535 z tym, że niektóre z wartości są zarezerwowane dla typowych usług (patrz wykład o protokołach warstwy aplikacji). W związku z tym typowo przyjmuje się, że aplikacje użytkowników korzystają z portów o numerach powyżej 1024.

W systemach operacyjnych dostępne są trzy rodzaje gniazd. Pierwsze z nich, gniazdo do przesyłania datagramów (ang. data gram socket), wykorzystuje protokół bezpołączeniowy UDP a co za tym idzie nie gwarantuje dostarczenia danych. Bloki mogą przybywać także w innej kolejności niż zostały wysłane. Drugi typ to gniazda strumieniowe (ang. stream sockets). Używają one protokołu TCP na warstwie transportowej a co za tym idzie gwarantują zarówno dostarczenie wysłanego pakietu (w przypadku jego zagubienia następuje retransmisja) jak i kolejność zgodną z tą w jakiej zostały wysłane. Ostatni typ gniazda, który nie jest dostępny z poziomu JAVAy (ewentualnie poprzez JNI) to tzw. gniazda surowe (ang. raw sockets). Umożliwiają one bezpośredni dostęp do protokołów niższej warstwy. Schematycznie idea gniazd przedstawiona jest na rys.1.

Model OSI

Rys.1. Komunikacja z wykorzystaniem gniazd i protokołu TCP.


Protokół UDP

W języku JAVA dostępne są trzy klasy odpowiadające za realizację przesyłanie danych z wykorzystaniem protokołu UDP: DatagramSocket, MulticastSocket, DatagramPacket. Pierwsza klasa jest gniazdem, które umożliwia komunikację z jednym, konkretnym hostem po drugiej stronie. Druga klas jest wersją gniazda, która korzysta z tego, że protokół UDP umożliwia zarówno broadcast jak i multicast. Trzecia klasa jest obiektem, który jest przesyłany przez gniazdo. Tworzone gniazdo musi zostać związane z konkretnym numerem portu. Możliwe jest zostawienie wyboru portu wirtualnej maszynie poprzez wywołanie konstruktora

DatagramSocket datagramSocket = new DatagramSocket();

Istnieje także możliwość samodzielnego wyboru numeru portu podając go jako parametr w konstruktorze:

DatagramSocket datagramSocket = new DatagramSocket(4443);

Najczęściej po stronie serwera port z którym będzie związany socket wybiera się arbitralnie po to, aby klienci wiedzieli gdzie zwracać się z zapytaniami (HTTP – 80, TELNET – 25, ECHO – 7 etc.). Natomiast po stronie klienta z reguły zostawia się wirtualnej maszynie wybór portu bo pierwszy wysłany do serwera datagram i tak będzie zawierał adres portu z jakiego został wysłany.

Standardowo gniazdo klasy DatagramSocket może odbierać i wysyłać dane wysłane z dowolnego miejsca w Internecie, ale można to domyślne zachowanie zmienić łącząc gniazdo z konkretnym gniazdem na innym komputerze poprzez wywołanie metody connect(InetAddress address, int port). Wtedy zarówno wysyłanie jak i odbieranie datagramów możliwe jest tylko z tej jednej lokalizacji. Rozłączenie następuje po wywołaniu metody disconnect().

Wysłanie datagramu poprzez stworzone i zainicjowane gniazdo wymaga stworzenia obiektu klasy DatagramPacket. Konstruktor wymaga podania adresu docelowego, portu, tablicy bajtów zawierającej dane i jej długości:

byte[] buf = new byte[256];
InetAddress address = InetAddress.getByName(“www.server.com”);
DatagramPacket packet = new DatagramPacket(buf, buf.length, address, 2224);

Pierwsza linijka tworzy tablicę składającą się z 256 bajtów. W drugiej tworzony jest obiekt reprezentujący adres docelowego hosta. Trzecia linijka tworzy pakiet ustawiając docelowy adres (obiekt klasy InetAddress) i port (2224). Chcąc wysłać ten datagram wystarczy wywołać metodę send() w stosunku do stworzonego wcześniej gniazda podając jako parametr przygotowany wcześniej.

datagramSocket.send(packet);

Otrzymywanie datagramów z gniazda możliwe jest z wykorzystaniem metody receive(DatagramPacket packet). Wywołuje się ją w stosunku do istniejącego gniazda, z którego chcielibyśmy odczytać dane. Należy pamiętać, że jest to metoda blokująca co oznacza, że wykonanie programu jest wstrzymane do momentu otrzymania datagramu.

packet = new DatagramPacket(buf, buf.length);
datagramSocket.receive(packet);

Istnieje możliwość ustawienia czasu po jakim metoda receive() wyrzuci wyjątek jeśli nie otrzyma datagramu. Robi się to przy pomocy metody setSoTimeout(int timeout) obiektu DatagramSocket. Wyrzucany wyjątek to java.net.SocketTimeoutException. Otrzymany datagram zawiera informacje o tym skąd został wysłany. Można ją uzyskać wywołują metodę getSocketAddress(), która zwraca obiekt typu SocketAddress zawierający adres IP i port nadawcy komunikatu. Metoda getData() zwracająca tablicę bajtów umożliwia dostęp do danych otrzymanych w datagramie. Wygodny sposób zamiany otrzymanych danych na obiekt typu String jest pokazany poniżej

String received = new String(packet.getData(), 0, packet.getLength());

Protokół UDP umożliwia wysyłanie pakietów w trybie multicast’u (tzn. ten sam pakiet do więcej niż jednego odbiorcy). Do realizacji tej funkcjonalności najwygodniej jest korzystać ze specjalnej, dedykowanej klasy MulticastSocket. Tworzenie obiektów tej klasy obywa się podobnie jak dla zwykłego gniazda:

MulticastSocket s = new MulticastSocket(6789);

Kolejny krok to dołączenie gniazda do wybranej grupy. Grupy to właściwie adres IP z przedziału od 224.0.0.0 do 239.255.255.255 (pierwszy adres jest zarezerwowany i nie powinien być używany). Wszystkie komputery (w praktyce te w ramach naszej podsieci) na których działają gniazda podłączone pod dany adres dostaną komunikat wysłany na niego. Podłączenie gniazda do grupy wykonuje się przy pomocy metody joinGroup(InetAddress address).

InetAddress group = InetAddress.getByName("228.1.1.1");
s.joinGroup(group);

Protokół TCP

Wykorzystanie protokołu połączeniowego z punktu widzenia języka JAVA nie różni się specjalnie od wersji korzystające z protokołu UDP. Konstruktor tworzący gniazdo wygląda praktycznie tak samo:

Socket socket = new Socket("komputer_w_sieci", 37);

Konstruktor tworzy gniazdo połączone z komputerem “komputer_w_sieci” na porcie 37 (komputer z którym chcemy się połączyć powinien słuchać na wybranym przez nas porcie). Ponieważ z punktu widzenia funkcjonowania protokołu TCP ważny jest ciąg danych a nie zawartość pojedynczego datagramu do odczytu i wysyłania danych służy mechanizm strumieni a nie obiekty typu DatagramSocket. Przykładowe zainicjowanie strumienia wejściowego i wyjściowego pokazane jest poniżej:

PrintWriter out = new PrintWriter(echoSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new
InputStreamReader(echoSocket.getInputStream()));

Takie połączenie klas odpowiadających za strumienie umożliwia odczyt i zapis w standardzie Unicode. Wysłanie znaku przez tak zainicjowany strumień wyjściowy połączony z gniazdem jest proste. Wystarczy wywołać metodę println(String text) w stosunku do strumienia wyjściowego:

out.println(text)

Odczytać dane z gniazda można na przykład wywołując metodę readLine() w stosunku do strumienia wejściowego. Podobnie jak w przypadku klasy DatagramSocket jest to metoda blokująca, która wstrzymuje wykonanie programu do momentu otrzymania porcji danych. Przy pomocy metody setSoTimeout(int) istnieje możliwość wymuszenia czasu po jakim ma nastąpić wyrzucenie wyjątku jeśli odczyt danych z gniazda się nie powiódł.

Kończąc połączenie należy zamknąć zarówno gniazdo jak i strumienie wywołując metodę close() dla każdego z tych obiektów. Najpierw należy zamknąć strumienie a dopiero późnie gniazdo.

Do obsługi połączeń po stronie serwera JAVA udostępnia klasę ServerSocket, która posiada dodatkowe funkcjonalności umożliwiające równoczesną obsługę wielu połączeń. Tworzenie gniazda typu ServerSocket odbywa się tak samo jak gniazda Socket:

serverSocket = new ServerSocket(1234);

Jeśli wirtualna maszyna Javy nie będzie mogła zarezerwować wybranego portu dla tego gniazda to zostanie wyrzucony wyjątek IOException. Jeśli gniazdo uda się stworzyć, to kolejnym krokiem jest wywołanie metody accept(). Jest to metoda blokująca, która czeka na nadejście połączenia, a jeśli żądanie takie połączenia nadejdzie, to zwraca nowe gniazdo służące do obsługi tego połączenia. W tym samym czasie gniazdo typu ServerSocket może znów oczekiwać na nadejście połączenia.

clientSocket = serverSocket.accept();

Efektywne wspieranie wielu połączeń równocześnie wymaga na programiście zastosowania wątków. Dzięki temu może istnieć jeden proces, który oczekuje na przyjście połączenia i jeśli takie żądanie połączenia nastąpi to uruchamia oddzielny wątek dedykowany do obsługi tego żądania. Praktycznie wszystkie serwery sieciowe są wielowątkowe.



Zadania 

Zadania

  1. Zadanie punktowane z programowania zostało ogłoszone, a jego opis znajduje się w zakładce materiały.

Słownik 



Pliki 

Ten dział zostanie uzupełniony wkrótce...

W sieci