Az eddigi cikkek mind a TCP protokoll, azaz kapcsolat alapú protokoll programozásáról szóltak. A TCP a használata a programozó szempontjából igen kényelmes megoldás, mivel szinte figyelmen kívül hagyhatja a hálózat kezelését, fájlként írhat, illetve olvashat a socket-ről. Ennek megfelelően széles körben alkalmazott. A kényelmet persze a teljesítményben kell megfizetni.

A mostani szám az UDP, azaz datagram orientált protokoll használatába vezet be. Az UDP a programozótól több munkát igényel, olyan alkalmazásokban indokolt használata, ahol nincs szükség kapcsolat felépítésére, például csak egy egyszerű kérdés-válasz az egész kommunikáció.

Az UDP működési elve
Az UDP protokoll használatakor nem épül fel kapcsolat a két kommunikáló folyamat között, az adatok külön adatcsomagok (datagram-ok) formájában közlekednek. Minden egyes datagram a többitől függetlenül kézbesítődik, különböző utakon haladhatnak, és az utat nem ugyanannyi idő alatt teszik meg. Ebből következik, hogy az elküldött adatcsomagok más sorrendben érkezhetnek meg, mint ahogy feladták őket. Sőt kivételes esetekben többször is megérkezhet ugyanaz az adatcsomag, illetve el is veszhet. Ezekre az eseményekre a programnak számítania kell. Az elv hasonlít a postán feladott csomagokhoz. Nem lehet tudni, hogy milyen sorrendben érkeznek meg, milyen utat járnak be, vagy egyáltalán mindegyik megérkezik-e.
Az UDP használatához szükséges, függvények
Socket létrehozása

Ez a már ismert socket függvény alkalmazásával történik. Meg kell adni, hogy UDP legyen a protokoll. (SOCK_DGRAM)

Pl.:

if ((sock=socket(AF_INET, SOCK_DGRAM, 0))<0)
{
  perror("socket");
  exit(1);
}

Port-hoz kötés

Kliens esetében általában nincs szükség rá, hogy befolyásoljuk a port számát, ahová a socket kötve van. Szerver esetében viszont kötelező, és a bind függvénnyel történhet, a már ismertetett módon.

Cím megadása a connect függvénnyel

Általában nincs szükség rá, de UDP protokoll esetében is használhatjuk a connect függvényt. Ez a függvény TCP-nél arra való, hogy kiépítse a kapcsolatot. UDP esetében csak annyit tesz, hogy a megadott címet megjegyzi, így később nem kell minden egyes kérés elküldésekor újra megadni a cél címet. Tehát a connect függvény, nevével ellentétben nem épít fel semmiféle kapcsolatot, UDP-nél csupán egy kényelmi szolgáltatás.

Kommunikáció
A TCP esetében a socket-et írhatjuk, és olvashatjuk, mintha csak egy fájl leíró volna. Amit az egyik oldal egyszerre kiírt a csatornára, a másik oldal több olvasással is be tudja olvasni. Az UDP nem ilyen egyszerű eset. Amit az egyik oldal egy írással küldött, azt a másik oldalnak is egyszerre kell beolvasni. Ez azt jelenti, hogy mind a szerver, mind a kliens oldalnak tudnia kell, hogy mekkora az a csomagméret, amivel kommunikálnak. Mi az a méret, amit egyszerre kell írni, olvasni.

Mivel nem épül ki kapcsolat a két fél között, a szerver csak a kérésre tud válaszolni, azelőtt nem tud adatot küldeni. Az UDP protokoll használatakor mindig a kliens szólal meg először, a szerver a kérésből tudja meg, hogy hova küldje a választ. Így az UDP akkor alkalmazható jól, ha kommunikáció egyszerű kérés-válasz párokból áll. Több függvény is áll rendelkezésünkre az írásra és az olvasásra is.

Olvasás

Használhatjuk a már jól ismert read függvényt. Ezzel kapcsolatban azt kell tudni, hogy a szerver oldalon nincs értelme, mivel nem szolgáltatja a küldő címét, ahová válaszolni lehetne, de kliens oldalon jól és egyszerűen használható.

Ezen kívül használhatjuk a recv, és recvfrom függvényeket.

int recv(int s, void *buf, size_t len, int flags);
int recvfrom(int s, void *buf,size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);

Ahol:

  • s - a socket leíró
  • buf - a buffer, ahová olvasunk
  • len - a buffer mérete
  • flags - különböző tulajdonságokat állíthatunk be. Jól megteszi a nulla is. Bővebben 'man recvfrom'
  • from - ide fog kerülni a küldő címe.
  • fromlen - a címet tartalmazó buffer hossza.
Látható, hogy csak a recvfrom függvény adja vissza a küldő címét. Ha nincs rá szükség, akkor használjuk a recv -et.

A függvények visszatérési értéke az olvasott byte-ok száma, vagy hiba esetén -1.

Felmerül a kérdés, hogy a három közül mikor melyiket használjuk. Kliens oldalon a legegyszerűbb a read használata, szerver oldalon pedig a recvfrom praktikus, hiszen ekkor a kapott címre vissza is tudja írni a választ.

Pl.:

int sock;
char buf[BUFSIZE];
int length;
struct sockaddr_in client_addr;
...
if (recvfrom(sock, buf, BUFSIZE, 0, &client_addr, &length) < 0)
perror("recvfrom");

Írás

Elvileg használható a write függvény is, de én azt tanácsolnám, hogy kerüljük a használatát. Mivel nem lehet megadni neki a cél címet, nem sok hasznát vehetjük. Kivéve persze, ha a connect függvényt is használjuk, ekkor viszont ott a send.

Helyette két másik függvény:

int send(int s, const void *msg, size_t len, int flags);
int sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);

Ahol:

  • s - a socket leíró
  • msg - az elküldendő adat
  • len - az adat hossza
  • flags - különböző tulajdonságok
  • to - A címzett
  • tolen - a címet tartalmazó buffer hossza.
A send függvénynek nem kell megadni a címet, mert a connect függvényben megadott címre küld.

A visszatérési értékük az írt byte-ok száma, hiba esetén -1.

Egy példa, ahol a szerver olvas, majd válaszol a kapott címre:

int sock;
char data[BUFSIZE];
struct sockaddr_in name;
int length;
struct sockaddr_in client_addr;
...
/* Kérés olvasása. Megkapjuk a kliens cimét is */
if (recvfrom(sock, data, BUFSIZE, 0, &client_addr, &length) < 0)
perror("recvfrom");
/* válasz küldése a kérésre */
if (sendto(sock, data, strlen(data) * sizeof(char), 0, (struct sockaddr *)&name, sizeof(name)) < 0)
perror("sendto");

Socket lezárása

Az ismert close, illetve shutdown függvénnyel történhet.

A kliens oldal által végrehajtandó lépések:

socket létrehozása => [porthoz kötés] => [ cím megadása ] => kommunikácó => socket lezárása

A szögletes zárójelben lévő elemek opcionálisak.

A szerver oldal által végrehajtandó lépések

socket létrehozása => porthoz kötés => kommunikácó => socket lezárása
Egy nagyon egyszerű példa UDP kliens-szerver párra
A kliens program elküld egy üres adatcsomagot. Amit visszakap, azt kiírja a képernyőre. A csomagméret 1024 byte. Ez fontos, hiszen a program ilyen méretű adatot próbál majd olvasni. Ha a szerver nem ennyi byte-ot írt ki, akkor nem fog működni az egész.

A kliens:

#define MAXBUF 1024 //A csomagméret fontos, ennyi lesz a szerver-nél is

void main(int argc, char *argv[])
{
 int sock; /* socket leiro */
 struct sockaddr_in name; /* server cime */
 struct hostent hp; /* IP cím */
 char data[MAXBUF]; /* buffer */

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

  /*IP cim lekerdezese*/
  if ((hp = gethostbyname(HOSTNAME)) == 0)
  {
    herror("gethostbyname");
   exit(2);
  }

  bcopy((char *)hp->h_addr, (char *)&name.sin_addr, hp->h_length);
  name.sin_family = AF_INET;
  name.sin_port = htons(atoi(PORTNUM));
  bzero(data, MAXBUF);

  /*Az üres adatot elküldjük, hogy lássa a szerver a címet*/
  if (sendto(sock, data, strlen(data) * sizeof(char), 0, (struct sockaddr *)&name, sizeof(name)) < 0)
  perror("sendto");

  /* válasz olvasása */
  if(read(sock, data, MAXBUF) < 0)
  {
   perror("read");
   exit(1);
  }
  printf("Data from server %s\n", data);

  /* socket lezáráas*/
  close(sock);

  exit(0);
}

A szerver:

#define MAXBUF 1024 /*egyeznie kell a kliens által használt értékkel*/

void main(int argc, char *argv[])
{
 int sock; /* socket leiro */
 struct sockaddr_in name; /* cim */
 int length; /* cim hossza */
 char buf[MAXBUF]; /* buffer */
 struct sockaddr_in client_addr; /* kliens cim */
 long tm; /* idő */

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

  bzero((char *)&name, sizeof(name));
  name.sin_family = AF_INET;
  name.sin_addr.s_addr = INADDR_ANY;
  name.sin_port = htons(PORTNUM);

  /* porthoz kötés */
  if (bind (sock, (struct sockaddr *)&name,sizeof(name)) < 0)
  {
    perror("bind");
    exit(1);
  }
  length = sizeof(struct sockaddr);

  /* kiszolgálás végtelen ciklusban */
  while(1)
  {
    /* kliens kérését beolvasni */
    if (recvfrom(sock, buf, MAXBUF, 0, &client_addr, &length) < 0)
    perror("recvfrom");

    /* Az időt írja vissza */
    time(&tm);
    strcpy(buf, ctime(&tm));

    /* válasz */
   if(sendto(sock, buf, MAXBUF, 0, &client_addr, length) < 0)
   perror("sendto");
  }
}

A két program jó esetben ugyan működik, de semmit nem tesznek az esetleges csomagvesztések elkerülése végett. Maga a protokoll nem biztosít bennünket arról, hogy minden csomag célbaér, így a munka ránk hárul. Többféle módszert lehet használni. Lehet nyugtát küldeni minden egyes megérkezett csomag után, (A tcp így csinálja) vagy bizonyos időkorlátokat bevezetni. Az utóbbi az egyszerűbb, nézzünk erre egy példát.

Ha a kliens egy kérésére nem érkezik válasz meghatározott időn belül, akkor megismétli a kérést. Ennél a módszernél a szervert nem kell módosítani, hiszen ő minden kérésre eddig is próbált válaszolni.

Timeout -ok kezelése
Gyakorlati megvalósításban az ismert select függvény jól használható.

Például az alábbi ciklus addig küld kéréseket öt másodpercenként, amíg nem érkezik válasz.

resp=0;
while (!resp)
{
  /*kérés küldése*/
  sendto(sock, data, strlen(data) * sizeof(char), 0, (struct sockaddr *)&name, sizeof(name);
  wait.tv_sec = 5;
  wait.tv_usec = 0;
  FD_ZERO(&read_template);
  FD_SET(sock, &read_template);
  if (select(FD_SETSIZE, &read_template, NULL,NULL, &wait)<=0)
  {
    printf("Timeout\n");
    continue;
  }
  if (FD_ISSET(sock, &read_template))
  {
    if(read(sock, data,(1024 * 2)) < 0)
    {
       perror("read");
       exit(1);
    }
    resp=1;
  }
}