Portmapper z RPC po TCP
Stworzyć prostą aplikację portmapera z RPC dla komunikacji TCP. Przyjmujemy, że wszystkie usługi rejetrowane w portmaperze wczytują zawsze jedną linię od klienta, po czym odsyłają zawsze jedną linię z odpowiedzią. Portmapper powinien działać na ustalonym porcie (domyslnie 12345) i obsługiwać następujące komendy (wysyłane tekstowo w jednej lini):
REGISTER nazwa ip port
gdzie nazwa to nazwa usługi rejestrowanej w portmaperze, a ip i port to adres i port, na którym działa usługa; komenda ta powinna zarejestrować usługę pod zadaną nazwą i odpowiedzieć klientowi rejestrującemu usługę jedną linjką z napisaem "OK", w przypadku powodzenia zarejestrowania usługi, lub informacją o błędzie w przeciwnym razieGET nazwa
gdzie nazwa to nazwa usługi zarejestrowanej uprzednio w portmaperze; komenda ta powinna odpowiedzieć klientowi jedną linijką w formacie "ip port" zawierającą adres ip i port zarejestrowanej usługiCALL nazwa args...
gdzie nazwa to nazwa usługi zarejestrowanej uprzednio w portmaperze; komenda ta powinna połączyć się z usługą nazwa zarejestrowaną uprzednio w portmaperze pod zadanym adresem ip i portem, przesłać jej linię "args..." (tj. podnapisa linni komendy CALL począwszy od pierwszego niebiałego znaku po nazwie usługi), odebrać linię odpowiedzi i przekazać ja klientowi wywołującemu komendę
Pierwsze zadanie zaliczeniowe
Pojawiła się finalna wersja pierwszego zadania zaliczeniowego.
Gniazda UDP
Typowe sytuacje, gdzie protokół UDP ma zastosowanie to:
- aplikacje generujące o wiele więcej danych niż jest się skłonnym przesłać, a wybór, które z generowanych danych akurat przesłać jest dość arbitralny (np. transmisje audio/wideo)
- istnienie wielu odbiorców dla jednej wiadomości (np. broadcast, multicast), kiedy przesyłanie wielu kopii jest z jakichś względów niepraktyczne
- ogólnie - konieczność sprawowania dokładnej kontroli nad transmisją danych (DHCP, DNS, serwery gier, ...)
Możliwy scenariusz działania serwera wygląda następująco:
- ustawienie gniazda datagramowego na znanym porcie; ewentualnie na pierwszym wolnym (konstruktor bezargumentowy) i przekazanie o tym informacji:
server = new DatagramSocket();
- Przygotowanie datagramu do odbioru danych
buff = new byte[MAX_DATAGRAM_SIZE]; datagram = new DatagramPacket(buff, buff.length);
- czekanie na pojawienie się datagramu
server.recive(datagram);
- utworzenie osobnego wątku obsługującego klienta (i powrót do przygotowania do odbioru kolejnego datagramu)
new Thread() { //... public void run() { // obsługa klienta //... } }.start();
- w prologu obsługi klienta pobranie informacji o gnieździe nadawcy datagramu i utworzenie datagramu do wysłania:
clientAddress = datagram.getAddress(); clientPort = datagram.getPort(); // przygotownie danych // ... datagram = DatagramPacket(buff, buff.length, clientAddress, clientPort);
Jak ustalić wielkość datagramu?
Oczywiście zgodnie ze specyfiką problemu, który się rozwiązuje. Lepszym pytaniem jest: jakiej maksymalnej wielkości datagramy powinniśmy przesyłać? Prosta odpowiedź to: takiej aby datagramy mieściły się w buforach urządzeń znajdujących się na ścieżkach, którymi owe datagramy się poruszają - choć warstwa IP potrafi dzielić datagramy na fragmenty, zazwyczaj takie podziały prowadzą do sporych nieefektywności w komunikacji. Niestety, nie istnieje dobry sposób na określenie tej wielkości. Standard gwarantuje tylko 576 bajtów na datagram w warstwie IP - odejmując od tego 60 bajtów maksymlnej wielkości nagłówka IP (tj. 4 * (2^4 - 1)) i 8 bajtów wielkości nagłówka UDP, zostajemy z gwarantowaną obsługą 508 bajtów danych.Zadanie ćwiczeniowe z TCP
- Stworzyć aplikację klienta, która wczyta dwie liczby naturalne ze standardowego wejścia, wyśle je do serwera, po czym przyjmie od serwera jedną linię i wypisze ją na standardowe wyjście.
- Stworzyć aplikację serwera: serwer powinien wczytać dwie liczby X, Y od klienta i odesłać jedną linię zawierającą sumę X i Y.
Gniazda TCP
Wyróżniamy dwa rodzaje gniazd TCP:
- nasłuchowe - wykorzystywane po stronie aplikacji serwera do nawiązywania połączeń z klientami; takie gniazda są unikalnie identyfikowane przez adres hosta i numer portu, na którym stoją
- połączeniowe - wykorzystywane do wymiany danych; takie gniazda (po związaniu) są unikalnie identyfikowane przez czwórkę - host źródłowy, port źródłowy, host docelowy, port docelowy
Typowy scenariusz działania serwera wygląda następująco:
- ustawienie gniazda nasłuchowego na znanym porcie; ewentualnie na pierwszym wolnym (wartość 0) i przekazanie o tym informacji:
server = new ServerSocket(0);
- czekanie na akceptację połączenia z klientem
- akceptacja połączenia, podczas której dokonywany jest standardowy handshake TCP i w wyniku której zwrócone zostaje gniazdo połączeniowe, ustawione na tym samym hoście i porcie co gniazdo nasłuchowe, związane z gniazdem połączeniowym klienta
client = server.accept();
- utworzenie osobnego wątku obsługującego klienta (i powrót do nasłuchiwania)
new Thread() { //... public void run() { // obsługa klienta //... } }.start();
- utworzenie gniazda komunikacyjnego i związanie go z gniazdem po stronie serwera; takie gniazdo zostanie utworzone na dowolnym wolnym porcie:
client = new Socket(serverAddress, serverPort);
- komunikacja z serwerem i czynności porządkowe
Warriors of the Net
Film "Warriors of the Net" można ściągnąć z nasepującej strony: www.warriorsofthe.net
Pliki
Wielkość datagramu (UDP): UDP.java |
Przykład prostego klienta (UDP): UDPClient.java |
Przykład prostego serwera (UDP): UDPServer.java, Factorial.java |
Szkielet prostego klienta (TCP): ClientTemplate.java |
Przykład prostego serwera (TCP): TestServer.java, ServerThread.java |
Informacje o hostach: TestAddr.java |