Protokol UDP

V této kapitole vám ukáži příklad na nespolehlivé spojení, tedy posílání datagramů přes UDP. Uvidíte, že je to o něco jednodušší, než TCP spojení.

M$ Ve windows můžete najít knihovnu WinSock, která funguje velmi podobně jako linuxové sockety. Popisovat ji tu sice nebudu, ale pokud porozumíte linuxovým socketům, WinSocku porozumíte o mnoho snadněji. Viz třeba is.muni.cz/th/51885/fi_b/WinSock.pdf.

Úvod

Server a klient budou dělat to samé, co dělali příklady v minulých kapitolách. Protože ale protokol UDP nenavazuje spojení, nepracuje se s deskriptory. Příklady tak tentokrát budou vypadat trochu jinak.

Pro komunikaci použiji opět struktury scitance a vysledek.

/*------------------------------------------------*/
/* 42udp/connection.h                             */

typedef struct {
    int x;
    int y;
} scitance;

typedef struct {
    int z;
} vysledek;

#define PORT 50505
/*------------------------------------------------*/

Port jsem nechal stejný, jako v minulém příkladu, ale nenechte se zmílit. UDP klient nemůže komunikovat s TCP serverem, protože TCP vyžaduje navázání spojení a potvrzování přenesení dat (o což se starají dříve popsané funkce automaticky). Stejně tak se nemůže TCP klient domluvit s UDP serverem.

UDP server

První změna, oproti TCP, je, že funkce sock() má jako druhý argument SOCK_DGRAM místo SOCK_STREAM.

Další změnou je, že se nepoužívají funkce listen() ani accept(). Místo toho se rovnou přijme obsah datagramu funkcí recvfrom().

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

Prvním argumentem je socket. Druhý argument ukazuje na paměť, kam se uloží přijatá data. Třetí argument je délka této paměti. Čtvrtý argument, flags, se používá pro ovlivnění chování recvfrom(). Co můžete do flags dosadit se dočtete v manuálových stránkách (0 znamená žádný flag). Do pátého argumentu se uloží adresa odesílatele datagramu a do posledního argumentu se uloží délka této adresy.

Packet se odesílá pomocí funkce sendto().

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

Prvním argumentem je socket. Druhý argument ukazuje na paměť, odkud se načtou posílaná data. Třetí argument je délka této paměti. Čtvrtý argument, flags, se používá pro ovlivnění chování sendto(). Co můžete do flags dosadit se dočtete v manuálových stránkách (0 znamená žádný flag). Do pátého argumentu se uloží adresa příjemce datagramu a do posledního argumentu se uloží délka této adresy.

Mimochodem, tyto funkce bude používat pro posílání a příjem dat i klient.

Funkce recvfrom() předává délku adresy jako odkaz, protože se do proměnné uloží skutečná délka uložené adresy. Měli byste zkontrolovat, zda odpovídá délka té očekávané (to v příkladu nedělám) a proměnnou client_len před každým voláním recvfrom() znovu nastavit (to v příkladu dělám).

Takto jednoduše pak vypadá server:

  1. /*------------------------------------------------*/
  2. /* 42udp/server.c                                 */
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <sys/types.h>
  6. #include <sys/socket.h>
  7. #include <signal.h>
  8. #include <arpa/inet.h>
  9. #include <unistd.h>
  10. #include <stdbool.h>
  11. #include <string.h>
  12. #include <errno.h>
  13. #include "connection.h"
  14.  
  15.  
  16. static void closeServerHandler()
  17. {
  18.     printf("\nClosing server ...\n");
  19.     close(sd);
  20.     exit(EXIT_SUCCESS);
  21. }
  22.  
  23. int main(void)
  24. {
  25.     scitance sc;
  26.     vysledek v;
  27.     struct sockaddr_in server, client;
  28.     socklen_t client_len;
  29.     int ret;
  30.  
  31.     memset(&server, 0, sizeof(server));
  32.     server.sin_family = AF_INET;
  33.     server.sin_addr.s_addr = htonl(INADDR_ANY);
  34.     server.sin_port = htons(PORT);
  35.     sd = socket(AF_INET, SOCK_DGRAM, 0);
  36.  
  37.     bind(sd, (struct sockaddr *)&server, sizeof(server));
  38.  
  39.     signal(SIGINT, closeServerHandler);
  40.  
  41.     while (true) {
  42.         printf("Čekám na packet od klienta ...\n");
  43.         client_len = sizeof(client);
  44.         ret =
  45.             recvfrom(sd, &sc, sizeof(sc), 0,
  46.                  (struct sockaddr *)&client, &client_len);
  47.         if (ret < 0) {
  48.             perror("recvfrom");
  49.             exit(EXIT_FAILURE);
  50.         }
  51.         printf("Sčítám %i + %i.\n", sc.x, sc.y);
  52.         v.z = sc.x + sc.y;
  53.         printf("Write ...\n");
  54.         sendto(sd, &v, sizeof(v), MSG_DONTWAIT,
  55.                (struct sockaddr *)&client, client_len);
  56.     }
  57.  
  58.     return 0;
  59. }
  60. /*------------------------------------------------*/

Maximální délka, kterou můžete v datagramu poslat, je 65.527 bajtů (65.535 bajtů - UDP (8) a IP (20) hlavičky). Pokud se pokusíte v jednom datagramu poslat více dat, tak funkce sendto() selže, vrátí -1 a nastaví errno na EMSGSIZE.

Něco jiného je maximální zaručená délka. Může se totiž stát, že někde cestou na internetu bude nějaké zařízení, které max. délku datagramu ještě omezuje. Nejmenší maximální délka může být (po odečtení UDP a IP hlaviček) 508. (Min. délka datagramu musí být 576, max. délka IP hlavičky je 60, UDP 8). V praxi se používá 512 bajtů, protože je to hezké číslo a je nepravděpodobné, že by IP hlavička měla někdy 60 bajtů).
Ve skutečnosti se dá ale spolehnout na to, že na cestě nenarazíte na zařízení, které by nepodporovalo alespoň 1500 bajtů, tedy zhruba 1472 bajtů na data.

Pokud se dostane k zařízení datagram větší, než je zařízení schopné najednou poslat, pak se rozdělí na několik částí, které se v cíli zase složí. To není velký problém, jen toto rozkládání a skládání zdržuje. Což pro UDP je problém, protože UDP se používá hlavně kvůli rychlosti :-).

UDP client

A takto jednoduše vypadá klient. V podstatě používám to samé, jako server, jenom místo recvfrom() používám kratší recv(). Je kratší o adresu odesílatele packetu. Ta mě nezajímá, neboť ji znám – je to přeci adresa serveru, na který nejdříve posílám dotaz.

Ačkoliv, kdybych byl paraonidní, asi bych použil recvfrom(), abych si mohl zkontrolovat, zda příchozí datagram skutečně přišel ze serveru, kterého jsem se ptal. Ovšem vychytralý útočník stejně dokáže poslat datagram s podvrženou odesílací adresou, takže by to moc účinná obrana proti záškodníkům nebyla. (Proti záškodníkům pomáhá nejlépe šifrovaná komunikace SSH atp.)

V kódu ale přeci jen najdete tři nové věci. Jednak jsem použil setsockopt() s parametrem SO_RCVTIMEO pro omezení času čekání na odpověď. Pokud funkce recv() nedostane odpověď do 2 sekund (tm.tv_sec) a 0 milisekund (tm.tv_usec), přestane čekat a vrátí chybu.

Druhá novinka je použití flagu MSG_DONTWAIT s funkcí sendto(). To způsobí, že volání této funkce bude neblokujcí, čili pokud není možné poslat data ihned, funkce vrátí chybu (EAGAIN nebo EWOULDBLOCK). V příkladu návratovou hodnotu sendto() nekontroluji, aby byl kratší a přehlednější, ale v praxi by to byla chyba, za kterou se programátorům píchají špendlíky pod nechty.

Poslední novinkou je funkce gethostbyname(). Té dáte jako argument doménové jméno (např. www.sallyx.org) a ona vám na oplátku vrátí odkaz na strukturu hostent.

#include <netdb.h>
struct hostent *gethostbyname(const char *name);
struct hostent {
        char  *h_name;            /* official name of host */
        char **h_aliases;         /* alias list */
        int    h_addrtype;        /* host address type */
        int    h_length;          /* length of address */
        char **h_addr_list;       /* list of addresses */
}

Kvůli zpětné kompatibilitě je ještě v struct hostent definována položka h_addr odpovídající položce h_addr_list[0]. h_addr je tedy první IP adresa ze seznamu IP adres h_addr_list. (Ano, jedno doménové jméno může mít více IP adres. A dřív to tak nebylo, proto bylo dřív v struct hostent h_addr a ne h_addr_list. Chápete to správně :-)).

Podrobnější popis funkce gethostbyname() a položek struktury hostent bude v příští kapitole.

  1. /*------------------------------------------------*/
  2. /* 42udp/client.c                                 */
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <unistd.h>
  6. #include <netdb.h>
  7. #include <sys/types.h>
  8. #include <sys/socket.h>
  9. #include <time.h>
  10. #include <string.h>
  11. #include "connection.h"
  12.  
  13. int main(void)
  14. {
  15.     scitance sc;
  16.     vysledek v;
  17.     int sd;
  18.     struct hostent *hp;
  19.     struct sockaddr_in server;
  20.     struct timeval tm;
  21.     int ret;
  22.  
  23.     srand(time(NULL));
  24.     sc.x = rand() % 10;
  25.     sc.y = rand() % 10;
  26.  
  27.     printf("Connect ...\n");
  28.  
  29.     sd = socket(AF_INET, SOCK_DGRAM, 0);
  30.     tm.tv_sec = 2;
  31.     tm.tv_usec = 0;
  32.     setsockopt(sd, SOL_SOCKET, SO_RCVTIMEO, (const char *)&tm, sizeof(tm));
  33.     server.sin_family = AF_INET;
  34.     server.sin_port = htons(PORT);
  35.     hp = gethostbyname("localhost");
  36.     memcpy(&(server.sin_addr.s_addr), hp->h_addr, hp->h_length);
  37.  
  38.     printf("Write ...\n");
  39.     sendto(sd, &sc, sizeof(sc), MSG_DONTWAIT, (struct sockaddr *)&server,
  40.            sizeof(server));
  41.  
  42.     printf("Read ...\n");
  43.     ret = recv(sd, &v, sizeof(v), 0);
  44.     if (ret != sizeof(v)) {
  45.         perror("recv");
  46.         exit(EXIT_FAILURE);
  47.     }
  48.     printf("%i + %i = %i\n", sc.x, sc.y, v.z);
  49.  
  50.     close(sd);
  51.  
  52.     return 0;
  53. }
  54. /*------------------------------------------------*/

Takto vypadá výstup z klienta, pokud není spuštěný server. Mezi Read ... a chybou recv: … uplynou 2 sekundy:

Connect ...
Write ...
Read ...
recv: Resource temporarily unavailable

Výstup z puštěného serveru a klienta vypadá úplně stejně, jako v předchozí kapitole o TCP.

Co říci závěrem. UDP je jednodušší a rychlejší, ale není to spolehlivá služba jako TCP. Nemůžete se spolehnout na to, že datagramy dojdou ve stejném pořadí, jako byly odeslány a v případě problémů se nedozvíte, že datagram nedošel, nebo že nedošel celý, nepoškozený.

To, jestli budete používat TCP nebo UDP protokol je proto jen na zvážení selským rozumem. Pro hry dává smysl UDP, pro většinu ostatního TCP.

Komentář Hlášení chyby
Vytvořeno: 6.10.2014
Naposledy upraveno: 10.10.2014
Tato stánka používá ke svému běhu cookies, díky kterým je možné monitorovat, co tu provádíte (ne že bych to bez cookies nezvládl). Také vás tu bude špehovat google analytics. Jestli si myslíte, že je to problém, vypněte si cookies ve vašem prohlížeči, nebo odejděte a už se nevracejte :-). Prohlížením tohoto webu souhlasíte s používáním cookies. Dozvědět se více..