Programovßnφ internetov²ch aplikacφ s knihovnou WinSock (2)

V tomto druhΘm a poslednφm dφle Φlßnku o programovßnφ sφ¥ov²ch aplikacφ se zam∞°φme p°edevÜφm na znalosti souvisejφcφ s tvorbou serverov²ch aplikacφ a na asynchronnφ re₧im prßce se sφtφ, umo₧≥ujφcφ integraci sφ¥ov²ch funkcφ do grafickΘho u₧ivatelskΘho rozhranφ.

Prvnφ dφl 

Obsah

Porty a funkce WinSock
Principy prßce serveru
Prßce v asynchronnφm re₧imu
Zprßvy socketu
P°ipojovßnφ a odpojovßnφ
P°enos dat v asynchronnφm re₧imu
Konec

Porty a funkce WinSock

S Φφsly port∙ jako souΦßstφ internetovΘ adresy jsme se zatφm setkali p°i p°ipojovßnφ socketu v protokolu TCP a takΘ p°i posφlßnφ a p°φjmu datagram∙ v protokolu UDP. Zatφmco adresy IP umo₧≥ujφ jednoznaΦn∞ rozliÜit jednotlivΘ poΦφtaΦe v sφti (p°φpadn∞ jednotlivΘ sφ¥ovΘ adaptΘry), porty slou₧φ k identifikaci aplikacφ v rßmci jednoho poΦφtaΦe. M∙₧eme si p°edstavit, co by bylo, kdyby porty nebyly. Ka₧d² program, a¥ u₧ WWW prohlφ₧eΦ, e-mailov² nebo chatovacφ klient, FTP nebo WWW server, zkrßtka ka₧d², kter² mß n∞co spoleΦnΘho s Internetem, by musel mφt p°i°azenu svou vlastnφ adresu IP (dokonce ka₧dß jeho instance). Vzhledem k omezenΘmu poΦtu adres v souΦasnΘ verzi IPv4 by se adresy jednoduÜe nedostßvaly. Porty ale nejsou dobrΘ jen k tomu, aby kompenzovaly zastaralost standardu IP. Bez ohledu na dostupn² poΦet adres IP by bylo dost nßroΦnΘ p°i°azovat a udr₧ovat unikßtnφ adresy pro vÜechny aplikace. Bez existence port∙ by se p°φliÜ dob°e nepracovalo ani s adresami URL. Tolik k ujasn∞nφ, k Φemu vlastn∞ porty jsou.

Ka₧d² p°ipojen² socket ve WinSock je ve skuteΦnosti charakterizovßn p∞ti informacemi. Jsou to:

Jak je vid∞t, svΘ Φφslo portu mß i klientskß aplikace, p°esto₧e my jsme v₧dy pracovali s porty vzdßlenΘho serveru. Port klienta je zvolen automaticky systΘmem, a to nap°φklad p°i volßnφ funkcφ connect nebo sendto. Programßtor o tomto Φφsle portu nerozhoduje a v∞tÜinou ani nemß d∙vody jej zjiÜ¥ovat. Klientsk² poΦφtaΦ m∙₧e provozovat v jednom okam₧iku vφce spojenφ, a to t°eba i s jednou stejnou aplikacφ na jednom serveru. V takovΘm p°φpad∞ jsou to prßv∞ porty klienta, kter²mi se jednotlivß spojenφ odliÜujφ, a kterΘ umo₧≥ujφ jejich neruÜenou koexistenci. Spustφme-li nap°φklad dv∞ instance prohlφ₧eΦe a otev°eme v nich stejnou strßnku, vytvo°φ se dv∞ r∙znß spojenφ s r∙zn²mi lokßlnφmi Φφsly portu.

Porty jsou popisovßny 16bitov²mi Φφsly bez znamΘnka, v rozsahu od 1 do 65535. Jestli₧e budeme programovat sv∙j server, p°edstoupφme p°ed problΘm v²b∞ru Φφsla portu. B∞₧n∞ pou₧φvanΘ protokoly majφ svß standardnφ Φφsla port∙, nap°φklad:

HTTP 80 DayTime 13
FTP 21 Telnet 23
SMTP 25 POP3 110

To ale neznamenß, ₧e server pat°iΦnΘ aplikace musφ nutn∞ b∞₧et na standardn∞ urΦenΘm portu. Jestli₧e tomu tak nenφ, musφ Φφslo portu zadßvat u₧ivatel klientskΘ aplikace, obvykle jako souΦßst adresy URL. Standardnφ Φφslo portu je programem pou₧ito implicitn∞. Tvo°φme-li sv∙j vlastnφ protokol, musφme v∞d∞t n∞kolik informacφ o zßsadßch p°id∞lovßnφ port∙. ╚φsla port∙ jsou rozd∞lena do nßsledujφcφch skupin:

1-1023 Porty vyhrazenΘ pro obvyklΘ slu₧by, jako jsou ty v²Üe uvedenΘ.
1024-5000 Jsou Φasto p°id∞lovßny systΘmem klientsk²m aplikacφm.
5001-49151 Jsou vyhrazeny pro nejr∙zn∞jÜφ novΘ aplikace, a tedy vhodnΘ k v²b∞ru standardnφho portu pro nßÜ program.
49152-65535 Tyto porty jsou aplikacφm op∞t dynamicky p°id∞lovßny systΘmem. Pro naÜe pot°eby jsou proto nevhodnΘ.

Jestli₧e chceme minimalizovat pravd∞podobnost kolize s jinou aplikacφ pou₧φvajφcφ stejn² port, je vhodnΘ volit spφÜe Φφslo "nic ne°φkajφcφ", nap°φklad 10751 nebo 36847 (ale zase ne vÜichni!), ne₧ efektnφ a p°ita₧livΘ 22222 nebo 10000. V ka₧dΘm p°φpad∞ je vhodnΘ netrvat na standardnφm Φφsle portu a umo₧nit u₧ivateli jeho rekonfiguraci pro p°φpad kolize. ╚φslo portu m∙₧e b²t k socketu p°ipojeno pomocφ funkce bind volanΘ po vytvo°enφ socketu. Vlastnφ Φφslo portu m∙₧eme nastavit i klientskΘmu socketu, to ale nemß ₧ßdn² v∞tÜφ smysl a nep°inese to vesm∞s nic pozitivnφho. My se budeme zab²vat jen nastavovßnφm serverovΘho portu.

Principy prßce serveru

Server se svou Φinnostφ od klienta podstatn∞ liÜφ. Klient obvykle iniciuje novΘ spojenφ a potΘ sd∞luje serveru, co od n∞j pot°ebuje. Naproti tomu chovßnφ serveru je spφÜe pasφvnφ. Po v∞tÜinu Φasu server jednoduÜe Φekß na po₧adavek klienta, tzv. naslouchß. V okam₧iku, kdy se klient pokusφ p°ipojit, server TCP tzv. p°ijme spojenφ, Φφm₧ vytvo°φ nov² socket a pomocφ n∞j komunikuje s klientem. Server UDP nevytvß°φ ₧ßdnΘ spojenφ, jen odpovφdß na p°φchozφ datagramy od libovoln²ch klient∙.

Prvnφ krok, kter² je spoleΦn² pro servery TCP i UDP, je vytvo°enφ socketu a jeho svßzßnφ s urΦitou adresou. Server UDP p°es tento socket odb²vß vÜechnu svou komunikaci. Naopak server TCP pou₧φvß tento prvnφ socket jen k naslouchßnφ a ₧ßdnß data jφm nep°enßÜφ. Svßzßnφ socketu s konkrΘtnφ adresou provedeme volßnφm funkce bind hned po ·sp∞ÜnΘm vytvo°enφ socketu. Funkce mß t°i parametry, podobnΘ jako v p°φpad∞ funkce connect. Prvnφm parametrem je socket, se kter²m se mß tato operace provΘst. Dßle je to adresa v podob∞ ukazatele na strukturu SOCKADDR a nakonec velikost tΘto struktury. Socket m∙₧e b²t takto svßzßn jak s urΦit²m portem, tak i s konkrΘtnφ adresou IP. Obvykle ale pot°ebujeme nastavit pouze Φφslo portu. V takovΘm p°φpad∞ do polo₧ky sin_addr.s_addr pou₧itΘ struktury SOCKADDR_IN p°i°adφme konstantu INADDR_ANY, Φφm₧ zajistφme, ₧e bude pou₧ita implicitnφ dostupnß adresa IP. Funkce vracφ nulu v p°φpad∞ ·sp∞chu, jinak SOCKET_ERROR.
Podφvejme se na p°φklad inicializace serveru, kter² bude naslouchat na portu 13.


SOCKET listen_sock
SOCKADDR_IN si;
...
listen_sock = socket(AF_INET, SOCK_DGRAM, 0);

si.sin_family 	= AF_INET;
si.sin_addr.s_addr	= INADDR_ANY;
si.sin_port		= htons(80)

bind(listen_sock, (SOCKADDR *) &si, sizeof si)
Jestli₧e programujeme server nad UDP, pak v tuto chvφli u₧ m∙₧eme jednoduÜe Φφst datagramy od klient∙ a odpovφdat na n∞. V nejjednoduÜÜφm provedenφ bude nßsledovat cyklus, kde budeme v ka₧dΘm pr∙chodu provßd∞t tyto operace:

Ve druhΘm z v²Üe uveden²ch krok∙ se skr²vß samotnß tv∙rΦφ Φinnost aplikace. Server si takΘ m∙₧e pamatovat informace o jednotliv²ch klientech (jejich stav) a pracovat tak s vφce fßzemi po₧adavk∙ a odpov∞dφ.

Jestli₧e obsluha ka₧dΘho klienta trvß delÜφ dobu a p°edpoklßdß se vysokß frekvence p°φstup∙, je vhodnΘ odd∞lit v₧dy samostatnΘ vlßkno pro zpracovßnφ ka₧dΘho po₧adavku. Server tak m∙₧e bez zpo₧d∞nφ p°ejφt k dalÜφmu klientovi. Jinou mo₧nostφ je vytvo°enφ jednoho vlßkna pro zpracovßnφ vÜech po₧adavk∙. Hlavnφ cyklus serveru pak jednotlivΘ po₧adavky umφs¥uje do fronty a druhΘ vlßkno je z fronty vybφrß a odpovφdß na n∞. Oba uvedenΘ p°φstupy sni₧ujφ riziko ztrßty p°φchozφch datagram∙, kterΘ se hromadφ ve vstupnφm bufferu socketu a kterΘ server ve chvilkßch nejv∞tÜφ frekvence po₧adavk∙ nestφhß zpracovßvat.

P°ejd∞me te∩ k serveru TCP. Vytvo°en² naslouchajφcφ socket se nepou₧φvß ke komunikaci s klienty. Mφsto toho se pro ka₧dΘ spojenφ vytvß°φ socket nov². K tomu se nepou₧φvß funkce socket, jak jsme byli zatφm zvyklφ, ale funkce accept. Tato funkce Φekß, dokud se k naÜemu serveru nepokusφ p°ipojit libovoln² klient, a potom vrßtφ vytvo°en² socket p°ipojen² k danΘmu klientovi (nebo p°esn∞ji s klientem p°ipojen²m k n∞mu). Odpadß tedy jakΘkoli volßnφ funkce connect, kterΘ by n∞koho mohlo lßkat.
Za prvnφ parametr funkce accept dosadφme deskriptor socketu pou₧itΘho k naslouchßnφ. DalÜφ dva parametry jsou nepovinnΘ. Jsou to ukazatel na buffer, do n∞ho₧ bude ulo₧ena struktura typu SOCKADDR_IN obsahujφcφ adresu klienta, a dßle ukazatel na celoΦφselnou prom∞nnou, do nφ₧ p°ijde velikost onΘ struktury (na vstupu musφ b²t inicializovßna na velikost bufferu). Jestli₧e nßs tyto informace nezajφmajφ, co₧ je v p°φpad∞ protokolu TCP nejΦast∞jÜφ p°φpad, pak m∙₧eme jako oba poslednφ parametry dosadit NULL. V p°φpad∞, ₧e p°ipojenφ klienta sel₧e, funkce vrßtφ konstantu INVALID_SOCKET.

Ka₧d² socket vytvo°en² funkcφ accept po skonΦenφ komunikace uzav°eme nßm u₧ znßm²m postupem pomocφ volßnφ shutdown a closesocket. Nßsleduje nßznak k≤du pro obsluhu jednoho klienta.


SOCKET listen_sock, serv_sock;
...
listen_sock = socket(AF_INET, SOCK_STREAM, 0);
/* svazani socketu listen_sock s adresou */
...
serv_sock = accept(listen_sock, NULL, NULL);
/* dialog s klientem ... */
...
shutdown(serv_sock, 1);
closesocket(serv_sock);
K≤d pro obsluhu klient∙ se bude op∞t provßd∞t v cyklu, v n∞m₧ budeme na zaΦßtku volat funkci accept vytvß°ejφcφ spojenφ s prvnφm Φekajφcφm klientem. Servery, kterΘ pot°ebujφ obslou₧it velk² poΦet klient∙, majφ mo₧nost odd∞lit tuto prßci do samostatnΘho vlßkna nebo pou₧φt asynchronnφ komunikaci (viz nφ₧e).
Krom∞ toho existuje mo₧nost nastavenφ maximßlnφho poΦtu klient∙ Φekajφcφch na spojenφ. Implicitn∞ je toti₧ ka₧dΘmu klientovi, kter² nem∙₧e b²t okam₧it∞ obslou₧en, odmφtnuto spojenφ. To se u klienta projevφ tφm, ₧e jeho volßnφ funkce connect vrßtφ chybovou hodnotu. Pro hodn∞ vytφ₧enΘ servery je mo₧no takovΘ chovßnφ zm∞nit nastavenφm fronty Φekajφcφch klient∙ (tzv. backlog) volßnφm funkce listen hned po vytvo°enφ "naslouchacφho" socketu a jeho svßzßnφ s portem. Parametry funkce listen jsou deskriptor socketu a dovolen² poΦet Φekajφcφch klient∙. Jako tento poΦet m∙₧eme pou₧φt konstantu SOMAXCONN, Φφm₧ se velikost fronty nastavφ na co nejv∞tÜφ hodnotu, kterou systΘm uznß za p°im∞°enou. Funkci listen je mo₧nΘ volat i opakovan∞ a m∞nit tak velikost fronty pr∙b∞₧n∞.

/* vytvoreni socketu a volani bind */
listen(listen_sock, 8);
/* cyklus s volßnφm accept */
K zßklad∙m problematiky server∙ je to vÜe.

Prßce v asynchronnφm re₧imu

Do tΘto chvφle jsme probrali vÜechny nejd∙le₧it∞jÜφ dovednosti, kterΘ jsou pot°ebnΘ k tvorb∞ b∞₧n²ch sφ¥ov²ch aplikacφ. Stßle to ale k napsßnφ vlastnφho webovΘho prohlφ₧eΦe nebo FTP klienta tak docela nestaΦφ. V∞tÜina probran²ch funkcφ toti₧ zp∙sobuje blokovßnφ zbytku programu, Φasto i po dost dlouhou dobu. A to urΦit∞ nenφ to, z Φeho by potencißlnφ u₧ivatelΘ naÜich program∙ byli nadÜeni. ╪eÜenφm je prßce v neblokujφcφm re₧imu. Ve zbytku tohoto Φlßnku se budeme zab²vat prßv∞ p°evedenφm naÜich dosud zφskan²ch dovednostφ do neblokujφcφho re₧imu.

Ve skuteΦnosti knihovna WinSock nabφzφ hodn∞ variant neblokujφcφho re₧imu. My se budeme v zßjmu jednoduchosti a p°φmoΦarosti pln∞ soust°edit na jedin² postup - asynchronnφ operace s vyu₧itφm zprßv Windows. Jednß se o mechanismus, kter² je mo₧nΘ pom∞rn∞ efektivn∞ a jednoduÜe zaΦlenit do aplikace, bez nutnosti rozd∞lovat prßci do vφce vlßken.

Shr≥me si, kterΘ pot°ebnΘ operace jsou blokujφcφ a pot°ebujφ b²t urΦit²m zp∙sobem p°epracovßny:

PopiÜme si obecn∞ postup p°i prßci v asynchronnφm re₧imu. V prvnφ °ad∞ je nutnΘ si nadefinovat konstantu pro novou zprßvu Windows, kterß nßm bude oznamovat spln∞nφ urΦitΘ operace. Jejφ hodnotu odvozujeme od konstanty WM_USER, kterß ve Windows API urΦuje dolnφ mez hodnot u₧ivatelem definovan²ch zprßv.
Nap°φklad si m∙₧eme definovat takovΘto zprßvy:

#define WM_ADDRESS 		(WM_USER + 1)
#define WM_SOCKETEVENT 	(WM_USER + 2)
#define WM_ACCEPT		(WM_USER + 3)
Tyto zprßvy pak m∙₧eme zaregistrovat pro pat°iΦnΘ operace. P°i volßnφ t∞chto operacφ pak nedojde k zablokovßnφ programu. Zprßvy zaslanΘ oknu aplikace nßs budou informovat o dokonΦenφ operacφ a budou zajiÜ¥ovat synchronizaci dalÜφ sφ¥ovΘ prßce.

Z v²Üe uveden²ch operacφ, kterΘ nßs budou zajφmat, se od ostatnφch trochu liÜφ p°eklad domΘnovΘho jmΘna. Ten toti₧ nenφ spojen s ₧ßdn²m socketem, a proto je i jeho asynchronnφ provedenφ odliÜnΘ. P°eklad domΘnovΘho jmΘna bude prvnφ operacφ v asynchronnφm re₧imu, na kterou se podφvßme.
Mφsto funkce gethostbyname pou₧ijeme jejφ asynchronnφ obdobu WSAAsyncGetHostByName. Prvnφm parametrem je deskriptor okna, kterΘ obdr₧φ zprßvu v okam₧iku, kdy bude p°eklad jmΘna proveden. Druh²m parametrem je nßmi definovanß hodnota tΘto zprßvy. T°etφ parametr je to nejd∙le₧it∞jÜφ - ukazatel na nulou ukonΦen² °et∞zec obsahujφcφ domΘnovΘ jmΘno. P°edposlednφ parametr musφ obsahovat ukazatel na p°ipraven² buffer, kter² bude vypln∞n informacemi o po₧adovanΘ adrese. DΘlka tohoto bufferu by se m∞la rovnat konstant∞ MAXGETHOSTSTRUCT. Za poslednφ parametr dosadφme velikost naÜeho bufferu, tedy nejspφÜ prßv∞ konstantu MAXGETHOSTSTRUCT. Funkce okam₧it∞ (bez Φekßnφ) vracφ v²sledek, tentokrßt nulu v p°φpad∞, ₧e asynchronnφ operace nebyla v∙bec spuÜt∞na, v opaΦnΘm p°φpad∞ vrßtφ nenulovou hodnotu, kterou m∙₧eme pozd∞ji pou₧φt k identifikaci, o kterou adresu jde.
V okam₧iku, kdy p°iÜla odpov∞∩ z DNS serveru nebo doÜlo k chyb∞, urΦenΘ okno obdr₧φ definovanou zprßvu. Jejφ parametr wParam obsahuje tu hodnotu, kterou vrßtila funkce WSAAsyncGetHostByName, a v p°φpad∞, ₧e zrovna provßdφme vφce p°eklad∙ jmen, m∙₧eme podle tohoto parametru rozliÜit, o kterou adresu se vlastn∞ jednß. Hornφch 16 bit∙ parametru lParam obsahuje chybov² k≤d, v p°φpad∞ ·sp∞hu nulu. V tΘto chvφli m∙₧eme, pokud nedoÜlo k chyb∞, p°eΦφst informace z bufferu, kter² jsme prve p°ipravili.
Hodnotu adresy IP zφskßme stejn∞ p°φÜern²m typecastem jako v p°φpad∞ blokujφcφ operace.

NejlepÜφ bude podφvat se na p°φklad:


#define WM_ADDRESS 	(WM_USER + 1)

union {
  char buf[MAXGETHOSTSTRUCT];
  HOSTENT he;
}
unsigned long ip_addr;

/* telo funkce zprav okna */
...
case WM_ADDRESS: 
  /* pokud nenastala chyba... */
  if (HIWORD(lParam) == 0)
    ip_addr = *((unsigned long *) he.h_addr);
    /* ted teprve muzeme s adresou IP pracovat */
    ...

/* telo jine funkce - zadost o preklad jmena */
...
WSAAsyncGetHostByName(hWnd, WM_ADDRESS, "www.chip.cz", buf, MAXGETHOSTSTRUCT);
/* tady jeste nemame zadny vysledek */
/* musime cekat na zpravu okna */

Zprßvy socketu

Asynchronnφ operace nad sockety jsou naÜt∞stφ o n∞co mßlo jednoduÜÜφ ne₧ operace naposled probranß. Ka₧d² socket musφme zaregistrovat pro asynchronnφ operace pomocφ funkce WSAAsyncSelect. Dßle u₧ budeme pracovat se stejn²mi funkcemi jako v re₧imu blokujφcφm, tedy connect, shutdown, send, sendto, recv, recvfrom a accept. Musφme si jen zvyknout na jejich mφrn∞ odliÜnΘ reakce a soust°edit se na jejich sprßvnou synchronizaci.

Funkci WSAAsyncSelect zavolßme hned po vytvo°enφ socketu. Prvnφm parametrem je socket, kter² tφmto uvßdφme do asynchronnφho re₧imu. Nßsleduje handle okna, kterΘ bude dostßvat zprßvy t²kajφcφ se danΘho socketu. DalÜφm parametrem je zprßva, kterß bude zasφlßna. Poslednφ a velmi d∙le₧it² parametr je seznam udßlostφ, o nich₧ chceme b²t informovßni prost°ednictvφm tΘto zprßvy (ta je pro vÜechny druhy udßlostφ stejnß). Za tento parametr dosazujeme konstanty, kterΘ m∙₧eme kombinovat pomocφ operßtoru bitovΘho logickΘho souΦtu. Nßs zajφmajφ tyto:

FD_CONNECT Oznßmenφ, ₧e doÜlo k ·sp∞ÜnΘmu p°ipojenφ socketu k serveru TCP. Po p°φchodu zprßvy s tφmto oznßmenφm m∙₧eme zaΦφt se socketem pracovat.
FD_READ Oznßmenφ, ₧e na socketu jsou k dispozici p°φchozφ data, kterß m∙₧eme okam₧it∞ p°eΦφst. V reakci na tuto udßlost m∙₧eme zavolat funkci recv (p°φpadn∞ recvfrom).
FD_WRITE Oznßmenφ, ₧e je volnΘ mφsto ve v²stupnφm bufferu socketu. M∙₧eme tedy s ·sp∞chem zavolat funkci send (p°φpadn∞ sendto).
FD_CLOSE Oznßmenφ konce spojenφ nßsledkem volßnφ funkce shutdown. V tΘto chvφli je bezpeΦnΘ zavolat closesocket.
FD_ACCEPT Informace o p°φchozφm spojenφ od klienta. Toto spojenφ m∙₧eme p°ijmout pomocφ funkce accept.

Pokud dojde k libovolnΘ z udßlostφ zaregistrovan²ch pro socket, je urΦenΘmu oknu zaslßna zprßva. Jejφ parametr wParam znaΦφ socket, k n∞mu₧ zprßva p°φsluÜφ (m∙₧eme mφt vφce socket∙ registrovan²ch se stejn²m oknem). Dolnφch 16 bit∙ parametru lParam popisuje udßlost, kterß nastala - je to jedna z v²Üe uveden²ch konstant. Hornφch 16 bit∙ stejnΘho parametru obsahuje eventußlnφ chybov² k≤d.
Opakovan²m volßnφm funkce WSAAsyncSelect m∙₧eme m∞nit okno, se kter²m je socket registrovßn, nebo konfiguraci udßlostφ, kterΘ nßs zajφmajφ. Sockety vytvo°enΘ funkcφ accept d∞dφ stejnou registraci udßlostφ, jakou m∞l socket, na kterΘm server naslouchß. Proto je vhodnΘ po volßnφ accept p°eregistrovat nov² socket podle naÜich pot°eb.
Funkce WSAAsyncSelect vracφ nulu nebo SOCKET_ERROR (co to znamenß, u₧ Φtenß° vφ).

P°φklad inicializace socketu klienta TCP (pot°ebujeme zprßvy pro navßzßnφ a zruÜenφ relace a p°enos dat):


SOCKET client_sock;
...
client_sock = socket(AF_INET, SOCK_STREAM, 0);
WSAAsyncSelect(client_sock, hWnd, WM_SOCKETEVENT, FD_CONNECT | FD_CLOSE | FD_READ | FDWRITE);
...
Inicializace socketu klienta nebo serveru UDP (odpadß sprßva spojenφ):

SOCKET udp_sock;
...
udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
WSAAsyncSelect(udp_sock, hWnd, WM_SOCKETEVENT, FD_READ | FD_WRITE);
...
Inicializace socketu TCP pou₧itΘho k naslouchßnφ (jde o socket se zvlßÜtnφm v²znamem, tak₧e m∙₧eme pou₧φt odliÜnou zprßvu):

SOCKET listen_sock;
...
listen_sock = socket(AF_INET, SOCK_STREAM, 0);
WSAAsyncSelect(listen_sock, hWnd, WM_ACCEPT, FD_ACCEPT);
...
P°φjem spojenφ serverem TCP (pot°ebujeme zm∞nit registraci novΘho socketu):

SOCKET listen_sock, serv_sock;
...
serv_sock = accept(listen_sock, NULL, NULL);
WSAAsyncSelect(serv_sock, hWnd, WM_SOCKETEVENT, FD_READ | FD_WRITE | FD_CLOSE);
...
Te∩ bude vhodnΘ zmφnit se o chybov²ch k≤dech knihovny WinSock. Zatφm jsme si °φkali jen tolik, ₧e n∞jakß funkce selhala. Ve WinSock mßme k dispozici funkci WSAGetLastError, kterß nemß parametry a vracφ k≤d poslednφ chyby, kterß nastala. Kdo mß zßjem, m∙₧e se podφvat na v²znam chybov²ch k≤d∙ do hlaviΦkovΘho souboru winsock.h a do dokumentace API (soubor nßpov∞dy sock2.hlp). My v tΘto situaci pot°ebujeme znßt jedin² chybov² k≤d, WSAEWOULDBLOCK. M∙₧e se nßm toti₧ stßt, ₧e n∞kterß operace v asynchronnφm re₧imu oznßmφ chybu. (Myslφm tφm nßvratovou hodnotu funkce jako recv nebo send, nemßm na mysli chybov² k≤d, jen₧ je souΦßstφ zprßvy.) Pokud zjistφme jako poslednφ chybov² k≤d prßv∞ WSAEWOULDBLOCK, je ve skuteΦnosti vÜechno v po°ßdku, nedoÜlo k ₧ßdnΘ vß₧nΘ chyb∞. Jde o informaci, ₧e funkci nelze v tomto okam₧iku provΘst, proto₧e by musela blokovat. Musφme tedy Φekat na p°φchod pat°iΦnΘ zprßvy, nap°φklad o p°ipravenosti ke Φtenφ nebo zßpisu, a potom volßnφ opakovat.

Cel² trik asynchronnφho re₧imu je v tom, ₧e vÜechny jinak blokujφcφ operace volßme jen v okam₧iku, kdy mohou b²t provedeny bezprost°edn∞, bez Φekßnφ. Tak t°eba funkci recv volßme jen potΘ, co vφme, ₧e dorazila n∞jakß p°φchozφ data. Naopak funkci send volßme, kdy₧ se ve v²stupnφm bufferu uvolnilo mφsto. Hlavnφm rozdφlem proti blokujφcφ variant∞ je rozkouskovßnφ p∙vodnφ sekvence operacφ do vφce mφst v programu. Tato sekvence bude rozd∞lena v mφstech p∙vodnφch volßnφ blokujφcφch funkcφ a logicky navazujφcφ k≤d bude umφst∞n v bloku pro obsluhu zprßvy oznamujφcφ dokonΦenφ p°edeÜlΘ operace. Nep°φjemnΘ je, ₧e stejnß udßlost m∙₧e nastat v mnoha odliÜn²ch situacφch. Proto si aplikace musφ uchovßvat urΦitou stavovou informaci, aby si pamatovala, co zrovna d∞lß, a aby v∞d∞la, jak²m k≤dem mß navßzat p°i oznßmenφ udßlosti. Nap°φklad FTP klient by mohl mφt stavy typu:

FTP klient dostßvß mnoho oznßmenφ o mo₧nosti zßpisu nebo Φtenφ dat, ale jen dφky stavovΘ informaci vφ, co mß zrovna Φφst nebo zapisovat.

P°ipojovßnφ a odpojovßnφ

P°edve∩me si scΘnß° provßd∞nφ konkrΘtnφch pot°ebn²ch operacφ. ZaΦneme p°ipojenφm klienta k serveru typu TCP. Po sprßvnΘ inicializaci asynchronnφho socketu klienta zavolßme funkci connect standardnφm zp∙sobem. Bezprost°edn∞ po jejφm zavolßnφ ale nesmφme se socketem pracovat, proto₧e p°ipojovßnφ v tomto okam₧iku teprve probφhß mimo naÜe v∞domφ. V dalÜφ prßci se socketem, nap°φklad zaslßnφ n∞jakΘho p°φkazu serveru, pokraΦujeme a₧ po zachycenφ zprßvy s oznßmenφm FD_CONNECT. Jestli₧e mßme v akci vφce socket∙, nezapomeneme zkontrolovat handle socketu p°enßÜen² se zprßvou v parametru wParam.

/* funkce pro obsluhu zprav */
...
case WM_SOCKETEVENT:
  /* vyber druhu udalosti */
  ...
  case FD_CONNECT:
    /* nedoslo-li k chybe... */
    if (HIWORD(lParam) == 0) 
      /* nasleduje dalsi prace se socketem */
      ...

/* jine misto v programu */
...
connect(clnt_sock, (SOCKADDR_IN *) &si, sizeof si);
/* pokracujeme po oznameni udalosti FD_CONNECT */
Kdy₧ budeme uzavφrat spojenφ socketu TCP, zavolßme funkci shutdown. Ta, jak u₧ m∙₧eme Φekat, nepoΦkß, a₧ bude spojenφ zruÜeno, pouze tento proces nastartuje. Ve chvφli, kdy je spojenφ opravdu zav°eno, obdr₧φme zprßvu s oznßmenφm FD_CLOSE. V reakci na ni m∙₧eme zav°φt socket pomocφ closesocket. Dokud nenφ socket °ßdn∞ zav°en, nem∞la by aplikace nechat zav°φt svΘ hlavnφ okno. (Nebo m∙₧e okno skr²t, a teprve po zruÜenφ socketu je skuteΦn∞ odstranit a skonΦit svou Φinnost. Spokojen² tak bude jak u₧ivatel, tak i systΘm.) Zlomkovit² v²pis k≤du u₧ dßle neuvßdφm, proto₧e je v podstat∞ po°ßd stejn² - nap°ed zavolßnφ neblokujφcφ funkce, potom reakce na udßlost.

Kdy₧ u₧ jsme v takovΘm tempu, proberme i p°φjem spojenφ serverem. Je-li naslouchajφcφ socket sprßvn∞ zaregistrovßn pro obsluhu udßlosti FD_ACCEPT, dostane sp°φzn∞nΘ okno toto oznßmenφ v₧dy, kdy₧ nov² klient ₧ßdß o navßzßnφ spojenφ. V takovΘ situaci bez obav zavolßme klasickou funkci accept, kterß prob∞hne neblokujφcφm zp∙sobem, a m∙₧eme ihned pokraΦovat dalÜφmi akcemi.

P°enos dat v asynchronnφm re₧imu

KoneΦn∞ nßm zb²vß to nejd∙le₧it∞jÜφ, p°enos dat v obou sm∞rech. Kdy₧ budeme posφlat data, musφme b²t v podstat∞ v jakΘmkoli okam₧iku p°ipraveni na vrßcenφ chybovΘ hodnoty. Pokud dojde k chyb∞ WSAEWOULDBLOCK, musφme poΦkat, a₧ dostaneme dalÜφ oznßmenφ o mo₧nosti zßpisu. Po ka₧dΘm volßnφ funkce send zaznamenßme poΦet skuteΦn∞ p°enesen²ch byt∙, abychom v∞d∞li, kterou Φßst dat jsme p°enesli, a kde mßme pokraΦovat p°i p°φÜtφm "povolenφ" k zßpisu. Oznßmenφ FD_WRITE je aplikaci doruΦeno jednak hned po p°ipojenφ socketu, jednak po ka₧dΘm ne·sp∞ÜnΘm pokusu o zßpis, potΘ, co se uvolnφ mφsto ve v²stupnφm bufferu. Z toho vidφme, ₧e v²skyt chyby WSAEWOULDBLOCK je nejen normßlnφ, ale pro funkci aplikace i nutn².

Postup p°i Φtenφ dat je analogick². V₧dy, kdy₧ dorazφ n∞jakß data, dostane aplikace zprßvu FD_READ. PotΘ zavolßme neblokujφcφ funkci recv (nebo snad recvfrom). K naΦtenφ pot°ebnΘ zprßvy nebo bloku dat bude Φasto pot°ebnΘ mnohonßsobnΘ volßnφ funkce recv a sklßdßnφ p°ijat²ch blok∙ dat za sebe. Aplikace musφ z obsahu zprßvy vyhodnotit, kdy jsou pot°ebnß data kompletnφ.

Konec

Ode mne je to vÜe. DalÜφ informace o programovßnφ sφ¥ov²ch aplikacφ m∙₧ete najφt v dokumentaci k API WinSock, kterß je dodßvßna s b∞₧n²mi v²vojov²mi nßstroji pro Windows. Dßle doporuΦuji web s informacemi o programovßnφ ve WinSock - WinSock Programmer's FAQ. Jestli₧e budete pot°ebovat technickΘ detaily o aplikaΦnφch protokolech a sφ¥ov²ch technologiφch v∙bec, zkuste databßzi dokument∙ RFC. Jinak jsou (jako v₧dy) k dispozici diskusnφ skupiny a internetovΘ vyhledßvaΦe. Kdo hledß, najde.
Defnitivnφ konec.

Ond°ej Hrabal (e-mail)