Narzędzia użytkownika

Narzędzia witryny


bio-psiec:tcp_server

Różnice

Różnice między wybraną wersją a wersją aktualną.

Odnośnik do tego porównania

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>​
 +++++
  
bio-psiec/tcp_server.txt · ostatnio zmienione: 2025/11/12 21:11 przez jkonczak