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 :

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 :


 

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:

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:

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:

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 :

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;

– jest to nasz socket
addr – jest to nasza struktura opisująca gniazdo
namelen – jest to wielkość naszej struktury

Przykład:

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:

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:

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 :

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:

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

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:

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:

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 :

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 !

Autor: filuu