Wykład 4 p. 1/44 Programowanie aplikacji równoległych i rozproszonych Wykład 4 Dr inż. Tomasz Olas olas@icis.pcz.pl Instytut Informatyki Teoretycznej i Stosowanej Politechnika Częstochowska
Gniazda - Wstęp Podstawa mechanizmu wejścia-wyjścia dla sieci w systemie Unix jest abstrakcyjny mechanizm znany jako gniazda (ang. socket). Gniazdo należy traktować jako jako uogólnienie mechanizmu dostępu do plików udostępniajace jeden z końców łacza komunikacyjnego. Każdy z komunikujacych się ze soba procesów tworzy po swojej stronie jedno gniazdo. Główna różnica pomiędzy deskryptorami plików i gniazdami polega na tym, że system operacyjny wiaże deskryptor z określonym plikiem lub urzadzeniem, natomiast gniazda można tworzyć bez wiazania ich z określonym adresem odbiorcy. Gniazdo jest opisywane za pomoca kombinacji trzech atrybutów: domeny adresowej, sposobu komunikacji i protokołu sieciowego. Wykład 4 p. 2/44
Wykład 4 p. 3/44 Domena adresowa Domena adresowa określa domenę w której będzie realizowana komunikacja: PF_LOCAL (PF_UNIX) - komunikacja w obrębie jednej maszyny, PF_INET - Internet, czyli używamy protokołów z rodziny TCP/IP, PF_IPX - protokoły IPX/SPX (Novell), PF_PACKET - niskopoziomowy interfejs do odbierania pakietów w tzw. surowej (ang. raw) postaci. Zamiennie używa się notacji PF_xxx (PF - Protocol Family) oraz AF_xxx (AF - Address Family).
Wykład 4 p. 4/44 Sposób komunikacji Gniazda typu SOCK_STREAM - komunikacja w modelu połaczeniowym (zwykle zwiazane z protokołem TCP warstwy transportowej) Gniazda typu SOCK_DGRAM - komunikacja w modelu bezpołaczeniowym korzystajaca z tzw. datagramów (zwykle zwiazane z protokołem UDP warstwy transportowej) Gniazda typu SOCK_RAW - komunikacja na niskim poziomie - dostęp do surowych pakietów na poziomie warstwy sieciowej TCP/IP.
Wykład 4 p. 5/44 Funkcja socketpair Do tworzenia gniazdek w domenie unixowej może zostać wykorzystana funkcja socketpair: #include <sys/types.h> #include <sys/socket.h> int socketpair(int family, int type, int protocol, int sockvec[2]); Funkcja zwraca deskryptor gniazda. family - oznacza domenę komunikacyjną (czyli rodzinę protokołów adresów) w jakiej będzie funkcjonować nowe gniazdo. W systemie Linux jedyna obsługiwana domena jest PF_UNIX. type - określa rodzaj gniazda, protocol - protokół z jakiego gniazdo korzysta, sockver - gdy nie wystapi bład zostana do tej zmiennej zapisana para deskryptorów gniazd w domenie Unixa. Otrzymuje się w ten sposób łacze strumieniowe z możliwościa dwustronnej komunikacji.
Wykład 4 p. 6/44 Zamknięcie gniazda Funkcję close służy do zamykania gniazda i kończenia połaczenia: int close(int fd); fd - deskryptor gniazda, wynik: 0 - w przypadku sukcesu, -1 - jeśli wystapił bład. Przy wywoływaniu funkcji close zmniejszany jest licznik odwołań do deskryptora. Gdy proces się kończy, system operacyjny zamyka wszystkie gniazda, które były wówczas otwarte.
Wykład 4 p. 7/44 Przesyłanie danych Przesyłanie danych poprzez gniazda może odbywać się z wykorzystaniem pięciu różnych funkcji systemowych: send/write, sendto/recvfrom, sendmsg/recvmsg, write/read, writev/readv. Procedury send/recv, sendto/redvfrom oraz sendmsg/recvmsg wymagaja aby gniazdo było przyłaczone, gdyż nie umożliwiaja podania adresu odbiorcy. read/write readv/writev + send/recv + wiele dodatkowe adres informacje buforów znaczniki partnera kontrolne sendto/recvfrom + + sendmsg/recvmsg + + + +
Wykład 4 p. 8/44 read/write ssize_t read(int fd, void *buf, size_t count) ssize_t write(int fd, const void *buf, size_t count) fd - deskryptor, buff - adres danych do odczytania/zapisania, length - rozmiar danych do odczytania/zapisania. wynik: liczba odczytanych/zapisanych bajtów - w przypadku sukcesu, -1 - w przypadku błędu.
Wykład 4 p. 9/44 socketpair - przykład (1) #include <sys/types.h> #include <sys/socket.h> #include <unistd.h> #include <iostream> const int SIZE = 40; int main() { int sockets[2]; socketpair(pf_unix, SOCK_STREAM, 0, sockets); int pid = fork(); if (pid == 0) { // proces potomny close(sockets[1]); char buf[size]; sprintf(buf, "proces potomny: %d", getpid()); write(sockets[0], buf, sizeof(buf) + 1); read(sockets[0], buf, SIZE); close(sockets[0]); std::cout << "#1: " << getpid() << "\n#1: " << buf << std::endl; }
Wykład 4 p. 10/44 socketpair - przykład (2)... else { // proces macierzysty close(sockets[0]); char buf[size]; read(sockets[1], buf, SIZE); std::cout << "#0: " << getpid() << "\n#0: " << buf << std::endl; } sprintf(buf, "proces macierzysty: %d", getpid()); write(sockets[1], buf, sizeof(buf) + 1); close(sockets[1]); } return 0;
Wykład 4 p. 11/44 socketpair - przykład (3) Przykładowy rezultat działania programu: #0: 1458 #0: proces potomny: 1459 #1: 1459 #1: proces macierzysty: 1458
Komunikacja bezpołaczeniowa Wykład 4 p. 12/44
Wykład 4 p. 13/44 Tworzenie gniazd Użytkownik identyfikuje gniazda za pomoca deskryptorów (wykorzystywanych przy każdorazowym odwoływaniu się do gniazda) Funkcja socket tworzy nowe gniazdo i zwraca jego deskryptor: int socket (int family, int type, int protocol); family - oznacza domenę komunikacyjną (czyli rodzinę protokołów adresów) w jakiej będzie funkcjonować nowe gniazdo, np: PF_INET - domena internetowa - sieć TCP/IP, PF_UNIX - domena uniksowa - komunikacja lokalna (system plików systemu Unix), PF_APPLETALK - sieć firmy Apple Computer Incorporated, type - określa rodzaj gniazda, protocol - protokół z jakiego gniazdo korzysta (w przypadku 0 będzie to domyślny protokół dla danego rodzaju gniazda).
Wykład 4 p. 14/44 Określanie adresu lokalnego Poczatkowo nowo utworzone gniazdo nie jest zwiazane z żadnym adresem lokalnym ani odległym. Po utworzeniu gniazda serwer wiaże z nim adres lokalny za pomoca funkcji systemowej bind: int bind(int fd, struct sockaddr *my_addr, int addrlen) fd - deskryptor gniazda, my_addr - wskaźnik na strukturę adresów odpowiednia dla rodziny protokołów, do której należy gniazdo, addrlen - rozmiar tej struktury.
Wykład 4 p. 15/44 Struktura sockaddr Dla domeny uniksowej: struct sockaddr_un { short sun_family; /* AF_UNIX */ char sun_data; /* ścieżka */ }; Dla domeny internetowej: struct sockaddr_in { short sin_family; /* AF_INET */ u_short sin_port; /* 16-bitowy numer portu */ struct in_addr sin_addr; /* 32-bitowy internetowy */ /* adres hosta */ char sin_zero[8]; /* zarezerwowane */ };
Funkcje send i sendto ssize_t send(int s, const void *buf, size_t len, int flags); ssize_t sendto(int s, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen); ssize_t recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen); s - deskryptor gniazda, przez które wysyłamy dane, buf - wskaźnik do bufora, który chcemy wysłać, len - liczba bajtów bufora do wysłania, flags - dodatkowe flagi (np. codemsg_dontwait - Przełacza funkcję w tryb nieblokujacy), to - wskaźnik do struktury zawierajacej adres odbiorcy, addrlen - rozmiar struktury zawierajacej adres. wynik: liczba odczytanych/zapisanych bajtów - w przypadku sukcesu, Wykład 4 p. 16/44
Wykład 4 p. 17/44 Funkcje recv i recvfrom ssize_t recv(int s, void *buf, size_t len, int flags); ssize_t recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen); s - deskryptor gniazda, przez które odbieramy dane, buf - adres (wskaźnik) bufora, który będzie zawierał dane po ich odebraniu, len - rozmiar bufora (maksymalna liczba bajtów, która można jednorazowo odebrać), flags - dodatkowe flagi, to - wwskaźnik do struktury, przez która zwrócony zostanie adres nadawcy, addrlen - rozmiar struktury zawierajacej adres.
} Wykład 4 p. 18/44 Przykład (I) const char name[] = "my_socket"; int main() { int orginal_socket = socket(af_unix, SOCK_DGRAM, 0); unlink(name); sockaddr_un server_address; server_address.sun_family = AF_UNIX; strcpy(server_address.sun_path, name); bind(orginal_socket, (sockaddr*)&server_address, sizeof(server_address.sun_family) + sizeof(server_address.sun_path)); char buf[255]; while (true) { socklen_t client_length; recvfrom(orginal_socket, buf, sizeof(buf), 0, (sockaddr*)&server_address, &client_length); std::cout << buf << std::endl; } close(orginal_socket); return 0;
Wykład 4 p. 19/44 Przykład (II) const char name[] = "my_socket"; int main(int argc, char** argv) { int orginal_socket = socket(af_unix, SOCK_DGRAM, 0); sockaddr_un server_address; server_address.sun_family = AF_UNIX; strcpy(server_address.sun_path, name); bind(orginal_socket, (sockaddr*)&server_address, sizeof(server_address.sun_family) + sizeof(server_address.sun_path)); } sendto(orginal_socket, argv[1], sizeof(argv[1]), 0, (sockaddr*)&server_address, sizeof(sockaddr)); close(orginal_socket); return 0;
Wykład 4 p. 20/44 Model klient serwer Termin serwer odnosi się do każdego programu, który oferuje usługę dostępna poprzez sieć. Serwer przyjmuje przez sieć zamówienia, wykonuje usługę i zwraca wyniki zamawiajacemu. Klientem staje się program, który wysyła zamówienie do serwera i czeka na odpowiedź. W przypadku modelu klient serwer każde połaczenie sieciowe jest tworzone przez klienta wysyłajacego żadania do stale oczekujacego na nie serwera. Gniazda używane przez procesy klienckie nazywane sa gniazdami aktywnymi (one inicjuja połaczenie), natomiast gnizda wykorzystywane w serwerach nazywane sa analogicznie gniazdami pasywnymi.
Komunikacja połaczeniowa Wykład 4 p. 21/44
Wykład 4 p. 22/44 Serwer - przyjmowanie połaczeń (I) Serwer używa funkcji socket, bind i listen do utworzenia gniazda. Wywołanie bind wiaże gniazdo z powszechnie znanym portem protokołu, ale nie powoduje podłaczenia gniazda do żadnego odległego odbiorcy. Zamiast odległego odbiorcy podaje się adres uogólniajacy (ang. wildcard), by gniazdo mogło przyjmować połaczenia od dowolnych klientów. Po utworzeniu gniazda serwer czeka na połaczenia. W tym celu wywołuje procedurę systemowa accept.
Wykład 4 p. 23/44 Serwer - przyjmowanie połaczeń (II) Gdy nadchodzi żadanie, system wypełnia strukturę addr adresem klienta, który je zgłosił. Następnie system tworzy nowe gniazdo, które jest połaczone z klientem i dostarcza wywołujacemu programowi deskryptor tego gniazda. Pierwotne gniazdo nadal nie jest połaczone z żadnym konkretnym odległym klientem i pozostaje w dalszym ciagu otwarte. Dzięki temu serwer nadal może czekać na żadania nadchodzace za pośrednictwem tego gniazda. Wywołanie accept kończy się po nadejściu żadania połaczenia. Serwer może obsługiwać nadchodzace żadania po kolei, badź też współbieżnie.
Wykład 4 p. 24/44 listen Funkcja listen ustala maksymalna długość kolejki klientów oczekujacych na połaczenie. #include <sys/types.h> #include <sys/socket.h> int listen(int fd, int backlog); fd - deskryptor gniazda, backlog - maksymalna liczba zgłoszeń połaczenia z serwerem. wynik: 0 - w przypadku sukcesu, wartość ujemna - jeśli wystapił bład. W przypadku domeny uniksowej gniazdo musi być strumieniowe i mieć dowiazan a nazwę. W przypadku domeny internetowej, jeśli gniazdo nie ma nazwy, to funkcja listen automatycznie je dowiaże.
Wykład 4 p. 25/44 accept Funkcja accept służy do oczekiwania na połaczenie - proces jest blokowany aż do momentu nawiazania połaczenia przez klienta. W momencie nawiazania połaczenia funkcja accept zwraca nowy deskryptor gniazda, który następnie jest używany do komunikacji z klientem: #include <sys/types.h> #include <sys/socket.h> int accept(int fd, struct sockaddr *upeer_sockaddr, int *upeer_addrlen); fd - deskryptor gniazda, upeer_sockaddr - wskaźnik na nazwę gniazda klienta (jako dodatkowy wynik) upeer_addrlen - rozmiar nazwy upeer_sockaddr (jako dodatkowy wynik) wynik: 0 - w przypadku sukcesu, -1 - jeśli wystapił bład.
Wykład 4 p. 26/44 Serwer - przykład (I) #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> #include <iostream> const char name[] = "my_socket"; int main() { int orginal_socket = socket(af_unix, SOCK_STREAM, 0); sockaddr_un server_address; server_address.sun_family = AF_UNIX; strcpy(server_address.sun_path, name); unlink(name); bind(orginal_socket, (sockaddr*)&server_address, sizeof(server_address.sun_family) + sizeof(server_address.sun_path));
Wykład 4 p. 27/44 Server - przykład (II) listen(orginal_socket, 1); sockaddr_un client_address; socklen_t client_length = sizeof(client_address); int new_socket = accept(orginal_socket, (sockaddr*)&client_address, &client_length); char buf[255]; read(new_socket, buf, sizeof(buf)); std::cout << buf << std::endl; close(new_socket); close(orginal_socket); unlink(name); } return 0;
Wykład 4 p. 28/44 Łaczenie gniazda z adresem odbiorcy Bezpośrednio po utworzeniu gniazda znajduje się ono w stanie niepołaczonym, co oznacza, że nie jest ono zwiazane z żadnym adresatem. Procedura systemowa connect trwale łaczy gniazdo z adresem odbiorcy, zmieniajac jednocześnie stan gniazda na połaczony. W przypadku komunikacji w trybie połaczeniowym klient przed rozpoczęciem przesyłania danych za pośrednictwem gniazda musi wywołać procedurę connect w celu uzyskania połaczenia. Gniazda używane w trybie bezpołaczeniowym nie wymagaja podłaczania przed użyciem, ale dzięki podłaczeniu można wysyłać dane do gniazda, nie określajac za każdym razem adresu odbiorcy.
Wykład 4 p. 29/44 connect Funkcja connect: int connect(int fd, struct sockaddr *uservaddr, int addrlen); fd - deskryptor gniazda, uservaddr - struktura określajaca adres, z którym należy zwiazać wskazane gniazdo, addrlen - rozmiar struktury uservaddr. wynik: 0 - w przypadku sukcesu, wartość ujemna - w przypadku błędu, np: EBADF - fd jest nieprawidłowym deskryptorem, ENOTSOCK - fd nie jest deskryptorem gniazda, EISCONN - jest już zrealizowane polaczenie.
} Wykład 4 p. 30/44 Klient - przykład #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #include <unistd.h> #include <iostream> const char name[] = "my_socket"; int main() { int orginal_socket = socket(af_unix, SOCK_STREAM, 0); sockaddr_un server_address; server_address.sun_family = AF_UNIX; strcpy(server_address.sun_path, name); connect(orginal_socket, (sockaddr*)&server_address, sizeof(server_address.sun_family) + sizeof(server_address.sun_path)); char buf[] = "hello"; write(orginal_socket, buf, sizeof(buf)); close(orginal_socket); return 0;
Wykład 4 p. 31/44 Adres sieciowy Każdy host w sieci ma przynajmniej dwa tylko właściwe sobie adresy: 48-bitowy adres ethernetowy (przypisywany ethernetowej karcie sieciowej przez producenta), 32-bitowy adres internetowy (tzw. numer IP). W interakcji z użytkownikiem jest on podzielony na cztery 8-bitowe liczby dziesiętne separowane kropkami. Każda z tych liczb może się zawierać w przedziale od 0 do 255, choć wartości brzegowe sa używane jako wartości specjalnego przeznaczenia.
Wykład 4 p. 32/44 Numer portu Numer portu jest liczba 16-bitowa bez znaku. Jednoznacznie identyfikuje połaczenie sieciowe w ramach jednego adresu IP. Para (adres IP, numer portu) jednoznacznie określa komputer w sieci, jak również konkretny proces działajacy na tym komputerze.
Wykład 4 p. 33/44 Funkcja gethostbyname (I) W przypadku domeny internetowej procesy musza dysponować swoimi adresami hostów i numerami portów, aby mogły ze soba nawiazać kontakt. Funkcja gethostbyname zwraca pełne informacje o hoście, którego nazwę podano w argumentach: #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> hostent* gethostbyname(const char* name); name - wskaźnik na napis zawierajacy nazwę hosta. Jeśli nazwa hosta zostanie znaleziona funkcja zwróci wskaźnik na strukturę hostent, w przeciwnym wypadku zwróci NULL.
Wykład 4 p. 34/44 Funkcja gethostbyname (II) struct hostent { char *h_name; char **h_aliases; int h_addrtype; int h_length; char **h_addr_list; #define h_addr h_addr_list[0] }; h_name - oficjalna nazwa hosta, h_aliases - lista aliasów, h_addrtype - typ adresu hosta, h_length - długość adresu, h_addr_list - lista adresów z serwera nazw.
Wykład 4 p. 35/44 Zmiana kolejności bajtów Poszczególne typy komputerów różnia się sposobem przechowywania liczb, a protokoły TCP/IP określaja niezależny od maszyny standard porzadku bajtów liczb. System Unix BSD oferuje cztery procedury biblioteczne służace do konwersji między porzadkiem bajtów na danym komputerze oraz sieciowym porzadkiem bajtów (<netinet/in.h>): Do zamiany liczby z reprezentacji sieciowej do reprezentacji lokalnej (network to host) służa funkcje: uint16_t ntohs(uint16_t netshort); uint32_t ntohl(uint32_t netlong); Do konwersji liczb z porzadku danego komputera na porzadek sieciowy (host to network) służa funkcje: uint16_t htons(uint16_t hostshort); uint32_t htonl(uint32_t hostlong);
Wykład 4 p. 36/44 gethostbyname - przykład (I) hostent *host; std::string hostname; std::cout << "Podaj nazwe hosta: " << std::flush; std::getline(std::cin, hostname); host = gethostbyname(hostname.c_str()); if (host!= NULL) { std::cout << "Oficialna nazwa: " << host->h_name << std::endl << "Aliasy: " << std::endl; while (*host->h_aliases) { std::cout << " " << *host->h_aliases << std::endl; ++host->h_aliases; } std::cout << "Typ adresu: " << host->h_addrtype << std::endl << "Dlugosc adresu: " << host->h_length << std::endl << "Lista adresów: " << std::endl;
Wykład 4 p. 37/44 gethostbyname - przykład (II) } } while (*host->h_addr_list) { in_addr in; memcpy(&in.s_addr, *host->h_addr_list, sizeof(in.s_addr)); std::cout << "[" << host->h_addr_list << "] = " << inet_ntoa(in) << std::endl; ++host->h_addr_list; } Przykładowy wynik działania programu: Podaj nazwe hosta: icis Oficialna nazwa: icis.pcz.pl Aliasy: Typ adresu: 2 Dlugosc adresu: 4 Lista adresów: [0x6031d0] = 212.87.224.6
Domena internetowa - serwer (I) #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <iostream> #include "local.h" int main() { int orginal_socket = socket(af_inet, SOCK_STREAM, 0); if (orginal_socket < 0) { perror("blad generowania"); exit(1); } sockaddr_in server_address; memset(&server_address, 0, sizeof(sockaddr_in)); server_address.sin_family = AF_INET; server_address.sin_addr.s_addr = htonl(inaddr_any); server_address.sin_port = htons(port); Wykład 4 p. 38/44
Wykład 4 p. 39/44 Domena internetowa - serwer (II) if (bind(orginal_socket, (sockaddr*)&server_address, sizeof(server_address)) < 0) { perror("blad dowiazania"); exit(2); } if (listen(orginal_socket, 5) < 0) { perror("blad nasluchu"); exit(3); } do { sockaddr_in client_address; socklen_t client_len = sizeof(client_address); int new_socket = accept(orginal_socket, (sockaddr*)&client_address, &client_len); if (new_socket < 0) { perror("blad akceptowania"); exit(4); }
Wykład 4 p. 40/44 Domena internetowa - serwer (III) } if (fork() == 0) { int len; const int BUFSIZE = 255; char buf[bufsize]; while ((len = read(new_socket, buf, BUFSIZE)) > 0) { for (int i = 0; i < len; i++) buf[i] = toupper(buf[i]); write(new_socket, buf, len); if (buf[0] ==. ) break; } close(new_socket); exit(0); } else close(new_socket); } while (true);
Domena internetowa - klient (I) int main(int argc, char** argv) { hostent *host = gethostbyname(argv[1]); sockaddr_in server_address; memset(&server_address, 0, sizeof(server_address)); server_address.sin_family = AF_INET; memcpy(&server_address.sin_addr, host->h_addr, host->h_length); server_address.sin_port = htons(port); int orginal_socket = socket(af_inet, SOCK_STREAM, 0); if (orginal_socket < 0) { perror("blad generowania"); exit(3); } if (connect(orginal_socket, (sockaddr*)&server_address, sizeof(server_address)) < 0) { perror("blad polaczenia"); exit(4); } Wykład 4 p. 41/44
Wykład 4 p. 42/44 Domena internetowa - klient (II) char buf[255]; std::string line; do { std::cout << "> " << std::flush; getline(std::cin, line); } write(orginal_socket, line.c_str(), line.size()+1); read(orginal_socket, buf, 255); std::cout << buf << std::endl; } while (line[0]!=. ); close(orginal_socket); exit(0);
Wykład 4 p. 43/44 Lokalne i odległe adresy gniazd Do ustalenia adresu odbiorcy, z którym jest połaczone dane gniazdo służy funkcja: #include <sys/socket.h> int getpeername(int s, struct sockaddr *name, socklen_t *namelen); Procedura getsockname dostarcza lokalnego adresu zwiazanego z danym gniazdem: #include <sys/socket.h> int getsockname(int s, struct sockaddr *name, socklen_t *namelen);
Wykład 4 p. 44/44 Obsługa wielu klientów Funkcje accept, send, recv, sendto, recfrom sa blokujace. Aby serwer mógł obsługiwać wielu klientów należy: utworzyć dla każdego klienta oddzielny proces, obsługiwać poszczególnych klientów przy użyciu dodatkowych watków, wykorzystać funkcję select.