Socket API

Socket je obecný mechanizmus pro síťovou komunikaci, původně pomocí rodiny protokolů rodiny TCP/IP, nezávisle na implementaci protokolů, poprvé použitý v systémech BSD UNIX. Socket (z angličtiny zásuvka, objímka, hrdlo trubky) je programové komunikační rozhraní uzlu pro příjem a odesílání dat po síti (datagramů, paketů i rámců). Socket obsahuje informace potřebné pro přenos dat a datové spojení (protokol, adresy atd.). Programové rozhraní Socket API z BSD UNIX je stejné pro všechny unixové operační systémy (a tedy i GNU/Linux) a velice podobné na systémech MS Windows, které jej převzaly.

V unixových systémech je Socket API přímo součástí C knihovny. Pro použití funkcí API je nutné vkládat různé hlavičkové soubory. U každé funkce jsou potřebné hlavičkové soubory uvedeny. Číselné chybové kódy funkcí se ukládají do globální proměnné errno nebo h_errno. Pro podrobnosti k funkcím viz manuálové stránky, které jsou uvedeny u každé funkce.

V MS Windows jsou funkce pro práci se sockety implementovány v knihovně Windows Socket API (Winsock API). Winsock API je velice podobné původnímu Socket API z BSD UNIX, ale některé funkce a struktury se mírně odlišují a API bylo rozšířeno o vlastnosti událostmi řízeného prostředí. Podstatné rozdíly budou zmíněny, drobnější (např. typy parametrů a položek struktur) viz MSDN. V novějších verzích 2.* jsou zavedeny také nové funkce WSA*, které se dají použít místo původních (které jsou označeny jako "staré"), ale z důvodu minimální odlišnosti (téměř shodnosti) se Socket API budeme používat starší verzi Winsock API 1.1. Verze 2.* jsou s verzí 1.1 zpětně kompatibilní. Knihovnu Winsock je před používáním socketů nutné nejdříve inicializovat pomocí funkce WSAStartup. Bez inicializace knihovny nebudou funkce pro sockety fungovat. Všechny funkce jsou deklarovány v hlavičkovém souboru winsock.h (popř. winsock2.h), který je vkládán z hlavičkového souboru windows.h Win32 API (winsock2.h ovšem ne), a implementovány v knihovním souboru wsock32.dll (popř. ws2_32.dll), který je nutné předat linkeru (skrze wsock32.lib, popř. ws2_32.lib). Pro podrobnosti k funkcím viz MSDN.

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
  • inicializuje knihovnu socketů, při úspěchu vrací 0, jinak chybový kód
  • wVersionRequested je číslo požadované verze knihovny, dolní byte hlavní (1), horní číslo podverze (1), vytvoří se např. pomocí makra MAKEWORD(1, 1)
  • lpWSAData je ukazatel na alokovanou (ale neinicializovanou) strukturu WSADATA, kterou funkce naplní, některé položky (ostatní viz MSDN)
    • WORD wVersion, WORD wHighVersion: číslo verze a maximální verze knihovny, byty získáme pomocí maker LOBYTE a HIBYTE
    • unsigned short iMaxSockets: maximální použitelný počet socketů
    • unsigned short iMaxUdpDg: maximální velikost UDP datagramu v bytech

Po ukončení práce se sockety v MS Windows by se měla zavolat funkce WASCleanup. Kód chyby při selhání nějaké funkce lze získat funkcí WSAGetLastError, pro návratový kód -1 funkcí API existují makra SOCKET_ERROR a INVALID_SOCKET.

Vytvoření socketu

Před příjmem nebo odesíláním dat po síti je potřeba vytvořit/otevřít rozhraní - socket. Otevření socketu je analogické otevření souboru, socket se chová jako otevřený soubor (ve Winsock 2), je tedy identifikovaný souborovým deskriptorem nebo handlem (file descriptor, socket/file handle).

socket(2)
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
SOCKET socket(int af, int type, int protocol);
  • vytvoří socket v komunikační doméně domain typu type s použitím komunikačního protokolu protocol
  • doména specifikuje rodinu protokolů (resp. adres) použitých při komunikaci, nepřímo i vrstvu (vzhledem k vrstvovému modelu OSI ISO), na které se bude komunikovat:
    • makra PF_INET/AF_INET: rodina protokolů TCP/IP, síťová a transportní vrstva
    • makra PF_PACKET/AF_PACKET: síťové technologie, např. Ethernet, linková vrstva, pouze pro unixové systémy
    • makra AF_UNIX/PF_UNIX a AF_LOCAL/PF_LOCAL: lokální komunikaci v rámci počítače, pouze pro unixové systémy
  • typ socketu určuje typ komunikace, spojovanou "spolehlivou" (např. protokolem TCP) nebo nespojovanou "nespolehlivou" (např. UDP), popř. s přístupem k hlavičkám protokolů:
    • makro SOCK_STREAM: socket pro spojovanou "spolehlivou" komunikaci
    • makro SOCK_DGRAM: socket pro nespojovanou "nespolehlivou" komunikaci
    • makro SOCK_RAW: socket zpřístupňující hlavičky protokolů
  • protocol specifikuje protokol, kterým se bude v rámci domény (rodiny protokolů) komunikovat, jako číslo v síťovém tvaru, většinou jediný možný, např. pro TCP je makro IPPROTO_TCP, protocol rovno 0 znamená použití výchozího protokolu pro použitou doménu a typ socketu
  • vrátí file descriptor nebo socket handle socketu, při chybě -1 (na MS Windows INVALID_SOCKET)

Pro zrušení socketu lze na unixových systémech použít funkci close pro uzavření souboru, na MS Windows je nutné použít funkci closesocket (close socket nezruší!).

Různá čísla (protokolů, adres atd.) přenášená sítí jsou funkcím v Socket API předávána a funkcemi vracena v tzv. síťovém tvaru (network byte order), tvaru pro přenos sítí. Toto je binární tvar s pořadím bytů big endian, tj. v pořadí od nevýznamějších bytů k méně významným. Lokální uložení čísel např. na architektuře i386 je ale opačné, little endian. Pro převod čísel mezi lokálním tvarem a síťovým tvarem slouží funkce htonl, htons, ntohl a ntohs.

byteorder(3)
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
u_long htonl(u_long hostlong);
u_short htons(u_short hostshort);
u_long ntohl(u_long netlong);
u_short ntohs(u_short netshort);
  • funkce převádí čtyřbajtová (*l) nebo dvoubajtová (*s) čísla z lokálního tvaru na síťový tvar (hton*) a obráceně (ntoh*)

Odesílání a přijímání dat

Nyní můžeme skrze vytvořený socket vysílat nebo přijímat data do/ze sítě. Pro sockety typu SOCK_RAW a SOCK_DGRAM existují k tomuto účelu speciální funkce identifikující příjemce/odesílatele.

send(2)
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendto(int s, const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
int sendto(SOCKET s, const char *buf, int len, int flags, const struct sockaddr *to, int tolen);
  • odešle data z místa na buf délky len socketem s na adresu to délky tolen
  • socket má odesílací buffer, pokud se do něj data nevlezou, operace blokuje, dokud se neuvolní v bufferu místo (odešlou data)
  • flags je bitový součet vlajek např. MSG_DONTWAIT (neblokovat, vrátit chybu), viz man
  • vrací počet skutečně odeslaných bajtů nebo -1 při chybě
  • úspěšné odeslání dat neznamená, že data byla (adresátem) uspěšně přijata

recv(2)
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
int recvfrom(SOCKET s, char *buf, int len, int flags, struct sockaddr *from, int *fromlen);
  • přijme data do místa na buf délky len socketem s
  • pokud from není NULL, musí být na fromlen délka místa na from, pak na místo na from je zapsána adresa odesílatele a na místo na fromlen její délka (délka místa na from)
  • pokud na příjmu socketu nejsou žádná data, funkce na nějaká čeká (blokuje), ale nečeká až do naplnění buf délky len
  • pro testování dat na příjmu socketu lze použít funkce select(2) a poll(2)
  • pokud se přijímaná data nevlezou do buf, mohou být oříznuta (v závislosti na typu socketu)
  • flags je bitový součet vlajek např. MSG_DONTWAIT (neblokovat, vrátit chybu), MSG_WAITALL (čekat na naplnění celého buf), viz man
  • vrací počet skutečně přijatých bajtů nebo -1 při chybě

Pro na hardwaru nezávislé uložení adresy se používá struktura sockaddr. V každé doméně (rodině protokolů, resp. adres) se ale používá jiný typ adres a pro uložení každého typu adresy se používá jiná struktura. Pro uložení adresy v doméně PF_INET je to struktura sockaddr_in, v doméně PF_PACKET struktura sockaddr_ll a v doméně PF_UNIX struktura sockaddr_un. Datový typ sockaddr vlastně není struktura, ale union, který může nést libovolnou z výše uvedených struktur, proto se ve funkcích používajících typ ukazatele na sockaddr používá typ ukazatele na některou tuto strukturu přetypovaný na ukazatel na sockaddr.

Linková vrstva – packet socket (Ethernet, pouze unixové systémy)

Pro odesílání a příjem linkových rámců se používají sockety z domény PF_PACKET, tzv. paketové (packet) sockety. Zde můžeme vytvořit jen dva typy socketů, SOCK_DGRAM nebo SOCK_RAW. Při použití socketu typu SOCK_RAW odesíláme nebo příjímáme celé linkové rámce včetně záhlaví a zápatí, naopak při použití socketu typu SOCK_DGRAM pracujeme pouze s daty linkového rámce (části paketů síťové vrtsvy).

Linkových protokolů, které můžeme použít je spousta, ale omezíme se Ethernet, tj. makro ETH_P_ALL. Dále se můžeme omezovat na ethernetové rámce nesoucí určitý síťový paket, např. ETH_P_IP, ETH_P_ARP, a jiné (makra pro čísla dle normy IEEE 802.3 jsou definována v linux/if_ether.h). Čísla protokolů jsou dvoubajtová, proto je nutné použít funkci htons pro převod z lokálního tvaru do síťového. U paketových socketů typu SOCK_DGRAM není při příjmu odstraněna a při odesílání doplňována IEEE 802.2 LLC hlavička (např. pro IEEE 802.3 ethernetové rámce, protokol ETH_P_802_3).

Při příjmu rámců jsou všechny rámce zvoleného protokolu vloženy do socketu a pak teprve dále zpracovány systémem. Paketové sockety může otevřít pouze proces superuživatele (root) nebo proces se speciálními právy (capability CAP_NET_RAW).

Pro doménu PF_PACKET se používá pro linkovou adresu struktura sockaddr_ll. Vybrané položky:

packet(7)
#include <sys/socket.h>
#include <netpacket/packet.h>
#include <net/ethernet.h>
struct sockaddr_ll;
  • unsigned short sll_family: doména, pro kterou je adresa určena, vždy AF_PACKET
  • unsigned short sll_protocol: linkový protokol, číslo v síťovém tvaru, výchozí je protokol socketu, např. pro Ethernet ETH_P_ALL
  • int sll_ifindex: číslo síťového rozhraní, získá se pomocí ioctl SIOCGIFINDEX, viz man netdevice(7)
  • unsigned char sll_pkttype: typ rámce, tohoto počítače (PACKET_HOST), všesměrový (PACKET_BROADCAST), jiného počítače (PACKET_OTHERHOST, při promiskuitním režimu) aj.
  • unsigned char sll_halen: délka linkové adresy v bytech, pro MAC adresy 6
  • unsigned char sll_addr[8]: linková adresa, např. MAC

Pro odeslání rámce je potřeba vyplnit pole sll_family, sll_addr, sll_halen a sll_ifindex, ostatní by měla být nulová. U přijatých rámců je struktura sockaddr_ll vyplněna přijímací funkcí.

Linková adresa je ve struktuře sockaddr_ll (a v datech rámce) uložena v (binárním) síťovém tvaru. Pro převod na řetězec pro MAC šesti šestnáctkových čísel z rozsahu 0 až FF oddělených dvojtečkami (dvojtečkový tvar) a obráceně slouží funkce ether_ntoa a ether_aton.

ether_aton(3)
#include <netinet/ether.h>
char * ether_ntoa(const struct ether_addr *addr);
struct ether_addr *ether_aton(const char *asc);
  • ether_ntoa převádí adresu addr v síťovém tvaru na dvojtečkový tvar, který vrací (ve statickém bufferu, přepisovaném opakovaným voláním funkce)
  • ether_aton převádí adresu asc v dvojtečkovém tvaru na síťový tvar a vratí adresu v tomto tvaru (ve statickém bufferu), nebo NULL při chybné adrese
  • struktura ether_addr obsahuje jedinou položku u_int8_t ether_addr_octet[6]

S daty z hlavičky ethernetového rámce můžeme pracovat pomocí struktury ether_header:

#include <net/ethernet.h>
struct ether_header;
  • u_int8_t ether_dhost[6]: adresa příjemce
  • u_int8_t ether_shost[6]: adresa odesílatele
  • u_int16_t ether_type

Př. Napište program pro příjem všech ethernetových rámců vypisující délku rámce, linkový protokol (druh ethernetu) a všechny informace ze záhlaví rámce. Adresy odesílatele a příjemce vypisujte v dvojtečkovém tvaru.

Síťová vrstva – raw socket (Protokol IP)

Síťové pakety lze odesílat a přijímat pomocí paketových socketů (z domény PF_PACKET) typu SOCK_DGRAM, kdy pracujeme s celými pakety včetně hlavičky. Paketové sockety jsou ale dostupné jen na unixových systémech. Proto si ukážeme práci se síťovými pakety pomocí (obyčejných) socketů z rodiny PF_INET, které jsou dostupné i ve Winsock API. Rodina PF_INET pracuje se síťovými pakety protokolu IP verze 4.

Podobně jako linkové rámce včetně záhlaví můžeme odesílat a přijímat pomocí socketu typu SOCK_RAW z rodiny PF_PACKET, pro odesílání a příjem IP paketů včetně záhlaví se používají také sockety typu SOCK_RAW, tentokrát z rodiny PF_INET. Tyto tzv. syrové (raw) sockety zpřístupňují hlavičky IP paketu i transportního segmentu, popř. linkového "podprotokolu", např. ARP, nebo IP "podprotokolu", např. služebního ICMP. Níže (dle vrstvového modelu k linkovým rámcům) se se sockety z rodiny PF_INET dostat nelze. Stejně jako u paketových socketů při příjmu paketů jsou všechny pakety zvoleného protokolu vloženy do raw socketu a pak teprve dále zpracovány systémem a raw sockety může otevřít pouze proces superuživatele (root nebo Administrátor) nebo proces se speciálními právy (capability CAP_NET_RAW).

Pro odeslání paketu skrze raw socket s vyplněním hlavičky paketu lze jako protokol (ve funkci socket) použít makro IPPROTO_RAW. Hodnoty (pseudo)protokolů rodiny PF_INET jsou jednobytové, není tedy nutné používat konverzní funkce hton* do síťového tvaru. U Winsock API je jestě nutné zapnout volbu socketu IP_HDRINCL (viz dále, na unixových systémech je volba zapnuta automaticky), a navíc lze makro IPPROTO_RAW použít až s Winsock API verzí 2.*.

Pro doménu PF_INET se používá pro IP adresu (spolu s portem transportního protokolu) struktura sockaddr_in. Položky:

ip(7)
#include <sys/socket.h>
#include <netinet/ip.h>
struct sockaddr_in;
  • sa_family_t sin_family / short sin_family: doména, pro kterou je adresa určena, vždy AF_INET
  • u_int16_t sin_port / u_short sin_port: port transportního protokolu v síťovém tvaru, na unixových systémech pro raw sockety při odesílání ignorována (měla by být 0), při příjmu síťový protokol
  • struct in_addr sin_addr: IP adresa, struktura in_addr obsahuje jedinou položku u_int32_t s_addr nesoucí IP adresu v síťovém tvaru, možno použít speciální hodnoty INADDR_LOOPBACK pro 127.0.0.1, INADDR_BROADCAST pro 255.255.255.255 a další

Pro odeslání paketu je potřeba vyplnit všechny, u přijatých paketů je struktura sockaddr_in vyplněna přijímací funkcí.

IP adresa je ve struktuře sockaddr_in (a v datech paketu) uložena v (binárním) síťovém tvaru. Pro převod na řetězec čtyř desítkových čísel z rozsahu 0 až 255 oddělených tečkami (tečkový tvar) a obráceně slouží funkce inet_ntoa a inet_aton.

inet(3)
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);
int inet_aton(const char *cp, struct in_addr *inp);
  • inet_ntoa převádí adresu in v síťovém tvaru na tečkový tvar, který vrací (ve statickém bufferu)
  • inet_aton převádí adresu cp v tečkovém tvaru na síťový tvar a uloží na místo inp, vrací 0 při chybě, jinak nenulové číslo
  • čísla v adrese v tečkovém tvaru začínající 0 jsou uvažována v osmičkové soustavě, čísla začínající 0x v šestnáctkové soustavě!

Pro zpřístupnění povinných položek hlavičky IP paketu lze použít strukturu iphdr:

#include <netinet/ip.h>
struct iphdr;
  • unsigned int version:4: verze IP, tedy 4
  • unsigned int ihl:4: délka záhlaví (včetně volitelných položek), v jednotkách 4 B
  • u_int8_t tos: typ služby, při nepoužití 0
  • u_int16_t tot_len: celková délka IP paketu (hlavička + data) v bytech
  • u_int16_t id: identifikátor paketu přidělený OS
  • u_int16_t frag_off: první tři bity jsou příznaky pro fragmentaci, první vždy 0 (rezervován), druhý DF - zakázání fragmentace, třetí MF - fragment není poslední, zbylých 13 bitů udává posunutí dat fragmentu v původním paketu
  • u_int8_t ttl: doba životnosti paketu (TTL)
  • u_int8_t protocol: protokol vyšší vrstvy, transportní nebo síťový "podprotokol" (např. IPPROTO_TCP, IPPROTO_UDP, IPPROTO_ICMP)
  • u_int16_t check: kontrolní součet záhlaví
  • u_int32_t saddr: IP adresa odesílatele
  • u_int32_t daddr: IP adresa příjemce

Při odesílání paketu skrze raw socket s pseudoprotokolem IPPROTO_RAW musí být součástí odesílaných dat IP hlavička s vyplněnými (téměř) všemi položkami. Pro výpočet kontrolního součtu vyplněného záhlaví můžeme použít funkci v RFC 1071, pole pro kontrolní součet v záhlaví, ze kterého se součet teprve počítá, je rovno 0. Skutečně odeslaná hlavička nemusí být stejná jako vyplněná, na unixových systémech jsou automaticky vyplňovány kontrolní součet a celková délka, a při nulových hodnotách také identifikátor (při nenulovém hrozí kolize s jiným paketem!) a IP adresa odesílatele. Za IP adresu příjemce se bere ta ze struktury sockaddr_in předaná funkci sendto. Pakety větší než MTU linky nejsou systémem automaticky fragmentovány a jejich velikost je tak omezena na MTU linky.

Pseudoprotokol IPPROTO_RAW ovšem nelze použít pro příjem paketů včetně hlavičky. K tomuto účelu lze použít protokol IPPROTO_IP a volbu socketu IP_HDRINCL (viz dále). Pak je součástí přijímaných dat i IP hlavička. Přijímané IP pakety jsou automaticky systémem defragmentovány, pro příjem jednotlivých fragmentů je nutné použít paketový socket (např. s protokolem ETH_P_IP). Protokol IPPROTO_IP s volbou IP_HDRINCL lze použít i pro odesílání paketů (součástí dat musí být vyplněná IP hlavička).

Volby socketu

Parametry socketu a přenášených protokolů (zejména položky hlaviček) lze upravovat pomocí voleb socketu (socket options). K získání a nastavování hodnot voleb slouží dvojice funkcí getsockopt a setsockopt.

getsockopt(2)
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int s, int level, int optname, void *optval, socklen_t *optlen);
int getsockopt(SOCKET s, int level, int optname, char* optval, int* optlen);
int setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);
int setsockopt(SOCKET s, int level, int optname, const char* optval, int optlen);
  • getsockopt vrací hodnotu volby optname socketu s na úrovni level na místo na optval a délku hodnoty na místo na optlen (kde je při volání funkce délka místa na optval, socklen_t unsigned long)
  • setsockopt nastavuje hodnotu volby optname socketu s na úrovni level na hodnotu na místě na optval délky optlen
  • úrovně level voleb odpovídají protokolům, kterých se volby týkají, např. IPPROTO_IP, IPPROTO_TCP apod. (je potřeba vložit hlavičkový soubor pro protokol, např. netinet/ip.h nebo netinet/tcp.h), nebo nejvyšší úrovni socketu, SOL_SOCKET
  • volby jsou popsány v socket(7) pro úroveň socketu, ip(7), tcp(7) apod. pro protokolové úrovně
  • vrací 0 nebo (při typu int) hodnotu volby, při chybě -1

Voleb socketu existuje mnoho. Na úrovni protokolů volby ovlivňují každý odesílaný nebo přijímaný paket (IP) nebo segment/datagram (TCP/UDP) a kromě voleb pro parametry protokolu existují i volby odpovídající některým položkám záhlaví protokolu. Např. pro protokol IP, tj. úroveň IPPROTO_IP, jsou volby pro přítomnost IP záhlaví před daty (IP_HDRINCL, pouze u raw socketů, viz výše), u odchozích paketů pro typ služby (IP_TOS), dobu živostnosti paketu (IP_TTL), volitelné položky IP záhlaví (IP_OPTIONS), dále zjištění a nastavení MTU linky (IP_MTU_DISCOVER), aktuální MTU linky (IP_MTU, u socketů spojení), pro skupinové adresování (multicast, IP_ADD_MEMBERSHIP, IP_DROP_MEMBERSHIP, IP_MULTICAST_TTL, IP_MULTICAST_LOOP) a další.

Na úrovni socketu jsou dostupné volby pro obecné parametry socketu pro odesílání, příjem či spojení, např. max. velikosti vstupního (SO_RCVBUF) a výstupního (SO_SNDBUF) bufferu, možnost odesílat/přijímat na/z všesměrové adresy (SO_BROADCAST), priorita odesílaných dat (SO_PRIORITY), časový interval pro odeslání (SO_SNDTIMEO) a příjem (SO_RCVTIMEO) dat před ukončením blokování odesílací nebo přijímací funkce s chybou a další.

Př. Modifikujte (nebo rozšiřte) předchozí program tak, aby přijímal všechny IP pakety a vypisoval všechny informace ze záhlaví paketu. Fragmentační údaje vypisujte pohromadě a IP adresy odesílatele a příjemce v tečkovém tvaru.

Protokol ICMP

Pro odesílání a příjem ICMP paketů lze použít makro IPPROTO_ICMP jako protokol funkce socket při vytváření raw socketu. Při odesílání pak pracujeme pouze s ICMP hlavičkou a daty, IP hlavičku vyplňuje OS. Některé položky IP hlavičky nicméně můžeme měnit pomocí voleb socketu. Přijímaná data ovšem budou obsahovat jak ICMP, tak IP hlavičku! Pro práci s ICMP hlavičkou lze využít strukturu icmphdr:

#include <netinet/ip_icmp.h>
struct icmphdr;
  • u_int8_t type: typ ICMP paketu
  • u_int8_t code: kód typu paketu
  • u_int16_t checksum: kontrolní součet záhlaví a dat
  • union un: proměnná část paketu podle typu a kódu, obsahuje např.
    • struct { u_int16_t id; u_int16_t sequence; } echo: identifikátor id a pořadové číslo sequence echo žádosti (typ 8, kód 0) a odpovědi (typ 0, kód 0)

V hlavičkovém souboru netinet/ip_icmp.h jsou také definovány konstanty pro různé typy ICMP paketů, např. ICMP_ECHO, ICMP_ECHOREPLY, ICMP_DEST_UNREACH, ICMP_TIME_EXCEEDED, a kódy typů, např. pro typ ICMP_TIME_EXCEEDED jsou ICMP_EXC_TTL a ICMP_EXC_FRAGTIME.

Nepsaným pravidlem pro jednoznačný identifikátor v ICMP echo žádosti je identifikátor procesu, který žádost vysílá. Ten lze na unixových systémech získat funkcí getpid(2), na MS Windows funkcí GetCurrentProcessId.

Př. Modifikujte (nebo rozšiřte) předchozí program tak, aby přijímal všechny ICMP pakety a vypisoval všechny informace ze záhlaví paketu. U ICMP echo paketů vypisujte identifikátor a pořadové číslo žádosti/odpovědi.

Př. Implementujte zjednodušenou verzi programu ping. Program odešle několik ICMP echo žádostí na IP adresu zadanou jako parametr programu a po odeslání každé přijme odpovídající echo odpověď. Nastavte TTL odchozích IP paketů na 255 a časový interval pro příjem odpovídající odpovědi na 5 sekund (pomocí volby socketu SO_RCVTIMEO). U každé dvojice žádost-odpověď vypište čas mezi nimi (round trip time, RTT) pomocí funkce gettimeofday(2). Pro výpočet kontrolního součtu ICMP záhlaví použijte funkci z RFC 1071.

Transportní vrstva (Protokoly TCP a UDP)

Protokol TCP poskytuje spojovanou, "spolehlivou" službu typu klient/server. Klient navazuje spojení se serverem čekajícím na spojení na určeném portu a přijetí dat (TCP segmentů) ve správném pořadí je potvrzováno, případně jsou zaslána znovu. Při přenosu dat je použito řízení toku dat a další techniky. Poté je spojení ukončeno. Naproti tomu protokol UDP poskytuje "pouze" nespojovanou "nespolehlivou" službu, taktéž charakteru klient/server. Spojení mezi uzly se nenavazuje, klient data (UDP datagram) odešle na port serveru, server přijme, nic se nepotvrzuje, neexistuje žádný tok dat, uzly si vyměňují kousky dat (datagramy), které mohou dorazit v jiném pořadí a duplikovaně. UDP narozdíl od TCP téměř nijak nezatěžuje síť.

Celé transportní TCP segmenty nebo UDP datagramy včetně hlavičky můžeme odesílat a přijímat pomocí raw socketů (typu SOCK_RAW z rodiny PF_INET). V tomto případě při odesílání použijeme ve funkci socket pseudoprotokol IPPROTO_RAW a vyplníme (vedle záhlaví IP paketu) záhlaví TCP segmentu nebo UDP datagramu a při příjmu protokol IPPROTO_IP s volbou socketu IP_HDRINCL a záhlaví obdržíme jakou součást dat. Jako protokol vyšší vrstvy v položce protocol IP záhlaví použijeme makra IPPROTO_TCP nebo IPPROTO_UDP. Pokud nechceme pracovat s IP záhlavím, ale jen s TCP nebo UDP záhlavím, můžeme pro odesílání i příjem použít ve funkci socket také makra IPPROTO_TCP a IPROTO_UDP. Cílový port protější strany se uvádí v položce sin_port struktury sockaddr_in (v síťovém tvaru).

S hlavičkou TCP segmentu nebo UDP datagramu můžeme pracovat pomocí struktury tcphdr nebo udphdr:

#include <netinet/tcp.h>
struct tcphdr;
  • u_int16_t source: zdrojový port odesílatele
  • u_int16_t dest: cílový port příjemce
  • u_int32_t seq: pořadové číslo odesílaného bytu (1. bytu odesílanýchc dat) v toku dat
  • u_int32_t akc_seq: pořadové číslo přijatého bytu (následujícího, který má být přijat) v toku dat
  • u_int16_t doff:4: délka záhlaví v jednotkách 4 B
  • u_int16_t res1:4, u_int16_t res2:2: rezervováno
  • u_int16_t urg:1, u_int16_t ack:1, u_int16_t psh:1, u_int16_t rst:1, u_int16_t syn:1, u_int16_t fin:1: příznaky segmentů spojení
  • u_int16_t window: délka (posuvného) okna, tj. množství bytů, které je příjemce schopen přijmout
  • u_int16_t check: kontrolní součet tzv. pseudozáhlaví, tj. záhlaví, dat a některých položek IP zahlaví (IP adresy odesílatele a příjemce, 1 B bin. nul, protokol vyšší vrstvy – TCP nebo UDP a celková délka IP paketu)
  • u_int16_t urg_ptr: ukazatel naléhavých dat při příznaku URG

#include <netinet/udp.h>
struct udphdr;
  • u_int16_t source: zdrojový port odesílatele
  • u_int16_t dest: cílový port příjemce
  • u_int16_t len: délka datagramu (hlavička + data) v bytech
  • u_int16_t check: kontrolní součet tzv. pseudozáhlaví, tj. záhlaví, dat a některých položek IP zahlaví (IP adresy odesílatele a příjemce, 1 B bin. nul, protokol vyšší vrstvy – TCP nebo UDP a celková délka IP paketu), nepovinný (0)

Při odesílání TCP segmentů nebo UDP datagramů tímto způsobem NENÍ na unixových systémech rozhodující číslo portu v položce sin_port ve struktuře sockaddr_in adresy (ignorováno, mělo by být 0), ale číslo cílového portu v odesílaném záhlaví. Na MS Windows u Winsock si naopak MUSÍ být tato čísla rovna. Pro výpočet kontrolního součtu TCP i UDP záhlaví můžeme opět použít funkci v RFC 1071.

Př. Modifikujte (nebo rozšiřte) předchozí program tak, aby přijímal všechny TCP segmenty a UDP datagramy a vypisoval všechny informace z jejich záhlaví. U TCP segmentu vypište přehledně všechny nastavené příznaky.

Navázání a ukončení spojení – streamovaný a datagramový socket

Odesílání a přijímání transportních paketů pomocí raw socketu není zrovna nejpříjemnější, zvláště pro TCP segmenty, jejichž záhlaví je poměrně složité a navíc je potřeba ("ručně") navazovat, udržovat a ukončovat spojení a řídit tok dat, včetně potvrzování přijetí ve správném pořadí a případného opakování přenosu dat. Tyto poměrně náročné a složité akce za nás může zařídit knihovna implementující socket API.

Operaci navázání TCP spojení na straně klienta si můžeme výrazně zjednodušit využitím funkce connect, pokud použijeme socket typu SOCK_STREAM, tzv. streamovaný (spojovaný) socket. U protokolu UDP pro socket typu SOCK_DGRAM (z rodiny PF_INET), tzv. datagramový (nespojovaný) socket, nám funkce connect nastaví výchozí adresu serveru, kterou pak již nemusíme zadávat při odesílání a přijímání dat. Parametr protocol funkce socket může obsahovat hodnotu 0, tj. použije se výchozí protokol pro rodinu PF_INET a typ socketu, tzn. makro IPPROTO_TCP nebo IPPROTO_UDP. U těchto "vysokoúrovňových" socketů se už nestaráme o záhlaví IP paketů a TCP segmentů nebo UDP datagramů, tj. nevyplňujeme při odesílání ani neobdržímě při přijímání dat. Odesíláme a přijímáme pouze aplikační data.

U socketu typu SOCK_STREAM a SOCK_DGRAM (z rodiny PF_INET) jsou přijatá data vložena do socketu procesu a již nejsou dále zpracovávána systémem, jsou určena a předávána pouze procesu. Tyto sockety již samozřejmě může otevřít proces libovolného uživatele, bez potřeby speciálních práv.

connect(2)
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
int connect(SOCKET s, const struct sockaddr *name, int namelen);
  • připojí socket sockfd/s na adresu serv_addr/name délky addrlen/namelen, tj. provede navázání TCP spojení nebo nastaví adresu serveru pro UDP datagramy na IP adresu a port serveru z adresy serv_addr
  • serv_addr je ukazatel na sockaddr_in strukturu používanou pro adresy domény PF_INET, přetypovaný na ukazatel na strukturu sockaddr
  • zdrojový port spojení nebo odchozích datagramů je vybrán OS náhodně z volných neprivilegovaných portů (> 1023)
  • vrací 0 při navázání spojení, při chybě -1

Server narozdíl od klienta TCP spojení očekává, na určeném portu a lokální IP adrese (síťového rozhraní). Opět musíme socket na tuto adresu s portem připojit a to pomocí funkce bind. Na straně serveru se tomuto přiřazení adresy socketu nazývá pojmenování socketu; ostatní sockety jsou potom nepojmenované, anonymní. Pojmenovat socket, tj. přiřadit mu adresu pro příchozí data, lze i kterýkoliv z ostatních, paketový, raw i datagramový (pro UDP). Pak socket přijímá data (rámce, pakety apod.) pouze z této adresy (a portu). U TCP spojení je před vlastní komunikací s klientem (přijímání a odesílání dat) navíc ještě potřeba zahájit na serveru naslouchání příchozích spojení od klientů (což zařídí OS) a vytvořit frontu pro příchozí požadavky na spojení, funkce listen.

Na privilegovaný port (< 1024) může připojit socket pouze proces superuživatele (root nebo Administrátor) nebo proces se speciálními právy (capability CAP_NET_BIND_SERVICE).

bind(2)
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
int bind(SOCKET s, const struct sockaddr* name, int namelen);
  • pojmenuje socket, tzn. připojí socket sockfd/s na adresu my_addr/name délky addrlen/namelen, tj. přiřadí socketu lokální IP adresu a port, na které má server očekávat TCP spojení nebo ze kterého má číst příchozí UDP datagramy
  • my_addr je ukazatel na sockaddr_in strukturu používanou pro adresy domény PF_INET, přetypovaný na ukazatel na strukturu sockaddr
  • jako adresa se zadává IP adresa rozhraní do sítě, při adrese 127.0.0.1 (makro INADDR_LOOPBACK) jsou možná spojení a příjem datagramů pouze z lokálního stroje (přes zpětnou smyčku), naopak pro spojení na a příjem datagramů z libovolné lokální adresy (libovolného síťového rozhraní a cílového portu ze spojení nebo datagramu) existuje makro INADDR_ANY
  • vrací 0, při chybě -1

listen(2)
#include <sys/socket.h>
int listen(int sockfd, int backlog);
int listen(SOCKET s, int backlog);
  • zahájí naslouchání příchozích TCP spojení na socketu sockfd/s a vytvoří frontu pro požadavky na spojení délky backlog (počet nevyzvednutých požadavků)
  • další požadavky na spojení při zaplněné frontě jsou zamítány, tj. spojení je odmítnuto
  • pro nepojmenovaný socket (bez přiřazené adresy a portu funkcí bind) OS náhodně vybere pro příchozí spojení volný neprivilegovaný port, IP adresa je libovolná (lokální)
  • vrací 0, při chybě -1

Jednotlivé požadavky na TCP spojení poté z fronty vybíráme pomocí funkce accept, čímž vlastně spojení přijmeme. Funkce vrátí nový socket pro toto spojení, pomocí kterého pak můžeme komunikovat s klientem, tj. přijímat od něj a odesílat mu data. Starý socket slouží pouze k přijímání spojení a pro každé spojení od klientů je vytvořen socket nový. Server tak může obsluhovat víc klientů zároveň a ještě přijímat nová spojení (skutečně zároveň v případě paralelně implementovaného serveru, např. vícevláknového).

accept(2)
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
SOCKET accept(int SOCKET, struct sockaddr *addr, int *addrlen);
  • přijme TCP spojení na socketu sockfd/SOCKET, tzn. vyjme a potvrdí požadavek na spojení z fronty příchozích spojení
  • pokud addr není NULL, musí být na addrlen délka místa na addr, pak na místo na addr je zapsána adresa klienta (IP adresa a zdrojový port) a na místo na addrlen její délka (délka místa na from)
  • není-li ve frontě požadavek na spojení, funkce na nějaké čeká (blokuje)
  • pro testování požadavků na příchozí spojení lze použít funkce select(2) a poll(2)
  • vrací nový socket (file descriptor nebo handle) pro spojení s klientem, při chybě -1

TCP spojení ukončíme (na klientu nebo serveru) jednoduše zrušením/zavřením socketu, tj. na unixových systémech funkcí close, na MS Windows funkcí closesocket.

Odesílání a přijímání dat

Po navázání TCP spojení se serverem (odeslání požadavku na klientu funkcí connect a přijmutí na serveru funkcí accept) můžeme v daném čase zároveň odesílat a přijímat data, na klientu i na serveru. Propojení klienta se serverem je tedy plně obousměrné (full duplex) a navíc automaticky synchronizované. Pro daný socket může na klientu existovat nejvýše jedno spojení vytvořené funkcí connect a na serveru nejvýše jedna přiřazená adresa (a port) funkcí bind. U datagramového socketu můžeme funkcí connect měnit (výchozí) adresu příjemce a funkcí bind měnit (výchozí) adresu odesílatele datagramu opakovaně a po nastavení adresy odesílat a přijímat datagramy. Nastavení adresy lze zrušit nastavením adresy s položkou sa_family struktury sockaddr_in rovnou makru AF_UNSPEC.

Pro odeslání a příjem dat na streamovaném socketu se používají funkce send a recv, podobné funkcím sendto a recvfrom, které se spíše používají pro datagramový socket. Obecně, funkce send a recv lze použít i pro datagramový socket (použije se výchozí adresa zadaná funkci connect) a funkce sendto a recvfrom lze použít i pro streamovaný socket (pak jsou parametry to, tolen, from a fromlen ignorovány, měly by být NULL a 0). Data jsou přijata (a u TCP potvrzena), jestliže jsou uložena do vstupního bufferu socketu, tj. nemusí být přečtena funkcí recv nebo recvfrom.

Jelikož se socket chová jako file descriptor nebo file handle (pro Winsock 2 API), můžeme jej zároveň použít ve všech funkcích očekávajících jako parametr file descriptor nebo file handle (pouze pro typ SOCK_STREAM), tedy např. pro odeslání dat použít funkci write/WriteFile pro zápis do a pro příjem dat funkci read/ReadFile pro čtení z file descriptoru nebo socket/file handlu. Na unixových systémech také můžeme descriptor socketu převést na strukturu FILE pomocí funkce fdopen a používat ve funkcích fprintf, fscanf apod.

send(2)
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int s, const void *buf, size_t len, int flags);
int send(SOCKET s, char* buf, int len, int flags);
  • odešle data z místa na buf délky len socketem s s navázaným TCP spojením nebo nastavenou adresou serveru (pro UDP) po volání connect
  • send(s, buf, len, flags) je ekvivalentní sendto(s, buf, len, flags, NULL, 0)
  • send(s, buf, len, 0) je ekvivalentní write(s, buf, len)/WriteFile(s, buf, len)

recv(2)
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int s, void *buf, size_t len, int flags);
int recv(SOCKET s, char *buf, int len, int flags);
  • přijme data do místa na buf délky len socketem s s navázaným TCP spojením nebo nastavenou adresou serveru (pro UDP) po volání connect
  • recv(s, buf, len, flags) je ekvivalentní recvfrom(s, buf, len, flags, NULL, NULL)
  • recv(s, buf, len, 0) je ekvivalentní read(s, buf, len)/ReadFile(s, buf, len)

Pro přenos dat pomocí funkcí send a recv člověk nepotřebuje vědět nic o fungování počítačové sítě, snad až na odlišnost protokolů TCP a UDP. V případě protokolu TCP jsou data přenášena jako datový proud podobně jako ze/do souboru na disku, v případě UDP jsou vyměňovány kousky dat (datagramy). Všechny detaily fungování protokolu TCP (pořadí segmentů, potvrzování přijetí, opakování přenosu, řízení toku apod.) za nás zařídí knihovna implementující Socket API.

Př. Implementujte jednoduchý síťový chatovací program. Server bude očekávat spojení (datagramy) na nějakém neprivilegovaném portu zadaném jako parametr programu (na všech lokálních adresách či rozhraních). Klient naváže spojení se serverem (bude přijímat datagramy ze serveru) zadaném IP adresou jako první parametr programu a portem zadaným jako druhý parametr. Program bude implementovat klienta i server, podle zadaného počtu parametrů se bude chovat buď jako klient nebo jako server. Po navázání spojení (u TCP) bude program (jako klient i jako server) zobrazovat a odesílat řádky zadané na vstup programu protější straně, která je zobrazí jako text z druhé strany. Implementujte komunikaci nejdříve nad protokolem TCP a poté nad protokolem UDP a pozorujte (případné) rozdíly při komunikaci.

Aplikační vrstva – překlad doménového jména (DNS)

Pro komunikaci pomocí socketů potřebujeme znát IP adresu příjemce. Pro člověka (uživatele) je ale snáze zapamatovatelné doménové jméno příjemce. Toto jména je potřeba přeložit (pomocí systému DNS) na IP adresu, popř. i obráceně.

gethostbyname(3)
#include <netdb.h>
struct hostent *gethostbyname(const char *name);
  • pokusí se přeložit doménové jméno name na IP adresu vrácenou ve struktuře hostent reprezentující hostitelský počítač (uzel)
  • funkce resolveru, pro překlad používá lokální soubor, DNS atd.
  • struktura hostent má následující položky, viz man:
    • char *h_name: oficiální doménové jméno uzlu
    • char **h_aliases: alternativní doménová jména uzlu (aliasy), pole ukazatelů na céčkovské řetězce, poslední je NULL
    • int h_addr_type: typ adresy uzlu, pro IPv4 makro PF_INET (potřeba sys/socket.h)
    • int h_length: velikost adresy v bytech, pro IPv4 vždy 4
    • char **h_addr_list: pole ukazatelů na IP adresy v síťovém tvaru (každá má h_length bytů), poslední je NULL
  • při chybě vrací NULL a do h_errno chybový kód

gethostbyname(3)
#include <netdb.h>
struct hostent *gethostbyaddr(const void *addr, int len, int type);
struct hostent* gethostbyaddr(const char *addr, int len, int type);
  • vrátí strukturu hostent k IP adrese addr typu type (PF_INET) délky len (pro IPv4 vždy 4)
  • addr je ukazatel na strukturu in_addr, získanou např. funkcí inet_aton
  • při chybě vrací NULL a do h_errno chybový kód

IP adresa (adresy) je ve struktuře hostent uložena v (binárním) síťovém tvaru. Pro převod do tečkového tvaru lze použít funkci inet_ntoa. Pro opačný převod, např. do parametru addr funkce gethostbyaddr, je funkce inet_aton. Případné využití systému (a aplikačního protokolu) DNS je při překladu jména zcela transparentní a je implementováno Socket API.

Př. Doplňte do příkladů implementace zjednodušené verze programu ping a jednoduchého síťového chatovacího programu (popř. jiného TCP/UDP klienta a serveru) překlad doménového jména na IP adresu a obráceně. Parametrem programů bude místo IP adresy doménové jméno, které se před odesláním dat přeloží na IP adresu. Jestliže program vypisuje IP adresu (např. adresa odesílatele každé ICMP echo odpovědi), bude vypisovat doménové jméno přeložené z IP adresy.



Jan Outrata
outrata@phoenix.inf.upol.cz