Content extract
Bevezetés a Windows socketek programozásába Levendovszky Tihamér 2000. December 5 Jelen dokumentum szabadon terjeszthető, de felhasználás esetén a hivatkozás kötelező. Az esetleges megjegyzéseket és észrevételeket a tihamer@avalon.autbmehu címen köszönettel veszem. Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába 1. ÁLTALÁNOS ÁTTEKINTÉS 3 1.1 A HÁLÓZATELEMZÉS MODELLJEI 3 1.2 A TCP/IP PROTOKOLL 6 1.21 A TCP/IP protokoll felépítése és csomagformátumai 6 1.211 Az IP csomag 7 1.212 Az IMCP 8 1.213 Az UDP csomag 8 1.214 A TCP csomag [2] 9 1.22 A TCP/IP hálózat működése 10 1.221 A névfeloldás 10 1.222 Inverz névfeloldás 11 2. BEVEZETÉS A MICROSOFT WINDOWS HÁLÓZATOK PROGRAMOZÁSÁBA 12 2.1 A WINSOCK API PROGRAMOZÁSA 12 2.11 A Winsock inicializálása 12 2.12 Socketek létrehozása 13 2.13 A kapcsolat felállítása és lebontása 14 2.14
Kommunikáció Winsock alatt 15 2.141 A select modell 16 2.142 A WSAAsyncSelect modell 17 2.143 A WSAEventSelect modell 18 2.144 Az átfedett (overlapped) modell 19 2.1441 Értesítés eseményobjektumokkal 20 2.1442 Értesítés végrehajtó függvénnyel 21 2.2 SOCKET PROGRAMOZÁS A MICROSOFT FOUNDATION CLASSES ALKALMAZÁSÁVAL 22 2.21 A CAsyncSocket osztály 22 2.22 A CSocket osztály 23 2.23 A konkrét megvalósítás kérdései 25 2.24 Esettanulmány: Chat alkalmazás készítése 26 2.241 A szerver elkészítése 26 2.242 A kliens létrehozása 27 Irodalomjegyzék: [1] Tannenbaum, Andrew S.: Computer Networks, First Edition, Prentice Hall, 1980 [2] Tannenbaum, Andrew S.: Számítógép Hálózatok, 3 kiadás, Panem Könyvkiadó Kft, 1999 [3] Butzen, Fred és Hilton, Christopher: Linux hálózatok, Kiskapu Kft., 1999 [4] Jones, Anthony and Ohlund, Jim: Network Programming for Microsoft Windows, Microsoft Press, 1999 [5] Tannenbaum, Andrew S. és Woodhull, Albert S:
Operációs Rendszerek, 2 kiadás, Panem Könyvkiadó Kft, 1999 [6] MSDN Library January, 2000 2 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába 1. Általános áttekintés 1.1 A hálózatelemzés modelljei A hálózatok szabványosítása révén az International Standards Organization (Nemzetközi Szabványügyi szervezet, ISO) által bevezetett modell szolgált alapul a számítógép-hálózatok területén. A modell neve Open System Interconnection (OSI, Nyílt Rendszerek Összekapcsolása), amit az 1.1 ábra tekint át[2]: 1.1 ábra: Az ISO OSI modell 3 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába Az OSI modellt a következő alapelvek határozták meg: A rétegek jól definiált feladatot hajtsanak végre, amely feladatok különböző absztrakciós szinteknek
feleljenek meg. Ez a függőleges irányú modularitást, a réteges szerkezetet szolgálja Ezek a rétegek minimális információcserét bonyolítsanak le. A cél az volt, hogy a különböző feladatok különböző rétegbe kerüljenek, ugyanakkor a túlzott szétválasztás miatt az architektúra ne legyen kezelhetetlen. Az OSI modell kifejezetten alkalmas számítógép-hálózatok elemzésére, mert nagy hangsúlyt fektet a szolgálatokra, amely azt definiálja, hogy egy adott réteg mit csinál, a réteg interfészeire, amely specifikálja, a réteg feletti folyamatoknak, hogy milyen módon érik el a réteg szolgálatait, valamint a protokollokra, amelyek azt határozzák meg, hogy miként valósul meg a szolgálat. A réteg belső működésére bármilyen protokoll használható, amivel meg tudja valósítani szolgálatait. A protokoll tehát azt határozza meg, hogy az összes lehetséges helyzetben pontosan mit kell tenni ahhoz, hogy megvalósítsuk az adott szolgálatot.
Az egyes rétegek szolgálatait két részre osztjuk: összeköttetés alapú (connectionoriented) és összeköttetés nélküli (connectionless) szolgálatokra [2]. Az összeköttetés alapú szolgálat egy csővezetékhez hasonlít: amit az egyik végén beraktunk, az a másik végén távozik. Ehhez előbb létre kell hozni az összeköttetést (a „csővezetéket”), majd az adattovábbítás befejeztével le kell bontanunk. Az összeköttetés nélküli szolgálat a postai levelezőrendszerhez hasonlít: minden egyes üzenet rendelkezik a célállomás címével, ahova független és esetleg különböző útvonalon jut el. Így előfordulhat, hogy az elsőnek elküldött üzenet a második után érkezik, vagyis az üzenetek sorrendje felcserélődik, ami összeköttetés alapú szolgálat esetén elképzelhetetlen lenne. Minden szolgálat jellemezhető egy szolgálati minőséggel (Quality of Service, QoS). A QoS alapján megkülönböztetünk megbízható és megbízhatatlan
szolgálatokat. A szolgálatokat nyugtázással teszik megbízhatóvá: minden egyes üzenetre egy „nyugta” kell érkezzen válaszként. A megbízható összeköttetés alapú szolgálatok üzenetsorozat vagy bájtfolyam lehet attól függően, hogy megmaradnak-e az üzenethatárok. Üzenetsorozat esetén két egymás utáni 1 KB-os üzenet két 1 KB-os üzenetként érkezik meg, bájtfolyam esetén egy 2 KB-osként. Összeköttetés nélküli szolgálat esetén a megbízhatatlan szolgálatot a távirat analógiájára datagram szolgálatnak nevezik. A megbízható összeköttetés mentes szolgálatot nyugtázott datagram szolgálatnak hívjuk. A teljesség kedvéért megemlítjük a kérés-válasz szolgálatot, amit főként a kliens/szerver modellben használnak: a kliens elküld egy kérés datagramot (például egy SQL lekérdezés), amire a szerver (példánkban az adatbázis szerver) válaszol (jelen esetben a lekérdezésnek megfelelő rekorddal). Összeköttetés alapú
Összeköttetés mentes szolgálat szolgálat Megbízható üzenetfolyam Megbízhatatlan datagram Megbízható bájtfolyam Nyugtázott datagram Megbízhatatlan összeköttetés Kérés-válasz 1.1 Táblázat: Szolgálatok típusai Sajnos azonban az OSI modell protokolljai nem lettek igazán népszerűek. A szakemberek nem tudtak kiigazodni a bonyolult protokollokon, és az egyes rétegek feladat-hozzárendelése sem volt igazán világos. A modell kidolgozásakor nem vették figyelembe a szoftver és hardver lehetőségeket sem, így az OSI modell nehezen implementálható lett. 4 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába Ugyanakkor az elsőként a Berkeley UNIX operációs rendszerben implementált TCP/IP (Transmission Control Protocol/Internet Protocol, Átvitel Vezérlési Protokoll/Internet Protokoll) protokoll a gyakorlatban elterjedt, de facto szabvány lett. Míg az
OSI-nál a modell előbb létezett, mint a protokollok és az implementáció, a TCP/IP modell ugyanakkor gyakorlatilag nem is létezik, de a protokollok nagyon elterjedtek. Emiatt nem használható új technológiákon alapuló hálózatok tervezésénél. A két modell összevetését láthatjuk az 1.2 ábrán 1.2 ábra: Az OSI modell és a TCP/IP összevetése 5 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába Kompromisszumként [2] egy hibrid modellt javasol, amely az OSI modell egy módosítása, és lehetővé teszi a TCP/IP kényelmes tárgyalását is. Ezt láthatjuk az 13 ábrán: 1.3 ábra: A hibrid modell rétegei A továbbiakban a hibrid modellt vesszük alapul az IP protokoll részletes tárgyalásához. A különféle hálózatok részletes tárgyalása megtalálható [2]-ben. 1.2 A TCP/IP protokoll A TCP-IP alapú protokollok az amerikai Védelmi Minisztériumhoz
tartozó Fejlett Védelmi Rendszerek Kutató Központ [Advanced Research Projects Agency (ARPA)] vezetésével jött létre a 60-as, 70-es években. A Pentagon célja egy olyan katonai összeköttetések létrehozására alkalmas számítógép-hálózat létrehozása volt, amely szélsőséges esetekben is megállja a helyét. Ezért meg kellett felelnie az alábbi követelményeknek [3]: A hálózat nem lehet centralizált, mert a központ megsemmisítése végzetes következményekkel jár. Az összeköttetés megbízható legyen, az adott üzenetnek mindenféleképpen meg kell érkeznie. A Pentagon külön kívánsága volt a kommunikáció aszinkron módja: az adatok küldése és fogadása egymástól független legyen. Később a Védelmi Minisztérium hozzájárult a protokoll Berkeley UNIX-ba implementálásához, amely az egyetemeken keresztül az egész világon elterjedve létrehozta az Internetet. 1.21 A TCP/IP protokoll felépítése és csomagformátumai A
hibrid modellből az adatkapcsolati és a fizikai réteg nem bír nagy jelentőséggel a Windows Socketek programozása szempontjából. A két alsó (és a többi) rétegről részletesebb tárgyalás a [2] irodalomban található. A hálózati réteg szempontjából az Internet Protocol és az Internet Control Message Protocol [Internet Vezérlőüzenet Protokoll, IMCP] lényeges számunkra. A szállítási rétegben kétféle protokollt részletezünk: ez a TCP (Transmission Control Protocol) és az UDP (User Datagram Protocol). 6 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába 1.211 Az IP csomag Az Internet Protocol megértéséhez tekintsük az IP csomagot az 1.4 ábrán 1.4 ábra: Az IP datagram szerkezete A verziószám a protokoll verziója. Jelenleg az IPv4 elterjedt, de az IPv6 bevezetése folyamatban van. Az IHL 32 bites szavakban megadja a fejrész hosszát A
darabeltolás a darab helyét adja meg, hogy a tördelt csomagokat sorrendbe lehessen rakni. Az élettartam mezőt minden egyes router csökkenti eggyel, majd amikor elérte 0-t, az megsemmisítő hoszt vagy router küld egy időtúllépés IMCP üzenetet. A az IP címek az 1.5 ábrán látható formátumban adottak: 1.5 ábra: Az IP címek formátuma 7 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába Az Internet Protocol minden egyes interfészhez (ez legtöbbször minden egyes hálózati kártyát jelent) egyedi IP címet rendel. Fent látható a címek osztályokhoz rendelése Az osztályok közötti különbség abban rejlik, hogy hány biten címezi meg a hálózatot, és hány biten a hosztot. Az A osztály 16777216 gép megcímzését teszi lehetővé, ugyanakkor csak 125 ilyen hálózat létezhet az Interneten. B osztályú hálózatnál csak 65536 gép lehet egy
hálózaton, ugyanakkor 16382 B osztályú hálózat létezik. A hexadecimális jelölés helyett amely az 15 ábra bitjeit jelenti inkább a négy, pontokkal elválasztott, emberi olvasásra alkalmasabb, az 1.5 ábra jobb oldalán található jelölést használjuk. Így az a wwwautbmehu szerver IP címe 1526670194, ami B osztályú IP cím. 1.212 Az IMCP Az IMCP csomag segítségével vezérlőüzeneteket küldhetünk. A fontosabb üzeneteket az 12 táblázat tartalmazza[2]. Üzenet típusa Cél elérhetetlen Időtúllépés Paraméter probléma Átirányítás Visszhang kérés Visszhang válasz Időbélyeg kérés Időbélyeg válasz Leírás A csomagot nem lehet kézbesíteni Az élettartam mező (ld. később) elérte a 0-át Érvénytelen fejléc Egy routert tanít meg a topológiára. Ha egy router észreveszi, hogy egy csomag rosszul irányított, értesíti az a küldő hosztot a feltételezett hibáról. Kérés, hogy a gép életben van-e (V.ö: ping) Igen, életben
vagyok Ugyanaz, mint a visszhang kérés, csak időbélyeggel Ugyanaz, mint a visszhang válasz, csak időbélyeggel 1.2 Táblázat: Az IMCP fontosabb üzenettípusai Minden IMCP üzenetet egy IP csomagba ágyaznak be. Az IMCP-t az RFC 792 részletezi 1.213 Az UDP csomag Az Internet összeköttetés nélküli szállítási protokollja az UDP. Létezik megbízhatatlan és nyugtázott formája is, általában a nyugtázott formát használják. Csomagformátuma az 16 ábrán található. 1.7 ábra: Az UDP csomag szerkezete 8 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába Amint az 1.6 ábrából kitűnik, az UDP nem tördeli az IP csomagokat, mindössze egy fejléccel ellátva továbbítja. 1.214 A TCP csomag [2] A TCP két pont közötti megbízható bájtfolyam kapcsolatot valósít meg. A TCP menedzseli az adatfolyamokat 64 KByte-ot meg nem haladó darabokra tördeli azokat.
Általában ez az érték a gyakorlatban 1500 byte körül mozog. A TCP csomagformátuma az 18 ábrán látható 1.8 ábra: A TCP protokoll csomagformátuma A TCP említésre méltó szolgáltatása a sürgős adat (urgent data, out-of-band data). Ilyenkor a TCP megszünteti az adatok gyűjtését, a buffer tartalmát elküldi, és „sürgős” jelzéssel látja el. A fogadó operációs rendszertől függően kezeli, az eredeti UNIX implementáció szerint az alkalmazás megszakad (signalt kap), és beolvassa az érkezett csomagokat, hogy megtalálja a sürgős adatot. Ezt az URG jelzőbit 1 értéke jelzi, a sürgősségi mutató ilyenkor a sügős adat csomagon belüli elhelyezkedése. Az ACK a nyugtát jelzi, a PSH azt kérheti, hogy a vevő nem buffereljen, az RST valamilyen problémát jelent (például kapcsolat összeomlása) a SYN az összeköttetés létesítésére, a FIN annak lebontására szolgál. Az ablakméret a forgalomszabályozásra szolgál: mekkora az a
maximális adatmennyiség, ami még elküldhető a másik félnek. Ez a különböző teljesítményű gazdagépek (host) összehangolását teszi lehetővé 9 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába Maga a TCP bájtfolyam kapcsolat úgy valósul meg, hogy mind a küldő létrehoz egy socketnek („csatlakozó”) nevezett végpontot. Minden socketnek van egy címe, ami a hoszt IP címe és egy 16 bites szám, az ún. port azonosító A cél port és a forrás port a vevő, illetve a küldő port azonosítóját jelenti. Az 19 ábrán látható egy példa a TCP portok közötti virtuális kapcsolatokra különböző port azonosítókkal. 1.9 ábra: Hosztok TCP portjai közötti virtuális kapcsolat Az 1024-nél kisebb számú portokat jól ismert portoknak (well-known ports) nevezzük, amelyek meghatározott szolgálatokra vannak fenntartva. Az alkalmazói programok
1024-től 49151-ig lévő tartományt használhatják (regisztrált portok – registered ports). A dinamikus és magánportok (Dynamic and Private Ports) a 49152- 65535 intervllumban helyezkednek el. A részletes portkiosztás az RFC 1700-as szabványleírásban található meg. Itt csak a TCP legszükségesebb jellemzőit adtuk meg, a [2] irodalom erről a témáról is részletes leírást ad. 1.22 A TCP/IP hálózat működése A TCP/IP hálózat tartalmaz routereket, amelyek a csomagok irányításáért felelősek, illetve névfeloldást végző hosztokat. Lássuk, mit is jelent, hogyan működik ez utóbbi szolgáltatás 1.221 A névfeloldás A hálózat IP címei a felhasználó számára idegennek és nehezen megjegyezhetőnek tűnik. Ezért a hosztokat felhasználóbarát, úgynevezett domain névvel látják el, például avalon.autbmehu Amennyiben a domain név után pont van, az abszolút domain nevet jelent, egyébként relatív a domain név. A domain név szolgáltatás
(Domain Name Service, DNS) feladata a domain névhez megadni az IP címet. Nézzük meg, hogyan találjuk meg az avalonautbmehu szerverről az altavista.com-ot Ez a következő lépésekben történik: 1. A gép hosts nevű file-jában található megfeleltetések alapján 2. Az operációs rendszer cache-eli a legutóbb használt domain nevek és IP címek összerendelését 3. Utána – mivel az altavistacom után nincs pont – relatív domain névként értelmezi, és az altavista.comautbmehu gépet keresi a hely hálózatban 4. Az előbbi lépések sikertelenségéből arra következtethetünk, hogy a gép az Internet másik szegletében található. Ezért egy hozzáadásával a relatív domain névből teljesen minősített domain nevet (Fully Qualified Domain Name, FQDN) képez, és elküldi ezt egy nagy nemzetközi domain név szervernek, például a A.ROOT-SERVERSNET-nek, aminek tudja az IP címét (198.4101) 5. Ezután megkérdezi, hogy melyik domain név szerver tartja
karban a com domain-t 6. A com domain név szervere, pedig tudja az altavistacom IP címét 10 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába 1.222 Inverz névfeloldás A névfeloldásnál jóval nehezebb feladat annak a kérdésnek a megválaszolása, hogy egy adott IP címhez milyen domain név tartozik, ugyanis az IP cím nem tartalmaz zónákra jellemző adatokat. Ennek megoldására létrehoztak egy in-addr.arpa nevű zónát, amely segítségével megkaphatjuk a kívánt domain nevet. Ha a 1526670194 IP címet viselő hoszt nevére vagyunk kíváncsiak, akkor képeznünk kell az in-addr.arpa nevét, ami az IP cím megfordításából, egy pontból és az inaddrarpa utótagból áll Példánk esetében ez 1947066152in-addrarpa nevet jelenti 11 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába
2. Bevezetés a Microsoft Windows hálózatok programozásába A Win32 felület számos hálózati protokollt támogat. Ezek név szerint: IP [Internet Protocol] IPX/SPX [Internet Packet Exchange/Sequenced Packet Exchange] NetBios AppleTalk ATM [Asyncronous Transfer Mode] Infravörös Socketek A fenti protokollok programozása sok tekintetben hasonló, azonban az alkalmazói programok elsöprő többsége IP-t használ, így a továbbiakban az IP tárgyalására szorítkozunk, az érdeklődő a [4] irodalomban talál részletes, forráskóddal illusztrált leírást az itt felsorolt protokollok mindegyikéről. 2.1 A Winsock API programozása A Winsock API-nak három különböző verziója van: 1.0, 11 és Winsock 2 A jelölési konvenciók az 1.1-es verziótól az, hogy a Winsock-hoz tartozó változók, makrók konstansok és függvények WSA előtaggal kezdődnek. Jelen jegyzetet mellékletként számos - főként [4]-ben található –
működő példa illusztrálja. A fejezet részletességének meghatározásakor feltételeztük a példák tanulmányozását. Általánosságban problémát jelent a különböző gépek különböző számábrázolása is. Ezért definiáltak egy hálózati byte sorrendet („a nagyobb van hátul” - big endian), ami történetesen éppen eltér az Intel által alkalmazottól („a kisebb van hátul” – little endian). Ezért amikor a hálózat számára értelmezendő adatot közlünk, vagy más típusú géppel kommunikálunk, végezzük el a konverziót a 2.1 táblázatban adott függvények segítségével Függvény Leírás ntohs 16 bites mennyiséget hálózati byte sorrendből átvált a gép byte sorrendjébe (Big-Endian Little-Endian). ntohl 32 bites mennyiséget hálózati byte sorrendből átvált a gép byte sorrendjébe (Big-Endian Little-Endian). htons 16 bites mennyiséget a gép byte sorrendjéből átvált hálózati byte sorrendbe (Little-Endian
Big-Endian). htonl 32 bites mennyiséget a gép byte sorrendjéből átvált hálózati byte sorrendbe (Little-Endian Big-Endian). 2.1 táblázat: Gépek közti számábrázolást konvertáló függvények 2.11 A Winsock inicializálása Használat előtt a int WSAStartup ( WORD wVersionRequested, LPWSADATA lpWSAData); függvényt kell meghívnunk. Az első paraméterben a magasabb helyiértékű byte az igényelt fő verziószám, az alacsonyabb a mellék verzió. Ezt legegyszerűbben a MAKEWORD(x,y) makró alkalmazásával érhetjük el, ahol az x a magas, y az alacsony helyiértékű byte. A 22-es verzió a 12 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába Win95-be még ingyenes upgrade 1 segítségével telepíthető, az afölötti NT és 9x verziók már alapértelmezésben támogatják. Így például a MAKEWORD(2,2) számunkra megfelelő paraméter. A második
paraméterben visszakapott WSAData struktúra felépítése a következő: typedef struct WSAData { WORD wVersion; WORD wHighVersion; char szDescription[WSADESCRIPTION LEN+1]; char szSystemStatus[WSASYS STATUS LEN+1]; unsigned short iMaxSockets; unsigned short iMaxUdpDg; char FAR * lpVendorInfo; } WSADATA, *LPWSADATA; Az első paraméter az a verziószám, amit használni fogunk, a második paraméter a legmagasabb rendelkezésre álló verzió. a többi paraméter egy része verziófüggő, és egyik sem különösebben érdekes számunkra. 2.12 Socketek létrehozása 2 A socketek számára Windows alatt egy külön típust hoztak létre: ez a SOCKET típus. Socketeket a WSASocket illetve a socket függvénnyel hozhatunk létre. A socket függvény deklarációja: SOCKET socket ( int af, int type, int protocol ); Az első paraméter a címcsalád (address family). UDP és TCP socket számára ez AFX INET A második paraméter SOCK STREAM, SOCK DGRAM és SOCK RAW lehet. Az utolsó
paraméter a protokoll. A 22 táblázat mutatja a címcsalád, a típus és a protokollok közötti összefüggést. Protokoll Címcsalád Típus Típus Protokoll megnevezése megnevezése Internet Protocol AF INET TCP SOCK STREAM IPPROTO IP Internet Protocol AF INET UDP SOCK DGRAM IPPROTO UDP Internet Protocol AF INET Nyers Socket SOCK RAW IPPROTO RAW Internet Protocol AF INET ICMP SOCK RAW IPPROTO ICMP 2.2 táblázat: Az Internet Protocol fajtái Win32 alatt Ezek után a táblázat alapján könnyen létrehozhatjuk a kívánt fajtájú socketet. 1 A Winsock 2 Upgrade Win95 alá a http://www.microsoftcom/windows95/downloads/ címen található A 2.12, a 213 fejezetet valamint a 214 fejezet bevezetőjét az összeköttetés alapú esetben a TCP, összeköttetés nélküli esetben a z UDP példa illusztrálja 2 13 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába 2.13 A kapcsolat
felállítása és lebontása A kliens és a szerver létrehozásához szükséges függvények a következő táblázatban láthatóak: Server Kliens socket/WSASocket socket/WSASocket bind listen (csak összeköttetés alapú) accept/WSAAccept (csak összeköttetés alapú) connect/WSAConnect 2.3 táblázat: Winsock szerver és kliens létrehozása összeköttetés alapú kapcsolat esetén Látható, hogy az alacsonyabb verzió mellé a Winsock 1.1-től kezdve WSA előtagú függvények társultak. Vegyük sorra a függvényeket! int bind ( SOCKET s, const struct sockaddr FAR * name, int namelen); A második paraméterben IP esetén a következő struktúrát kell cast-olnunk: struct sockaddr in { short sin family; u short sin port; struct in addr sin addr; char sin zero[8]; }; A sin family értéke IP esetén AF INET, a sin port a TCP port címe, a sin addr pedig az IP cím. A bind függvény hozzárendeli egy adott interfész adott TCP portjához az első paraméterben megadott
socket-et. Ha azt szeretnénk, hogy a socket várakozzon a bejövő hívásokra, a int listen (SOCKET s, int backlog ); hívjuk meg. Az első paraméter a bind függvénnyel már porthoz rendelt socket, a második az egy időben lehetséges maximális kapcsolatok száma. Ha több kapcsolódási kérelem érkezik, az WSAECONNREFUSED hibaüzenettel tér vissza, mert szerverünk visszautasította. Most már készen állunk arra, hogy fogadjunk egy összeköttetést. Ezt a SOCKET accept ( SOCKET s, struct sockaddr FAR *addr, int FAR addrlen ); függvénnyel tehetjük meg. Az s egy, a listen függvény segítségével figyelő állapotba állított socket. A második paraméter egy érvényes sockaddr in struktúra, amelyben a kliens adatait kapjuk a visszatérés után. Az utolsó paraméter a sockaddr in struktúra hossza Kliens esetén szintén létrehozzuk a socketet a már ismertetett socket függvény segítségével, majd meghívjuk a int connect (SOCKET s, const struct sockaddr
FAR *name, int namelen); függvényt. Az első paraméter egy, már létrehozott socket, a második egy sockaddr in struktúra, amely annak a szervernek az adatait tartalmazza, amelyhez kapcsolódni szeretnénk. Az utolsó paraméter ennek a struktúrának a hossza. Visszatérési értéke 0, ha sikerült a kapcsolódás, egyébként SOCKET ERROR. Utóbbi esetben a konkrét hibát a int WSAGetLastError (void); függvénnyel kérdezhetjük le. Az összeköttetést a int shutdown (SOCKET s, int how ); hívással bonthatjuk. A második paraméter az összeköttetés módját határozza meg: 14 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába SD RECEIVE, SD SEND és SD BOTH lehet attól függően, hogy a fogadás, küldés, illetve mindkét művelet befejeződött. Ezután a sockethez tartozó erőforrásokat a int closesocket (SOCKET s); függvénnyel szabadíthatjuk fel.
Összeköttetés nélküli protokoll esetén vevő oldalon létrehozzuk a socketet, meghívjuk a bind függvényt és a továbbiakba részletezett recvfrom vagy WSARecvFrom függvény alkalmazható. A másik vevő oldali megoldás a connect vagy a WSAConnect hívása, utána a később tárgyalt recv vagy WSARecv függvényt használjuk. Természetesen ez utóbbi megoldásban sem jön létre összeköttetés. Küldő oldalon a sendto vagy WSASendTo függvényekkel élhetünk, de itt is adott a lehetőség a connect vagy a WSAConnect hívására, ahonnan már csak a send és a WSASend használható. 2.14 Kommunikáció Winsock alatt Első közelítésben a következő küldő és vevő függvényeket használhatjuk: int send ( SOCKET s, const char FAR *buf, int len, int flags ); Ez a függvény elküldi a buf-ban levő len hosszúságú byte-sorozatot az s socketen keresztül. Ha a flags paraméter MSG OOB, akkor egy sürgős adatot (out-of-bound data, urgent data) küldünk. A send
párja a int recv (SOCKET s, char FAR *buf, int len, int flags); A flag paraméter itt lehet MSG PEEK is, ami azt jelenti, hogy nem veszi ki a bufferből a kiolvasott értéket, csak visszatér vele. Ez a rossz teljesítmény miatt nem ajánlott Összeköttetés nélküli protokollok esetén használható a int recvfrom ( SOCKET s, char FAR* buf, int len, int flags, struct sockaddr FAR *from, int FAR *fromlen ); aminek az utolsó két paramétere a figyelő socket címe és annak hossza. Küldésre használjuk a int sendto ( SOCKET s, const char FAR *buf, int len, int flags, const struct sockaddr FAR *to, int tolen ); függvényt, aminél a címzett paramétereit adjuk meg a to struktúrában. Win32 API alatt a socketek kétféle üzemmódban kommunikálhatnak: blokkolt (blocking) és blokkolatlan (nonblocking). Blokkolt üzemmód esetén egy hívás addig nem tér vissza, még az általa végrehajtandó műveletet be nem fejezte. Ez az üzemmód az alapértelmezett Blokkolatlan
mód esetén az adott függvény azonnal visszatér, és később valamilyen módon értesíti a hívót arról, hogy a művelet befejeződött. A művelet tényleges befejezéséig az adott szál más feladattal foglalkozhat. A blokkolatlan mód beállítása a unsigned long ul=1; int nRet; nRet = ioctlsocket(s, FIOBIO, &ul); if(nRet = = SOCKET ERROR) { // Error! } 15 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába Ahhoz, hogy kiválasszuk a socketek és üzemmódjaik számunkra legkedvezőbb felhasználását, a Winsock különböző programozási modelleket ajánl fel. Ezek neveit a 24 táblázat foglalja össze Modellek 1. select 2. WSAAsyncSelect 3. WSAEventSelect 4. Átfedett (overlapped) 5. Completion Port 2.4 táblázat: A Winsock I/O programozási modelljei Az ötödikként megadott modell csak az NT technológiával készült operációs rendszereken használható,
ezért átfogó jellegű ismertetésünkből mellőzzük. Ha egy blokkolatlan műveletet meghívunk, az rögtön visszatér, és valahogy értesülnünk kell arról, hogy sikeresen, vagy sikertelenül végrehajtódtak. Ez történhet úgy, hogy egy adott függvényt hívunk, és az állapítja meg, hogy történt-e valami, esetleg kérhetünk üzenetet az ablakkezelő függvényünk számára (window messages), netán szinkronizációs objektumok beállítását kérhetjük (például eseményobjektumokét), vagy az előbbi lehetőséggel kombinálva egy függvény címét is megadhatjuk, amelyet az operációs rendszer az adott művelet végrehajtása után meghív. Azt, hogy ezeket a lehetőségeket hogyan érdemes kihasználnunk, a következőkben részletezett modellek mutatják meg. 2.141 A select modell 1 A select modell legnagyobb előnye, hogy felülről kompatibilis a Berkeley UNIX socketeivel. Ennek a modellnek a kulcsa a int select ( int nfds, fd set FAR *readfds, fd set FAR
*writefds, fd set FAR *exceptfds, const struct timeval FAR *timeout ); függvény. Ha végrehajtódott egy blokkolatlan művelet, ez a függvény jelzi úgy, hogy felveszi egy, a műveletnek megfelelő listába. Az első paraméter lényegtelen, csak a kompatibilitás miatt került a függvénybe. Az fd set adattípus socketek gyűjteménye A második, harmadik és negyedik paraméter mindegyike egy lista, amelyek rendre az olvasás az írás illetve a sürgős adatok (urgent data, out-of-bound data) ellenőrzésére szolgálnak. Visszatérés után a readfds halmaz tartalmazza azokat a socketeket, amelyek esetében olvasásra váró adat áll rendelkezésre a kapcsolatot lezárták, újrakezdték (reset) vagy befejezték. egy listen függvényhívás után a várakozó server socket accept függvényhívása sikeres lesz. A writefds tartalmazza azokat a socketeket, amelyekre adat küldhető a kapcsolat (blokkolatlan connect függvényhívás után) sikeresen
létrejött 1 Ehhez a fejezethez a select példa tartozik 16 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába Végül az exeptfds elemei azok a socketek, amelyekre igaz, hogy blokkolatlan connect függvényhívás sikertelen sürgős adat áll rendelkezésre Az utolsó paraméter a timeout-ot jelöli. Típusa a struct timeval { long tv sec; long tv usec; }; struktúra, ahol az első tagváltozó másodpercben, a második microsecundumban értendő. Ha ez a paraméter NULL, a select nem tér vissza addig, amíg legalább egy socket eleme lesz valamelyik halmaznak. A visszatérési érték timeout esetén 0, ha végrehajtódott valamelyik blokkolatlan művelet, akkor a halmazok elemeinek száma, egyébként SOCKET ERROR. Az fd set-et manipuláló makrókkal könnyen beállíthatjuk a select paramétereit. FD CLR(s, *set) FD ISSET(s, *set) FD SET(s, *set) FD ZERO(*set) A
fenti első makró töröl egy s elemet set halmazból, az FD ISSET megállapítja, hogy az s elemee a set halmaznak, az FD SET hozzáad egy s elemet a set halmazhoz, míg az FD ZERO kiürít egy set halmazt. Ezek után a teendőnk létrehozni három üres halmazt, inicializálni őket az FD ZERO makróval, hozzáadni socketeinket az FD SET segítségével, majd megnézni az FD ISSET makróval, hogy milyen művelet ért véget vagy hajtható végre, és ennek megfelelően cselekedni. 2.142 A WSAAsyncSelect modell 1 Ehhez a modellhez egy ablakkezelő függvény szükséges. Ennek a modellnek a kulcsa a int WSAAsyncSelect (SOCKET s, HWND hWnd, unsigned int wMsg, long lEvent ); függvény. Ezzel megadhatjuk azt, hogy melyik socket után érdeklődünk, az ablakleírót, ami az üzenetet váró ablak HANDLE leírója, míg az lEvent a küldendő üzenet maszkja, azt határozza meg, hogy milyen eseményekre vagyunk kíváncsiak. A lényegesebb üzeneteket a 25 táblázat tartalmazza.
Esemény Leírás FD READ Adat érkezett, a socket olvasható FD WRITE A socket kész az írásra FD OOB Értesítés sürgős adatról (Out-of-band data, urgent data) FD ACCEPT Értesítés bejövő kapcsolatok FD CONNECT Értesítés a végrehajtott kapcsolatról FD CLOSE Értesítés a socket lezárásáról 2.5 táblázat: Az eseménymaszk lényegesebb elemei Az eseménymaszk a fentiek valamelyike, esetleg többjük OR kapcsolata. Ezek után üzenetet kapunk: az MSG struktúra message tagja WM SOCKET lesz, a wParam a socket azonosítója. Az lParam összetettebb: a magasabb helyiértékű szó jelent a hibát, az alacsonyabb helyiértékű szó a 2.4 táblázatból az eseménymaszkban is megjelölt konstansok valamelyike. Ezt megkönnyítendő két makró áll rendelkezésre: 1 A 2.142 fejezethez a WSAAsyncSelect példa kapcsolódik 17 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába
#define WSAGETSELECTERROR(lParam) #define WSAGETSELECTEVENT(lParam) HIWORD(lParam) LOWORD(lParam) Az FD WRITE üzenet akkor generálódik, miután egy socket kapcsolódott (kliens) egy socketet elfogadtunk (server) egy küldés sikertelen volt, és a küldő függvény visszatért WSAWOULDBLOCK hibával. Ezért az alkalmazásnak feltételezheti, hogy az küldés mindig megengedett az első FD WRITE után, mindaddig, amíg egy küldő függvény vissza nem tér WSAWOULDBLOCK hibával. A következő küldés a legközelebbi FD WRITE érkezése után lehetséges. 2.143 A WSAEventSelect modell 1 Ez a modell abban különbözik az előzőtől, hogy ablakoknak küldött üzenetek helyett eseményobjektumokkal kommunikál. Ehhez először létrehozunk egy eseményobjektumot: WSAEVENT WSACreateEvent (void); Ez a függvény az eseményobjektum WSAEVENT leírójával tér vissza. Ennek a modellnek a kulcsa a int WSAEventSelect (SOCKET s,WSAEVENT hEventObject, long
lNetworkEvents); függvény. Az első paraméter az adott socket, a másik az eseményobjektum, az utolsó pedig a 2.142 pontban taglalt FD XXX konstansok valamelyike Ez a függvény kézi (manual) üzemmódba állítja az eseményt, ezért a törlésről magunknak kell gondoskodnunk a BOOL WSAResetEvent ( WSAEVENT hEvent ); függvény meghívásával. Mikor végeztünk az eseményobjektummal, a BOOL WSACloseEvent ( WSAEVENT hEvent ); függvénnyel kell felszabadítanunk az eseményobjektumot. Az eseményobjektum jelzett (signaled) állapotára a DWORD WSAWaitForMultipleEvents ( DWORD cEvents, const WSAEVENT FAR *lphEvents, BOOL fWaitAll, DWORD dwTimeout, BOOL fAlertable ); függvénnyel várakozhatunk, melynek első paramétere az eseményobjektumok tömbjeinek száma, a második paraméterben maguk az eseményobjektumok szerepelnek. Ha az fWaitAll igaz, akkor a várakozási feltétel a tömbben levő összes esemény jelzett volta, ellenkező esetben elég, hacsak az egyik kerül
jelzett állapotba. Ez a függvény akkor tér vissza, ha az előbb részletezett várakozási feltétel teljesül, vagy a dwTimeout paraméterben meghatározott idő eltelik. Utóbbi esetben a visszatérési érték WSA WAIT TIMEOUT. Egyébként ha az fWaitAll hamis volt, a visszatérési érték az eseménytömb jelzett állapotba került elemének indexe összegezve a WSA WAIT EVENT 0 értékkel, egyébként WSA WAIT EVENT 0 és cEvents-1 között. Különben a visszatérési érték valamilyen hibát jelez. Miután kiderítettük, hogy egy adott sockethez esemény érkezett, meg kell állapítanunk, hogy mi történt pontosan. Ezt a 1 Az illusztráció az EventSelect példa 18 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába int WSAEnumNetworkEvents ( SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents ); függvénnyel kérdezhetjük le. A socket és az
eseményobjektum leírója után egy typedef struct WSANETWORKEVENTS { long lNetworkEvents; int iErrorCodes[FD MAX EVENTS]; } WSANETWORKEVENTS, *LPWSANETWORKEVENTS; típusú struktúrát kapunk vissza. A lNetworkEvents paraméter egy FD XXX esemény, vagy azok OR kapcsolatai, a második paraméter a hibakód. Ez utóbbi kiolvasására speciális makrók állnak rendelkezésre, amelyek az FD XXX BIT elnevezési konvenciót követik. Például egy olvasás eseményt így dolgozhatunk fel: if ( networkEvents.lNetworkEvents & FD READ ) { if ( NetworkEvents.iErrorCode[FD READ BIT] ! = 0 ) { printf ( “FD READ failed. Error code:%d”, NetworkEventsiErrorCode[ FD READ BIT ] ); } } 2.144 Az átfedett (overlapped) modell A teljesítőképesség szempontjából a legelőnyösebb az átfedett modell, amelynek működési elve a következő. Függetlenül a socket blokkolt illetve blokkolatlan állapotától egy átfedett művelet azonnal visszatér SOCKET ERROR-ral, és a int
WSAGetLastError (void); általános hibalekérdező függvény a WSA IO PENDING értékkel tér vissza. Utána kétféleképpen kaphatunk értesítést a socketről: eseményobjektummal és végrehajtó függvénnyel (completion routine). Elsőként hozzuk létre a socketet a SOCKET WSASocket ( int af, int type, int protocol, LPWSAPROTOCOL INFO lpProtocolInfo, GROUP g, DWORD dwFlags ); függvénnyel. A socket() függvénynél már láthattuk az első három paramétert, a g nem használt, a negyedik paraméter információt ad vissza a protokollról, ha nincs rá szükségünk, adjunk meg NULL értéket. Jelen esetben a dwFlags legyen WSA FLAG OVERLAPPED Nézzük ezek után a küldő és vevő függvényeket: int WSASend ( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, 19 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába
LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED COMPLETION ROUTINE lpCompletionRoutine ); int WSASendTo ( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, const struct sockaddr FAR *lpTo, int iToLen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED COMPLETION ROUTINE lpCompletionRoutine ); int WSARecv ( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED COMPLETION ROUTINE lpCompletionROUTINE ); int WSARecvFrom ( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, struct sockaddr FAR *lpFrom, LPINT lpFromlen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED COMPLETION ROUTINE lpCompletionROUTINE ); Megfigyelhetjük, hogy mindegyik művelet tartalmaz egy WSAOVERLAPPED struktúrát, vagy egy WSAOVERLAPPED COMPLETION ROUTINE-t. 2.1441 Értesítés eseményobjektumokkal 1 A fenti WSAOVERLAPPED struktúra
deklarációja a következő: typedef struct WSAOVERLAPPED { DWORD Internal; DWORD InternalHigh; DWORD Offset; DWORD OffsetHigh; WSAEVENT hEvent; } WSAOVERLAPPED, *LPWSAOVERLAPPED; 1 A 2.1441 fejezethez az Overlapped – Event példaprogram tartozik 20 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába Ebből a felhasználó csak a hEvent paramétert használhatja, amit a 2.143 pontban részletezett módon hozunk létre. A jól ismert módon a WSAWaitForMultipleEvents függvénnyel megvárjuk, hogy az operációs rendszer jelezze a végrehajtást, majd feladatunk a szokásos: megállapítani azt, hogy pontosan mi is történt. Ezt a BOOL WSAGetOverlappedResult ( SOCKET s, LPWSAOVERLAPPED lpOverlapped, LPDWORD lpcbTransfer, BOOL fWait, LPDWORD lpdwFlags ); függvényhívással állapíthatjuk meg. Ha a visszatérési érték FALSE, akkor a művelet még folyamatban van, vagy
hibával tért vissza, illetve rosszul paramétereztük a függvényt. Ha igaz, akkor az lpcbTransfer mutató értéke helyes, és a küldött vagy vett adatmennyiséget tartalmazza. Ha az fWait igaz, a WSAGetOverlappedResult addig vár, amíg a művelet be nem fejeződik. 2.1442 Értesítés végrehajtó függvénnyel 1 Azonban használhatjuk a lpCompletionROUTINE paramétert is, ekkor a WSAOVERLAPPED struktúra hEvent paramétere nem játszik szerepet. Ilyenkor megadunk egy void CALLBACK CompletionROUTINE ( DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags ); függvényt, amely a végrehajtási állapot, a második paraméter megegyezik az eseményobjektum használatánál tárgyaltaknál, az lpOverlapped a művelet meghívásakor megadott struktúra, a dwFlags mindig 0. Egy problémát kell még megoldanunk: ha a szál nincs riasztható (alertable) állapotban, akkor az operációs rendszer nem fogja meghívni a végrehajtó függvényt. Ezt a már
tárgyalt WSAWaitForMultipleEvents fAlertable paraméterével állíthatjuk be, vagy – ha nem használunk eseményobjektumot – a DWORD SleepEx( DWORD dwMilliseconds, BOOL bAlertable); API hívással. Ha a függvényt INFINITE első paraméterrel és TRUE második paraméterrel hívjuk meg, csak akkor tér vissza, ha a végrehajtó függvényünk meghívódott. 1 Ehhez a fejezethez az Overlapped – Callback példa tartozik 21 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába 2.2 Socket programozás a Microsoft Foundation Classes alkalmazásával Az MFC osztályok WSAAsyncSelect modellt használnak, amit egy rejtett ablak kezelésével oldanak meg. 2.1 ábra: Az MFC socket osztályok belsejében levő rejtett ablak UML osztálydiagramja Két MFC osztály áll rendelkezésünkre, amit a 2.2 ábra mutat 2.2 ábra: Az MFC socket könyvtárának hierarchiája UML-ben Az
öröklődés balról jobbra történik MFC osztályok használata esetén vagy az InitInstance() függvényben, vagy, ha szálból használjuk a socketet, akkor a szál elején meg kell hívnunk a BOOL AfxSocketInit( WSADATA* lpwsaData = NULL ); függvényt. A paraméterként átadható struktúrát a 211 fejezetben részleteztük Alapértelmezésben az MFC az 1.1-es Winsock verziót kéri Amennyiben az MFC AppWizard 4 lépésénél bejelöljük a Windows Sockets használatát, akkor az AppWizard az AfxSocketInit() függvényt az InitInstance( )-ba helyezi. Ugyanez a hatása a Windows sockets Visual C++ komponens beillesztésének is. Ezek után nézzük meg, hogyan tudjuk felhasználni ezeket az előre gyártott osztályokat! 2.21 A CAsyncSocket osztály 1 Ez az osztály elég alacsony szinten zárja egységbe a Socket API-t. Használható összeköttetés alapú és összeköttetés nélküli adattovábbításra is. A socket osztályok a fontosabb eseményekről visszahívható
(callback) virtuális tagfüggvényeken keresztül értesítik az alkalmazást, mint ahogy azt a 2.6 táblázat részletezi 1 A CAsyncSocket osztály helyett legtöbbször érdemesebb API-t használni, mert az osztály elég alacsonyszintű ahhoz, hogy nélkülözze a magasszintű egységbezárás kényelmét, ugyanakkor nem a legjobb teljesítőképességű modellt használja (az átfedett (overlapped) API modell a leghatékonyabb). A CAsyncSocket előnye a 16 bites APIval való kompatibilitása, azonban ha erre nincs szükség, érdemesebb lehet saját osztálykönyvtár kifejlesztése egy másik API modellhez. Ismerete azonban CSocket osztály használatához elengedhetetlen 22 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába Tagfüggvény OnReceive OnSend OnAccept Leírás Akkor hívódik meg, ha a bufferbe kiolvasandó adat került Értesíti az alkalmazást, hogy lehet
adatot küldeni Értesít egy figyelő (listening) állapotban levő socketet, hogy egy kliens kapcsolódni szeretne, amit az Accept függvényhívással tudunk elfogadni OnConnect Értesíti a kapcsolódni akaró kliens socketet, hogy a kapcsolódás – sikeresen vagy sikertelenül -, de befejeződött OnClose Értesíti a socketet, hogy a kapcsolatot lezárták OnOutOfBandData Sürgős adat érkezett (urgent data, out-of-bound data) 2.6 táblázat: A CAsyncSocket eseményről értesítő tagfüggvényei A CAsyncSocket létrehozásának és használatának forgatókönyvét a 2.22 pontban részletezzük Itt részletezzük azonban a BOOL CAsyncSocket::Create( UINT nSocketPort = 0, int nSocketType = SOCK STREAM, long lEvent = FD READ | FD WRITE | FD OOB | FD ACCEPT | FD CONNECT | FD CLOSE, LPCTSTR lpszSocketAddress = NULL ); tagfüggvényt. Az első paraméterben megadhatunk egy jól ismert portot, ha 0, akkor a Winsock választ portot számunkra. A második paraméter SOCK DGRAM
vagy SOCK STREAM lehet attól függően, hogy UDP vagy TCP protokollt használunk. Az lEvent paraméter tartalmazza azokat az eseményeket, amelyekről értesítést szeretnénk kapni a 2.6 táblázatban összefoglalt tagfüggvényeken keresztül Az egyes eseményazonosítók jelentését a 2.5 táblázat foglalja össze Utolsó argumentumként visszakapjuk a kapcsolódott socket IP címét. A Create függvény létrehoz egy socketet, majd azt az utolsó paraméterben visszaadott címhez rendeli. Mint már említettük, a CAsyncSocket osztály egy alacsonyszintű egységbezárása a WSAAsyncSelect modellnek. Ennek következtében a felhasználónak kell gondoskodnia a nem blokkolt üzenetek kezeléséről, ami 2.142-ben tárgyalt API megoldásnál annyiban könnyebb, hogy virtuális értesítőfüggvényeket kell átdefiniálnunk üzenetkezelés helyett, és jóval könnyebb a létrehozás is. CAsyncSocket alkalmazása esetén nekünk kell gondoskodnunk a 2.1 fejezetben tárgyalt
számábrázolási problémákról. 2.22 A CSocket osztály Mint azt a 2.1 ábrán láthattuk, a CSocket a CAsyncSocket osztályból származik A kettő között azonban jelentős különbség van: amíg a CAsyncSocket egy blokkolatlan kommunikációt valósít meg, addig a CSocket mindezt elrejti az alkalmazás elől, és kifelé egy szinkron jellegű kommunikációt mutat. Azonban van némi lehetőségünk kisebb feladatok elvégzésére ezen „szinkron” műveletek felfüggesztett állapota alatt. Valahányszor végrehajtunk egy socket műveletet a WSAAsyncSelect modellben, az rögtön visszatér, és megkezdődik a várakozás a megadott ablaknak szóló üzenetekre. Így zajlik ez a CSocket osztály belsejében is, de amennyiben van egy kis „idő” az üzenetkezelő függvényben, meghívódik a 23 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába virtual BOOL
OnMessagePending( ); tagfüggvény, amely alapértelmezésben feldolgozza a képernyő újrafestéséhez szükséges üzeneteket. Ha felüldefiniáljuk a fenti függvényt, mi is végrehajthatunk hasonló rövid feladatokat a blokkolatlan socket műveletek kezdete és tényleges végrehajtódása között. A CSocket osztály használatát az MFC jobban támogatja, mint az előző pontban tárgyalt ősosztályét. Itt egy programozási modell áll rendelkezésre, amelynek osztálydiagramját a 22 ábra mutatja. 2.3 ábra: A CSocket programozási modell közvetlen résztvevőinek UML osztálydiagramja Annak érdekében, hogy kényelmesen használjuk a már megszokott, magas szintű felületet biztosító CArchive osztályt, a CSocket fölé egy alapvető file műveleteket támogató CSocketFile osztály került. A CSocket programozási modelljét a következő lépések jelentik: Kliens Szerver 1. Hozzunk létre egy CSocket objektumot! 1. Hozzunk létre egy CSocket objektumot! 2.
Inicializáljuk a Create( ) függvény 2. Inicializáljuk a Create( ) függvény meghívásával! meghívásával! 3. Hívjuk meg a Connect( ) tagfüggvényt! 3. Hívjuk Listen( ) és az Accept( ) 4. Hozzunk létre egy CSocketFile objektumot, tagfüggvényt! és társítsuk a socket-ünkkel! 4. Hozzunk létre egy CSocketFile objektumot, 5. Hozzunk létre egy CArchive-ot a CSocket és társítsuk a socket-ünkkel! file-unk felett! 5. Hozzunk létre egy CArchive-ot a CSocket 6. Használjuk a CArchive-ból származó file-unk felett! objektumot a kommunikációra! 6. Használjuk a CArchive-ból származó 7. Szabadítsuk fel az CArchive, CSocketFile objektumot a kommunikációra! és CSocket osztályokból származó 7. Szabadítsuk fel az CArchive, CSocketFile objektumainkat! és CSocket osztályokból származó objektumainkat! Amennyiben a CSocket::Create paramétereként SOCK DGRAM-ot adunk meg, úgy a CArchive osztályt nem használhatjuk. Mindezek forráskóddal illusztrálva [6]:
24 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába Kliens // socket létrhozása CSocket sockClient; // a SOCKET leíró létrehozása sockClient.Create( ); // kapcsolat keresése sockClient.Connect(strAddr, nPort); // a file objektum létrehozása CSocketFile file(&sockClient); // az archive elkészítése CArchive arIn(&file, CArchive::load); CArchive arOut(&file, CArchive::store); // az archive használata kommunikációra: arIn >> dwValue; // olvasás arOut << dwValue; // írás Szerver // a szerver socket létrehozása CSocket sockSrvr; // a SOCKET leíró (handle) létrehozása sockSrvr.Create(nPort); // elkezdjük figyelni a portot sockSrvr.Listen( ); // egy új, üres, klienssel kommunikáló socket CSocket sockComm; // elfogadjuk a kapcsolódást sockSrvr.Accept( sockComm); // a file objektum létrehozása CSocketFile file(&sockComm); // az
archive elkészítése CArchive arIn(&file, CArchive::load); CArchive arOut(&file, CArchive::store); // az archive használata kommunikációra: arIn >> dwValue; // olvasás arOut << dwValue; // írás 2.23 A konkrét megvalósítás kérdései Az előzőekben részleteztük az MFC socketek általános működését és programozási modelljét. Most a közvetlen implementációs részletek kerülnek sorra. Tételezzük fel, hogy létrehoztunk egy CSocket – CSocketFile - CArchive hármast, kapcsolódtunk a szerverhez és adatfogadásra készülünk. Ha az alkalmazásból meghívnánk a Receive( ) tagfüggvényt, az addig blokkolná a programot, amíg üzenet nem érkezne. Mindez azt jelenti, hogy addig nem tudunk adatot küldeni, vagy egyéb feladattal foglalkozni. Első gondolatunk lehet a CArchive::IsBufferEmpty( ) függvény meghívása. Először azonban olvasnunk kell a bufferből, hogy ez a függvény helyes információval szolgáljon. Emlékezhetünk
azonban, hogy a 2.6 táblázatban található OnReceive függvény pontosan ekkor hívódik meg Így elsőként át kell definiálnunk az OnReceive tagfüggvényt, majd addig kell olvasnunk CArchiveból példányosított objektumunkat, ameddig a CArchive::IsBufferEmpty( ) hamis értékkel tér vissza. Amikor adatot küldünk, előfordulhat, hogy nem érkezik meg, majd ha lebontjuk a kapcsolatot, az összes elküldött adat hirtelen célba ér. Ennek oka a belső bufferelés Ezért, ha az adat összeállt és küldhető állapotban van, hívjuk meg a CArchive::Flush( ) függvényét, aminek hatására az adat ténylegesen el lesz küldve. A kliens oldalon ezzel lényegében nincs több megoldandó feladat. A szerver oldalon azonban mindig számítanunk kell arra, hogy egyszerre több kapcsolatot is kezelnünk kell. Ha valaki jártas más környezetbeli socket programozásban, például a Sun Java rendszerében, a következő már bevált javaslattal állhat elő: Akárhányszor
kapcsolódási kérelem érkezik, a szerver elfogadva a kapcsolatot új szálat indít. Ez azonban Win32 alatt csak blokkolt üzemmódban ésszerű, egyébként fölösleges. A megoldás az, hogy felüldefiniáljuk a 2.6 táblázatban található értesítőfüggvényeket, valamint egy listában számon tartjuk a beérkezett CSocketekre mutató pointereket. Az alkalmazáslogika frissítése az 25 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába értesítőfüggvényekből indul, és célszerűen valamely alkalmazáslogikabeli – például Document/View architektúra esetén a Document objektumhoz tartozó − tagfüggvény hívását jelenti. Így az alkalmazás végén elegánsan le tudjuk zárni a még aktív kapcsolatokat a lista alapján. 2.24 Esettanulmány: Chat alkalmazás készítése 1 Célunk egy olyan Chat kliens és szerver elkészítése, ahol a kliens küld egy
karaktersorozatot a szervernek, majd a szerver az összes kliensnek – beleértve a küldőt is – elküldi az adott karaktersorozatot, amit a kliensek megjelenítenek. A könnyebb tesztelés érdekében a szerver megjeleníti az érkezett karaktersorozatot. 2.241 A szerver elkészítése A fenti megfogalmazás alapján a szerver feladata számon tartani a klienseket, majd ha egy üzenet érkezik, azt kiírjuk, majd továbbítjuk az összes számon tartott kliensnek. Az általánosság kedvéért a szerver legyen Document/View alapú, a kliens főablaka pedig egy párbeszédpanel (dialog box) legyen. A szerverben – mint tudjuk – kétféle socket van. Az egyik a szerver socket, amelyik figyeli (listen) azt a portot, amihez hozzárendelték (bind), és a kliens kapcsolatkérését (connect) elfogadja (accept). Ha a szerver socket elfogad (accept) egy kapcsolatot, annak eredményeképpen létrejön egy klienssel kommunikáló socket. Ahány sikeres kapcsolat jött létre, annyi
klienssel kommunikáló socketünk van. Mint már a 23 pontban megállapítottuk célszerű ezekről egy listát vezetünk. Itt kihasználhatjuk azt, hogy – amint a 2ábrán látható - a socketek a CObject ősosztályból származik, így egy CObList nevű MFC osztályt, amely CObject típusú mutatókkal dolgozik. Ezek után a szerver socketet a következőképp hozzuk létre: 1. Létrehozunk egy – a CSocket osztályból származó CServerSocket nevű osztályt 2. Esetleg a konstruktorban átvehetünk egy mutatót az alkalmazáslogikára (Document/View esetén a Documentre), amit a könnyebb kommunikáció érdekében eltárolunk. 3. Új osztályunkban átdefiniáljuk a CAsyncSocket::OnAccept( ) függvényt 4. Ebben az új függvényben meghívjuk az ősosztály OnAccept( )-jét, majd az alkalmazáslogika (Document/View esetén a Document) megfelelő tagfüggvényét hívjuk. 5. Az alkalmazáslogika elfogadja (accept) a kapcsolatot, és frissíti a listát A klienssel
kommunikáló socket estén már figyelembe kell vennünk azt is, hogy ebből annyi van, amennyi kliens éppen kapcsolódik a szerverhez. Ez azt jelenti, hogy az alkalmazáslogikában mindig át kell vennünk az aktuális klienssel kommunikáló socketre mutató pointert. A lépések: 1. Hozzunk létre egy CCommSocket típusú osztályt, ami a CSocketből származik! 2. Adjuk hozzá a következő tagváltozókat: az alkalmazáslogikára mutató pointer a CSocketFile-ra mutató pointert a vevő és küldő irányú kommunikáció CArchive osztályból származó objektumpéldányait 3. A konstruktorban hozzuk inicializáljuk NULL-ra ezeket a változókat 4. Hozzunk létre egy Init( ) függvényt, ahol létrehozzuk a tagváltozók objektumait 5. A destruktorban szabadítsuk fel ezeket 6. Definiáljuk át a CAsyncSocket::OnReceive virtuális tagfüggvényt 7. Ebben az új függvényben meghívjuk az ősosztály OnReceive( )-jét, majd az alkalmazáslogika (Document/View
esetén a Document) megfelelő tagfüggvényét hívjuk, paraméterként megadva a this pointert. 1 A fejezethez a Chat példaprogram tartozik 26 Levendovszky Tihamér: Bevezetés a Windows Socketek programozásába 8. Esetleg egyéb értesítő tagfüggvényeket is átdefiniálhatunk Jelen alkalmazásban fontos lehet az OnClose( ) függvény felhasználása, amely szintén egy alkalmazáslogikában elhelyezkedő függvény hív, amelynek feladata kivenni az adott socketet a listából, majd magát a socketet is felszabadítani. Példánkban ezek után a CDocument-ből származó osztályunk megfelelő függvényei vannak hátra. Ez lényegében a socketek létrehozása, a lista karbantartása, a kapcsolat elfogadása és az érkező adat elküldése a lista minden tagjának. Egy lehetséges megvalósítás például a következő: 1. Definiáljuk át a Class Wizard segítségével
CDocument::OnNewDocument( ) függvényét! 2. Hozzuk létre a CServerSocket egy példányát, és hívjuk meg a Create( ) és Listen( ) tagfüggvényeit. 3. Adjuk hozzá a ProcessPendingAccept( ) függvényt a Document osztályunkhoz Ezt hívjuk majd a CServerSocket::OnAccept( ) függvényből (ld. a szerver socketek létrehozásának 4 pontját) 4. A ProcessPendingAccept( )-ben Hozunk létre egy példányt a CCommSocket osztályból! Ne hívjuk meg a Create( ) tagfüggvényét! Hívjuk meg a 2. pontban létrehozott CServerSocket típusú objektumpéldányunk Accept( ) tagfüggvényét az előző pontban létrehozott CCommSocket típusú paraméterrel! Hívjuk meg a CCommSocket 4. pontjában definiált Init( ) függvényt! Adjuk hozzá az új socketet a listához! 5. Adjuk hozzá a Document osztályunkhoz a ProcessPendingRead(CCommSocket* pSocket ) tagfüggvény! Ezt a függvényt hívjuk a CCommSocket létrehozásának 7. pontjában részletezett OnReceive( )
tagfüggvényből. 6. Az előző pontban létrehozott függvény törzséből olvassuk addig a paraméterben megadott CCommSocket típusú objektumot, amíg a socket bemenetéhez rendelt CArchive típusú objektum buffere üres nem lesz: do { // Olvassuk az adatokat, például: pSocket−>m pArchiveIn>>str; } while ( ! pSocket−>m pArchiveIn−>IsBufferEmpty( ) ); 7. Ha végeztünk az olvasással, frissítsük a klienseket: járjuk be a listát, és minden socketnek küldjük el a beérkezett adatot. 8. Kezeljük a kapcsolat lebontását is: frissítsük a listát és szabadítsuk fel a szükségtelenné vált klienssel kommunikáló socketet. Ezt a műveletet a klienssel kommunikáló socket 7 pontjában tárgyalt OnClose( ) függvény kezdeményezi. 9. A Document DeleteContents( ) tagfüggvényében töröljük a CServerSocket típusú objektumunkat, a listában levő klienssel kommunikáló socketeket, majd a listát magát. A fentiek alapján a szervert
elkészítettük. 2.242 A kliens létrehozása A 2.23-ban felvetett és megoldott probléma - az adat fogadásának blokkolása - most is megoldásra vár. Ezért származtatnunk kell a CSocket osztályból egy CClientSocket osztályt, amelynek a az OnReceive( ) tagfüggvényét a CCommSocket osztály létrehozásának 6. illetve 7 lépésében ismertetett módon átdefiniáljuk. Értelemszerűen a Document osztálynál is el kell végeznünk a szervernél ismertetett 5. és 6 lépést Amennyiben az alkalmazás blokkolása megengedett, a 2.22-ben ismertetett programozási modell szabadon alkalmazható. 27