Wprowadzenie do gniazd BSD ========================== .. todo:: 3 opisać wprowadzenie do gniazd BSD Wstęp ----- Począwszy od wersji 4.1 systemu UNIX BSD, wprowadzono do niego obsługę protokołów TCP/IP wraz z interfejsem dostępu dla programów o nazwie **gniazda** (ang. *sockets*). Interfejs ten jest także dostępny w systemach Linux, a jego zadaniem jest pośredniczenie pomiędzy programami użytkowników, a implementacją stosu TCP/IP. Do dyspozycji programistów oddano zestaw funkcji bibliotecznych, które służą do obsługi komunikacji pomiędzy procesami działającymi na różnych węzłach w sieci (możliwa jest także komunikacji pomiędzy procesami działającymi lokalnie), które składają się na interfejs sieci. Funkcje te ukrywają przed programistą warstwę transportową oraz wymagają utworzenia specjalnego punktu końcowego kanału komunikacji, które nazywany jest *gniazdem*. W procesie systemowym gniazdo jest traktowane jak plik specjalny, po jego utworzeniu programista otrzymuje deskryptor, co pozwala na realizację funkcji odczytu i zapisu, w sposób analogiczny jak na pliku (np. :func:`read()` i :func:`write()`). Model oraz trybu komunikacji ---------------------------- Nawiązanie połączenia i rozpoczęcie komunikacji wymaga aby jeden z uczestniczących procesów oczekiwał na zgłoszenia; wówczas inny proces może nawiązać z nim komunikację, w dowolnym, obsługi przez siebie momencie. Proces, który oczekuje na połączenie nazywany jest **serwerem** (ang. *server*). Serwer może działać według dwóch schematów: 1. po odebraniu połączenia od innego procesu następuje przetwarzanie przyjętego zgłoszenia oraz ewentualna odpowiedź, po tym serwer przechodzi w stan dalszego oczekiwania na nowe zgłoszenia, schemat taki nazywany jest **iteracyjnym**, 2. odbieranie połączenia od klientów obsługiwane są przez serwer jednocześnie, np. poprzez wykorzystanie procesów potomnych lub wątków, główny proces serwera w tym czasie może dalej nasłuchiwać nowych połączeń; model ten nazywany jest **współbieżnym**. Proces, który nawiązuje połączenie z serwerem nazywany jest **klientem**. Proces ten musi posiadać informacje o adresie IP serwera oraz porcie TCP lub UDP, na którym serwer nasłuchuje. Proces serwera może uzyskać informacje o kliencie, dopiero po nawiązaniu przez tego połączenia. Interfejs gniazd obsługuje dwa tryby komunikacji: połączeniowy, za pomocą protokołu transportowego TCP (:rfc:`793`), oraz bezpołączeniowy, z wykorzystaniem protokołu transportowego UDP (:rfc:`768`). .. todo:: dodac tutaj mozna odwołania do inncyh stron jak już powstaną. Interfejs gniazd ---------------- Korzystanie z mechanizmu komunikacji TCP/IP wymaga utworzenia obiektu zwanego gniazdem, które traktowane jest przez system jako plik specjalny. Gniazdo tworzone jest przy pomocy funkcji bibliotecznej :func:`socket()`, która w wyniku zwraca deskryptor utworzonego gniazda. Oba komunikujące się procesy muszą utworzyć gniazda, jedno gniazdo może służyć zarówno do odbioru jak i do wysyłania danych. Przez użyciem gniazda należy określić adres oraz port, a także wybrać tryb komunikacji. .. note:: Na jednym komputerze można utworzyć dwa gniazda z tym samym numerem portu, jednak muszą mieć różne tryby komunikacji. Główne funkcje interfejsu gniazd -------------------------------- Aby wykorzystać omawiane poniżej funkcje, należy skorzystać z następujących plików nagłówkowych: :file:``, :file:``, a dodatkowo aby korzystać z pozostałych funkcji, należy użyć: :file:``, :file:``, :file:``. .. function:: int socket(int domain, int type, int protocol) Funkcja tworzy nowe gniazdo do komunikacji sieciowej, w wyniku zwraca deskryptor gniazda lub wartość `-1` w przypadku błędu, strona man: :manpage:`socket(7)` * :samp:`int domain` określa domenę komunikacyjną i służy do wyboru rodziny protokołów: `AF_UNIX` i `AF_LOCAL` dla komunikacji wewnątrzsystemowej, `AF_INET` oraz `AF_INET6` dla komunikacji zdalnej protokołem IPv4 oraz IPv6. * :samp:`int type` określa tryb komunikacji: `SOCK_STREAM` dla gniazd TCP, `SOCK_DGRAM` dla UDP oraz `SOCK_RAW` dla gniazd surowych. * :samp:`int protocol` określa protokół, najcześciej parametr ten przyjmuje wartość 0 .. function:: int bind(int sockfd, struct sockaddr \*addr, socklen_t addrlen) Funckja pozwala na związanie wcześniej utworzonego gniazda z adresem lokalnej maszyny i jest używana głównie przez serwery i klientów UDP. Poprawnie wykonana funkcja zwraca `0`, błąd oznaczany jest wartością `-1`. * :samp:`int sockfd` to deskryptor gniazda utworzony przez funckję :func:`socket()` * :samp:`struct sockaddr \* addr` struktura adresu opisana poniżej * :samp:`socklen_t addrlen` rozmiar drugiego parametru .. function:: int listen(int sockfd, int backlog) Funkcja pozwala na przygotowanie gniazda do obierania zgłoszeń i umozliwia określenie rozmiaru kolejki żądań, oczekujących na wykonanie funkcji :func:`accept()`. Wynikiem poprawnie wykonanej funkcji jest wartość `0`, błąd oznaczany jest przez wartość `-1`. * :samp:`int sockfd` to deskryptor gniazda utworzonego przez funckję :func:`socket()` * :samp:`int backlog` to rozmiar kolejki dla nadchodzących żądań .. function:: int accept(int sockfd, struct sockaddr \*addr, socklen_t \* addrlen) Funkcja oczekuje na zgłoszenie lub pobiera takowe z kolejki dla gniazd strumieniowych. W wyniku otrzymujemy nowy deskryptor gniazda, w przypadku błędu `-1` * :samp:`int sockfd` to deskryptor gniazda utworzonego przez funckję :func:`socket()` * :samp:`struct sockaddr \*addr` wskaźnik na strukturę, która zostanie wypełniona danymi połączenia, adresem i numerem portu klienta * :samp:`socklen_t \*addrlen` wskaźnik na rozmiar struktury adresowej .. function:: int connect(int sockfd, const struct sockaddr \*srvaddr, socklen_t addrlen) Funkcja użwana przez klientów, wiąże stworzone gniazdo z adresem serwera. Jeśli gniazdo zostało uwtworzone w trybie połączeniowym, funkcja nawiązuje połączenie z serwerem, jeśli w trybie bezpołączeniowym, funkcja jedynie przypisuje gniazdu adres i numer portu zdalnego serwera. * :samp:`int sockfd` to deskryptor gniazda utworzonego przez funckję :func:`socket()` * :samp:`const struct sockaddr \*srvaddr` adres zdalnej maszyny * :samp:`socklen_t addrlen` rozmiar struktury drugiego patrametru .. function:: int close(int fd) Funkcja zamyka gniazdo i usuwa jego deskryptor, jako parametr podajemy deskryptor zamyknaego gniazda. Do odbierania i wysyłania danych należy używać "tradycyjnych" funkcji :func:`read()` oraz :func:`write()`. W przypadku komunikacji bezpołączeniowej należy używać funkcji :func:`recv()` oraz :func:`send()`. Istnieją także ich odpowiedniki, w których jawnie należy podać adres adresata lub wysyłającego: :func:`sendto()` i :func:`recvfrom()`. Poniżej przedstawiono struktury używane przez funkcję interfejsu gniazd. .. code-block:: c struct sockaddr_in { u_short sin_family; // AF_INET u_short sin_port; // numer portu struct in_addr sin_addr; //adres węzła char sin_zero[8] } struct in_addr { u_long s_addr; // 32 bitowy adres } .. note:: W domenie protokołów internetowych, zamiast :samp:`sockaddr` używamy :samp:`sockaddr_in`, wykonując rzutowanie :samp:`(struct sockaddr\*)` Poniższy rysunek przedstawia kolejność wywołań kolejnych funkcji interfejsu gniazd dla serwera i klienta TCP: .. _Rys. 1: .. figure:: img/socket_control_flow.png :align: center Rys.1. Przepływ sterowania dla serwera i klienta TCP. .. figure:: img/udp_flow.png :align: center Rys.2. Przepływ sterowania dla serwera i klienta UDP. W przypadku klienta i serwera UDP należy wywołać metodę bind. Funkcje pomocnicze ------------------ Dla protokołów TCP/IP przyjęto standard określający *sieciowy porządek bajtów* [Wiki20131]_, zgodnie z którym kolejność bajtów reprezentujących liczbę liczony jest od najmniej znaczącego bajtu (czyli od "prawej" strony). Niektóre parametry funkcji interfejsu gniazd należy podawać zgodnie z tym właśnie porządkiem, np. numer portu w strukturze :samp:`sockaddr_in`. Dostępne są zatem funkcje, które umożliwiają konwersję z porządku systemu operacyjnego do porządku sieciowego i odwrotnie. Istnieją dwa zestawy tych funkcji, odpowiednio dla 16 oraz 32 bitowych liczb. .. function:: uint32_t htonl(uint32_t hostlong) Konwersja z reprezentacji lokalnek do sieciowej dla liczb 16 bitowych .. function:: uint16_t htons(uint16_t hostshort) Konwersja z reprezentacji lokalnek do sieciowej dla liczb 32 bitowych .. function:: uint32_t ntohl(uint32_t netlong) Konwersja z reprezentacji sieciowej do lokalnej dla liczb 32 bitowych .. function:: uint16_t ntohs(uint16_t netshort) Konwersja z reprezentacji lokalnej do sieciowej dla liczb 16 bitowych .. function:: inet_addr(const char \*cp) Zamiana adresu IP zapisanego w formacie dziesiętnym (XXX.YYY.ZZZ.VVV) na jego reprezentację sieciową Pozostałe funkcje katalogowe i odwzorowujące: .. function:: struct hostent \*gethostbyaddr(const char \*addr, int len, int type) Zamiana adresu IP na nazwę domenową .. function:: struct hostent \*gethostbyname(const char \*name) Zamiana nazwy domenowej na adres IP .. function:: int getpeername(int sockfd, struct sockaddr \*name, socklen_t \*namelen) Zwraca adres zdalnego komputera, z którym połączone jest gniazdo. .. function:: struct protent \*getprotobyname(const char \*name) Pobiera informacje o protokole z :file:`/etc/protocols` na podstawie nazwy i zwracastrukturę :samp:`protent`, zawierającą między innymi numer protokołu (:manpage:`getprotobyname(3)`). .. function:: struct servent \*getservbyname(const char \*name) Pobiera informacje o usłudze z :file:`/etc/services` na podstawie nazwy i zwraca strukturę :samp:`servent`, zawierającą numer między innymi numer portu (:manpage:`getservbyname(3)`) .. note:: Dokładny opis wszystkich funkcji można znaleźć w manualu !! Interfejs gniazd w systemach Windows ------------------------------------ Interfejs gniazd dostępny jest także dla systemów Windows - nosi on nazwę *Windows Sockets* lub *WinSock*. Interfejs ten udostępnia dwie grupy poleceń: pierwsza obejmuje funkcje niemal całkowicie zgodne z interfejsem BSD, druga grupa natomiast jest specyficzna sla systemów Windows i charakteryzuje się przedrostkiem :samp:`WSA_`. Definicje funkcji ujęte są w pliku nagłówkowym :file:`winsock2.h` lub w starszych systemach w :file:`winsock.h`. Podstawowe różnice to: * przed rozpoczęciem pracy należy zainicjować bibliotekę *WinSock* funkcją :func:`WSAStartup`, funkcja ta przyjmuje dwa argumenty wywołania: minimalną wymaganą wersję biblioteki oraz strukturę :samp:`WSADATA`, której postać jest następująca: .. code-block:: c++ typedef struct WSADATA { WCRD wVersion; //oczekiwany nr wersji WCRD wHighVersion; //najwyższy akceptowany wersji char szDescription[WSADESCRIPTION_LEN+1] //opis implementacji char szSystemStatus[WSASYS_STATUS_LEN+1] //informacje konfiguracyjne unsigned short iMaxSockets; unsigned short iMaxUdpDg; char FAR* lpVendorInfo; } WSADATA, *LPWSADATA * zakończenie pracy z biblioteką wymaga wywołania bezargumentowej funkcji :func:`WSACleanup` * część funkcji w wyniku przekazuje wartości innych typów, np. :func:`socket()` i :func:`accept` przekazują w wyniku wartości typu :samp:`SOCKET` * zamknięcie gniazda realizowane jest przez funkcję :func:`closesocket` * odczyt i zapis z gniazda realizowany jest funkcjami :samp:`recv()/recvfrom()` oraz :samp:`send()/sendto()` Więcej: [WinSock]_ Przykłady --------- .. _client_example: Klient usługi daytime ````````````````````` .. literalinclude:: src/daytime_tcp.c :language: c .. _funkcje_katalogowe: Przykład użycia funkcji katalogowych ```````````````````````````````````` .. code-block:: c // deklaracje struct sockaddr_in sck_adr; struct hostent *host_ptr; struct protoent *protocol_ptr; struct servent *service_ptr; host_ptr = gethostbyname (ADRES_IP); protocol_ptr = getprotobyname("tcp"); service_ptr = getservbyname("daytime", "tcp"); // wypełnienie struktury memset(&sck_addr, 0, sizeof sck_addr); memcpy ((char*) &sck_addr.sin_addr, (char*) host_ptr -> h_addr, host_ptr -> h_length); sck_addr.sin_port = service_ptr -> s_port; // tworzymy gniazdo sock = socket (PF_INET, SOCK_STREAM, protocol_ptr->p_proto) .. _udp_skeleton: Klient UDP - szkielet ````````````````````` .. code-block:: c ... struct sockaddr_in myaddr, serv_addr; // struktura adresu własnego memset(&myaddr, 0, sizeof(struct sockaddr)); myaddr.sin_family = AF_INET; myaddr.sin_addr.s_addr = INADDR_ANY; // jakikolwiek adres myaddr.sin_port = 0; // -> jakikolwiek port wybrany przez system // struktura adresu serwera memset(&serv_addr, 0, sizeof(struct sockaddr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = inet_addr("1.2.3.4"); serv_addr.sin_port = htons(atoi("23000")); sock = socket (PF_INET, SOCK_DGRAM, IPPROTO_UDP); socklen_t serv_addr_size = sizeof(struct sockaddr); bind(sock, (struct sockaddr*)&myaddr, sizeof(struct sockaddr)); sendto(sock, buf, strlen(buf+1), 0, (struct sockaddr*)&serv_addr, &serv_addr_size); recvfrom(sock, buf, strlen(buf+1), 0, (struct sockaddr*)&serv_addr, &serv_addr_size); close(sock); ... Serwer UDP - szkielet ````````````````````` .. code-block:: c ... struct sockaddr_in myaddr, serv_addr; int nFoo = 1 // struktura adresu serwera memset(&myaddr, 0, sizeof(struct sockaddr)); myaddr.sin_family = AF_INET; myaddr.sin_addr.s_addr = INADDR_ANY; myaddr.sin_port = htons(atoi("PORT SERVERA")); sock = socket (PF_INET, SOCK_DGRAM, IPPROTO_UDP); setsockopt(nSocket, SOL_SOCKET, SO_REUSEADDR, (char*)&nFoo, sizeof(nFoo)); bind(sock, (struct sockaddr*)&myaddr, sizeof(struct sockaddr)); while(1) { ... recvfrom( sendto ... } close(sock); ... Serwer daytime - TCP ```````````````````` .. literalinclude:: src/server.c :language: c .. _threads: Wątki ````` Kompilacja z biblioteką :samp:`pthread` z przełącznikiem :command:`-lpthread`. .. code-block:: c #include ... sock = socket(...); bind(sock, ... ); listen(..); while(is_running) { tmp_sck = accept(...) pthread_t id; pthread_create(&id, NULL, client_loop, sck); } ... .. code-block:: c void* client_loop(void *arg) { int rcvd; char buffer[1204]; int sck = *((int*) arg); while(rcvd = recv(sck, buffer, 1024, 0) { send(sck, buffer, rcvd, 0); close(sck); pthread_exit(NULL); } Blokowanie w celu dostępu równoczesnego za pomocą zamka (:manpage:`pthread_mutex_lock(3p)`). .. code-block:: c pthread_mutex_lock(&sock_mutex) .. operacje na współdzielonej strukturze np. client_threds pthread_mutex_unlock(&sock_mutex) .. _winsock: Przykład klienta z WinSock `````````````````````````` .. code-block:: c #include #include #include ... WORD WRequiredVersion; WSADATA WData; SOCKET SSocket; WRequiredVersion = MAKEWORD(2, 0); WSAStartup(WRequiredVersion, &WData) SSocket = socket(..); connect(..); read(..); closesocket(SSocket); WSACleanup(); Zadania ------- 1. Używając programu :command:`telnet` lub netcat (komenda :command:`nc`) przetestuj serwery usług echo oraz daytime uruchomionych na wskazanym przez prowadzącego adresie oraz porcie. 2. Uruchom program nc w trybie nasłuchiwania na dowolnym porcie. Poproś koleżankę lub kolegę o podłączenie się do ciebie. Wykonaj to także dla protokołu UDP. 3. Skompiluj klienta (z :ref:`client_example`) i zmień tak, aby połączyć się do usługi daytime (:download:`src/daytime_tcp.c`). 4. Przerób klienta aby używał funkcji katalogowych, skorzystać z pomocy z :ref:`funkcje_katalogowe`. 5. Przerób klienta daytime aby używał protokołu UDP, skorzystaj z pomocy z :ref:`udp_skeleton`. 6. Napisz iteracyjny serwer usługi echo lub daytime, sprawdź jak zachowa się przy 2 i więcej połączeniach jednoczesnych. 7. Przerób serwer tak aby obsługiwał równoczesne połączenia za pomocą wątków, zobacz przykład :ref:`threads` oraz przykładowy program :download:`src/thread_example.c`, race condition :download:`src/race_example.c`, :download:`src/mutex_example.c`. 8. Napisz prostego klienta z pomocą biblioteki WinSock, zobacz przykład :ref:`winsock`. 9. Napisz klienta w bibliotece Qt. Literatura ---------- .. [Wiki20131] http://en.wikipedia.org/wiki/Endianness#Endianness_in_networking .. [Tanen] A.S. Tannenbaum: Computer Networks, Prentice Hall PTR, 2003. .. [Steve] W. R. Stevens: UNIX programowanie usług sieciowych, WNT, 2002. .. [WinSock] http://www.sockets.com/winsock2.htm