online kép - Fájl  tubefájl feltöltés file feltöltés - adja hozzá a fájlokat onlinefedezze fel a legújabb online dokumentumokKapcsolat
  
 

Letöltheto dokumentumok, programok, törvények, tervezetek, javaslatok, egyéb hasznos információk, receptek - Fájl kiterjesztések - fajltube.com

 

Online dokumentumok - kep
  

TCP szerver készítése

számítógépes

Fájl küldése e-mail



egyéb tételek

 
Rekurzió (particiószam, Hanoi tornyai, postfix konverzió)
Sémakezelés és tarolasi struktúrak hierarchikus adatbazisokban
Az írasjelek kódolasa
MOS/CMOS technológia és digitalis alkalmazasai
A WORD eszközei II
A Paint Shop Pro grafikai program hasznalata
Part Design CATIA V5 – Start
Hattértarak, tömegtarolók
Informaciós tarsadalom
Xara Webstyle
 
 

TCP szerver készítése



A labor feladat célja, hogy a hallgatót megismertesse a TCP/IP protokollt használó programok készítésével. Ezen belül bemutatásra kerül a Berkley Socket API, amelynek segítségével lehetővé válik a hálózati kommunikáció implementálása Linux/Unix és MS Windows rendszereken.

A labor elvégzéséhez szükséges a C programozói tudás!


A Berkeley socket API ismertetése

A következő fejezetekben a Berkeley socket API használatába tekintünk be. Ennek használatával lehetséges a Linux/Unix és MS Windows rendszereken a hálózati kommunikáció implementálása.


A socket

A socket az applikációk közötti kommunikációt lehetővé tévő kétirányú kapcsolat elnevezéseként értelmezhető. Tulajdonképpen egy SOCKET típusú leíró[1], amelyet létre kell hoznunk, majd az egyes rendszerhívásoknál ezzel h 555j91f ivatkozhatunk az adott kommunikációs csatornára.

Új socketeket a socket() rendszerhívással hozhatunk létre. Létrehozáskor a sockethez egy protokollt rendelünk, amelyet az majd használni fog. Azonban ebben az állapotban a socket még nem kapcsolódik sehova, ezért kommunikációra még nem használható.




SOCKET socket(int domain, int type, int protocol);


A függvény 0-nál kisebb értékkel tér vissza hiba esetén. Ha sikeres, akkor egy socket leíróval, amely 0 vagy nagyobb érték.

A három paraméter a használandó protokollt definiálja. Az első (domain) megadja a protokoll családot. A következő táblázat néhány lehetséges értékét tartalmazza.


Protokoll

Jelentés

PF_UNIX, PF_LOCAL

Unix domain (gépen belüli kommunikáció)

PF_INET

IPv4 protokoll

PF_INET6

IPv6 protokoll


A következő paraméter (type) kiválasztja a protokoll családon belül a kommunikáció típusát. Lehetséges értékei az alábbiak:


Típus

Jelentés

SOCK_STREAM

Sorrendtartó, megbízható, kétirányú, kapcsolatalapú bájtfolyam-kommunikációt valósít meg.

SOCK_DGRAM

Datagramalapú (kapcsolatmentes, nem megbízható) kommunikáció.

SOCK_SEQPACKET

Sorrendtartó, megbízható, kétirányú, kapcsolatalapú kommunikációs vonal, fix méretű datagramok számára.

SOCK_RAW

Nyers hálózati protokoll hozzáférést tesz lehetővé.

SOCK_RDM

Megbízható datagramalapú kommunikációs réteg. (Nem sorrendtartó.)


A harmadik paraméter (protocol) a protokoll család kiválasztott típusán belül választ ki egy protokollt. Mivel egy protokoll családon belül egy kommunikáció típust többnyire csak egy protokoll implementál, ezért a tipikus értéke: 0


Példa

Az alábbi példa egy TCP socketet hoz létre:


SOCKET sock;

if((sock = socket(PF_INET, SOCK_STREAM, 0)) < 0)



A kapcsolat felépítése

Ha összeköttetés-alapú kommunikációt szeretnénk folytatni egy kliens és egy szerver program között, akkor ehhez stream jelegű socketet kell létrehoznunk. Azonban ez önmagában nem elegendő a kommunikációhoz. Össze kell kapcsolnunk a két fél socketét, hogy adatokat vihessünk át rajta. Ennek a kapcsolat felépítésnek a részleteit a következő ábrán láthatjuk.



. ábra A socket kapcsolat felépítése


Az ábrán a kliens és a szerver oldalon egymás után meghívandó függvények láthatóak. Elsőre látható, hogy a kapcsolat felépítési mechanizmus aszimmetrikus. A szerver oldalon a feladat, hogy egy bizonyos címen várja a program a kapcsolódási igényeket, majd amikor ezek befutnak, akkor kiépítse a kapcsolatot. A kliens oldalon ezzel szemben a feladat azl, hogy a szerver címére kapcsolódási kérést küldjünk, amelyre az reagálhat. Ezen feladatok végrehajtását tekintsük át lépésenként.


Szerver:

A szerver oldalon létre kell hoznunk egy socketet, amelyet a későbbiek során szerver socketként fogunk használni. A szerver socket kommunikációra nem alkalmas, csak kapcsolatok fogadására.

Össze kell állítanunk egy cím struktúrát, amelyben leírjuk, hogy a szerver hol várja a kapcsolódásokat.

Az előbbi lépésben összeállított címhez hozzá kell kötnünk a socketet.

Be kell kapcsolnunk a szerver-socket módot, vagyis hogy a socket várja a kapcsolódásokat.

Fogadnunk kell az egyes kapcsolódásokat. Ennek során minden kapcsolathoz létrejön egy kliens socket, amely az adott féllel való kommunikációra használható.


Kliens:

A kliens oldalon is létre kell hoznunk socketet. Ezt az előző esettől eltérően kliens-socketként, a kommunikációra fogjuk használni.



Össze kell állítanunk egy cím struktúrát, amelyben leírjuk, hogy a szerver milyen címen érhető el.

Kapcsolódnunk kell a szerverhez. Amikor ez a kapcsolat létrejött, akkor a továbbiakban a socket képes továbbítani az adatokat.


A következőkben áttekintjük, hogy az egyes lépések milyen függvények segítségével valósítható meg. Azonban mivel a feladat szerver készítése, ezért a szerver lépéseire koncentrálunk első sorban.


Cím összeállítása

Mielőtt a cím összeállításra rátérnénk meg kell vizsgálnunk egy problémát. A címek összeállítása során több bájtos adattípusokat használunk (pl.: IPv4 cím 4 bájtos long, a port szám 2 bájtos short). Azonban az egyes processzorok különböző módon tárolják az egyes adattípusokat. Két tárolási mód terjedt el: a big endian, amely a magasabb helyi értékű bájttal kezdi a tárolást, illetve ennek ellentéte a little endian. Big endian architektúra például a Sun Sparc, little endian pedig az Intel x86. Azonban a hálózaton ezeknek a különböző architektúráknak is meg kell érteniük egymást, ezért közös adatábrázolásra van szükség. Ezt nevezzük hálózati bájt-sorrendnek, amely a big endian ábrázolást követi. Az adott architektúrán használt ábrázolást pedig hoszt bájt-sorrendnek.

A két ábrázolás közötti váltásokat a következő függvények végzik:


Függvény

Jelentés

ntohs

Egy 16-bites számot a hálózati bájt-sorrendből a hoszt bájt-sorrend sorrendjébe vált át. 

ntohl

Egy 32-bites számot a hálózati bájt-sorrendből a hoszt bájt-sorrendjébe vált át.

htons

Egy 16-bites számot a hoszt bájt-sorrendjéből hálózati bájt-sorrendbe vált át.

htonl

Egy 32-bites számot a gép bájt-sorrendjéből hálózati hoszt bájt-sorrendbe vált át.


Azokon az architektúrákon, ahol szükséges a konverzió ezek a függvények megfordítják a bájt-sorrendet, ahol pedig nem, ott csak visszaadják a paraméter értékét.


A címábrázoláshoz a socket API a következő általános struktúrát alkalmazza:


struct sockaddr



A címet használó függvények paraméterként ilyen típust várnak. Azonban egyes protokollokhoz létezik ennek specializált változata is, amely az adott protokoll esetén könnyebben használható. IPv4 esetén ez a következő képen néz ki:


struct sockaddr_in



Ezen belül az in_addr struktúra az alábbi:


struct in_addr



Ebben helyezhetjük el a cím egyes bájtjait. Ne feledjük, hogy mind a címnek, mind a portnak hálózati bájtsorrendűnek kell lennie, ezért a korábban látott htonl() illetve htons() függvényeket kell használnunk a konverziójukhoz.

A szerver esetén a cím beállításnál valójában csak a portot kell megadnunk, míg a címre használhatjuk az INADDR_ANY makrót. Ez valójában a 0.0.0.0 címet jelenti, amely azt mondja meg, hogy minden hálózati interfész adott portján várjuk a kapcsolódást.


Példa

Az alábbiakban két példát láthatunk a cím összeállítására.

Az első példában egy szerver számára állítjuk össze a címhez kötéshez a cím struktúrát. A példában a szerver a számítógép össze hálózati interfészén az 1234-es porton lesz majd elérhető:


sockaddr_in addr;

addr.sin_family = AF_INET;

addr.sin_addr.s_addr = INADDR_ANY;

addr.sin_port = htons(1234);


A másik példa azt mutatja meg, hogy hogyan állíthatjuk össze a kliensben a címstruktúrát, amiben a szerver címe szerepel. A szerver IP címe esetünkben 192.168.0.1 és portja 1234 lesz:


sockaddr_in addr;

addr.sin_family = AF_INET;

addr.sin_addr.s_addr = inet_addr("192.168.0.1");

addr.sin_port = htons(1234);


A socket címhez kötése

Az előző lépésben összeállított címet hozzá kell rendelnünk a sockethez. Ezt a műveletet kötésnek nevezzük, és a következő rendszerhívással lehet végre hajtani:


int bind(SOCKET sock, struct sockaddr *my_addr, socklen_t addrlen


Az első paraméter a socket leírója, a második a címet leíró struktúra, az utolsó a címet leíró struktúra hossza. A visszatérési érték sikeres végrehajtás esetén 0, egyébként pedig mínusz érték.


Példa

Az alábbi példában a korábban összeállított címmel hajtjuk végre a címhez kötés műveletét:


if(bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0)



A szerver mód bekapcsolása

A címhez kötés után a socketet szerver módba kapcsolhatjuk. Ez után már kapcsolódhatunk hozzá klienssel. A szerver módba kapcsolást a következő rendszerhívással végezhetjük el:


int listen(SOCKET sock, int backlog);


Az első paraméter a socket leírója. A második paraméter, a backlog megadja, hogy hány kapcsolódni kívánó socket kérelme után utasítsa vissza az újakat. Ebbe csak azok a kapcsolódó socketek számítanak bele, amelyeket még nem fogadott a szerver. A visszatérési érték sikeres végrehajtás esetén 0, egyébként pedig mínusz érték.


Példa

A korábban már címhez kötött socketet az alábbi módon kapcsolhatjuk szerver-socket módba:


if(listen(sock, 5) < 0)



Kliensek kapcsolódásának fogadása

A kliensek kapcsolódását a következő rendszerhívás fogadja:




SOCKET accept(SOCKET sock, struct sockaddr *addr, socklen_t *addrlen);


Az első paraméter a szerver socket leírója. Az addr és addrlen paraméterekben a másik oldal címét kapjuk meg. Az addrlen egy integer szám, amely megadja az addr változó méretét. Amennyiben nem vagyunk kíváncsiak a csatlakozó kliens címére, akkor az addr és addrlen paramétereknek megadhatunk NULL értéket.

A függvény visszatérési értéke az új kapcsolat leírója, egy kliens socket. A későbbiekben ezt használhatjuk a kommunikációhoz. Ha a visszatérési érték 0-nál kisebb, akkor hibát jelez.


Kapcsolódás a szerverhez

A kliens oldalon a cím összeállítása után kapcsolódnunk kell a szerverhez. Ezt a következő rendszerhívással tehetjük meg:


int connect(SOCKET sock, struct sockaddr *addr, socklen_t addrlen);


Az első paraméter a kliens socket leírója. Az addr és addrlen paraméterekben a szerver címét adjuk meg. Az addr paraméter tartalmazza a címet, míg az addrlen az addr paraméterben átadott cím méretét kell, hogy megadja. A függvény 0 értékkel jelzi a sikeres végrehajtást, míg mínusz értékkel a hibát.


Példa

A korábbi példában összeállított szerver címre a kapcsolódást az alábbi példa mutatja be:


if(connect(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0)



Adatok küldése és fogadása

A kliens és a szerver program összekapcsolt kliens socketét egy kétirányú csővezetéknek tekinthetjük, amelyen bájtokat küldhetünk és fogadhatunk.

Az adatok küldésére a következő rendszerhívást használhatjuk:


int send(SOCKET s, const void *msg, size_t len, int flags);


Az első argumentum a socket leírója, a második az elküldendő adat pufferére mutató pointer, a harmadik az elküldendő adat mérete. A flags paraméter a kommunikáció jellegét módosító beállításokat tartalmazhat. A gyakorlat keretein belül használjuk a 0 értéket. A függvény visszatérési értéke tartalmazza az elküldött bájtok számát. A mínusz érték a hibát jelzi.


Adatokat a recv() rendszerhívással fogadhatunk. Ez a függvény addig nem tér vissza, amíg valamilyen információ nem érkezik. Az alakja a következő:


int recv(SOCKET s, void *buf, size_t len, int flags);


Az első paraméter itt is a kliens socket leírója, a második a fogadó puffer mutatója, a harmadik pedig a puffer mérete. A flags paraméter itt is a kommunikáció jellegét módosító beállításokat tartalmazhat, azonban jelenleg használjuk a 0 értéket. A függvény visszatérési értéke a beolvasott bájtok számát tartalmazza. Ez kisebb vagy egyenlő azzal, amit mi puffer méretnek megadtunk. Figyeljünk arra, hogy a puffer tartalmából csak annyi bájt a hasznos információ, amennyit ez a visszatérési érték megad. Ha a visszatérési érték 0, akkor az azt jelzi, hogy a kapcsolat lezárult. Ebben az esetben befejeződött a kommunikáció és le kell zárnunk a socketet. Ha a visszatérési érték mínusz szám, akkor az hibát jelez.


A kapcsolat lezárása

A kapcsolatot a következő rendszerhívással zárhatjuk le[2]:


int closesocket(SOCKET s);


A paraméter a socket leírója. A visszatérési érték sikeres végrehajtás esetén 0, egyébként pedig mínusz érték.


Ellenőrző kérdések

Mi a különbség a szerver és a kliens socket között?

A cím összeállításánál miért szükséges a számokat konvertálni?

Miért szükséges a szerver socketet címhez kötni és miért nem kell a kliens socketet?

Az accept() függvény meghívásakor mi történik, ha éppen nincs bejövő kapcsolat?

A kommunikációs kapcsolatot hogyan zárhatja le a kliens, illetve a szerver oldal?

Írjon C nyelvű kódrészletet, amely az s leíróval reprezentált kliens socketből képes 16 byte adat fogadására!

Írjon C nyelvű kódrészletet, amely az s leíróval reprezentált kliens socketen keresztül elküldi a „hello” stringet!

Írjon C nyelvű kódrészletet, amely megvizsgálja, hogy az str1 és str2 nevű karakter tömbök tartalma megegyezik-e!

Írjon C nyelvű kódrészletet, amely megvizsgálja, hogy az str1 nevű karakter tömb tartalmazza-e az str2 nevű karakter tömb értékét!

Írjon C nyelvű kódrészletet, amely megvizsgálja, hogy az str1 nevű karakter tömb tartalmazza-e a ch nevű karaktert!


A feladat

A gyakorlat során a hallgató feladata, hogy elkészítsen egy TCP szerver applikációt C nyelven, amely a következőket teljesíti:

Egy paraméterként megadott TCP porton fogadja a kliensek kapcsolódásait! Vagyis megvalósítja azokat a funkciókat, amelyeket a szerver esetén a segédlet leír.

Egy kapcsolat fogadása után elegendő, ha csak az adott klienssel foglalkozik. Azonban amikor lezárul a kapcsolat, akkor fogadja új kliens kapcsolódását!

A klienssel való kommunikáció során elvégzendő műveleteket a gyakorlatvezető ismerteti!


Megjegyzés

A gyakorlat során létrehozott szerver program az „Egyszerű Web szerver készítése” gyakorlaton kiindulási pontként használható, ezért célszerű megőrizni és a gyakorlatra elhozni!


Függelék A – MS Windows segítség

Az MS Windows alatti fejlesztést MS Visual Studio program használatával végezzük. A project létrehozásakor célszerű az üres C++ alkalmazás választani kiindulási pontnak.


A program lefordítása és futtatása

Az elkészült program lefordításához szükséges a ws2_32.lib könyvtár állomány használata, mert különben a hálózatkezelő függvényeink implementációját a linker nem találja meg. Vagyis a project beállításainál a linkelés parancssorához vegyük hozzá a ws2_32.lib állományt.



A programot paraméterezve kell futtatnunk. Ennek legegyszerűbb módja, ha nyitunk egy parancsterminált (cmd.exe). Megkeressük a lefordított exe állományt és lefuttatjuk. Paraméterként meg kell adnunk a port számát.


A szerver kipróbálása

Az elkészült TCP szerver programot a telnet program segítségével próbálhatjuk ki, amely egy TCP kliens. A telnet programnak első paraméterként meg kell adnunk a szerver gép nevét, amely lokális futtatás esetén localhost, második paraméterként pedig a szerver portját. Például:


telnet localhost 1234


A program jelzi, ha nem sikerül kapcsolódnia a szerverhez. Ha a kapcsolat felépült, akkor a begépelt sorokat elküldi a szerver programnak. A telnet program MS Windows implementációja karakterenként küldi el azt, amit begépeltünk, így a szervernek egyből reagálnia kell minden karakter után.


A program váza

Az alábbi C (C++) nyelvű programváz segítséget nyújt a fejlesztés elkezdéséhez MS Windows alatt:


#include <stdio.h>

#include <winsock2.h>


int main(int argc, char* argv[])



if(argc < 2)



//TODO


WSACleanup();


return 0;



Függelék B – Linux segítség

A feladat megvalósításához szükség van egy terminál, egy szövegszerkesztő, és a gcc program használatára. Szövegszerkesztőnek a kwrite program használata javasolt, mivel kezelőfelülete egyszerű és rendelkezik szintaxis kiemelő funkcióval. Az alábbiakban ismertetett parancsokat a terminál ablakba kell beírni.


A programozói segítség

Az egyes rendszerhívásokról és C függvényekről a man parancs segítségével angol nyelvű segítséget érhetünk el. Ennek formája:

Rendszerhívások esetén:


man 2 rendszerhívás


C függvények esetén:


man 3 C-függvény


A program lefordítása és futtatása

Az elkészült forráskódot a gcc parancs segítségével fordíthatjuk le. Ennek paraméterezése a következő:


gcc -Wall -o kimenet forrás.c


A fordító sikeres fordítás esetén nem ír ki semmit, csak előállítja a futtatható program kódot. Probléma esetén a problémákat három csoportra oszthatjuk:


Típus

Leírás

Figyelmeztetés

(warning)

A jelzett sor szintaktikailag nem hibás, de érdemes ellenőrizni, mert elvi hibás lehet. Viszont a program lefordul és használható.

Fordítási hiba

(compiler error)

A jelzett sor szintaktikailag hibás, vagy valamely korábbi sorban elkövetett hiba hatása.

Linker hiba

(linker error)

A linker nem találja a hivatkozott kódot. Többnyire függvény név elírások okozzák.


A lefordított programot a következő módon lehet futtatni:


./program param1


Ez az aktuális könyvtárból lefuttatja a program nevű programot a param1 paraméterrel. A programot leállítani a Ctrl+C billentyű kombinációval lehet.


A szerver kipróbálása

Az elkészült TCP szerver programot a telnet program segítségével próbálhatjuk ki, amely egy TCP kliens. A telnet programnak első paraméterként meg kell adnunk a szerver gép nevét, amely lokális futtatás esetén localhost, második paraméterként pedig a szerver portját. Például:


telnet localhost 1234


A program jelzi, ha nem sikerül kapcsolódnia a szerverhez. Ha a kapcsolat felépült, akkor a begépelt sorokat elküldi a szerver programnak. Figyeljünk arra, hogy mivel a terminál kanonikus, ezért sorokat fog elküldeni az Enter lenyomása után!


A program váza

Az alábbi C nyelvű programváz segítséget nyújt a fejlesztés elkezdéséhez Linux alatt:


#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <string.h>

#include <sys/socket.h>

#include <netinet/in.h>


int main(int argc, char* argv[])



if((ssock = socket(PF_INET, SOCK_STREAM, 0)) < 0)



setsockopt(ssock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));


//TODO


return 0;






Linux alatt: int típusú

Linux alatt: int close(int s)

Találat: 1449