Katedra Inżynierii Komputerowej Koszalin 2002 Programowanie dla sieci Wykłady i ćwiczenia Dariusz Rataj Część 2 Programowanie aplikacji w architekturze Klient-Serwer - UDP Kontakt email: rataj@man.koszalin.pl Materiały dostępne pod adresem: http://java.ie.tu.koszalin.pl
Spis treści Spis treści... 2 1. Komunikacja datagramowa - UDP... 2 2. Klasy języka Java obsługujące połączenia datagramowe... 3 2.1. Klasa DatagramPacket... 3 2.1.1. Datagramy wysyłane - konstruktory... 3 2.1.2. Datagramy odbierane - konstruktory... 4 2.1.3. Metody pomocnicze klasy DatagramPacket... 4 2.2. Klasa DatagramSocket... 5 2.2.1. Gniazda wysyłające dane... 5 2.2.2. Gniazda odbierające dane... 5 3. Przykłady realizacji usług opartych na UDP...6 3.1. Komunikacja jednokierunkowa... 6 3.2. Proste usługi (przykład realizacji usługi daytime)... 8 3.3. Przesyłanie większych bloków danych (przykład usługi copyfile)... 10 3. Aplety i komunikacja sieciowa... 14 5. Podsumowanie UDP-TCP... 15 Dodatek. Ćwiczenia do samodzielnego wykonania... 15 1. Komunikacja datagramowa - UDP W odróżnieniu od rozwiązań opartych na połączeniach TCP komunikacja datagramowa charakteryzuje się dużą zawodnością. Powodem tego jest, przede wszystkim, brak połączenia między klientem a serwerem a także brak potwierdzenia otrzymania danych realizowanego na poziomie protokołu UDP (w warstwie 4 modelu OSI). Ważną cechą komunikacji opartej na UDP jest brak gniazda serwera. UDP (datagram) Aplikacja wysyłane dane nagłowek UDP wysyłane dane IP nagłowek IP nagłowek UDP wysyłane dane Ethernet nagłowek Ethernet nagłowek IP nagłowek UDP wysyłane dane końcówka Ethernet Rys. 1. Opakowywanie datagramu w komunikacji przez UDP Gniazda datagramowe mogą jedynie wysyłać i odbierać datagramy. Datagram jest pakietem UDP i wiąże się bezpośrednio z fizycznym opakowaniem danych w warstwie transportowej (Rys. 1). - 2 -
Nagłówek pakietu UDP (datagramu) zawiera informacje o porcie docelowym, porcie źródłowym i długości datagramu wraz z nagłówkiem. Wielkość bloku danych datagramu jest ograniczona do wielkości 65507 bajtów co jest spowodowane maksymalną wielkością pakietu IP. W niektórych systemach wielkości te mogą się różnić i w praktyce stosuje się dużo mniejsze bloki danych umieszczane w datagramach. Zalecane jest stosowanie datagramów z blokiem danych nie większym niż 8kB (8192 bajty). Takie ograniczenie ma dostosować wielkość datagramu do różnych rozwiązań w warstwie IP i w warstwie fizycznej a tym samym zmniejszyć zawodność komunikacji. 2. Klasy języka Java obsługujące połączenia datagramowe Do realizacji połączeń datagramowych w języku Java wykorzystywane są dwie klasy: DatagramPacket i DatagramSocket. Klasa DatagramPacket definiuje wysyłany lub odbierany datagram, klasa DatagramSocket wysyła lub odbiera datagramy. Obydwie klasy są wykorzystywane zarówno po stronie serwera jak i klienta. 2.1. Klasa DatagramPacket Obiekt klasy DatagramPacket reprezentuje pakiet UDP inaczej nazywany datagramem. Klasa posiada sześć konstruktorów. Poniżej przedstawione zostały cztery konstruktory stosowane najczęściej w praktyce z czego dwa pierwsze używane są do tworzenia datagramów do wysłania, dwa kolejne do tworzenia obiektów dla datagramów odbieranych. 2.1.1. Datagramy wysyłane - konstruktory Wysyłany datagram zawiera dane: bufor zawierający dane wysyłane, długość bloku danych wysyłanych, adres docelowy oraz port docelowy. public DatagramPacket(byte[] buf, int length, InetAddress address, int port) konstruktor paczki datagramowej z parametrami: buf - tablica wartości typu byte (bufor wysyłanych danych), length - ilość (długość) bajtów wysyłanych - typu int, address - adres docelowy - obiekt typu InetAddress, port - port docelowy - wartość typu int. Drugi z przedstawionych kostruktorów daje możliwość wysłania wybranego fragmentu bufora i posiada dodatkowy parametr offset (przesunięcie) określający początek bloku danych odczytywanych z bufora. public DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port) konstruktor paczki datagramowej z parametrami: buf - tablica wartości typu byte (bufor wysyłanych danych), offset - początek (przesunięcie od początku bufora) bloku danych wysyłanych - typu int, length - ilość (długość) bajtów wysyłanych - typu int, address - adres docelowy - obiekt typu InetAddress, port - port docelowy - wartość typu int. Przykładowy fragment poprawnie utworzonego datagramu może wyglądać następująco: byte buf[] = new byte[100]; // bufor wielkości 100 bajtów... // umieszczenie danych w buforze DatagramPacket packet = new DatagramPacket(buf, buf.length, InetAddress.getByName("kos.man.koszalin.pl"), 5501 ); - 3 -
2.1.2. Datagramy odbierane - konstruktory Kolejne dwa konstruktory klasy DatagramPacket stosowane są dla datagramów przychodzących. W tym zastosowaniu nie ma potrzeby definiowania adresu i portu (z oczywistych powodów). Definiowany jest jedynie bufor i długość bloku danych danych: public DatagramPacket(byte[] buf, int length) konstruktor datagramu z parametrami: buf - tablica wartości typu byte (bufor przychodzących danych), length - maksymalna ilość (długość) bajtów odbieranych - typu int. Kolejny konstruktor daje możliwość zapisu danych przychodzących począwszy od wybranego miejsca w buforze. public DatagramPacket(byte[] buf, int offset, int length) konstruktor datagramu z parametrami: buf - tablica wartości typu byte (bufor przychodzących danych), offset - początek (przesunięcie od początku bufora) bloku danych przychodzących - typu int, length - maksymalna ilość (długość) bajtów odbieranych - typu int. Przykładowy fragment poprawnie utworzonego datagramu dla danych przychodzących może wyglądać następująco: byte buf[] = new byte[100]; // bufor wielkości 100 bajtów // obiekt gotowy do przyjęcia 100 bajtów danych DatagramPacket packet = new DatagramPacket(buf, buf.length); 2.1.3. Metody pomocnicze klasy DatagramPacket Oprócz konstruktorów tworzących obiekt paczki datagramowej klasa DatagramPacket posiada szereg metod pozwalających na operowanie informacjami zapisanym w datagramie. Można tu wyróżnić dwie grupy: metody ustawiające informacje - setx oraz metody pobierające informacje z datagramu- getx. Metody ustawiające informacje: void setaddress(inetaddress addr) - ustawienie adresu docelowego, void setdata(byte[] buf) - ustawienie bufora, void setdata(byte[] buf, int offset, int length) - ustawienie bufora, długości bufora i przesunięcia w buforze, void setlength(int length) - ustawienie długości bloku danych, void setport(int port) - ustawienie portu docelowego. Metody pobierające informacje: InetAddress getaddress() - pobranie adresu docelowego, byte[] getdata() - pobranie danych z datagramu, int getlength() - pobranie długości bloku danych, int getoffset() - pobranie przesunięcia danych w buforze, int getport() - pobranie portu docelowego. - 4 -
2.2. Klasa DatagramSocket Klasa DatagramSocket służy do tworzenia obiektów gniazd datagramowych, które mogą zarówno wysyłać jak i odbierać dane (datagramy - wcześniej utworzone obiekty DatagramPacket). Podobnie jak w przypadku klasy DatagramPacket klasa DatagramSocket posiada oddzielne konstruktory dla strony odbierającej jak i wysyłającej datagramy. 2.2.1. Gniazda wysyłające dane Dla gniazd wysyłających dane stosowany jest jeden konstruktor klasy DatagramSocket - konstruktor bezparametrowy. Gniazda wysyłające dane mogą przejść w tryb odbierania danych. Nie ma wtedy konieczności tworzenia nowego obiektu gniazda - datagramy będą odbierane domyślnie na porcie na którym były poprzednio wysyłane. Konstruktor bezparametrowy stosowany jest najczęściej po stronie aplikacji klienta. public DatagramSocket() konstruktor bezparametrowy. Adres i port docelowy zdefiniowane są w obiekcie DatagramPacket wysyłanym przez gniazdo. Po utworzeniu gniazda można wysyłać datagramy metodą send: byte buf[] = new byte[100]; // bufor wielkości 100 bajtów... // umieszczenie danych w buforze DatagramPacket packet = new DatagramPacket(buf, buf.length, InetAddress.getByName("kos.man.koszalin.pl"), 13 ); DatagramSocket socket= new DatagramSocket(); // obiekt gniazda socket.send(packet); // wysłanie paczki (datagramu) 2.2.2. Gniazda odbierające dane Konstruktory stosowany jest po stronie aplikacji serwera muszą być przywiązane do numeru portu: public DatagramSocket(int port) konstruktor gniazda z parametrem: port - numer portu wiązanego z gniazdem - typu int. Gniazdo zostaje wiązane z portem numer port UDP. Drugi z konstruktorów dla gniazd odbierających dane ma możliwość związania z wybranym adresem przypisanym do hosta. Stosowany dla hostów posiadających więcej niż jeden adres IP. public DatagramSocket(int port, InetAddress addr) konstruktor gniazda z parametrami: port - numer portu wiązanego z gniazdem - typu int, addr - obiekt klasy InetAddress opisujący wiązany z gniazdem adres IP. Gniazdo zostaje wiązane z portem numer port UDP i adresem IP opisywanym przez addr. Po utworzeniu gniazda można odbierać datagramy metodą receive: - 5 -
byte buf[] = new byte[100]; // bufor wielkości 100 bajtów DatagramPacket packet = new DatagramPacket(buf, buf.length); socket.receive( packet ); // oczekiwanie na datagram Metoda receive oczekuje na przyjście datagramu podobnie jak metoda accept klasy ServerSocket (komunikacja TCP) i także jest jest to metoda blokująca. W sytuacji gdy gniazdo nie powinno oczekiwać w nieskończoność na dane przychodzące można ustawić czas oczekiwania gniazda metodą setsotimeout. Kod utworzenia gniazda datagramowego wymaga obsługi odpowiednich wyjątków. Konstruktory klasy mogą zwrócić wyjątki: SocketException - gdy wystąpi błąd otwarcia gniazda; SecurityException - wyjątek ochrony (wyrzucany przez obiekt SecurityManager, np. w przeglądarce internetowej). 3. Przykłady realizacji usług opartych na UDP W dalszej części przedstawione są przykłady rozwiązań komunikacji opartej na UDP. Największym problemem w tego typu komunikacji jest zapewnienie minimum niezawodności wymiany informacji i jest to zadanie niezwykle złożone. Przedstawione tu przykłady nie zapewniają pełnej niezawodności, są jedynie prezentacją kilku rozwiązań. 3.1. Komunikacja jednokierunkowa W najprostrzych rozwiązaniach komunikacji opartej na UDP realizuje się komunikację jednokierunkową gdzie jedna strona (klient) wysyła dane, druga (serwer) odbiera dane (Rys. 2). Warte podkreślenia jest to że strona wysyłająca w żaden sposób nie jest w stanie stwierdzić czy wysłane dane dotarły do celu. Takie rozwiązanie można zastosować gdy dotarcie wszystkich danych do adresata nie jest konieczne (dźwięk, obraz itp.). Zaletą tak prostego systemu komunikacji jest szybkość transferu danych. host klienta host serwera SendDatagram packet ReceiveDatagram packet socket send() Datagramy od klienta do serwera (w przykładzie klient wysyła 10 datagramów) socket receive() Rys. 2. Jednokierunkowe przesłanie datagramów (bez potwierdzenia!) - 6 -
Przedstawione przykłady - aplikacje SendDatagram, ReceiveDatagram - tworzą prosty system komunikacji jednokierunkowej. Aplikacja SendDatagram wysyła 10 datagramów zawierających datę i godzinę w odstępach jedno-sekundowych. Aplikacja ReceiveDatagram odbiera wszystkie datagramy przychodzące do gniazda i drukuje informacje na konsoli. Przykład 1. SendDatagram.java import java.net.*; import java.util.*; public class SendDatagram { public static void main(string args[]) { int port = 5500; for (int i =0; i<10; i++) { // petla wysyla 10 datagramow try { InetAddress servaddr = InetAddress.getByName("192.168.55.43"); String data = i + ". Data i godzina wyslania: " + new Date(); byte buf[] = data.getbytes(); DatagramSocket socket= new DatagramSocket(); DatagramPacket packet = new DatagramPacket(buf, buf.length, servaddr, port ); socket.send(packet); System.out.println("Wyslano datagram"); /* Zatrzymanie na 1s */ try { Thread.sleep(1000); catch (InterruptedException ex) {System.err.println("Blad sleep()!"); ; // try catch(exception ex) { System.err.println(ex); // for // main // SendDatagram Przykład 2. ReceiveDatagram.java import java.net.*; public class ReceiveDatagram { public static void main(string args[]){ int port = 5500; System.out.println("Aplikacja ReceiveDatagram czeka na datagramy: port " + port); try{ DatagramSocket socket = new DatagramSocket(port); while(true) { byte[] buf = new byte[100]; DatagramPacket packet = new DatagramPacket(buf, buf.length); socket.receive( packet ); System.out.println("Dane odebrane [" + new String(packet.getData()).trim() + "]" ); // while // try catch(exception e) { System.err.println(e); - 7 -
// main // ReceiveDatagram 3.2. Proste usługi (przykład realizacji usługi daytime) Większość usług opartych na UDP, realizowanych w praktyce, są to proste usługi działające na zasadzie zapytanie-odpowiedź. Wśród standardowych usług udostępnianych na serwerach sieciowych można wyróżnić: echo (port 7), discard (port 9), daytime (port 13), time (port 37), name (port 42),... itd. Wszystkie te usługi wysyłają datagram informujący serwer usługi o żądaniu realizacji usługi. W niektórych usługach wymagane jest skonstruowanie odpowiedniego zapytania przez klienta (np. name), w innych klient wysyła datagram pusty (daytime, time - dane zawarte w datagramie są bez znaczenia). Usługa diagnostyczna echo przyjmuje dowolne dane i odsyła je w takiej postaci w jakiej przyszły. host klienta host serwera DatagramClient packet DatagramServer packet socket send() receive() 1. Datagram od klienta do serwera - datagram pusty 2. odpowiedź serwera np. "Wed Sep 11 18:53:51 2002" socket receive() send() Rys. 3. Realizacja prostych usług zapytanie-opdpowiedź Przykładowa aplikacja DatagramClient realizuje funkcję klienta usługi daytime wysyłając datagram pusty. Zmiana portu na 9 lub 37 zmieni funkcję aplikacji odpowiednio na realizację usług discard i time. Prosta modyfikacja zawartości danych w wysyłanym datagramie pozwoliłaby na realizację usług echo, name. Przykład 3. DatagramClient.java import java.net.*; import java.io.*; public class DatagramClient { public static void main(string args[]) { try{ InetAddress servaddr = InetAddress.getByName("localhost"); /* wys³anie pustego datagramu */ byte buf[] = new byte[1]; DatagramSocket socket= new DatagramSocket(); socket.send(new DatagramPacket(buf, buf.length, servaddr, 13 )); System.out.println("Wyslano datagram: [" + new String(buf).trim() + "]"); /* odbior odpowiedzi */ - 8 -
buf = new byte[100]; DatagramPacket packet = new DatagramPacket(buf, buf.length); socket.setsotimeout(2000); socket.receive(packet); System.out.println("Odebrano datagram: [" + new String(buf).trim() + "]"); // try catch(unknownhostexception e) { System.err.println("Jakis blad - UnknownHostEx"); catch(socketexception e) { System.err.println("Jakis blad - SocketEx"); catch(ioexception e) { System.err.println("Jakis blad - IOEx"); // main // DatagramClient Przykład 4 przedstawia implementację serwera usługi daytime. Realizacja usługi zrealizowana jest w pętli nieskończonej while: while(true){ /* Odebranie datagramu */... socket.receive( packet );... /* Wyslanie datagramu z data i godzina */ socket.send( new DatagramPacket( buf, buf.length, packet.getaddress(), packet.getport() ) );... Według podobnego schematu działają serwery innych, wcześniej wymienionych usług i dostosowanie serwera do obsługi innych usług (podobnie jak w przypadku klienta) nie stanowi większego problemu. Przykład 4. DatagramServer.java import java.net.*; import java.util.*; import java.io.*; public class DatagramServer{ public static void main(string args[]){ try{ DatagramSocket socket = new DatagramSocket(13); while(true){ /* Odebranie pustego datagramu */ byte[] buf = new byte[100]; DatagramPacket packet = new DatagramPacket(buf, buf.length); socket.receive( packet ); System.out.println("Odebrano datagram: [" + new String(buf).trim() + "]"); /* Wyslanie datagramu z data i godzina */ buf = new Date().toString().getBytes(); socket.send( new DatagramPacket( buf, buf.length, packet.getaddress(), packet.getport() ) ); System.out.println("Odeslano datagram: [" + new String(buf).trim() + "]"); // while // try catch(unknownhostexception e) { System.err.println("Jakis blad - UnknownHostEx"); catch(socketexception e) { System.err.println("Jakis blad - SocketEx"); catch(ioexception e) { System.err.println("Jakis blad - IOEx"); // main - 9 -
// DatagramServer 3.3. Przesyłanie większych bloków danych (przykład usługi copyfile) Przykład aplikacji CopyFileServer realizuje przesłanie zawartości pliku do klienta z podziałem pliku na kawałki tzw. chunks (ang.: chunk - kawałek) o maksymalnej wielkości 8192 bajty (zalecany rozmiar bloku danych w datagramie). Podobne rozwiązanie było stosowane w kilku usługach sieciowych lecz zostało wyparte protokołami opartymi na TCP (np. usługa TFTP). W obecnych czasach można spotkać transfer pliku podzielonego na kawałki w różnych specyficznych sieciach komunikacyjnych m.in. w systemach typu P2P (peer to peer). Poniżej prezentowane rozwiązanie trudno nazwać rozwiązaniem niezawodnym z uwagi na brak kilku istotnych zabezpieczeń. Między innymi pominięta została kontrola źródła przychodzących datagramów, kontrola kolejności przychodzących kawałków. Aplikacja serwera oczekująca na porcie 5501 wykonuje działania: odbiera pusty datagram - zgłoszenie klienta, wysyła datagram zawierający prosty komunikat "OK" - potwierdzenie dla klienta, oblicza ilość kawałków na które zostanie podzielony plik, odczytuje w pętli for kolejne kawałki z pliku, wysyła odczytany kawałek w postaci datagramu, odczytuje potwierdzenie od klienta - datagram pusty, wysyła kolejne kawałki aż do wyczerpania zawartości pliku i odbierając potwierdzenia od klienta po każdym kawałku. Przykład 5. CopyFileServer.java import java.net.*; import java.io.*; public class CopyFileServer { int port = 5501; FileInputStream file; int filesize; String filename; private boolean openfile(string filename) { try { file = new FileInputStream(filename); filesize = file.available(); // wielkosc pliku catch (FileNotFoundException ex) { System.out.println("Nie znaleziono pliku!"); return false; catch (IOException ex) { System.out.println("Blad odczytu!"); return false; return true; private void closefile() { try { if (file!= null) file.close(); catch (IOException ex) { System.out.println("Blad przy zamykaniu pliku!"); public CopyFileServer(String localfile) { - 10 -
System.out.println("Startuje serwer CopyFile na porcie " + port); filename = localfile; // nazwa pliku try{ DatagramSocket socket = new DatagramSocket(port); // gniazdo serwera na porcie while (true) { /* odczyt zgloszenia - datagram pusty */ byte[] buf = new byte[100]; // bufor do odczytu datagramu DatagramPacket packet = new DatagramPacket(buf, buf.length); socket.receive( packet ); // oczekiwanie na datagram System.out.println("Zgloszenie od klienta..."); /* wyslanie potwierdzenia "OK" */ if (openfile(filename)) buf = "OK".getBytes(); else System.exit(0); socket.send(new DatagramPacket(buf, buf.length, packet.getaddress(), packet.getport())); /* wyslanie kolejnych kawalkow */ int chunks = (int)(filesize/8192); for (int i =0 ; i <= chunks; i++) { byte[] buffer; if (filesize-i*8192 >= 8192) buffer = new byte[8192]; else buffer = new byte[filesize-i*8192]; int count = file.read(buffer); System.out.println("Wysylam kawalek: " + count + " " + (i+1)+ "/" + (chunks+1)); socket.send( new DatagramPacket( buffer, buffer.length, packet.getaddress(), packet.getport())); socket.receive( packet ); // odbior potwierdzenia - datagram pusty if (file!= null) closefile(); // zamkniecie pliku // while // try catch(unknownhostexception e) { System.err.println("Jakis blad - UnknownHostEx"); catch(socketexception e) { System.err.println("Jakis blad - SocketEx"); catch(ioexception e) { System.err.println("Jakis blad - IOEx"); public static void main(string args[]){ if (args.length > 0 ) { new CopyFileServer(args[0]); else { System.out.println("Poprawne uruchomienie serwera:"); System.out.println("java CopyFileServer <nazwa pliku>"); // main // DatagramServer Aplikacja klienta wykonuje działania: wysyła pusty datagram - zgłoszenie do serwera, odbiera datagram zawierający prosty komunikat "OK" - potwierdzenie od serwera, oblicza ilość kawałków na które zostanie podzielony plik, odbiera w pętli for kolejne datagramy przychodzące od serwera, wysyła potwierdzenie do serwera - datagram pusty, odbiera kolejne kawałki aż do wyczerpania zawartości pliku za każdym razem potwierdzając datagramem pustym. Przykład 5. CopyFileClient.java import java.net.*; - 11 -
import java.io.*; public class CopyFileClient { int port = 5501; FileOutputStream file; int filesize; String filename; private boolean openfile(string filename) { try { file = new FileOutputStream(filename); catch (FileNotFoundException ex) { System.out.println("Nieznaleziono pliku!"); return false; catch (IOException ex) { System.out.println("Blad odczytu!"); return false; return true; private void closefile() { try { if (file!= null) file.close(); catch (IOException ex) { System.out.println("Blad przy zamykaniu pliku!"); public CopyFileClient(String localfile, int size) { filename = localfile; filesize = size; // parametry pliku: nazwa i rozmiar try{ /* zgloszenie uslugi - datagram pusty */ InetAddress servaddr = InetAddress.getByName("localhost"); byte buf[] = " ".getbytes(); DatagramSocket socket= new DatagramSocket(); socket.setsotimeout(3000); // timeout dla gniazda 3 sekundy socket.send(new DatagramPacket(buf, buf.length, servaddr, port )); /* odbior potwierdzenia przyjecia zgloszenia */ buf = new byte[100]; DatagramPacket packet = new DatagramPacket(buf, buf.length); socket.receive(packet); String answer = new String(packet.getData()).trim(); System.out.println("Informacja od serwera: " + answer); if (!answer.equals("ok")) { System.out.println("Blad po stronie serwera! Koniec"); System.exit(0); /* otwarcie pliku do zapisu */ openfile(filename); int chunks = (int)(filesize/8192); // ilosc wysylanych kawalkow for (int i = 0 ; i <= chunks; i++) { byte[] buffer; // bufor datagramu - maksymalny rozmiar kawalka 8192 if (filesize < 8192) buffer = new byte[filesize-i*8192]; else if (filesize-i*8192 >= 8192) buffer = new byte[8192]; else buffer = new byte[filesize-i*8192]; packet = new DatagramPacket(buffer, buffer.length); - 12 -
socket.receive(packet); // odbior kawalka file.write(packet.getdata()); // zapis do pliku socket.send(new DatagramPacket(new byte[1], 1, servaddr, 5501 )); // wyslanie potwierdzenia // for // try catch(unknownhostexception e) { System.err.println("Jakis blad - UnknownHostEx"); catch(socketexception e) { System.err.println("Jakis blad - SocketEx"); catch(ioexception e) { System.err.println("Jakis blad - IOEx"); finally { if (file!= null) closefile(); // zamkniecie pliku // konstruktor /* program glowny main*/ public static void main(string args[]){ new CopyFileClient("joke.jpg", 212385); /* program g³ówny koniec main*/ // CopyFileClient Zaprezentowane rozwiązanie jest jedynie demonstracją możliwości protokołu UDP i zastosowanie go w praktyce może okazać się niewystarczające. Do kopiowania większych bloków danych stosuje się częściej rozwiązania pozwalające na przesyłanie kawałków w kolejności losowej (w UDP nie ma gwarancji zachowania kolejności przychodzących datagramów) lecz takie rozwiązanie wymaga zastosowania systemu oznaczeń kawałków (numerowania) i systemu buforowania danych. - 13 -
3. Aplety i komunikacja sieciowa Aplet to specyficzny rodzaj aplikacji javy, której podstawową cechą jest działanie pod kontrolą maszyny wirtualnej java wbudowanej w inną aplikację. Taką aplikacją posiadającą własną, wbudowaną maszynę wirtualną może być np. popularna przeglądarka internetowa Netscape, MS Internet Explorer, przeglądarka appletviewer dołączana do JDK Sun itd. Ze względu na inny rodzaj środowiska pracy aplet powinien spełnić określone warunki bezpieczeństwa. W środowisku maszyny wirtualnej wbudowanej w przeglądarkę pracuje obiekt SecurityManager kontrolujący prawa dostępu do zasobów systemu. W zastosowaniach komunikacji sieciowej SM kontroluje prawa do połączeń sieciowych. Standardowo dopuszczane są jedynie próby połączeń z hostem z którego pobrany został aplet. Rys. 4. Połączenia sieciowe apletu W sytuacji gdy aplet jest uruchamiany lokalnie z lokalnego pliku html (bez serwera www) obiekt SecurityManager nie dopuści do żadnych połączeń, również do połączeń lokalnych. Większość omawianych wcześniej klas (konstruktory tych klas) wyrzucają wyjątek SecurityException w przypadku niedozwolonej próby połączenia. Jest to wynikiem działania obiektu SecurityManager. Aplikacje - aplety uruchamiane pod kontrolą przeglądarki powinny obsługiwać ten wyjątek przy tworzeniu obiektów gniazd. - 14 -
5. Podsumowanie UDP-TCP W tabeli 1 zestawione zostały najważniejsze cechy komunikacji opartej na TCP i UDP. Warto podkreślić że obydwa protokoły posiadają wady i zalety. Zadaniem programisty jest określenie potrzeb zależnych od zastosowań i wybór odpowiedniego rozwiązania. Tabela 1. Najważniejsze cechy połączeń opartych na TCP i UDP Gniazda TCP Gniazda UDP utrzymują połączenie potwierdzenie realizowane na poziomie protokołu TCP bez udziału programisty dane przychodzące w kolejności wysłania możliwa komunikacja aktywnego gniazda tylko z jednym klientem (lub serwerem) mniejsza szybkość komunikacji spowodowana koniecznością wymiany dodatkowych informacji i działań (potwierdzeń, zwalnianie szybkości, buforowanie itp.) brak połączenia potwierdzenia realizowane przez programistę na poziomie aplikacji dane (datagramy) mogą docierać do adresata w innej kolejności niż kolejność wysłania możliwe odbieranie datagramów od wielu klientów na pojedynczym gnieździe większa szybkość spowodowana brakiem dodatkowych informacji Na podstawie porównania z tabeli można wysnuć wnioski: realizacja usługi opartej na protokole TCP jest korzystna gdy potrzebna jest stabilność komunikacji, wszystkie dane muszą dotrzeć do adresata, szybkość komunikacji ma mniejsze znaczenie; realizacja usługi opartej na protokole UDP jest korzystna gdy większe znaczenie ma szybkość działania (przekazania danych), nie jest niezbędne dostarczenie wszystkich danych. Dodatek. Ćwiczenia do samodzielnego wykonania Poniżej przedstawione są zadania do wykonania na ćwiczeniach lub do samodzielnego wykonania w zaciszu domowym. 1. Skompilować wszystkie przykłady (od 1 do 9). Można to wykonać z linii poleceń OS, z pomocą pliku wsadowego, lub z wykorzystaniem środowiska RealJ zainstalowanego na salach komputerowych. 2. Dla przykładu 1 (SendDatagram) i 2 (ReceiveDatagram): uruchomić na lokalnym stanowisku aplikację ReceiveDatagram (serwer) i aplikację SendDatagram (klient) tak aby aplikacje mogły się komunikować; zestawić połączenie pomiędzy dwoma stanowiskami wszystkie wyniki zanotować. 3. Dla przykładu 3 (DatagramClient) i 4 (DatagramServer): - 15 -
uruchomić na lokalnym stanowisku aplikację DatagramClient, odczytać datę i godzinę na hostach: moskit.ie.tu.koszalin.pl, kos.man.koszalin.pl, innych wybranych hostach np. microsoft.com, wp.pl (usługi te mogą nie być dostępne lub czas odpowiedzi może być długi - ustawić timeout); zestawić połączenie pomiędzy dwoma stanowiskami: na jednym stanowisku pracujący serwer na drugim klient; wyniki wszystkich działań zanotować. 4. Dla przykładu 5 (CopyFileServer) i 6 (CopyFileClient): zestawić połączenie pomiędzy dwoma stanowiskami i skopiować wybrany plik (w prezentowanym przykładzie klient ma założony z góry rozmiar kopiowanego pliku - wartość tą należy ustawić dla własnego pliku), wyniki zanotować. - 16 -