User Tools

Site Tools


sk2:sockets_full

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
sk2:sockets_full [2019/10/14 19:00] – [Interface gniazd BSD (1/2)] jkonczaksk2:sockets_full [2026/11/01 18:31] (current) – [UDP vs TCP - przypomnienie] jkonczak
Line 1: Line 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.
  
 Gniazdo nasłuchujące tworzy się wywołując po stworzeniu gniazda (''socket(…)'') 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.\\ Gniazdo nasłuchujące tworzy się wywołując po stworzeniu gniazda (''socket(…)'') 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 funkcją ''listen(…)'' **należy funkcją ''bind(…)'' ustawić lokalny adresu 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. 
 +\\ 
 +<small>Stałe ''INADDR_ANY'' i ''INADDR_LOOPBACK'' są w lokalnym porządku bajtów 
 +(little/big endian), więc wpisując je np. do odpowiedniego pola struktury 
 +''sockaddr_in'' trzeba zmienić porządek bajtów na sieciowy funkcją ''htonl''. 
 +Dowolny adres IPv4 to 0.0.0.0, więc w tym szczególnym przypadku zmiana porządku 
 +bajtów jest niepotrzebna.</small>
  
 **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 mogą czekać 
 +w kolejce na odebranie przez program (tj. połączeń dla których nie wykonano 
 +jeszcze funkcji ''accept'', ale system operacyjny odpowiedział klientowi że 
 +udało mu się połączyć)((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. Jeśli w kolejce czeka 
 +już jakieś połączenie, funkcja ''accept'' natychmiast się kończy, w przeciwym 
 +razie blokuje się do czasu aż pojawi się nowy klient.
  
-//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'' na 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, &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 ====
  
Line 53: Line 114:
  
 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)
Line 58: Line 120:
 </code> </code>
  
-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''.  
 +\\ 
 +<small>Więcej o trybie pseudo-połączeniowym na wykładzie, warto też zajrzeć na 
 +[[https://blog.cloudflare.com/everything-you-ever-wanted-to-know-about-udp-sockets-but-were-afraid-to-ask-part-1/| wpis z bloga cloudflare]] streszczający ten temat. 
 +</small>
  
-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);
Line 69: Line 137:
 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 =====
Line 80: Line 148:
  
 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''.
- 
 ==== 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.
  
  
sk2/sockets_full.1571079659.txt.gz · Last modified: (external edit)