Dydaktyka:
FeedbackDo pracy programu w roli klienta TCP trzeba w kolejności użyć funkcji:
socket do stworzenia gniazda, podając typ SOCK_STREAM i protokół IPPROTO_TCP lub 0connect żeby połączyć utworzone gniazdo pod adres wskazany w argumentachwrite, send i read, recvshutdown żeby zakończyć połączenie na tym gnieździeclose żeby zwolnić zasoby systemowe związane z gniazdem
Zwróć uwagę że socket tworzy gniazdo protokołu TCP, a connect wybiera
żeby to gniazdo pełniło rolę klienta.
Aby oczekiwać na przychodzące połączenia TCP konieczne jest stworzenie gniazda
do odbierania nowych połączeń.
Takie gniazdo, tzw. gniazdo nasłuchujące (listening), nie umożliwia
odbierania ani wysyłania danych – pozwala tylko na odbieranie przychodzących
połączeń i tworzy dla każdego nowego połączenia kolejne gniazdo
reprezentujące to odebrane połączenie.
Do przygotowania nasłuchującego gniazda należy najpierw stworzyć gniazdo
funkcją socket, a następnie wywołać na tym gnieździe funkcję listen,
ale: jeśli wcześniej nie ustawi się adresu gniazda, system operacyjny wylosuje
efemeryczny port i rozpocznie nasłuch na dowolnym adresie IP na tym porcie.
Dlatego przed wywołaniem funkcji listen(…) należy funkcją bind
ustawić lokalny adres gniazda. Funkcja bind przyjmuje jako argument
wskaźnik na strukturę sockaddr, który musi wskazywać na strukturę opisująca
adres z rodziny obsługiwanej przez gniazdo.
Lokalny adres IP nasłuchującego gniazda zwykle ustawia się na dowolny, czyli
dla IPv4 adres 0.0.0.0, reprezentowany przez stałą INADDR_ANY.
Inny lokalny adres IP ustawia się, jeśli gniazdo ma odbierać połączenia
kierowane tylko na ten właśnie adres – np. 127.0.0.1 (INADDR_LOOPBACK)
jeśli połączenia mają być ograniczone do połączeń z tego komputera.
Wywołanie funkcji listen nakazuje systemowi operacyjnemu czekać na
połączenia (wybiera że gniazdo ma pełnić rolę serwera). Wykonanie funkcji
listen jest natychmiastowe – ta funkcja nie czeka na połączenie, tylko
informuje system operacyjny że nowe połączenia przychodzące na ustawiony
adres mają być kierowane na to gniazdo.
Argumentem funkcji listen jest ilość nowych połączeń które czekają
w kolejce na odebranie (tj. połączeń dla których nie wykonano jeszcze funkcji
accept)1).
Do odebrania nowych połączeń używa się funkcji accept(…). Funkcja
accept zwraca nowe gniazdo reprezentujące nawiązane połączenie.
Tworzenie kolejnych deskryptorów plików oraz wyniki lsof, strace
i ss są zaprezentowane tutaj
Zadanie 1 Napisz program, który:
socket,sockaddr_in i wypełni ją wpisując:AF_INET w pole określające rodzinę adresówINADDR_ANY w pole określające adres IPv4htons do zmiany kolejności bajtów)bind bind się powiodła listen listen określa ile nieodebranych połączeń może naraz czekać w systemie; tutaj wystarczy 1accept accept ustaw na 0 lub nullptraccept to deskryptor nowego gniazda, połączonego z klientem hello)shutdown i zamknie deskryptor pliku funkcją close
#include <arpa/inet.h> #include <cstdio> #include <netinet/in.h> #include <sys/socket.h> #include <unistd.h> int main(int argc, char **argv) { return 0; }
System operacyjny śledzi w jakim stanie jest połączenie TCP.
Poniższa ilustracja pokazuje kolejne stany połączenia po wykonaniu shutdown
z argumentem SHUT_WR lub SHUT_RDWR:
shutdown wysyła segment TCP z informacją że
ta strona nie będzie już więcej wysyłać danych (flaga FIN), a druga strona
musi potwierdzić że odebrała tą informację (flaga ACK).
Jeżeli potwierdzenie zamknięcia połączenia nie dotrze (bo np. pakiet z tą informacją został zgubiony w sieci), to po upływie określonego czasu system operacyjny ponownie wyśle informację o zamknięciu połączenia:
Strona która pierwsza zamknęła połączenie po wysłaniu ostatniego ACK nie wie czy wystąpi sytuacja z pierwszej ilustracji (w której żaden segment z tego połączenie już nie przyjdzie) czy sytuacja z drugiej ilustracji (w której przyjdzie ponowiona informacja że druga strona zamyka połączenie). Dlatego przez określony czas (w Linuksie około minuty2)) system operacyjny pamięta o tym że połączenie istniało utrzymując je w stanie TIME_WAIT.
Dla systemu operacyjnego port który był wykorzystany w tym połączeniu jest
uznawany za zajęty, więc domyślnie system nie pozwoli uruchomić serwera na
tym porcie. Można zmienić to zachowanie ustawiając dla gniazda opcję
SO_REUSEADDR, która pozwala użyć adresu (IP i portu) mimo tego ze jakieś
połączenie w stanie TIME_WAIT go jeszcze używa.
Poniższy kod ustawia opcję SO_REUSEADDR dla gniazda sockFd:
const int one = 1; setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
Oczywiście trzeba zmienić to zachowanie przed wywołaniem funkcji
bind
której działania ta opcja dotyczy.
Zadanie 2 Zmodyfikuj program z poprzedniego zadania tak, by ustawiał opcję
SO_REUSEADDR.
Sprawdź czy teraz możesz uruchomić serwer natychmiast po zakończeniu jego
poprzedniego uruchomienia.
Zadanie 3 Zmodyfikuj program z poprzedniego zadania tak, by w pętli obsługiwał kolejne połączenia.
Funkcja accept może przekazać informację o adresie z którego nawiązano
połączenie. W tym celu należy jej podać:
accept oczekuje typu sockaddr*, ale wskazuje mu się zmienną typu spodziewanego adresu (np. sockaddr_in)accept accept ma wpisany rozmiar przekazanej struktury, accept będzie mieć informację ile bajtów zostało tam wpisane przez accept
sockaddr_in cliAddr; socklen_t cliAddrSize = sizeof(cliAddr); client = accept(ssock, (sockaddr*)&cliAddr, &cliAddrSize);
getsockname i getpeername.
Zadanie 4 Dodaj do programu wyświetlanie z jakiego adresu IP i numeru portu
nawiązano połączenie.
Funkcje zmieniające adres IP zapisany jako liczba na tekst to inet_ntoa i inet_ntop
Zadanie 5 Zmień logikę serwera tak, by zamiast wysyłać stały tekst, odbierał od klienta (do zamknięcia połączenia przez klienta) dane i jeżeli serwer odbierze tekst:
data, to odeśle klientowi bieżącą datę i godzinęid, to odeśle klientowi liczbę większą o 1 od poprzednio wysłanej liczbyUwaga: klient może wysłać na końcu tekstu znak nowej linii - weź to pod uwagę
#include <sys/socket.h> #include <netinet/in.h> #include <cstdio> #include <cstring> #include <unistd.h> #include <algorithm> #define CHECK(code, txt) if(code==-1) {perror(txt); return 1;} enum ttt:char{e='.',X='x',O='o'} map[3][4]{{e,e,e},{e,e,e},{e,e,e}}; #define MAP_TO_BUF sprintf(buf, "\n 012\n0%s\n1%s\n2%s\n\n",\ (char*)map[0],(char*)map[1],(char*)map[2]); int main(){ int s = socket(AF_INET, SOCK_STREAM, 0), one = 1; CHECK(s, "socket"); setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); sockaddr_in saddr{AF_INET, htons(3003), {htonl(INADDR_ANY)}}; int rv = bind(s, (sockaddr*)&saddr, sizeof(saddr)); CHECK(rv, "bind"); rv = listen(s, 1); CHECK(rv, "listen"); int c1 = accept(s, nullptr, nullptr); CHECK(c1, "accept c1"); write(c1, "Poczekaj na drugiego gracza\n", 28); int c2 = accept(s, nullptr, nullptr); CHECK(c2, "accept c2"); close(s); for(;;std::swap(c1,c2)){ write(c2, "Ruch przeciwnika.\n", 18); write(c1, "Podaj x i y oddzielone spacją (zakres 0-2)\n", 44); char buf[256]{}; unsigned x=3, y=3; read(c1, buf, 255); sscanf(buf, "%u%u", &x, &y); if (x<3&&y<3 && map[y][x]==e) map[y][x]=c1<c2?X:O; MAP_TO_BUF; rv = write(c2, buf, 22); // program zakończy się kiedy dostanie rv = write(c1, buf, 22); // sygnał SIGPIPE przy próbie zapisu } // do zamkniętego gniazda }
listen powinien się zawierać w
zakresie 1÷SOMAXCONN (w tej chwili o wartości
4096 w Linuksie)
i jest traktowany jako podpowiedź, tzn. system operacyjny może używać innego
limitu niż podany w argumencie listen.