Szerver-kliens fogalma ld. előző szám.

Az előző részből egy kis emlékeztető a kliensről

A TCP alapú kliens által végrehajtandó lépések.

  • Socket létrehozása a socket függvénnyel.
  • Kapcsolódás a connect függvénnyel. Ehhez kell a szerver címe is, amit a gethostbyname függvény tud megmondani.
  • Kommunikáció, például read, write függvényekkel
  • Kapcsolat bezárása. Close, illetve fclose, ha stream-et húztunk rá.

Ez együtt:


    struct sockaddr_in servaddr; /*Szerver process címe*/
    struct hostent *hp; /*Ez mutat a host adataira a gethosbyname-től*/
    /* (IP cím miatt kell)*/
    int sd; /* socket leíró*/

    /* socket létrehozása*/
    if ((sd = socket(AF_INET, SOCK_STREAM, 0) < 0)
    {
    perror("socket");
    exit(1);
    }

    /* nullázzuk a szerver címet */
    bzero((char *)&servaddr, sizeof(servaddr));

    /* server port és domain típus*/
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERVERPORT);

    /* serer IP címét megtudjuk*/
    hp = gethostbyname(SERVERNAME);
    if (hp == 0) {fprintf(stderr, "gethostbyname hiba"); exit(1);}

    /*átmásoljuk az IP címet*/
    bcopy(hp->h_addr_list[0], &servaddr.sin_addr.s_addr, hp->h_length);

    /*kapcsolódunk*/
    if (connect(sd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
       perror("connect"); exit(1);}

    /*kommunikáció*/
    read(sd,....);
    write(sd,...);
    .
    .
    /* vége*/
    close(sd);
     

A szerver által végrehajtandó lépések, és az ezekhez szükséges függvények.

  • Socket létrehozása a socket függvénnyel.
    Ugyanúgy megy, mint a kliensnél.
  • A socket hozzákötése egy helyi port-hoz.
    Erre a bind függvényt használjuk. Ezen a porton keresztül lesz elérhető a szerver, erre a portra kell kapcsolódnia a kliensnek.
  • A socket felkészítése a kliensek fogadására.
    Erre a listen függvény való.
  • Klienstől érkező kapcsolatkérés elfogadása.
    Az accept függvény végzi.
  • Kommunikáció. Read,wite. Ugyanaz, mint a kliensnél.
  • Kapcsolat bezárása. Close, vagy shutdown.

A függvények leírása:

Socket hozzákötése egy port-hoz, Bind függvény

A bind függvénnyel azt adjuk meg, hogy a socket melyik porton fog üzeneteket fogadni, illetve küldeni. Úgy is mondhatjuk, hogy hozzákötjük a porthoz.

#include <sys/types.h>
#include <sys/socket.h>
int bind(int sd, const struct sockaddr *addr, int addrlen)
    sd: a socket leírója (socket függvénnyel lehet készíteni)
    addr
    : a pontos cím, ahová kötjük. A sockaddrstruktúra leírását ld. előző szám.
    addrlen
    : a struktúra hossza
    Egy integer-el tér vissza. Siker esetén nulla, hiba esetén -1.
Példa:
    struct sockaddr_in name;
    .....
    bzero((char *) &name, sizeof(name)); /*kinullázzuk a nevet*/
    name.sin_family = AF_INET; /*internet domain*/
    name.sin_port = htons(SERVERPORT); /* a port megadása, helyes bytesorrendben*/
    name.sin_addr.s_addr = htonl(INADDR_ANY); /* a szerver host összes IP címe*/
    if (bind(sd, (struct sockaddr *)&name, sizeof(name)) < 0) {
       perror("bind"); exit(1);
    }

Ha a portnak nullát adunk meg, akkor a kernel fog számunkra szabad portot keresni. Ez egy szerver esetében nem bölcs dolog, hiszen a kliensnek tudnia kell melyik porton vár a szerver. A porton kívül az IP címet is meg kell adni. Ez annak a gépnek az IP címe, ahol a szerver program fut. A gép összes IP címét az INADDR_ANY jelenti, ezt természetesen hálózati byte sorrendre kell fordítani a htonl függvénnyel.

A bind függvényt a socket létrehozása után kell meghívni.

A porthoz kötés kliens esetében nem kötelező, de használható, ha esetleg befolyásolni akarjuk a kliens által használt portot (de a port száma ilyenkor általában teljesen mindegy). Kliens esetében a függvény használata ugyanaz.

Ha egy socket már hozzá van kötve egy porthoz, a getsockname függvénnyel le tudjuk kérdezni. (ld. man getsockname)

Socket felkészítése a kliens fogadására (listening), Listen függvény

A listen függvényt a porthoz kötés után lehet meghívni, csak kapcsolat orientált (TCP) szervernél működik. Felkészíti a socket-et a kapcsolatok fogadására.

#include <sys/socket.h>
int listen(int sd, int qlen)
    sd:a socket leírója
    qlen:várakozó sor hossza
    Visszatérési érték siker esetén nulla, hiba esetén -1.

A várakozó sor azt jelenti, hogy hány kliens tud várakozni a kapcsolatra, ha éppen foglalt a szerver, azaz más klienst szolgál ki. Ha egy kliens még belefér a várakozó sorba, akkor addig nem tér vissza a connect függvénye, amíg nem tud kapcsolódni, sorra nem kerül. Ha túl nagy számot adunk meg, akkor túl sok klienst fogunk várakoztatni, ha meg túl kicsit, akkor kevesen férnek be a sorba, a várakozó sorból lemaradt klienseknek pedig nem működőnek látszik a szolgáltatás. A helyes szám attól függ, milyen hosszú egy-egy kiszolgálás, hány kliensnek van esélye belátható időn belül sorra kerülni.

Példa:

    if (listen(sd, 3) < 0) {
       perror("listen");
       exit(1);
    }
Kapcsolódás a klienssel, Accept függvény

Kapcsolat orientált (TCP) kommunikáció során az accept függvény segítségével fogadja el a szerver a kliens által (a connect függvénnyel) kezdeményezett kapcsolatot. A függvényt a listen függvény után lehet meghívni.

#include <sys/types.h>
#include <sys/socket.h>
int accept(int fd, struct sockaddr *addressp, int *addrlen)
    fda socket leírója, amit a listen-el előkészítettünk. ("listening socket")
    addresspmutató egy címstruktúrára. Ide fog kerülni a kliens IP-je és port-ja.
    addrlenmutató a címstruktúra hosszára. Ide fog kerülni a hossz.

A függvény egy socket leírót fog visszaadni ("client socket" avagy kliens socket), vagy hiba esetén -1-et.

A "clinet socket" van összekapcsolva a klienssel. Erre írhatunk, illetve olvashatunk read, write függvényeket használhatunk.

Az accept addig vár, amíg nem akar egy kliens kapcsolódni a "listening socket"-re. Felépíti a kapcsolatot, de NEM a "listening socket"-re (nem arra, amit megadtunk paraméterként!), hanem létrehoz egy új socket-et, ez lesz a kliens socket, ezzel kapcsolódik össze a kliens. Ezt adja vissza a függvény. Az eredeti socket megmarad utána nekünk, és akárhányszor meghívhatjuk vele az accept-et.

Egy "listening socket"-re többször is meghívhatjuk az accept függvényt, ami mindig más kliens socket-et fog visszaadni. Tehát egy szerver programból egyszerre több klienst is tudunk kiszolgálni, minden hívás az accept-re egy új, a többitől független csatornát hoz létre egy újabb kliens felé (persze, ha volt újabb kliens). A kliens socket-ek mind ugyanarra a portra vannak kötve, ahová a listening socket-et is kötöttük. Ezzel azonban nem lesz gondunk, a TCP nagyon jól tudja, hogy melyik üzenet hová megy, a kapcsolatokat multiplexálja. A kapcsolatokat négy adat alapján azonosítja : Kliens-IP,Kliens-Port,Szerver-IP,Szerver-PORT (plusz a protokoll (TCP,UDP....)).

Ahhoz persze, hogy több klienst tudjunk kiszolgálni, a szerver programot többszálasra, illetve többfolyamatosra (multithreading/multiprocessing) kell megírni.

Példa:
    struct sockaddr_in client_addr;
    int ssd, csd, length;
    ...........
    if ((cfd = accept(ssd, (struct sockaddr *)&client_addr, &length) < 0) {
       perror("accept");
       exit(1);
    }
    /* cfd most a klienssel összekapcsolt socket leíróját tartalmazza */
    /* innen lehet kommunikálni*/
Kommunikáció a klienssel

Ez ugyanúgy működik, mint a kliens esetében, egyszerű read, write függvényeket használhatunk. Megtehetjük azt is, hogy stream-et húzunk a kliens socket-re, így akár a stream függvényekkel is kényelmesen írhatunk, olvashatunk.

Példa a stream-re:


    FILE *sockstream;
    int cfd;
    ...........
    sockstream=fdopen(cfd,"r+");/* stream létrehozása*/
    fprintf("hello\n", sockstream); /* egyszerűen kiírunk egy stringet a socketre*/
    /* a kliens ezt fogja kapni, attól függetlenül, hogy stream-el, vagy anélkül olvassa*/
Kapcsolat bezárása: Close, illetve Shutdown függvények

A close függvényt ugyanúgy használjuk, mint a kliensnél. Ha stream-et használtunk a socketen, akkor a fclose függvény használatos.

A shutdown hasonló a close-hoz, csak sokkal rugalmasabban használható. A függvény kliens, és szerver esetében egyaránt használható, külön le tudjuk zárni a socket-et csak írás, csak olvasás, vagy mindkettő szempontjából.

Például le tudunk zárni úgy egy socket-et, hogy az éppen a csatornán nekünk írt adat nem veszik el. Lezárjuk az írásokra, és amíg a másik oldal észreveszi a lezárást, még tudjuk olvasni az éppen küldött adatokat. Ahogy a másik oldalon megjelenik a mi lezárási kérésünk, megszakítja a kapcsolatot, mi meg egy fájlvége jelet olvashatunk a csatornán, és mi is lezárunk. Lezártuk a kapcsolatot, és nem veszett el adat.

    int shutdown(int sd, int action)

      sd:
      a socket leíró
      action:
      0 = olvasásra zár, 1 = írásra zár, 2 = mindkettőre zár
      Siker esetén nulla, hiba esetén -1 a visszatérési érték.

Ennyit a függvényekről, egy egyszerű szerver ebből már kijön.

A példaprogram egy szerver-kliens pár. A szerver egyszerűen az időt adja át a kliensnek, a szerver elindításakor meg kell adni, melyik portra kötődjön. A szerver egyszerre egyetlen klienst szolgál ki. Egyéb bonyolultabb megoldás ld. következő szám.

Használat:

>timeserver 50000 Majd egy másik terminálon, ha ugyanazon a hoston vagyunk, ahol a szervert indítottuk: >timeclient localhost 50000 Ellenkező esetben meg kell adni a szerver IP címét is: >timeclient 192.168.1.6 50000

A szervert CTRL-C -vel kell lelőni.

A szerver-kliens kapcsolat állapotai

A szerver és kliens kapcsolata, a socket-ek állapota az adatcsere során különböző állapotokon megy keresztül. Egészen a kapcsolat kezdeményezésétől a lezárásáig számos állapotot megkülönböztetünk. Ezek azért is fontosak, mert segítenek megérteni a szerver és kliens kommunikációját, és megmutatják, melyik kliens függvénynek melyik szerver függvény a párja.

Az ábrán megtalálhatóak a kliens és szerver socket-ek állapotai, valamint a használt függvények egymással párhuzamosan. Az ábra egy elképzelt szerver-kliens kapcsolatot modellez.

Magyarázat:

A csupa nagybetűvel írt szavak a socket állapotait jelentik. Az ábrán az eddig tárgyalt függvények is szerepelnek, mivel egy socket állapotának befolyásolásában kulcsszerepet játszanak, azaz velük válthatjuk ki az socket állapot megváltozását. Ha egy függvény lefutása között több állapotváltás is történik, külön ki van írva, melyik állapot előtt hívjuk meg a függvényt, és melyik állapot után tér vissza. A két függőleges vonalat idő tengelynek képzelhetjük, felülről lefelé olvasva vannak időrendben az események. A két függőleges vonal között van az átviteli csatorna. A két vonal közti nyilak jelképezik a jelátvitelt a két socket között. A nyilak lejtenek, mivel bizonyos idő kell, hogy a jelek átérjenek a csatornán. A nyilakra rá van írva, mi megy át a csatornán. Ezek speciális jelek (ack, FIN M stb..) jelentésükről később.

Egy kapcsolat felépítése a kliens szempontjából

A kliens elkészíti a socket-jét (socket függvény), majd az accept függvénnyel kapcsolatot próbál nyitni a szerverhez, kezdeményez. Ezt a fajta kezdeményező kapcsolatnyitást aktív megnyitásnak (active open) hívjuk. Az aktív megnyitás mindig a kliens oldalán történik, mivel ő tudja a szerver címét, ahová kapcsolódni akar. Az accept függvény elküld a szervernek egy SYN (SYN J) jelet, ami jelzi a kapcsolódási szándékot. Ekkor kerül a socket SYN_SENT állapotba. A connect még nem tér vissza, hanem vár a szerver válaszára.

Ez a válasz tartalmazza az előző kliens kérés nyugtázását (ack J+1), és jó esetben hogy engedélyezi a kapcsolódást (SYN K). Ha ilyen válasz érkezik vissza, a socket ESTABLISHED állapotba kerül, és az accept függvény visszatér. A kliens szemszögéből megvan a kapcsolat.

Egy kapcsolat felépítése a szerver szempontjából

A szerver elkészíti a socket-jét (socket függvény), hozzáköti egy lokális porthoz (bind), majd felkészíti a kapcsolatok fogadására (listen). A listen függvény hatására a socket LISTEN állapotba kerül. Az accept függvény várakozik a kapcsolódó kliensekre. Ezt a fajta nyitást passzív nyitásnak (passive open) hívjuk. A passzív nyitás mindig a szerver oldalán történik. Ha accept függvény kap egy SYN jelet, azaz kapcsolódási kérést, a socket SYN_RCVD állapotba kerül. Visszaküld egy nyugtázást (ack J+1) arról hogy megkapta a kérelmet, és egy SYN (SYN K) jelet, jelezve hogy rendben van a kapcsolódás. Ez után vár a kliens nyugtázására arról, hogy megkapta az üzenetet. Ha megjött a nyugtázás (ack K+1) akkor a socket ESTABLISHED állapotba kerül. A szerver szemszögéből megvan a kapcsolat, következhet a kliens kiszolgálása, ami közben már nem lesz állapot váltás.

Kapcsolat lebontása az aktív oldalon

Az az oldal végez aktív bontást (active close), amelyik kezdeményezi a kapcsolat bezárását. Ez a szolgáltatástól függően lehet a szerver, vagy a kliens is. Az ábrán a kliens kezdeményez, de ez csak egy példa, kezdhette volna akár a másik oldal is.

A kezdeményező oldal meghívja a close függvényt. A függvény elküld a másik oldalnak egy FIN jelet (FIN N), ami a kapcsolat bezárására vonatkozó kérelmet jelent. A függvény rögtön visszatér, a kapcsolat a program számára megszűnt, azonban még számos változáson megy keresztül.

A socket FIN_WAIT_1 állapotba kerül. Miután megkapta a nyugtázást (ack N+1), FIN_WAIT_2 lesz az állapota. Vár a másik oldalról egy FIN jelet (FIN M), és az állapota TIME_WAIT lesz. Ez azt jelenti, hogy várakozik pár másodpercet beállítástól függően, és a socket CLOSED állapotba kerül. A kapcsolat ténylegesen bezárva.

Kapcsolat lebontása a passzív oldalon

Az az oldal végez passzív bontást, amely értesül a másik oldal bontási kéréséről. A socket egy FIN (FIN M) üzenet hatására CLOSE_WAIT állapotba kerül, és visszaküld egy nyugtázást a kérelem fogadásáról. Erről a program úgy értesülhet, hogy a read nullával tér vissza, de bizonyos szolgáltatásoknál egyébként is tudjuk, mikor kell zárni. Ekkor a program meghívja a close függvényt. A függvény küld egy FIN (FIN N) jelet, és a socket LAST_ACK állapotba kerül. Mikor megkapja az üzenetről a nyugtázást (ack N+1), a socket akkor kerül bezárásra, azaz CLOSED állapotba.

Az egyes kapcsolatok állapotát látjuk, ha beírjuk a netstat parancsot. Egy kis kivonat:

Active Internet connections (w/o servers)

Proto

Recv-Q

Send-Q

Local Address

Foreign Address

State

tcp

0

1

line-196-12.dial.m:1380

ns0.napfolt.hu:www

SYN_SENT

tcp

0

396

line-196-12.dial.m:1379

212.172.60.10:www

ESTABLISHED

tcp

0

401

line-196-12.dial.m:1378

ns0.napfolt.hu:www

ESTABLISHED

tcp

0

402

line-196-12.dial.m:1377

ns0.napfolt.hu:www

CLOSE

tcp

0

0

line-196-12.dial.m:1376

ns0.napfolt.hu:www

ESTABLISHED

tcp

0

0

line-196-12.dial.m:1375

ns0.napfolt.hu:www

TIME_WAIT

tcp

0

340

line-196-12.dial.m:1374

ns0.napfolt.hu:www

LAST_ACK

tcp

1

1

line-196-12.dial.m:1369

194.25.242.201:www

LAST_ACK

A cikkben használt forráskódok itt töltehetôek le:

timeclient.c és timeserver.c ZIP-pelve