przejście do zawartości
Jan Kończak
Narzędzia użytkownika
Zaloguj
Narzędzia witryny
Narzędzia
Pokaż stronę
Poprzednie wersje
Odnośniki
Ostatnie zmiany
Menadżer multimediów
Indeks
Zaloguj
Ostatnie zmiany
Menadżer multimediów
Indeks
Jesteś tutaj:
start
»
sk2
»
sockets_full
sk2:sockets_full
Ta strona jest tylko do odczytu. Możesz wyświetlić źródła tej strony ale nie możesz ich zmienić.
====== Interface gniazd BSD (1/2) ====== 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 ===== 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.\\ 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 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'')((Argument "backlog" funkcji ''listen'' powinien się zawierać w zakresie ''1''÷''SOMAXCONN'' (w tej chwili równe 128 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. <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, * 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''), * ''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), * zakończy program. ~~Zadanie.#~~a 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.#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> <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//'' ~~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. </small> ~~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ć: * 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.#~~ 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> ++++ 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 ==== TCP: Strumieniowy. Połączeniowy. Kontrola przepływu. Niezawodny (reliable). \\ UDP: Zorientowany na wiadomość. Bezpołączeniowy, bezstanowy. Brak kontroli przepływu. Best-effort. Tworząc gniazdo UDP należy użyć następujących argumentów: <code cpp> socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP) // lub: socket(PF_INET, SOCK_DGRAM, 0) </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''. 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. ~~Zadanie.#~~ Odpowiedz na pytanie dlaczego DNS używa UDP zamiast TCP do zapytań. ===== UDP ===== Program korzystający z UDP musi stworzyć funkcją ''socket'' gniazdu typu ''SOCK_DGRAM''. Następnie program może (a jeśli ma pełnić rolę serwera, to powinien) wywołać funkcję ''bind'', ustalając lokalny numer portu. Gniazdo UDP w każdym momencie po stworzeniu może wykonywać operacje odbioru i wysyłania danych – ''sendto'', ''recvfrom'', ''recv'', ''read''. UDP jest bezpołączeniowe, więc nie da się na nim wykonać funkcji ''shutdown''. ==== Klient UDP ==== ~~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 ==== ~~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> ~~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.1728325520.txt.gz
· ostatnio zmienione: 2024/10/07 20:25 przez
jkonczak
Narzędzia strony
Pokaż stronę
Poprzednie wersje
Odnośniki
Złóż / rozłóż wszystko
Do góry