Różnice między wybraną wersją a wersją aktualną.
| Both sides previous revision Poprzednia wersja Nowa wersja | Poprzednia wersja | ||
|
sk2:sockets_full [2020/10/13 18:05] jkonczak [Serwer TCP] |
sk2:sockets_full [2025/10/16 22:26] (aktualna) jkonczak |
||
|---|---|---|---|
| Linia 1: | Linia 1: | ||
| ====== Interface gniazd BSD (1/2) ====== | ====== Interface gniazd BSD (1/2) ====== | ||
| - | Schemat kolejności wywołać funkcji bibliotecznych: \\ | + | Schemat kolejności wywołań funkcji bibliotecznych: \\ |
| <html><object id="svg-object" data="/jkonczak/_media/sk2:sockets.svg" type="image/svg+xml"></object></html>\\ | <html><object id="svg-object" data="/jkonczak/_media/sk2:sockets.svg" type="image/svg+xml"></object></html>\\ | ||
| - | (Porównaj z: [[http://www.cs.put.poznan.pl/ddwornikowski/sieci/sieci2/bsdsockets.html#rys-1]].) | + | ----- |
| + | Funkcja ''socket'' i kolejność wywoływania funkcji z API gniazd na deskryptorze zwróconym przez tę funkcję:\\ | ||
| + | <html><object id="svg-object" data="/jkonczak/_media/sk2:gniazdo-diagram.svg" type="image/svg+xml"></object></html> | ||
| ===== Serwer TCP ===== | ===== Serwer TCP ===== | ||
| 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. | 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. | ||
| Linia 9: | Linia 12: | ||
| Dlatego przed funkcją ''listen(…)'' **należy funkcją ''bind(…)'' ustawić lokalny adres gniazda**. Funkcja ''bind'' przyjmuje jako argument strukturę ''sockaddr''. | Dlatego przed funkcją ''listen(…)'' **należy funkcją ''bind(…)'' ustawić lokalny adres gniazda**. Funkcja ''bind'' przyjmuje jako argument strukturę ''sockaddr''. | ||
| - | **Lokalny adres IP nasłuchującego gniazda zwykle ustawia się na ustawić na dowolny – ''INADDR_ANY''**. Inny lokalny adres IP ustawia się, jeśli gniazdo ma odbierać połączenia kierowane tylko na ten adres – np. 127.0.0.1 (''INADDR_LOOPBACK'') jeśli połączenia mają być ograniczone do połączeń z localhosta. | + | **Lokalny adres IP nasłuchującego gniazda zwykle ustawia się na dowolny – ''INADDR_ANY''**. |
| + | Inny lokalny adres IP ustawia się, jeśli gniazdo ma odbierać połączenia | ||
| + | kierowane tylko na ten adres – np. 127.0.0.1 (''INADDR_LOOPBACK'') jeśli | ||
| + | połączenia mają być ograniczone do połączeń z localhosta. | ||
| **Wywołanie funkcji ''listen'' nakazuje systemowi operacyjnemu czekać na połączenia** (porównaj z: ''connect''). 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. \\ | **Wywołanie funkcji ''listen'' nakazuje systemowi operacyjnemu czekać na połączenia** (porównaj z: ''connect''). 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''). | + | 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. | Do odebrania nowych połączeń używa się funkcji ''accept(…)''. **Funkcja ''accept'' zwraca nowe gniazdo** reprezentujące nawiązane połączenie. | ||
| - | //Zadanie 1:// Napisz program, który: | + | <html><small></html>Tworzenie kolejnych deskryptorów plików oraz wyniki ''lsof'', ''strace'' i ''ss'' są zaprezentowane [[sk2:sockets_full:tcp_srv_img|tutaj]]<html></small></html> |
| + | |||
| + | ~~Zadanie.#~~ Napisz program, który: | ||
| * stworzy gniazdo, | * stworzy gniazdo, | ||
| - | * ustali adres (''bind''), uwaga: sprawdzaj wynik funkcji ''bind'', | + | * ustali adres (''bind'') uwaga: sprawdzaj wynik funkcji ''bind'' \\ <small>(jeśli dostaniesz błąd ''//Address already in use//'' na porcie na którym chwilę temu udało się uruchomić serwer, przeczytaj i wykonaj kolejne zadanie)</small>, |
| * rozpocznie oczekiwanie na połączenia (''listen''), | * rozpocznie oczekiwanie na połączenia (''listen''), | ||
| * ''listen'' jako drugi argument przyjmuje ile nowych połączeń może naraz czekać na odebranie funkcją accept | * ''listen'' jako drugi argument przyjmuje ile nowych połączeń może naraz czekać na odebranie funkcją accept | ||
| - | * zaakceptuje połączenie (''accept''); na razie drugi i trzeci argument funkcji ''accept'' ustaw na ''0'' lub ''nullptr'', | + | * zaakceptuje połączenie (''accept'') \\ na razie drugi i trzeci argument funkcji ''accept'' ustaw na ''0'' lub ''nullptr'', |
| * wyśle tam dane (stały ciąg znaków), | * wyśle tam dane (stały ciąg znaków), | ||
| * zakończy program. | * zakończy program. | ||
| - | //Zadanie 2a:// Krótko((https://github.com/torvalds/linux/search?q=TCP_TIMEWAIT_LEN)) po zamknięciu programu serwera sprawdź poleceniem ''netstat -tpn'' w jakim stanie jest połączenie. Przypomnij sobie co oznacza ten stan - [[https://tools.ietf.org/html/rfc793#page-22|RFC793]]. Spróbuj w tym czasie uruchomić ponownie program serwera. \\ //Zadanie 2b:// Ustaw przed wywołaniem ''bind'' opcję ''SO_REUSEADDR'' gniazda (kod poniżej) i powtórz zadanie 2a.<code cpp>const int one = 1; | + | ~~Zadanie.#~~a Krótko((https://elixir.bootlin.com/linux/v6.17.1/source/include/net/tcp.h#L128)) po zamknięciu programu serwera sprawdź poleceniem ''netstat -tpn'' oraz ''ss -atnop'' w jakim stanie jest połączenie. Przypomnij sobie co oznacza ten stan – [[https://tools.ietf.org/html/rfc793#page-22|RFC793]]. Spróbuj w tym czasie uruchomić ponownie program serwera. \\ ~~Zadanie.#2~~b Ustaw przed wywołaniem ''bind'' opcję ''SO_REUSEADDR'' gniazda (kod poniżej) i powtórz zadanie 2a.<code cpp>const int one = 1; |
| setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));</code> | setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));</code> | ||
| + | <small> | ||
| + | Linux pozwala (przy włączonej odpowiedniej opcji w jądrze) usunąć dowolne połączenie sieciowe, też takie w stanie TIME-WAIT. Można do tego użyć np. komendy ''ss'' w połączeniu z filtrami i przełącznikiem ''-kill''/''-K'', np: ''ss --kill --tcp src //10.0.0.1// sport //2345// dst //10.0.0.6// dport //7890//'' | ||
| - | <html><small></html> | + | ~~Zadanie.#~~ Zakomentuj wywołanie bind, uruchom program i odczytaj z wyników komendy ''netstat -tlpn'' na jakim porcie nasłuchuje twój serwer. Następnie połącz się do niego. |
| - | //Zadanie 3:// Zakomentuj wywołanie bind, uruchom program i odczytaj z wyników komendy ''netstat -tlpn'' na jakim porcie nasłuchuje twój serwer. Następnie połącz się do niego.<html></small></html> | + | </small> |
| - | //Zadanie 4:// Zmodyfikuj program tak, by w pętli obsługiwał nowe połączenia | + | ~~Zadanie.#~~ Zmodyfikuj program tak, by w pętli obsługiwał nowe połączenia |
| Funkcja ''accept'' (podobnie jak wprowadzana za chwilę ''recvfrom'') może przekazać informację o adresie z którego nawiązano połączenie. W tym celu należy jej podać: | Funkcja ''accept'' (podobnie jak wprowadzana za chwilę ''recvfrom'') może przekazać informację o adresie z którego nawiązano połączenie. W tym celu należy jej podać: | ||
| * gdzie ma zapisać ten adres – tj. podać adres struktury sockaddr (drugi argument) | * gdzie ma zapisać ten adres – tj. podać adres struktury sockaddr (drugi argument) | ||
| - | * adres zmiennej, która w momencie wywołania ''accept'' ma wpisany rozmiar przekazanej struktury (trzeci argument)<html><small></html><code cpp> | + | * adres zmiennej, która w momencie wywołania ''accept'' ma wpisany rozmiar przekazanej struktury (trzeci argument)<html><small style="user-select: none;"></html><code cpp> |
| sockaddr_in nazwa_zmiennej; | sockaddr_in nazwa_zmiennej; | ||
| socklen_t inna_zmienna = sizeof(nazwa_zmiennej); | socklen_t inna_zmienna = sizeof(nazwa_zmiennej); | ||
| - | client = accept(ssock, (sockaddr*)&nazwa_zmiennej, &inna_zmienna); | + | client = accept(ssock, (sockaddr*)&nаzwa_zmiennej, &innа_zmienna); |
| </code><html></small></html> | </code><html></small></html> | ||
| Jeśli drugi i trzeci argument są niezerowe, ''accept'' wypisuje przekazaną strukturę (drugi argument) i ustawia ile bajtów przekazanej struktury wypełnił (trzeci argument). | Jeśli drugi i trzeci argument są niezerowe, ''accept'' wypisuje przekazaną strukturę (drugi argument) i ustawia ile bajtów przekazanej struktury wypełnił (trzeci argument). | ||
| - | //Zadanie 5:// Dodaj do programu wyświetlanie z jakiego adresu IP i numeru portu nawiązano połączenie | + | ~~Zadanie.#~~ Dodaj do programu wyświetlanie z jakiego adresu IP i numeru portu nawiązano połączenie |
| \\ | \\ | ||
| <html><small></html> | <html><small></html> | ||
| Funkcje zmieniające adres IP zapisany jako liczba na tekst to ''inet_ntoa'' i ''inet_ntop'' | Funkcje zmieniające adres IP zapisany jako liczba na tekst to ''inet_ntoa'' i ''inet_ntop'' | ||
| <html></small></html> | <html></small></html> | ||
| + | |||
| + | ++++ 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 zamniętego gniazda | ||
| + | } | ||
| + | </code> | ||
| + | ++++ | ||
| + | |||
| ===== UDP vs TCP - przypomnienie ==== | ===== UDP vs TCP - przypomnienie ==== | ||
| Linia 53: | Linia 104: | ||
| Tworząc gniazdo UDP należy użyć następujących argumentów: | Tworząc gniazdo UDP należy użyć następujących argumentów: | ||
| + | <html><div style=margin-top:-1.2em></div></html> | ||
| <code cpp> | <code cpp> | ||
| socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP) | socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP) | ||
| Linia 60: | Linia 112: | ||
| UDP nie nawiązuje połączenia – do odbioru i wysyłania wiadomości należy używać funkcji ''sendto'' i ''recvfrom'' (lub ''recv''/''read'', jeśli nie obchodzi nas nadawca). \\ W formie ułatwienia BSD socket API pozwala działać gniazdom UDP w trybie pseudo-połączeniowym - tzn. można wywołać funkcję ''connect'' (która ustali adres odbiorcy) i dalej korzystać z ''send'' / ''write''. | UDP nie nawiązuje połączenia – do odbioru i wysyłania wiadomości należy używać funkcji ''sendto'' i ''recvfrom'' (lub ''recv''/''read'', jeśli nie obchodzi nas nadawca). \\ W formie ułatwienia BSD socket API pozwala działać gniazdom UDP w trybie pseudo-połączeniowym - tzn. można wywołać funkcję ''connect'' (która ustali adres odbiorcy) i dalej korzystać z ''send'' / ''write''. | ||
| - | Przykład użycia funkcji ''sendto'' i ''recvfrom'': <html><small></html><code cpp> | + | Przykład użycia funkcji ''sendto'' i ''recvfrom'': |
| + | <html><div style=margin-top:-1.2em></div></html> | ||
| + | <html><small style="user-select: none;"></html><code cpp> | ||
| sockaddr_in nazwa_zmiennej {...}; | sockaddr_in nazwa_zmiennej {...}; | ||
| socklen_t inna_zmienna = sizeof(nazwa_zmiennej); | socklen_t inna_zmienna = sizeof(nazwa_zmiennej); | ||
| Linia 69: | Linia 123: | ||
| UDP jest zorientowane na wiadomość – wysyła i odbiera całe datagramy. Jeśli funkcja odbierająca dane zadeklaruje mniejszy bufor niż rozmiar wiadomości, nadmiarowe dane zostaną odrzucone. | UDP jest zorientowane na wiadomość – wysyła i odbiera całe datagramy. Jeśli funkcja odbierająca dane zadeklaruje mniejszy bufor niż rozmiar wiadomości, nadmiarowe dane zostaną odrzucone. | ||
| - | //Zadanie 6.// Odpowiedz na pytanie dlaczego DNS używa UDP zamiast TCP do zapytań. | + | ~~Zadanie.#~~ Odpowiedz na pytanie dlaczego DNS używa UDP zamiast TCP do zapytań. |
| ===== UDP ===== | ===== UDP ===== | ||
| Linia 80: | Linia 134: | ||
| UDP jest bezpołączeniowe, więc nie da się na nim wykonać funkcji ''shutdown''. | UDP jest bezpołączeniowe, więc nie da się na nim wykonać funkcji ''shutdown''. | ||
| - | |||
| - | <html><small><small></html> | ||
| - | Na gnieździe UDP można (jednokrotnie) wywołać funkcję ''connect'', która pozwoli odbierać dane tylko od podanego adresu i ustawi adres docelowy dla wysłania danych funkcją ''write''/''send''. | ||
| - | <html></small></small></html> | ||
| - | |||
| ==== Klient UDP ==== | ==== Klient UDP ==== | ||
| - | //Zadanie 7.// Napisz program, który korzystając z protokołu UDP wyśle dane (stały ciąg znaków) pod wskazany adres, następnie odbierze dane i się zakończy. | + | ~~Zadanie.#~~ Napisz program, który korzystając z protokołu UDP wyśle dane (stały ciąg znaków) pod wskazany adres, następnie odbierze dane i się zakończy. |
| ==== Serwer UDP ==== | ==== Serwer UDP ==== | ||
| - | //Zadanie 8.// Napisz program, który korzystając z protokołu UDP odbierze dane i odeśle je pod adres nadawcy zmieniając wielkość liter.\\ | + | ~~Zadanie.#~~ Napisz program, który korzystając z protokołu UDP odbierze dane i odeśle je pod adres nadawcy zmieniając wielkość liter.\\ |
| <html><small></html>Jeśli nie wiesz jak zmienić wielkość liter, to ''for(char *it=str; (*it=toupper(*it)); ++it);''<html></small></html> | <html><small></html>Jeśli nie wiesz jak zmienić wielkość liter, to ''for(char *it=str; (*it=toupper(*it)); ++it);''<html></small></html> | ||
| - | //Zadanie 9.// Napisz program, który będzie w pętli odbierać dane i odsyłać je pod adresy wszystkich wcześniejszych nadawców. | + | ~~Zadanie.#~~ Napisz program, który będzie w pętli odbierać dane i odsyłać je pod adresy wszystkich wcześniejszych nadawców. |