Różnice między wybraną wersją a wersją aktualną.
Both sides previous revision Poprzednia wersja Nowa wersja | Poprzednia wersja Nowa wersja Both sides next revision | ||
sk2:sockets_full [2016/10/19 18:24] jkonczak [Read i Write - ilość odczytanych / zapisanych danych] |
sk2:sockets_full [2022/10/10 16:36] jkonczak |
||
---|---|---|---|
Linia 1: | Linia 1: | ||
- | ====== Interface gniazd BSD ====== | + | ====== Interface gniazd BSD (1/2) ====== |
- | Schemat kolejności wywołać funkcji bibliotecznych znajduje się na: \\ [[http://www.cs.put.poznan.pl/ddwornikowski/sieci/sieci2/bsdsockets.html#rys-1]] | + | Schemat kolejności wywołać funkcji bibliotecznych: \\ |
+ | <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]].) | ||
===== Serwer TCP ===== | ===== Serwer TCP ===== | ||
- | Aby oczekiwać na przychodzące połączenia TCP, po stworzeniu gniazda (''socket(…)'') wystarczy wywołać 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.\\ | + | 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. |
- | Dlatego przed funkcją ''listen(…)'' należy użyć funkcji ''bind(…)'' do ustawienia lokalnego adresu gniazda. Funkcja ''bind'' przyjmuje jako argument strukturę ''sockaddr''. | + | |
- | Lokalny adres IP można ustawić na dowolny – ''INADDR_ANY'' lub wybrany przez siebie – np. 127.0.0.1 (''INADDR_LOOPBACK'') jeśli połączenia mają być ograniczone do localhosta. | + | 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 adres gniazda**. Funkcja ''bind'' przyjmuje jako argument strukturę ''sockaddr''. | ||
- | Po wywołaniu funkcji ''listen'' system operacyjny system operacyjny czeka na połączenia. Do odebrania nowego połączenia należy użyć funkcji ''accept(…)''. Funkcja accept zwróci nowe gniazdo reprezentujące nawiązane połączenie. | + | **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. |
+ | |||
+ | **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''). | ||
+ | |||
+ | Do odebrania nowych połączeń używa się funkcji ''accept(…)''. **Funkcja ''accept'' zwraca nowe gniazdo** reprezentujące nawiązane połączenie. | ||
+ | |||
+ | <html><small></html>Tworzenie kolejnych deskryptorów plików oraz wyniki strace/ss są zaprezentowane [[sk2:sockets_full:tcp_srv_img|tutaj]]<html></small></html> | ||
//Zadanie 1:// Napisz program, który: | //Zadanie 1:// Napisz program, który: | ||
* stworzy gniazdo, | * stworzy gniazdo, | ||
- | * ustali adres (''bind''), | + | * ustali adres (''bind''), uwaga: sprawdzaj wynik funkcji ''bind'', |
* rozpocznie oczekiwanie na połączenia (''listen''), | * rozpocznie oczekiwanie na połączenia (''listen''), | ||
- | * zaakceptuje połączenie (''accept''); na razie drugi i trzeci argument funkcji ''accept'' pozostaw pusty, | + | * ''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'', | ||
* 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 2:// Zmodyfikuj program tak, by w pętli obsługiwał nowe połączenia | + | //Zadanie 2a:// Krótko((https://github.com/torvalds/linux/search?q=TCP_TIMEWAIT_LEN)) 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 2b:// 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> | ||
- | 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 podać jej adres struktury sockaddr (drugi argument) i adres zmiennej, która w momencie wywołania ''accept'' na wppisany rozmiar przekazanej struktury, a do której ''accept'' zapisze ile bajtów przekazanej struktury wypełnił (trzeci argument). | + | <html><small></html> |
+ | //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> | ||
- | //Zadanie 3:// Dodaj do programu wyświetlanie z jakiego adreesu IP i numeru portu nawiązano połączenie | + | //Zadanie 4:// 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ć: | ||
+ | * 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> | ||
+ | sockaddr_in nazwa_zmiennej; | ||
+ | socklen_t inna_zmienna = sizeof(nazwa_zmiennej); | ||
+ | client = accept(ssock, (sockaddr*)&nаzwa_zmiennej, &innа_zmienna); | ||
+ | </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). | ||
+ | |||
+ | //Zadanie 5:// Dodaj do programu wyświetlanie z jakiego adresu IP i numeru portu nawiązano połączenie | ||
+ | \\ | ||
+ | <html><small></html> | ||
+ | Funkcje zmieniające adres IP zapisany jako liczba na tekst to ''inet_ntoa'' i ''inet_ntop'' | ||
+ | <html></small></html> | ||
===== UDP vs TCP - przypomnienie ==== | ===== UDP vs TCP - przypomnienie ==== | ||
Linia 34: | Linia 60: | ||
</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''. |
+ | |||
+ | Przykład użycia funkcji ''sendto'' i ''recvfrom'': <html><small></html><code cpp> | ||
+ | sockaddr_in nazwa_zmiennej {...}; | ||
+ | socklen_t inna_zmienna = sizeof(nazwa_zmiennej); | ||
+ | kolejna_zmienna = sendto(sockfd, "wiadomosc", 9, 0, (sockaddr*) &nazwa_zmiennej, inna_zmienna); | ||
+ | kolejna_zmienna = recvfrom(sockfd, bufor, rozmiar_bufora, 0, (sockaddr*) &nazwa_zmiennej, &inna_zmienna); | ||
+ | </code><html></small></html> | ||
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 4.// Odpowiedz na pytanie dlaczego DNS używa UDP zamiast TCP do zapytań. | + | //Zadanie 6.// Odpowiedz na pytanie dlaczego DNS używa UDP zamiast TCP do zapytań. |
- | ===== Klient UDP ===== | + | ===== UDP ===== |
- | //Zadanie 5.// Napisz program, który wyśle dane (stały ciąg znaków) pod wskazany adres, następnie odbierze dane i się zakończy. | + | Program korzystający z UDP musi stworzyć funkcją ''socket'' gniazdu typu ''SOCK_DGRAM''. |
- | ===== Serwer UDP ===== | + | Następnie program może (a jeśli ma pełnić rolę serwera, to powinien) wywołać funkcję ''bind'', ustalając lokalny numer portu. |
- | //Zadanie 6.// Napisz program, który odbierze dane i odeśle je pod adres nadawcy zmieniając wielkość liter.\\ | + | Gniazdo UDP w każdym momencie po stworzeniu może wykonywać operacje odbioru i wysyłania danych – ''sendto'', ''recvfrom'', ''recv'', ''read''. |
- | <html><small></html>Jeśli nie wiesz jak zmienić wielkość liter, to ''for(char *it=str; (*it=toupper(*it)); ++it);''<html></small></html> | + | |
- | //Zadanie 7.// Napisz program, który będzie w pętli odbierać dane i odsyłać je pod adresy wszystkich wcześniejszych nadawców. | + | UDP jest bezpołączeniowe, więc nie da się na nim wykonać funkcji ''shutdown''. |
+ | ==== Klient UDP ==== | ||
- | ===== Read i Write - ilość odczytanych / zapisanych danych ===== | + | //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. |
- | Polecenie ''read'', ''recv'',… często zwracają mniej danych niż podana w argumentach wielkość bufora. Powód? Mniej danych przyszło lub skończyło się miejsce w buforze odbiorczym (''setsockopt(socket, SOL_SOCKET, SO_RCVBUF, …)''). | + | ==== Serwer UDP ==== |
- | Polecenia ''write'', ''send'',… może też zwrócić w pewnych okolicznościach mniejszy rozmiar wysłanych danych niż podany rozmiar buforu. | + | //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.\\ |
- | Jest to spowodowane zapchaniem systemowego buforu wysyłania (czyli jeśli dane są dostarczane do wysłania szybciej niż można je wysyłać). Normalnie funkcje zapisujące blokują się do momentu aż w buforze będzie dość miejsca. Jeśli jednak ustawi się opcję ''O_NONBLOCK'' (typowa w wielu zastosowaniach), to funkcje takie jak ''write'', ''send'', … zapisują tylko część danych. | + | <html><small></html>Jeśli nie wiesz jak zmienić wielkość liter, to ''for(char *it=str; (*it=toupper(*it)); ++it);''<html></small></html> |
- | + | ||
- | Dlatego zarówno przy odbieraniu jak i wysyłaniu trzeba zwracać uwagę na zwracaną ilość przetworzonych danych. | + | |
- | + | ||
- | //Zadanie 8.// W serwerze TCP zmień rozmiar buforu nadawczego, przestaw gniazdo w tryb nieblokujący (kod poniżej) i każ wysłać ponad 4096 bajtów. Zaobserwuj co odbierze klient. __Uwaga:__ klient musi być z innego hosta. \\ <html><small></html>Nie wiesz jak wygenerować długi komunikat? ctrl+c, ctrl+v. Albo ''string s{"a"}; s.resize (22220,'='); s+="z";'' Potem: ''s.c_str()'' i ''s.length()''<html></small></html> | + | |
- | <code cpp> | + | |
- | int buffsize = 1024; | + | |
- | setsockopt(socket, SOL_SOCKET, SO_SNDBUF, &buffsize, sizeof(buffsize)); | + | |
- | fcntl(socket, F_SETFL, O_NONBLOCK, 1); | + | |
- | </code> | + | |
- | + | ||
- | //Zadanie 9.// W kliencie UDP i TCP zmniejsz rozmiar bufora przekazywanego do funkcji odbioru wiadomości i wykonaj odbiór dwa razy. Zaobserwuj co odbierze klient. | + | |
- | + | ||
- | ===== Opcje gniazd ===== | + | |
- | Strona podręcznika ''man 7 socket'' opisuje możliwe argumenty funkcji ''setsockopt'' i ''getsockopt''. Ważne opcje to: | + | //Zadanie 9.// Napisz program, który będzie w pętli odbierać dane i odsyłać je pod adresy wszystkich wcześniejszych nadawców. |
- | * ''SO_BROADCAST'' - wymagane do UDP broadcastu | + | |
- | * ''SO_KEEPALIVE'' - włącza mechanizm TCP keepalive [[http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html]] | + | |
- | * ''SO_LINGER'' – pozwala przed zamknięciem gniazda wysłać zakolejkowane dane | + | |
- | * ''SO_RCVBUF'' i ''SO_SNDBUF'' – ustawienie rozmiaru systemowych buforów odbiorczych / nadawczych | + | |
- | * ''SO_REUSEADDR'' – pomija stan TIME_WAIT po zamknięciu połączenia [[https://tools.ietf.org/html/rfc793#page-22]] | + | |
- | Funkcja ''fcntl'' (''man fcntl open'') pozwala na ustawienie (''F_SETFL'') opcji ''O_NONBLOCK'' potrzebnej do nieblokującej obsługi gniazd. (''O_NONBLOCK'' można też ustawić sumując ostatni argument funkcji ''socket'' z ''SOCK_NONBLOCK''.) | ||