Różnice między wybraną wersją a wersją aktualną.
| — |
bio-psiec:tcp_server [2025/11/12 21:11] (aktualna) jkonczak utworzono |
||
|---|---|---|---|
| Linia 1: | Linia 1: | ||
| + | ====== Klient TCP (przypomnienie) ====== | ||
| + | |||
| + | Do pracy programu w roli klienta TCP trzeba w kolejności użyć funkcji: | ||
| + | <html><div style=margin-top:-1.4em></div></html> | ||
| + | - ''socket'' do stworzenia gniazda, podając typ ''SOCK_STREAM'' i protokół ''IPPROTO_TCP'' lub ''0'' | ||
| + | - ''connect'' żeby połączyć utworzone gniazdo pod adres wskazany w argumentach | ||
| + | - po połączeniu można wysyłać i odbierać dane, np. funkcjami ''write'', ''send'' i ''read'', ''recv'' | ||
| + | - ''shutdown'' żeby zakończyć połączenie na tym gnieździe | ||
| + | - ''close'' ż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. | ||
| + | |||
| + | ====== Serwer TCP ====== | ||
| + | |||
| + | ===== Wstęp ===== | ||
| + | |||
| + | 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'')((Argument "backlog" funkcji ''listen'' powinien się zawierać w | ||
| + | zakresie ''1''÷''SOMAXCONN'' (w tej chwili o wartości | ||
| + | [[https://elixir.bootlin.com/linux/v6.17.1/source/include/linux/socket.h#L298|4096 w Linuksie]]) | ||
| + | i jest traktowany jako podpowiedź, tzn. system operacyjny może używać innego | ||
| + | limitu niż podany w argumencie ''listen''.)). | ||
| + | |||
| + | Do odebrania nowych połączeń używa się funkcji ''accept(…)''. **Funkcja | ||
| + | ''accept'' zwraca nowe gniazdo** reprezentujące nawiązane połączenie. | ||
| + | |||
| + | <small>Tworzenie kolejnych deskryptorów plików oraz wyniki ''lsof'', ''strace'' | ||
| + | i ''ss'' są zaprezentowane [[sk2:sockets_full:tcp_srv_img|tutaj]]</small> | ||
| + | |||
| + | ===== Zadania ===== | ||
| + | |||
| + | ~~Zadanie.#~~ Napisz program, który: | ||
| + | <html><div style=margin-top:-1.4em></div></html> | ||
| + | - stworzy gniazdo TCP funkcją ''socket'', | ||
| + | - stworzy zmienną typu ''sockaddr_in'' i wypełni ją wpisując: | ||
| + | * ''AF_INET'' w pole określające rodzinę adresów | ||
| + | * stałą ''INADDR_ANY'' w pole określające adres IPv4 | ||
| + | * wybrany numer portu na którym serwer ma czekać na połączenia w pole określające port \\ (pamiętaj o ''htons'' do zmiany kolejności bajtów) | ||
| + | - ustali lokalny adres gniazda funkcją ''bind'' \\ //uwaga//: koniecznie sprawdzaj czy funkcja ''bind'' się powiodła \\ jeśli zobaczysz błąd //address already in use//, przeczytaj tekst o stanie TIME_WAIT pod zadaniem | ||
| + | - rozpocznie oczekiwanie na połączenia wywołując funkcję ''listen'' \\ drugi argument ''listen'' określa ile nieodebranych połączeń może naraz czekać w systemie; tutaj wystarczy ''1'' | ||
| + | - zaakceptuje połączenie funkcją ''accept'' \\ na razie drugi i trzeci argument funkcji ''accept'' ustaw na ''0'' lub ''nullptr'' | ||
| + | - wynik funkcji ''accept'' to deskryptor nowego gniazda, połączonego z klientem \\ (w tym momencie możesz już zamknąć gniazdo nasłuchujące na połączenia) | ||
| + | - wyśle do gniazda połączonego z klientem stały ciąg znaków (np. ''hello'') | ||
| + | - zakończy połączenie z klientem funkcją ''shutdown'' i zamknie deskryptor pliku funkcją ''close'' | ||
| + | <html><div style="display:inline-block; margin-top:-1.4em; line-height:100%"></html><small> | ||
| + | Potrzebne pliki nagłówkowe: | ||
| + | <html><div style="margin-top:-1.4em;"></div></html> | ||
| + | <code c++> | ||
| + | #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; | ||
| + | } | ||
| + | </code></small> | ||
| + | <html></div></html> | ||
| + | <html><div style="margin-top:-1.4em;"></div></html> | ||
| + | | ||
| + | ===== Stan TIME_WAIT ===== | ||
| + | |||
| + | 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'': | ||
| + | <html><div style="text-align:center;margin-top:-1.4em"><object style="border:1px solid #bbb;height:80pt;padding:0.25em" data="/jkonczak/_media/bio-psiec:tcp_server:time-wait_1.svg" type="image/svg+xml"></object></div></html> | ||
| + | Zwróć uwagę, że wywołanie ''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: | ||
| + | <html><div style="text-align:center;margin-top:-1.4em"><object style="border:1px solid #bbb;height:80pt;padding:0.25em" data="/jkonczak/_media/bio-psiec:tcp_server:time-wait_2.svg" type="image/svg+xml"></object></div></html> | ||
| + | **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 minuty((https://elixir.bootlin.com/linux/v6.17.1/source/include/net/tcp.h#L128))) | ||
| + | system operacyjny **pamięta o tym że połączenie istniało utrzymując je w stanie | ||
| + | TIME_WAIT**. | ||
| + | \\ | ||
| + | <small> | ||
| + | Więcej informacji o [[https://datatracker.ietf.org/doc/html/rfc9293#normal_close|zamykaniu połączenia]] | ||
| + | i [[https://datatracker.ietf.org/doc/html/rfc9293#name-state-machine-overview|stanach połączenia]] | ||
| + | znajdziesz w odpowiednim RFC | ||
| + | </small> | ||
| + | |||
| + | 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'': | ||
| + | <html><div style=margin-top:-1.4em></div></html> | ||
| + | <code cpp>const int one = 1; | ||
| + | setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));</code> | ||
| + | <html><div style=margin-top:-1.4em></div></html> | ||
| + | Oczywiście trzeba zmienić to zachowanie przed wywołaniem funkcji ''bind'' | ||
| + | której działania ta opcja dotyczy. | ||
| + | |||
| + | ===== Zadania ===== | ||
| + | |||
| + | ~~Zadanie.#~~ 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.#~~ Zmodyfikuj program z poprzedniego zadania tak, by w pętli | ||
| + | obsługiwał kolejne połączenia. | ||
| + | |||
| + | ===== Odczytywanie informacji skąd przyszło połączenie ===== | ||
| + | |||
| + | Funkcja ''accept'' może przekazać informację o adresie z którego nawiązano | ||
| + | połączenie. W tym celu należy jej podać: | ||
| + | * jako drugi argument informację gdzie ma zapisać ten adres – tj. podać adres struktury którą ta funkcja ma wypełnić \\ ''accept'' oczekuje typu ''sockaddr*'', ale wskazuje mu się zmienną typu spodziewanego adresu (np. ''sockaddr_in'') | ||
| + | * trzeci argument to ilość bajtów struktury z adresem, która jest **czytana i nadpisywana** przez ''accept'' \\ czyli: trzeba podać adres zmiennej, która w momencie wywołania ''accept'' ma wpisany rozmiar przekazanej struktury, \\ a po wyjściu z funkcji ''accept'' będzie mieć informację ile bajtów zostało tam wpisane przez ''accept'' | ||
| + | <html><div style="display:inline-block; margin-top:-1.4em; line-height:100%"></html><small> | ||
| + | Przykład: | ||
| + | <html><div style="margin-top:-1.4em;"></div><div style="user-select: none;"></html> | ||
| + | <code c++> | ||
| + | sockaddr_in cliAddr; | ||
| + | socklen_t cliAddrSize = sizeof(cliAddr); | ||
| + | client = accept(ssock, (sockaddr*)&cliAddr, &cliAddrSize); | ||
| + | </code></small> | ||
| + | <html></div></div></html> | ||
| + | <html><div style="margin-top:-1.4em;"></div></html> | ||
| + | <small> | ||
| + | Adresy gniazda (lokalny i zdalny) można też odczytać w każdej chwili funkcjami | ||
| + | ''getsockname'' i ''getpeername''. | ||
| + | </small> | ||
| + | |||
| + | ===== Zadania ===== | ||
| + | |||
| + | ~~Zadanie.#~~ Dodaj do programu wyświetlanie z jakiego adresu IP i numeru portu | ||
| + | nawiązano połączenie. | ||
| + | \\ | ||
| + | <small> | ||
| + | Funkcje zmieniające adres IP zapisany jako liczba na tekst to ''inet_ntoa'' i ''inet_ntop'' | ||
| + | </small> | ||
| + | |||
| + | ~~Zadanie.#~~ 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 liczby | ||
| + | <html><div style=margin-top:-1.4em></div></html> | ||
| + | <small>Uwaga: klient może wysłać na końcu tekstu znak nowej linii - weź to pod uwagę</small> | ||
| + | |||
| + | ++++ Przykład minimalistycznego iteracyjnego serwera jednej gry w kółko i krzyżyk (bez sprawdzania warunku końca) | | ||
| + | <code c++> | ||
| + | #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 | ||
| + | } | ||
| + | </code> | ||
| + | ++++ | ||