Winsock Sieci Komputerowe II Wyk ład 3
Plan Przygotowanie środowiska Inicjacja Winsock Aplikacja klienta: tworzenie socketu łączenie z socketem serwera wysyłanie i odbieranie danych rozłączanie klienta Aplikacja serwera: tworzenie socketu wiązanie socketu nasłuchiwanie akceptowanie połączenia odbieranie i wysyłanie danych rozłączanie serwera
Przygotowanie środowiska Wszelkie prezentowane uwagi dotyczą środowiska MS Visual Studio W środowisku należy ustawić dostęp do biblioteki WS2_32.lib Na początku programu należy dołączyć dwa pliki nagłówkowe: winsock2.h zawierający większość funkcji, struktur i definicji do obsługi socketów ws2tcpip.h zawierający pozostałe funkcje i struktury nie ujęte w winsock2.h a zawarte w dokumencie WinSock 2 Protocol-Specific Annex dla protokołu TCP/IP Czasami może być konieczne użycie plików nagłówkowych iphlpapi.h (IP Helper API) i/lub windows.h o warunkach użycia tych plików należy doczytać w dokumentacji środowiska
Windows Inicjacja Winsock Przed przystąpieniem do korzystania z funkcji biblioteki Winsock należy zainicjować bibliotekę dynamiczną Windows Sockets DLL (WS2_32.dll) w następujący sposób: stworzyć obiekt typu WSADATA o nazwie wsadata WSADATA wsadata; wywołać funkcję WSAStartup i sprawdzić wynik wywołania pod względem błędów int iresult; // Initialize Winsock iresult = WSAStartup(MAKEWORD(2,2), &wsadata); if (iresult!= 0) { printf("wsastartup failed: %d\n", iresult); return 1; }
Tworzenie socketu dla klienta krok 1 Zadeklarować obiekt addrinfo zawierający strukturę sockaddr i zainicjować te wartości struct addrinfo *result = NULL, *ptr = NULL, hints; ZeroMemory( &hints, sizeof(hints) ); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; Nie jest określona wersja adresów IP zarówno adres IPv4, jak i IPv6 mogą być użyte (ai_family) Wymaganym przez aplikację typem socketu jest socket strumieniowy dla protokołu TCP (ai_socktype, ai_protocol)
Tworzenie socketu dla klienta krok 2 Wywołać funkcję getaddrinfo żądanie adresu IP dla nazwy serwera podanej w linii komend #define DEFAULT_PORT "27015" // Resolve the server address and port iresult = getaddrinfo(argv[1], DEFAULT_PORT, &hints, &result); if (iresult!= 0) { printf("getaddrinfo failed: %d\n", iresult); WSACleanup(); return 1; } Port TCP po stronie serwera, z jakim w tym przykładzie chce się połączyć klient, to port 27015 (stała DEFAULT_PORT) Funkcja getaddrinfo zwraca wartość, która jest sprawdzana pod kątem wystąpienia błędów. WSACleanup kończy użycie WS2_32.dll
Tworzenie socketu dla klienta krok 3 Utworzyć obiekt ConnectSocket typu SOCKET SOCKET ConnectSocket = INVALID_SOCKET;
Tworzenie socketu dla klienta krok 4 Wywołać funkcję socket i przypisać zwracaną przez nią wartość do zmiennej ConnectSocket // Attempt to connect to the first address returned by the call to getaddrinfo ptr=result; // Create a SOCKET for connecting to server ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol); Używany jest pierwszy adres IP zwrócony przez funkcję getaddrinfo, który odpowiada parametrom określonym w strukturze hints (typ socketu SOCK_STREAM, typ protokołu IPROTO_TCP, nieokreślona wersja adresów IP AF_UNSPEC) Jeśli klient ma się kontaktować z serwerem wyłącznie za pośrednictwem adresu IP w wersji 4 lub IP w wersji 6 to zamiast AF_UNSPEC należy użyć odpowiednio AF_INET lub AF_INET6
Łączenie z socketem serwera (1) Sprawdzenie czy nie wystąpiły błędy podczas tworzenia socketu // Connect to server. iresult = connect( ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen); if (iresult == SOCKET_ERROR) { closesocket(connectsocket); ConnectSocket = INVALID_SOCKET; } Użyta wcześniej funkcja getaddrinfo jest używana do określenia wartości w strukturze sockaddr() W przykładzie pierwszy adres IP zwrócony przez getaddrinfo służy do określenia zawartości struktury sockaddr, która następnie jest przekazywana do funkcji socket Informacje zawarte w strukturze sockaddr zawierają: adres IP serwera, z którym klient chce się połączyć numer portu serwera, z którym klient chce się połączyć (w tym przypadku port 27015 określony przy wywoływaniu funkcji getaddrinfo)
Łączenie z socketem serwera (2) Sprawdzenie czy nie wystąpiły błędy podczas tworzenia socketu // Should really try the next address returned by getaddrinfo if the connect call failed // But for this simple example we just free the resources // returned by getaddrinfo and print an error message freeaddrinfo(result); if (ConnectSocket == INVALID_SOCKET) { printf("unable to connect to server!\n"); WSACleanup(); return 1; } Jeśli funkcja connect nie wykona się poprawnie dla pierwszego z adresów zwróconych przez getaddrinfo, wtedy należy skorzystać z następnej struktury addrinfo z listy zwróconej przez getaddrinfo W tym przykładzie inne podejście po prostu zwolnienie zasobów i zakończenie programu komunikatem o błędzie
Wysyłanie i odbieranie danych (1) Po nawiązaniu połączenia z socketem serwera klient wysyła i odbiera dane za pomocą funkcji send i recv obydwie funkcje zwracają liczbę wysłanych (odebranych) bajtów lub kod błędu obydwie funkcje również korzystają z tych samych parametrów: aktywny socket, bufor znaków, liczba bajtów do wysłania lub odebrania, odpowiednie znaczniki #define DEFAULT_BUFLEN 512 int recvbuflen = DEFAULT_BUFLEN; char *sendbuf = "this is a test"; char recvbuf[default_buflen]; int iresult;
Wysyłanie i odbieranie danych (2) // Send an initial buffer iresult = send(connectsocket, sendbuf, (int) strlen(sendbuf), 0); if (iresult == SOCKET_ERROR) { printf("send failed: %d\n", WSAGetLastError()); closesocket(connectsocket); WSACleanup(); return 1; } printf("bytes Sent: %ld\n", iresult);
Wysyłanie i odbieranie danych (3) // shutdown the connection for sending since no more data will be sent // the client can still use the ConnectSocket for receiving data iresult = shutdown(connectsocket, SD_SEND); if (iresult == SOCKET_ERROR) { printf("shutdown failed: %d\n", WSAGetLastError()); closesocket(connectsocket); WSACleanup(); return 1; }
Wysyłanie i odbieranie danych (4) // Receive data until the server closes the connection do { iresult = recv(connectsocket, recvbuf, recvbuflen, 0); if (iresult > 0) printf("bytes received: %d\n", iresult); else if (iresult == 0) printf("connection closed\n"); else printf("recv failed: %d\n", WSAGetLastError()); } while (iresult > 0);
Rozłączanie klienta krok 1 Gdy klient zakończy wysyłanie danych, rozłącza się z serwerem // shutdown the send half of the connection since no more data will be sent iresult = shutdown(connectsocket, SD_SEND); if (iresult == SOCKET_ERROR) { printf("shutdown failed: %d\n", WSAGetLastError()); closesocket(connectsocket); WSACleanup(); return 1; } Funkcja shutdown z parametrem SD_SEND stosowana do zamknięcia strony wysyłającej Umożliwia to serwerowi zwolnienie wykorzystywanych w komunikacji zasobów, a klient może nadal otrzymywać dane przez ten socket
Rozłączanie klienta krok 2 Gdy klient zakończy odbieranie danych zamyka socket // cleanup closesocket(connectsocket); WSACleanup(); return 0; Funkcja closesocket stosowana do zamknięcia socketu Po zakończeniu korzystania przez aplikację klienta z biblioteki WS2_32.dll wywoływana jest funkcja WSACleanup zamykająca bibliotekę i zwalniająca używane zasoby.
Tworzenie socketu dla serwera krok 1 Zadeklarować obiekt addrinfo zawierający strukturę sockaddr #define DEFAULT_PORT "27015" struct addrinfo *result = NULL, *ptr = NULL, hints; ZeroMemory(&hints, sizeof (hints)); hints.ai_family = AF_INET; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; hints.ai_flags = AI_PASSIVE; AF_INET adres IPv4 STOCK_STREAM socket strumieniowy IPPROTO_TCP protokół TCP AI_PASSIVE znacznik oznaczający, że ma być użyta struktura adresu socketu zwrócona przez funkcję bind (parametr INADDR_ANY lub IN6ADDR_ANY_INIT) 27015 numer portu, do którego powinien się łączyć klient
Tworzenie socketu dla serwera krok 2 Wywołać funkcję getaddrinfo przy użyciu struktury addrinfo // Resolve the local address and port to be used by the server iresult = getaddrinfo(null, DEFAULT_PORT, &hints, &result); if (iresult!= 0) { printf("getaddrinfo failed: %d\n", iresult); WSACleanup(); return 1; } Funkcja getaddrinfo zwraca wartość, która jest sprawdzana pod kątem wystąpienia błędów. WSACleanup kończy użycie WS2_32.dll
Tworzenie socketu dla serwera krok 3 Utworzyć obiekt ListenSocket typu SOCKET SOCKET ListenSocket = INVALID_SOCKET;
Tworzenie socketu dla klienta krok 4 Wywołać funkcję socket i przypisać zwracaną przez nią wartość do zmiennej ListenSocket // Create a SOCKET for the server to listen for client connections ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol); Używany jest pierwszy adres IP zwrócony przez funkcję getaddrinfo, który odpowiada parametrom określonym w strukturze hints (typ socketu SOCK_STREAM, typ protokołu IPROTO_TCP, adres IPv4 AF_INET) Jeśli klient ma się kontaktować z serwerem wyłącznie za pośrednictwem adresu IP w wersji to zamiast AF_INET należy użyć AF_INET6, te dwa typy socketów muszą być osobno obsługiwane przez aplikację. Od wersji Vista istnieje możliwość stworzenia pojedynczego socketu IPv6, który może pracować w trybie dual-stack i nasłuchiwać jednocześnie na dwóch wersjach adresu IPv4 i IPv6
Tworzenie socketu dla klienta krok 5 Sprawdzenie, czy podczas tworzenia socketu nie wystąpiły błędy if (ListenSocket == INVALID_SOCKET) { printf("error at socket(): %ld\n", WSAGetLastError()); freeaddrinfo(result); WSACleanup(); return 1; } Jeśli wystąpił błąd to wyświetlany jest odpowiedni komunikat Zasoby są zwracane WSACleanup kończy użycie WS2_32.dll
Wiązanie z socketem Aby serwer mógł akceptować połączenia od klienta, to musi być związany z adresem sieciowycm // Setup the TCP listening socket iresult = bind( ListenSocket, result->ai_addr, (int)result->ai_addrlen); if (iresult == SOCKET_ERROR) { printf("bind failed: %d\n", WSAGetLastError()); freeaddrinfo(result); closesocket(listensocket); WSACleanup(); return 1; } Struktura sockaddr przechowuje informacje o adresie IP, jego typie i numerze portu Funkcja bind uruchamiana jest dla utworzonego socketu (parametry socket i sockaddr) W przypadku wystąpienia błędu, jego obsługa polega na wygenerowaniu napisu informującego o błędzie, zwróceniu zasobów i zakończeniu użycia WS2_32.dll
Nasłuchiwanie na sockecie Po związaniu socketu z adresem IP i portem, serwer przechodzi w stan nasłuchiwania (oczekiwania na połączenia z klientem) if ( listen( ListenSocket, SOMAXCONN ) == SOCKET_ERROR ) { printf( "Error at bind(): %ld\n", WSAGetLastError() ); closesocket(listensocket); WSACleanup(); return 1; } Funkcja listen wywoływana jest z dwoma parametrami utworzonym socketem i wartością określającą maksymalną długość kolejki połączeń oczekujących na zaakceptowanie (w tym przykładzie stała SOMAXCONN) Stała SOMAXCONN oznacza maksymalną rozsądnie możliwą liczbę oczekujących połączeń W przypadku wystąpienia błędu, jego obsługa polega na wygenerowaniu napisu zamknięciu socketu i zakończeniu użycia WS2_32.dll
Akceptowanie połączeń krok 1 Gdy socket jest w trybie nasłuchiwania, program musi obsługiwać żądania połączenia przychodzące na ten socket SOCKET ClientSocket; Utworzenie temporalnego obiektu typu SOCKET, służącego do obsługi połączeń przychodzących.
Akceptowanie połączeń krok 2 Akceptowanie połączenia ClientSocket = INVALID_SOCKET; // Accept a client socket ClientSocket = accept(listensocket, NULL, NULL); if (ClientSocket == INVALID_SOCKET) { printf("accept failed: %d\n", WSAGetLastError()); closesocket(listensocket); WSACleanup(); return 1; } Zazwyczaj aplikacja serwera nasłuchuje cały czas, aby przyjmować kolejne połączenia od klientów (kolejne połączenia są obsługiwane przez odrębne wątki) Po wystąpieniu próby nawiązania połączenia, aplikacja serwera akceptuje połączenie jedną z funkcji accept, AcceptEx lub WSAAccept i przekazuje sterowanie do innego wątku, który zajmuje się obsługą tego zgłoszenia W typ prostym przypadku serwer nasłuchuje i akceptuje wyłącznie jedno połączenie (nie ma obsługi wielu wątków)
Akceptowanie połączeń krok 3 Zaakceptowanie połączenia klienta przez serwer powinno spowodować przekazanie zaakceptowanego socketu klienta do wątku roboczego lub do odpowiedniego portu we/wy, a serwer powinien przejść do akceptowania kolejnych połączeń W tym prostym przykładzie serwer po zaakceptowaniu połączenia przechodzi do obsługi tego połączenia Istnieje wiele innych technik programistycznych, które mogą być użyte do nasłuchiwania i akceptowania połączeń przychodzących. Są to m.in. funkcje select lub WSAPoll. Przykłady znajdują się w MS Windows Software Development Kit (SDK)
Akceptowanie połączeń uwagi Główna różnica pomiędzy systemem Windows a systemami unixowymi w zastosowaniu socketów występuje w momencie zaakceptowania połączenia W systemach unixowych proces serwera wywołuje funkcję fork by utworzyć proces potomny do obsługi połączenia z klientem otrzymując od serwera socket Systemy windowsowe nie obsługują funkcji fork, używając w zamian techniki wielowątkowości
Odbieranie i wysyłanie danych (1) Po nawiązaniu połączenia z socketem klienta serwer odbiera i wysyła dane za pomocą funkcji recv i send obydwie funkcje zwracają liczbę wysłanych (odebranych) bajtów lub kod błędu obydwie funkcje również korzystają z tych samych parametrów: aktywny socket, bufor znaków, liczba bajtów do wysłania lub odebrania, odpowiednie znaczniki #define DEFAULT_BUFLEN 512 char recvbuf[default_buflen]; int iresult, isendresult; int recvbuflen = DEFAULT_BUFLEN; // Receive until the peer shuts down the connection do { } while (iresult > 0);
Odbieranie i wysyłanie danych (2) do { iresult = recv(clientsocket, recvbuf, recvbuflen, 0); if (iresult > 0) { printf("bytes received: %d\n", iresult); } // Echo the buffer back to the sender isendresult = send(clientsocket, recvbuf, iresult, 0); if (isendresult == SOCKET_ERROR) { printf("send failed: %d\n", WSAGetLastError()); closesocket(clientsocket); WSACleanup(); return 1; } printf("bytes sent: %d\n", isendresult);
Odbieranie i wysyłanie danych (3) else if (iresult == 0) printf("connection closing...\n"); else { printf("recv failed: %d\n", WSAGetLastError()); closesocket(clientsocket); WSACleanup(); return 1; } } while (iresult > 0);
Rozłączanie serwera krok 1 Gdy serwer zakończy odbieranie danych od klienta i wysyłanie informacji zwrotnych, rozłącza się z klientem // shutdown the send half of the connection since no more data will be sent iresult = shutdown(clientsocket, SD_SEND); if (iresult == SOCKET_ERROR) { printf("shutdown failed: %d\n", WSAGetLastError()); closesocket(clientsocket); WSACleanup(); return 1; } Funkcja shutdown z parametrem SD_SEND stosowana do zamknięcia strony wysyłającej Umożliwia to klientowi zwolnienie wykorzystywanych w komunikacji zasobów, a serwer może nadal otrzymywać dane przez ten socket
Rozłączanie serwera krok 2 Gdy klient zakończy odbieranie danych zamykany jest związany z nim socket // cleanup closesocket(clientsocket); WSACleanup(); return 0; Funkcja closesocket stosowana do zamknięcia socketu Po zakończeniu korzystania przez aplikację z biblioteki WS2_32.dll wywoływana jest funkcja WSACleanup zamykająca bibliotekę i zwalniająca używane zasoby.
Przykłady zaawansowane Dostępne w Windows SDK W przypadku Windows 7 zainstalowane w następującym katalogu: C:\Program Files\Microsoft SDKs\Windows\v7.0\Samples\NetDs\winsock Pięć katalogów: accept simple WSAPoll overlap iocp
Przykłady zaawansowane accept pokazuje zastosowanie funkcji select do obsługi wielu połączeń lub funkcji WSAAsyncSelect do akceptacji asynchronicznej simple trzy podstawowe programy ilustrujące korzystanie z wielu wątków przez serwer simples prosty serwer TCP/UDP simples_ioctl serwer wyłacznie TCP korzystający z funkcji select do obsługi wielu klientów simplec klient TCP/UDP WSAPoll prosty program pokazujący użycie funkcji WSAPoll overlap przykładowy serwer korzystający z nadpisywania portu I/O do obsługi wielu połączeń w sposób asynchroniczny z poziomu aplikacji jednowątkowej(acceptex) iocp trzy programy korzystające z funkcji WSAAccept (iocpserver), funkcji AcceptX (iocpserverx) i wielowątkowy klient (iocpclient)