Ka₧d² p°ipojen² socket ve WinSock je ve skuteΦnosti charakterizovßn p∞ti informacemi. Jsou to:
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.
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.
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: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 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.
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).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 zßklad∙m problematiky server∙ je to vÜe./* vytvoreni socketu a volani bind */ listen(listen_sock, 8); /* cyklus s volßnφm accept */
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:
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.#define WM_ADDRESS (WM_USER + 1) #define WM_SOCKETEVENT (WM_USER + 2) #define WM_ACCEPT (WM_USER + 3)
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 */
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):
Inicializace socketu klienta nebo serveru UDP (odpadß sprßva spojenφ):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 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 udp_sock; ... udp_sock = socket(AF_INET, SOCK_DGRAM, 0); WSAAsyncSelect(udp_sock, hWnd, WM_SOCKETEVENT, FD_READ | FD_WRITE); ...
P°φjem spojenφ serverem TCP (pot°ebujeme zm∞nit registraci novΘho socketu):SOCKET listen_sock; ... listen_sock = socket(AF_INET, SOCK_STREAM, 0); WSAAsyncSelect(listen_sock, hWnd, WM_ACCEPT, FD_ACCEPT); ...
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.SOCKET listen_sock, serv_sock; ... serv_sock = accept(listen_sock, NULL, NULL); WSAAsyncSelect(serv_sock, hWnd, WM_SOCKETEVENT, FD_READ | FD_WRITE | FD_CLOSE); ...
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:
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./* 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₧ 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.
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φ.
Ond°ej Hrabal (e-mail)