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í.
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
.
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()
.
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()
.
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:
- /*------------------------------------------------*/
- /* 42udp/server.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <signal.h>
- #include <arpa/inet.h>
- #include <unistd.h>
- #include <stdbool.h>
- #include <string.h>
- #include <errno.h>
- #include "connection.h"
- {
- close(sd);
- }
- {
- scitance sc;
- vysledek v;
- socklen_t client_len;
- int ret;
- server.sin_family = AF_INET;
- server.sin_addr.s_addr = htonl(INADDR_ANY);
- server.sin_port = htons(PORT);
- sd = socket(AF_INET, SOCK_DGRAM, 0);
- signal(SIGINT, closeServerHandler);
- ret =
- }
- v.z = sc.x + sc.y;
- }
- }
- /*------------------------------------------------*/
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
.
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.
- /*------------------------------------------------*/
- /* 42udp/client.c */
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <netdb.h>
- #include <sys/types.h>
- #include <sys/socket.h>
- #include <time.h>
- #include <string.h>
- #include "connection.h"
- {
- scitance sc;
- vysledek v;
- int sd;
- struct sockaddr_in server;
- struct timeval tm;
- int ret;
- sd = socket(AF_INET, SOCK_DGRAM, 0);
- tm.tv_sec = 2;
- tm.tv_usec = 0;
- server.sin_family = AF_INET;
- server.sin_port = htons(PORT);
- hp = gethostbyname("localhost");
- }
- close(sd);
- }
- /*------------------------------------------------*/
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.