kategória | ||||||||||
|
||||||||||
|
||
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 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 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
Az alábbi példa egy TCP socketet hoz létre:
SOCKET sock;
if((sock = socket(PF_INET, SOCK_STREAM, 0)) < 0)
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.
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.
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);
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.
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 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.
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)
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.
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.
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)
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 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.
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 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!
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!
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.
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.
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.
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;
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.
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
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.
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!
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;
Találat: 2256