====== Klient TCP (przypomnienie) ====== Do 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 ''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. Tworzenie kolejnych deskryptorów plików oraz wyniki ''lsof'', ''strace'' i ''ss'' są zaprezentowane [[sk2:sockets_full:tcp_srv_img|tutaj]] ===== Zadania ===== ~~Zadanie.#~~ Napisz program, który:
- 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''
Potrzebne pliki nagłówkowe:
#include #include #include #include #include int main(int argc, char **argv) { return 0; }
===== 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'':
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:
**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**. \\ 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 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. ===== 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''
Przykład:
sockaddr_in cliAddr; socklen_t cliAddrSize = sizeof(cliAddr); client = accept(ssock, (sockaddr*)&cliAddr, &cliAddrSize);
Adresy gniazda (lokalny i zdalny) można też odczytać w każdej chwili funkcjami ''getsockname'' i ''getpeername''. ===== Zadania ===== ~~Zadanie.#~~ 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.#~~ 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
Uwaga: klient może wysłać na końcu tekstu znak nowej linii - weź to pod uwagę ++++ Przykład minimalistycznego iteracyjnego serwera jednej gry w kółko i krzyżyk (bez sprawdzania warunku końca) | #include #include #include #include #include #include #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 ++++