Winsock – Wprowadzanie
W tym kursie przedstawię najpotężniejsze API do obsługi sieci w systemie Windows.
Dla kogo jest ten kurs ?
Jeśli czytasz ten artykuł to musisz posiadać:
– Podstawy na temat sieci (Adres IP, Klient , Serwer itp..)
– Wolny czas 😉
– Podstawy WinApi
– Znajomość Delphi/C++ na poziomie minimum przeciętnym
Przykłady do artykułu będą pisane w Delphi, właśnie – w Delphi !
Oczywiście w załączniku będzie także dostępna wersja z kodem źródłowym w języku C++ i artykuł w formie PDF(Delphi).
Załącznik nie będzie zawierać okienkowych aplikacji czy jakichkolwiek takich rzeczy. Dostaniecie skomentowany kod źródłowy. Nie chciałem robić niewiadomo jakich przykładów. Niemniej jednak kod jest czytelny i myślę, że łatwo zrozumiecie o co chodzi.
Kilka słów o Winsocku
Jest to biblioteka/API do obsługi gniazdek (ang. sockets) w systemie Windows.
Biblioteka ta nie tylko obsługuje protokół TCP/IP, obsługuje też inne protokoły tj. UDP,
IPX/SPX, IRDA czy nawet Bluetooth. W tym kursie będziemy najczęściej używać Winsock
w wersji 2.0 oraz protokołu TCP/IP. Pisanie aplikacji za pomocą tej biblioteki jest o wiele efektywniejsze niż za pomocą innych komponentów sieciowych, mamy bowiem o wiele większą kontrole nad wszystkimi procesami oraz bardzo rozbudowaną obsługę błędów.
Nagłówki i Inicjacja Gniazd
Teraz pokażę jak się przygotować swój program aby można było używać gniazdek.
Zacznijmy od Delphi będą nam potrzebne dwa moduły a mianowicie :
1 2 |
uses Windows,Winsock; |
Moduły te umożliwiają nam korzystanie z funkcji WinApi oraz Winsock.
W winsocku prawie każda funkcja zwraca nam kod błędu w czasie jakiegoś nie powodzenia , co jest bardzo wygodne – wiemy jak, gdzie i kiedy aplikacja nam się „wykrzaczy” 😉
W C++ będzie podobnie, musimy includować dwa pliki nagłówkowe :
1 2 |
#include <windows.h> #include <winsock2.h> |
oraz podlinkować biblioteką wsock32.lib lub libwsock32.a w zależności od kompilatora.
Uruchamianie i zamykanie WSA
Co to jest w ogóle WSA ? jest to składnik windows który umożliwia nam komunikacje miedzy komputerami. Każda aplikacja korzystająca z windowsowych gniazdek (Winsock) musi go dla siebie uruchomić.
function WSAStartup(wVersionRequired : Word, var WSAData : WSAData): Integer;
Przykład:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var wsa : WSAData; begin ErrorCode := WSAStartup(MAKEWORD(2,0),wsa); if (ErrorCode = SOCKET_ERROR) then MessageBox(0,'Nie możana wystartować gniazdek !','Winsock',MB_OK); end; |
Jak widać WSA uruchamia się funkcją WSAStartUp() , jej pierwszy parametr to wersja winsocka (funkcja MAKEWORD tworzy nam zmienna word, w tym wypadku 2.0), następny parametr to zmienna typu WSAData. Później sprawdzamy czy ErrorCode nie zwróciło nam błędu i jeżeli wystąpił to wyświetlamy stosowny komunikat.
To tyle co do uruchamiania, teraz czas na zamykanie/zwalnianie WSA (zwalniamy oczywiście kiedy nasza aplikacja kończy prace).
function WSACleanup(): Integer;
Przykład:
1 |
WSACleanUp; |
Tego chyba nie trzeba tłumaczyć 😉
Ok, gdy już mamy odpalone WSA można przejść do następnej części.
Tworzenie Gniazd
WSA wystartowało teraz musimy nawiązać połączenie, a żeby nawiązać połączenie trzeba mieć dwa gniazda, czyli klienta i serwer.
Tworzenie gniazda nasłuchującego (serwer)
Gniazdo serwera ma pełnić role nasłuchującą czyli ma czekać na podłączanie klientów, po czym zaakceptować połączenie . Dobra, ale jak to gniazdo utworzyć? W tym celu posłużymy się funkcją Socket która wygląda tak :
function Socket(af: Integer, Struct: Integer; protocol: Integer): Integer;
af – Jest to rodzina adresów jakich będziemy używać, my używać będziemy adresów
internetowych więc wpisujemy AF_INET.
Struct – Określa rodzaj naszego gniazda w wypadku protokołu TCP/IP wstawiamy
tam SOCK_STREAM, w wypadku UDP – SOCK_DGRAM.
protocol – określa nam protokół, wstawiając 0 wskazujemy na domyślny protokół kombinacji rodziny adresów (AF_INET) i typie naszego gniazda (SOCK_STREAM) czyli AF_INET + SOCK_STREAM = TCP/IP
Przykład funkcji Socket:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Var Gniazdo: Integer; Begin Gniazdo := Socket(AF_INET, SOCK_STREAM,0); if (Gniazdo = INVALID_SOCKET) then MessageBox(0,'Nie można otworzyć gniazda !','Winsock',MB_OK); End; |
Funkcja zwraca nam id nowo otwartego gniazda jeśli wszystko poszło dobrze, jeśli zwróci nam INVALID_SOCKET to znaczy, że coś poszło nie tak.
Tym sposobem utworzyliśmy gniazdko dla protokołu TCP/IP, tylko, do czego one teraz służy? Do niczego do póki nie zdefiniujemy co ono ma robić.
A zatem :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Var socket_info : sockaddr_in; Begin socket_info.sin_family := AF_INET; socket_info.sin_addr.s_addr := INADDR_ANY; socket_info.sin_port := htons(port); End; |
Teraz tak, sockaddr_in jest to struktura opisująca nasz socket, są tam zawarte informacje tj.port, adres ip etc. Mniejsza o to, będę tłumaczyć w miarę potrzeby.
sin_family – oznacza rodzine adresów i mamy ponownie AF_INET
sin_addr.s_addr – oznacza z jakimi adresami IP będziemy się łączyć, a że jest to serwer to ustawiamy INADDR_ANY, stała ta oznacza, że będziemy się łączyć ze wszystkimi IP.
sin_port – to jak można by się domyśleć jest port naszego gniazda, a do czego służy htons() ? – jest to zmiana reprezentacji bajtów ale o tym później , teraz można to tłumaczyć jako, że ta funkcja zamienia nasz wpisany port na takie dane które „rozumie” socket.
Hmm.. Teraz trzeba jakoś przypisać te wszystkie ustawienia naszemu gniazdku. Posłuży nam do tego funkcja bind():
function bind(s: Integer,var addr: sockaddr_in, namelen: Integer): Integer;
s – jest to nasz socket
addr – jest to nasza struktura opisująca gniazdo
namelen – jest to wielkość naszej struktury
Przykład:
1 2 3 |
ErrorCode := bind(Gniazdko, sock_info, SizeOf(sock_info); if ErrorCode = SOCKET_ERROR) then MessageBox(0,'Nie można przypisać właściwosci do gniazda!','Error',MB_OK); |
Gniazdko mamy już prawie przygotowane teraz wystarczy wywołać w funkcji głównej programu funkcje listen():
function listen(s: Integer, backlog: Integer): Integer;
s – wiadomo, nasz socket
backlog – jest to maksymalna liczba akceptowalnych połączeń
Warto tutaj też wspomnieć o stałej SOMAXCONN – dzięki niej możemy akceptować nieograniczoną ilość połączeń.
Przykład:
1 2 3 4 5 |
begin ErrorCode := listen(Gniazdo, SOMAXCONN); if (ErrorCode = SOCKET_ERROR) then MessageBox(0,'Nie można przejść w tryb nasłuchiwania','Error',MB_OK); |
Pozostaje nam jeszcze włączyć tryb nieblokujący (asynchroniczny). Co to jest tryb nieblokujący to chyba każdy wie, dla tych co nie wiedzą to wyjaśnię :
– Tryb Blokujacy : to tryb w którym wywołanie funkcji (na przykład odczyt danych z gniazda) zatrzymuje cały program i nie wznawia go do póki funkcja się nie zakończy.
– Tryb Nieblokujący : Czyli odwrotność tego co jest przy trybie blokującym, czyli wywołanie gniazda nie zatrzymuje programu.
Domyślnie gniazda ustawione są w tryb blokujący.
W trybie asynchronicznym do naszej aplikacji docierają komunikaty. Komunikaty WSA to są komunikaty które „dostaje” nasze okienko w momencie na przykład podłączenia klienta, dotarcia pakietu etc. Kto zna chociaż podstawy WinApi wie co to są komunikaty dlatego nie będę się tutaj rozpisywał i przejdę do rzeczy.
function WSAAsyncSelect(s: Integer, hwnd: HWND, wMsg: Integer, lEvent: Integer): Integer;
s – nasze gniazdo
hwnd – uchwyt naszego okna
wMsg – jest to numer komunikatu wiekszy lub równy WM_USER
llEvent – są to komunikaty które chcemy otrzymywać
Przykład:
1 |
WSAAsyncSelect(Gniazdo, Handle, WM_USER, FD_READ or FD_ACCEPT); |
Poniżej zamieszczam najczęściej używane komunikaty :
– FD_READ– dostajemy kiedy przyszły do nas informacje i „czekają sobie” w sockecie na odebranie, jest to najistotniejszy komunikat
– FD_CONNECT– dostajemy z momentem połączenia
– FD_ACCEPT– dostajemy z momentem akceptacji połączenia
Gniazdo nasłuchujące gotowe 😉
Gniazdo Podłączające się (Klient)
Tutaj będzie prościej (czyt. mniej pisania). Na samym początku stwórzmy sobie nowe gniazdo i przypiszmy mu odpowiednie właściwości :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Var Adr: String = '127.0.0.1'; socket_info: sockaddr_in; Gniazdo: Integer; Begin Gniazdo := Socket(AF_INET, SOCK_STREAM,0); socket_info.sin_family := AF_INET; socket_info.sin_addr.s_addr := inet_addr(PChar(Adr)); socket_info.sin_port := htons(port); end; |
Od razu rzuca się w oczy pole s_addr do którego przypisujemy adres ip. Funkcja inetaddr zamienia nam adres IP w postaci String-a do postaci rozumianej przez socket. Jest to klient więc ustawiamy jeden ip – ip serwera. Teraz wystarczy się połączyć, służy do tego funkcja connect():
function connect(s: integer,var name: sockaddr_in ,namelen : Integer): Integer;
s – nasz socket
name – nasza struktura sockaddr_in
namelen – wielkość struktury
Przykład użycia:
1 2 3 |
ErrorCode :=connect(Gniazdo,sock_info,SizeOf(sock_info)); if (ErrorCode = SOCKET_ERROR) then MessageBox(0,'Połączenie nie udane','Error',MB_OK); |
Zauważcie, że nie wywołujemy tutaj funkcji bind jak w przypadku serwera. Dzieje się tak dlatego, że funkcja bind jest wywoływana w środku funkcji connect.
Na koniec wystarczy jeszcze ustawić tryb asynchroniczny
1 |
WSAAsyncSelect(Gniazdo,Handle,WM_USER, FD_READ or FD_CONNECT or FD_ACCEPT); |
Komunikaty są takie same jak na serwerze.
Zamykanie gniazd
Po zakończonej pracy zamykamy gniazdo funkcją Closesocket() :
function CloseSocket(s: Integer);
Gdzie s to nasze gniazdko.
Przykład użycia:
1 |
CloseSocket(Gniazdo); |
Utworzyliśmy dwa gniazda w trybie nieblokującym – nasłuchujące i podłączające się.
Ale same gniazda i zestawione połączenie tak naprawdę nam nic nie dają, wszystko to co opisałem miało na celu przygotować połączenie do możliwości wymiany danych. Przedstawię teraz mały schemat całego połączenia.
Jak widać na rysunku wymiana danych występuje po zestawieniu połączenia. Do wymiany danych służą dwie funkcje odpowiednio :
– send() – dla Wysyłania
– recv() – dla Odbierania
Wysyłanie
function send(s: Integer, var Buf, len: Integer, flags: Integer);
s – nasze gniazdo
buf – Bufor który będziemy wysyłać
len – Rozmiar naszego buffora
flags – Specjalne flagi, u nas jest to 0 ponieważ ich nie używamy
Przykład użycia:
1 2 3 4 5 6 7 8 9 |
Var Buffer: array[0..20] of char; begin send(Gniazdo, Buffer, sizeof(Buffer), 0); end; |
Tym sposobem wysłaliśmy porcje danych. Teraz czas na odbieranie.
Odbieranie
Odbieranie wygląda tak samo jak wysyłanie, tylko używamy innej funkcji.
function recv(s: Integer, var Buf, len: Integer, flags: Integer);
Budowy funkcji nie ma już co opisywać, napisze tylko przykład użycia :
1 2 3 4 5 6 7 8 9 |
Var Buffer: array[0..20] of char; begin recv(Gniazdo, Buffer, sizeof(Buffer), 0); end; |
Dodam, że teraz przyda nam się komunikat FD_READ, było by grzechem go nie wykorzystać .
Podsumowanie
Tym sposobem utworzyliśmy gniazda, nawiązaliśmy połączenie i przeprowadziliśmy wymianę danych.
W następnych artykułach będę opisywał tę bibliotekę pod kątem bardziej pisanie gier. Pojawią się takie artykuły jak „Klasa Pakietu” czy „Server Wielowątkowy” . Ale to za jakiś czas (czt. Kiedy mi się zachce).
To by było na tyle w tym kursie, mam nadzieje, że teraz po przerobieniu tego skrawka informacji o tej potężnej bibliotece będziecie sami w stanie napisać aplikacje sieciową za pomocą Winsocka.
Miłego kodzenia !