Internetové programování s WinSock (2)
V prvním díle jsme se seznámili s WinSockem, dnes začneme poprvé nasazovat knihovnu WinSock v praxi. Naučíme se začlenit WinSock do našeho programu a správně jej inicializovat. Seznámíme se s použitím IP adres a portů. Naučíme se získávat adresy IP z doménových jmen a vytvoříme první socket. Kromě toho pochytíme několik dalších drobných ale důležitých dovedností.
Verze knihovny WinSock
Knihovna se v současnosti vyskytuje ve dvou hlavních verzích - WinSock 1 (přesněji 1.1) a WinSock 2 (neboli 2.2). Většina systémů obsahuje knihovnu ve verzi 2.2. Jen některé starší exempláře Windows 95 a samozřejmě Windows 3.x mají jen knihovnu starší verze.
Novinky ve WinSock 2 jsou zhruba tyto:
· WinSock 2 zavádí kromě TCP/IP systematickou podporu dalších transportních a síťových protokolů a jejich implementací. Zároveň jsou podporovány i jiné jmenné systémy než DNS. My si plně vystačíme s TCP/IP a DNS, a proto pro nás tyto možnosti nejsou příliš zajímavé.
· Se sockety je možné pracovat pomocí vstupně-výstupních funkcí Windows, jako jsou ReadFile a WriteFile. Je podporován tzv. překryvný vstup a výstup (overlapped I/O), což je zvláštní optimalizovaný a neblokující druh vstupu a výstupu dostupný jen ve Windows s jádrem NT (Windows NT, 2000, XP). Ani těmito schopnostmi se tady nebudeme zabývat.
· Jsou zavedeny tzv. události, které umožňují testovat stav operací se socketem. Tyto události jsou podobné událostem používaným pro synchronizaci v multithreadových aplikacích.
· Sockety je možné organizovat do skupin, nastavovat jejich priority, sdílet je mezi procesy, nastavovat parametry tzv. kvality služeb (quality of service, QoS) apod.
· Částečně je vylepšena podpora tzv. "surových" socketů (raw sockets), které umožňují pracovat na úrovni nižších protokolů, např. ICMP.
Pro většinu obvyklých úloh je možné vystačit s WinSock 1.1.
V C/C++ začleníme do programu hlavičkový soubor winsock.h nebo winsock2.h, podle toho, kterou verzi chceme importovat. V Delphi nebo jiném Pascalu pro Windows přidáme do sekce uses jméno jednotky WinSock.
V systémovém adresáři Windows můžeme najít knihovny DLL pro jednotlivé verze WinSock. Verze WinSock 1.x přebývá v souboru wsock32.dll, v 16bitové verzi se jmenuje winsock.dll. Verzi WinSock 2.x pak najdeme v souboru ws2_32.dll. Jména odpovídajících staticky linkovaných knihoven jsou stejná, liší se jen extenzí .lib.
WinSock a programovací jazyky
Pro ukázky zdrojových kódů budu používat jazyk C, protože ten se v podobných oblastech vyskytuje nejčastěji. Pro ty, kdo mají zájem programovat s knihovnou WinSock v Delphi, uvádím následující přehled konvencí:
· V Pascalu je samozřejmě možné používat identifikátory s odlišnou velikostí písmen.
· Datové typy mají prefix T (TSocket místo SOCKET, TWSAData místo WSADATA apod.). Pro struktury/záznamy jsou definovány odpovídající ukazatelové typy lišící se prefixem P.
· Parametry funkcí předávané pomocí ukazatele jsou nahrazeny parametry předávanými odkazem (s modifikátorem var). To má jednu nevýhodu. Za ukazatel je většinou dovoleno dosadit NULL/nil, jestliže parametr není potřebný, tady je nutné vždy dodat proměnnou.
· Místo maker jsou definovány funkce se stejným významem.
Sockety zabalené do objektů nabízejí komponentové knihovny VCL a MFC stejně jako různé freewarové produkty. My se budeme soustředit na programování pomocí funkcí API, které jsou dostupné všem programátorům. Přejít na objektovou architekturu není problém, protože principy jsou stejné.
Inicializace a úklid
Pro inicializaci knihovny zavoláme funkci WSAStartup. Ta musí být vždy první volanou funkcí WinSock. Prvním parametrem je 16bitové číslo specifikující požadovanou verzi knihovny. Nižší byte obsahuje první složku čísla verze a vyšší byte složku druhou. Druhým parametrem povinně předáváme platný ukazatel na strukturu WSADATA, která bude naplněna informacemi o inicializované verzi a jejich vlastnostech. Struktura nemusí být inicializována před voláním WSAStartup, jde jen o výstupní parametr. Z této struktury je podstatná především položka wVersion, která obsahuje číslo skutečně inicializované verze knihovny. Je to buď číslo požadované při volání WSAStartup, nebo, v případě, že taková verze není k dispozici, je vybrána nejvyšší verze dostupná na systému. Funkce vrátí nulu, jestliže inicializace proběhla úspěšně, jinak vrátí chybový kód a služby knihovny nebude možné použít.
Po skončení práce s knihovnou musí být volána úklidová funkce WSACleanup, která nemá žádné parametry.
Následuje výpis základní kostry programu používajícího knihovnu WinSock. V dalších příkladech budu uvádět už jen jejich funkční "vnitřek".
#include <winsock2.h>
int main (void)
{
WSADATA WSAData;
if (WSAStartup(0x0202,
&WSAData) != 0) {
/* stala se chyba
*/
}
/* pouzivam WinSock
*/
WSACleanup();
return 0;
}
Starosti s byty
Není procesor jako procesor a jednou z možných odlišností mezi různými hardwarovými platformami je i způsob reprezentace celých čísel.
Procesory Intelu ukládají číslo v paměti tak, že nejméně významný byte leží na nejnižší adrese a nejvýznamnější byte na adrese nejvyšší. To znamená přesně naopak, než jak to člověku připadá přirozené. Když do 32bitové celočíselné proměnné přiřadíme číslo 258, obsah paměti v tomto místě bude 02 01 00 00, tedy 2 + 256 + 0 + 0. Naproti tomu procesory Motorola a většina ostatních uloží výše uvedené číslo ve tvaru 00 00 01 02, tj. od nejvyššího bytu na nejnižší adrese. Formát Intelu se nazývá little-endian (LE), formát Motoroly big-endian (BE). Tyto termíny jsou inspirovány jmény dvou liliputánských kmenů, které se lišily tím, na kterém konci natloukaly vajíčka natvrdo.
Aby si různé počítače na Internetu mezi sebou rozuměly, musí se protokoly typu TCP/IP držet jednotného formátu (tzv. síťového). Za tento formát byl zvolen tvar big-endian, a proto se programy běžící na platformě Intel musí starat o konverzi předávaných číselných dat, jako jsou adresy IP a čísla portů.
Rozhraní WinSock pro tyto účely nabízí čtyři konverzní funkce. Funkce htons konvertuje 16bitové číslo z formátu hostitele (low-endian) do formátu síťového (big-endian). Funkce htonl pak pracuje s číslem 32bitovým. Funkce ntohs a ntohl jsou jména pro funkce provádějící stejnou konverzi v opačném směru.
Práce s adresami
Adresy IP budeme potřebovat například při navázání spojení TCP nebo při odesílání datagramů nespojovaným protokolem UDP.
Adresa IP je vnitřně reprezentována 32bitovým celým číslem. Jestliže potřebujeme zkonvertovat adresu IP z textového tvaru do vnitřní reprezentace, máme k dispozici funkci inet_addr. Té předáme jako parametr ukazatel na nulou zakončený řetězec obsahující jedno až čtyři čísla v rozsahu 0 až 255 oddělená tečkami (např. 192.111.8.1 nebo třeba 80.16). Funkce vrací výsledek konverze. V případě, že zadaná adresa měla neplatný tvar, je navrácena konstanta INADDR_NONE, odpovídající adrese 255.255.255.255. Má-li adresa IP správný tvar, pak funkce nekontroluje, jestli adresa skutečně existuje.
Také konverze v opačném směru je možná, a to pomocí funkce inet_ntoa. Funkci zadáme adresu v celočíselné podobě, kterou ještě musíme zabalit do struktury in_addr (viz příklad níže), a obdržíme ukazatel na vytvořený textový řetězec. Tento řetězec musíme ihned překopírovat do vlastního bufferu, protože je umístěn v paměti spavované knihovnou a následujícím voláním funkcí WinSock může být ztracen.
Jestliže budeme zapisovat adresu přímo v číselné podobě, nesmíme zapomnět na zápis jednotlivých bytů v opačném pořadí. Například adresu 127.0.0.1 zapíšeme hexadecimálně jako 0x0100007F a ne jako 0x7F000001. (Případně použijeme druhý tvar a zkonvertujeme jej funkcí htonl.)
Adresa 127.0.0.1 má speciální význam. Představuje vždy samotný místní počítač, který adresu použil. Tato adresa se výborně hodí pro testování klientské a serverové aplikace na jednom počítači bez nutnosti síťového připojení. V API WinSock pro ni existuje předdefinovaná konstanta INADDR_LOOPBACK. Pozor, dokonce i tuto konstantu musíme konvertovat pomocí ntohl.
Uživatel našeho programu pravděpodobně nebude mít zájem pamatovat si adresy IP a raději bude pracovat s doménovými jmény typu www.chip.cz. Vzniká tedy potřeba přeložit doménové jméno na IP adresu. Tuto práci má na starost protokol DNS, resp. DNS klient (resolver) dotazující se DNS serveru. DNS je aplikační protokol, ale naštěstí jej programátor nemusí sám implementovat. Podpora DNS je obsažena v knihovně WinSock.
Pro překlad jména na adresu IP použijeme funkci gethostbyname. Ta dostane jako parametr řetězec s doménovým jménem počítače a vrátí ukazatel na strukturu HOSTENT, kterou nesmíme modifikovat, ale zato můžeme přečíst její položku h_addr, což je ukazatel na adresu IP v číselném tvaru. Ukazatel je definován pro znakový typ a je proto nutno jej přetypovat.
Následující programový kód přečte uživatelem zadanou adresu (doménové jméno), přeloží ji a vytiskne adresu IP v čitelném tvaru.
#include <stdio.h>
#include <string.h>
...
HOSTENT* phe;
struct in_addr addr;
char buf[50];
...
printf("Zadejte adresu: ");
scanf("%s", buf);
if ((phe = gethostbyname(buf)) == NULL)
printf("Vzdaleny
pocitac nenalezen.");
else {
addr.s_addr = *((unsigned
long *) phe->h_addr);
strcpy(buf, inet_ntoa(addr));
printf(buf);
}
Doplňuji, že funkce gethostbyname selže, jestliže jí
dosadíme za parametr adresu IP.
Proto je vhodné nejdříve se pokusit konvertovat textový řetězec na adresu
IP pomocí inet_addr a použít systém DNS, jen když konverzí získáme neplatný
výsledek.
Při práci s funkcemi WinSock je adresa zadávána obvykle v podobě struktury SOCKADDR_IN. Ta obsahuje kromě adresy IP také číslo portu. V první řadě musíme vyplnit položku sin_family, která specifikuje použitou sadu protokolů. V našem případě to bude vždy konstanta AF_INET představující protokoly TCP/IP. Do položky sin_port zadáme 16bitové číslo portu, které nezapomeneme konvertovat pomocí funkce htons na síťový formát. Samotnou adresu IP umísťujeme do položky, která se jmenuje sin_addr.s_addr, aby to nebylo příliš jednoduché. Ve skutečnosti je položka sin_addr mnohem komplikovanější, ale těmito detaily si raději vůbec nebudeme plést hlavu. Jedná se o stejnou strukturu in_addr jako ve výše uvedeném příkladě.
Následuje příklad korektně inicializované struktury SOCKADDR_IN pro připojení k HTTP serveru na lokálním počítači.
SOCKADDR_IN si;
...
si.sin_family =
AF_INET;
/* adresa lokalniho pocitace */
si.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
/* obvykly port protokolu HTTP */
si.sin_port =
htons(80);
Vytvoření a zavření socketu
Teď přikročíme k důležitému kroku na cestě k přenosu dat - vytvoření socketu. Sockety jsou reprezentovány číselnými deskriptory, podobnými handlům oken, grafických objektů, souborů apod. Datový typ pro tyto deskriptory se jmenuje SOCKET. Nový socket může být vytvořen funkcí se stejným jménem, psáno malými písmeny, tedy socket. Prvním parametrem je rodina protokolů, což je nám už známý AF_INET. Pro nás nejvýznamnější je parametr druhý, typ socketu. Jestliže zadáme konstantu SOCK_STREAM, bude vytvořen socket pro proudový, spojovaný, spolehlivý přenos dat pomocí transportního protokolu TCP. Použijeme-li konstantu SOCK_DGRAM, dostaneme socket pro přenos nespojovaný, nespolehlivý, ve formě datagramů, tedy socket protokolu UDP. Za třetí parametr budeme dosazovat nulu. Funkce vrací deskriptor právě vytvořeného socketu. V případě neúspěchu je navrácena konstanta -1 neboli INVALID_SOCKET.
Po skončení práce musí aplikace zavřít všechny úspěšně vytvořené sockety voláním funkce closesocket.
SOCKET sock;
...
sock = socket(AF_INET, SOCK_STREAM, 0);
closesocket(sock);
Závěr
Teď už dokážeme vytvořit socket a jsme jen malý krůček od napsání kompletního funkčního programu.
Tento krůček uděláme v příštím díle seriálu, ve kterém se naučíme programovat kompletní klientskou aplikaci nad protokoly TCP i UDP.