UNIX a Internet

Sockety

Autor: Jiří Hnídek / jiri.hnidek@tul.cz

Začneme lehce - komunikace mezi procesy

  • Soubory
  • Roury
  • Signály
  • Semafory
  • Fronty zpráv (IPC)
  • Sdílená paměť
  • Sockety

Historie Socketů

  • Prvně v BSD UNIXu (Berkeley Software Distribution)
  • Rozšíření roury z UNIXu umožňující obousměrnou komunikaci.
  • Je možné použít pro komunikaci mezi procesy na jednom počítači (UNIX socket)
  • Nejčastěji se používá pro komunikaci mezi procesy, které běží na různých počítačích.
  • Windows Sockets (WinSock)

UNIX socket

  • Historický relikt?
  • 
    # su netstat -l -x
    Active UNIX domain sockets (only servers)
    Proto RefCnt Flags       Type       State         I-Node   Path
    unix  2      [ ACC ]     STREAM     LISTENING     28162    /tmp/.X11-unix/X0
    						
  • Architektura klient-server
  • Několik typů socketů
  • Popíšeme na příkladu:

API aneb systémová volání

int socket(int domain, int type, int protocol);
int bind(int sock_fd, struct sockaddr *addr);
int connect(int sock_fd, struct sockaddr *addr, int addr_len);
int listen(int sock_fd, int queue_len);
int accept(int sock_fd, struct sockaddr *addr, int addr_len);
int read(int sock_fd, char *buf, int buf_len);
int write(int sock_fd, char *buf, int buf_len);
int close(sock_fd);

Socket - deskriptor souborů

Systémové volání socket vrací deskriptor souborů, který je potřeba spojit (bind) s adresou a následně je možné ho použít pro komunikaci.

Otázka: kolik deskriptorů souborů může mít proces otevřený (ve výchozím nastavení)?

Defaultně může mít každý proces otevřeno 1024 deskriptorů souborů (viz. příkaz: ulimit -a).

Socket - domain

Určuje jmenný prostor, ve kterém se bude provádět komunikace. Přibližně odpovídá protokolu linkové vrstvy. Některé vybrané hodnoty:

  • AF_UNIX (unixové sockety)
  • AF_INET (IPv4)
  • AF_INET6 (IPv6)
  • AF_PACKET ("vlastní" linkový protokol)
  • AF_NETLINK (komunikace s kernelem)

Socket - type

Typ protokolu určuje druh komunikace mezi koncovými body.

  • SOCK_STREAM - proudový, spolehlivý, dvoucestný, spojovaný bytový proud
  • SOCK_DGRAM - datagramový, nespolehlivý, nespojovaný posílání zpráv
  • SOCK_RAW - vlastní transportní protokol

Socket - protocol

Specifikuje konrétní protokol, který bude použit pro komunikaci.

  • IPPROTO_TCP
  • IPPROTO_UDP
  • IPPROTO_SCTP
  • IPPROTO_DCCP

Bind

  • Propojí socket s adresou

Connect

  • Volá většinou pouze klient
  • Když je použit TCP jako transportní protokol, tak se klient pokusí udělat TCP handshake
  • Lze volitelně použít i UDP (pouze zjednodušuje další syst. volání)

Listen

  • Nastavuje velikost fronty příchozích spojení

Accept

  • Volá pouze server
  • Pokud je na transportní vrstvě použit TCP, tak se provede TCP handshake

Read & Write

  • Read načte data z bufferu příchozích dat. Write nakopíruje data do odchozího bufferu.
  • Pokud je socket v blokujícím režimu, tak funkce read() zablokuje program, pokud je buffer prázdný
  • Stejně tak funkce write() zablokuje program, pokud je odchozí buffer plný (congestion control, flow control)

Close

Provede TCP teardown

Adresy

  • IPv4 (AF_INET): struct sockaddr_in addres;
  • 
    addres.sin_family = AF_INET;
    addres.sin_addr.s_addr = inet_addr("127.0.0.1");
    addres.sin_port = htons(PORT);
    						
  • IPv6 (AF_INET6): struct sockaddr_in6 addres;
  • 
    addres.sin6_family = AF_INET6;
    inet_pton(AF_INET6, argv[1], &addres.sin6_addr);
    addres.sin6_port = htons(PORT);
    						
  • INADDR_ANY, in6addr_any

Big Endian vs. Little Endian

htons(), htonl(), ntohs(), ntohl()

Návratové hodnoty

V produkčním kódu je nutné kontrolovat návratové hodnoty všech systémových volání a patřičným způsobem na ně reagovat. V případě, že návratová hodnota bude ignorována může být výsledný kód nespolehlivý i nebezpečný.

Dokumentaci k návratovým hodnotám hledejte v manuálových stránkách, např.: man 2 accept

Socket - blokující/neblokující

Pokud je socket v blokujícím režimu, tak každé systémové volání bude zablokování v případě, kdy to nedovoluje některá ze síťových vrstev.

Příkladem může být systémové volání write() v případě, že dojde k ucpání přenosových cest nebo dojde k zahlcení příjemce.

Takové chování je ve většině případu nevhodné.

Přepnutí do neblokujícího režimu

Socket se do neblokujícího režimu musí přepnout explicitně:


int flag;
flag = fcntl(listen_sock_fd, F_GETFL, 0);
fcntl(listen_sock_fd, F_SETFL, flag | O_NONBLOCK);
						

Výhoda tohoto přístupu spočívá v tom, že proces se vám nemůže na systémovém volání zablokovat. Nevýhodou je, že je nutné reagovat na více variant (viz manuálové stránky).

Čekání na vstup

Proces (server i klient) mohou čekat na událost pomocí některého systémového volání (select, pselect, poll, epoll, atd.)

Více procesů a vláken

Pokud má server efektivně vyřizovat požadavky klientů, tak je vhodné to dělat paralelně. V některých případech je lepší použít víceprocesový přístup (např.: HTTP). V jiných případech je lepší použít vícevláknový přístup (např.: server sdílené virt. reality).

Buffer odchozích a příchozích dat

Např. při neblokujícím režimu socketů je vhodné pravidelně zjišťovat kolik dal je možné odeslat.

Velikost bufferu pro odchozího a příchozí data (jednoho spojení) při použití protokolu TCP/UDP lze zjistit mnoha způsoby.

TCP Buffer odchozích a příchozích dat

Informace o bufferu lze zjistit například v adresáři /proc:


cat /proc/sys/net/ipv4/tcp_rmem
						
Tento soubor obsahuje 3 hodnoty (v bytech): minimální, aktuální a maximální velikost bufferu pro příchozí data.

cat /proc/sys/net/ipv4/tcp_wmem
						
Informace pro odchozí data jednoho spojení.

Aktuální velikost bufferu

V programu můžeme získat aktuální velikost bufferu pomocí:


int rcv_buf_size, send_buf_size;
unsigned int int_size = sizeof(rcv_buf_size);

getsockopt(sock_fd, SOL_SOCKET, SO_RCVBUF,
	(void *)&rcv_buf_size, &int_size);
getsockopt(sock_fd, SOL_SOCKET, SO_SNDBUF,
	(void *)&send_buf_size, &int_size);
						

Buffer odchozích dat - volné místo

Na Linuxu lze zjistit využité místo v bufferu odchozích dat pomocí:


ioctl(sock_fd, SIOCOUTQ, &buf_size);
						

Velikost bufferu a propustnost


throughput = buffer_size / latency;
						

Změna velikost bufferu

Pokud nám velikost bufferu nestačí, tak ji můžeme navýšit pomocí:


setsockopt(sock_fd, SOL_SOCKET, SO_RCVBUF,
	(void *)&rcv_buf_size, int_size);
setsockopt(sock_fd, SOL_SOCKET, SO_SNDBUF,
	(void *)&send_buf_size, int_size);
						

Pozor: kernel násobí nastavovanou hodnotu dvěmi.

Doporučovaná velikost bufferu

buffer_size = RTT * bandwidth;

Buffer UDP socketu

I protokol UDP má samozřejmě omezení na bufferu:


cat /proc/sys/net/ipv4/udp_mem
						

Velikosti bufferu lze nastavovat/zjišťovat analogicky jako u TCP.

Transportní protokol UDP

Jelikož se jedná o nespojovaný protokol, tak mnohá systémová volání postrácejí smysl (listen, accept).

Na druhou stranu se některé věci komplikují.

Datagramy lze odesílat pomocí send(), sendto(), sendmsg().

Přijímat datagramy lze analogicky pomocí recv(), recvfrom(), recvmsg().

UDP Socket

  • Klient může použít jeden socket pro komunikaci s více servery.
  • Server často musí použít jeden socket pro komunikaci se všemi klienty.
  • Lze používat broadcast (IPv4) a multicast (IPv4 i IPV6) adresy.

Démon a server

Produkční server budeme chtít tzv. démonizovat:

  • Odpojit proces od terminálu a spustit na pozadí.
  • Double fork.
  • Změnit aktuální adresář na: /
  • Přesměrovat stdin, stdout a stderr do /dev/null
  • Lze jednoduše naprogramovat pomocí: daemon(0, 0)

Server budeme chtít pravidelně spouštět při startu, elegantně zastavovat či restartovat, atd. K tomu je potřeba udělat několik "drobných" změn a vytvořit spouštěcí skripty.

Děkuji za pozornost. Nějaké otázky?