Cvičení 4 – Sockety


Poslední změna: pondělí 9. března 2009

1. Zadání

Vytvořte jednoduchý chat server. Klienti se k serveru budou připojovat příkazem telnet (SOCK_STREAM, PF_INET). Cokoli co nějaký klient pošle serveru, server odešle zpět ostatním klientům.

2. Co jsou sockety

Sockety můžeme chápat jako jeden z prostředků meziprocesní komunikace. Od jiných komunikačních prostředků se liší především tím, že komunikující procesy nemusí být na stejném počítači. Co se týče terminologie, tak představíme-li si komunikaci jako rouru, kterou tečou data, socket by bylo pojmenování pro její konce. Při komunikaci si každý proces vytvoří svůj socket a nastaví jeho parametry tak, aby pomocí něj mohl komunikovat se socketem jiného procesu (na jiném počítači).

2.1. Parametry socketů

Sockety jsou univerzálním komunikačním prostředkem. Pomocí stejného API[1]je možno využívat různé síťové protokoly. To je také důvod proč pro nastavení socketu je potřeba zdánlivě velké množství operací.

Komunikační styl

Základní komunikační styly jsou dva SOCK_STREAM a SOCK_DGRAM.

SOCK_STREAM je styl orientovaný na spojení. Před vlastní komunikací se naváže spojení, čímž se obě strany ujistí, že jsou spolu ochotny komunikovat. Využívá podobně jako roury, tedy ke spolehlivému (žádná data se neztratí) přenášení dat, kdy máme jistotu, že data dorazí ve stejném pořadí v jakém byla odeslána. V prostředí internetu odpovídá tento styl protokolu TCP.

SOCK_DGRAM je styl orientovaný na datagramy (pakety, zprávy). Slouží k přenášení krátkých bloků dat, u kterých není zajištěno jejich doručení ani pořadí v jakém budou doručeny. Výhodnou toho druhu komunikace je rychlost – nemusí se navazovat žádné spojení a nepotvrzuje se, zda-li data opravdu dorazila. V prostředí internetu odpovídá tento styl protokolu UDP.

Jmenný prostor

Každý socket má své jméno. Při komunikaci po síti se jménu říká adresa. Jmenný prostor se nastavuje pomocí konstant začínajících na PF_. Je to trochu zmatek v pojmech, protože PF_ znamená protocol family. Ke každé PF_ konstantě existuje odpovídající konstanta AF_, která se používá při práci s adresami.

Hlavní jmenné prostory jsou PF_LOCAL pro sockety na lokálním počítači, PF_INET pro protokol IP (Internet) a PF_INET6 pro IPv6.

2.2. Práce s adresami

Adresy (jména) socketů se specifikují pomocí struktury struct sockaddr:

struct sockaddr {
        unsigned short    sa_family;    // address family, AF_xxx
        char              sa_data[14];  // 14 bytes of protocol address
       };

Tato struktura je společná pro všechny sockety a předává se jako parametr funkcím pro práci se sockety. Pro práci s Internetovými protokoly se kvůli pohodlí používá struktura sockaddr_in, která je stejná jako sockaddr, ale položku sa_data má rozdělenou na víc částí:

struct sockaddr_in {
        short int          sin_family;  // Address family
        unsigned short int sin_port;    // Port number
        struct in_addr     sin_addr;    // Internet address
        unsigned char      sin_zero[8]; // Same size as struct sockaddr
       };

Internetová adresa (IPv4) se skládá z čtyřbajtové IP adresy, která se zapisuje 147.32.86.1 a čísla portu (např. 80 pro web server). Struktura pro IP adresu je definována jako:

// Internet address (a structure for historical reasons)
    struct in_addr {
        unsigned long s_addr; // that's a 32-bit long, or 4 bytes
    };

Při práci s adresami (a nejenom s nimi) je potřeba dbát na pořádek bytů. Některé architektury používají tzv. big-endian, jiné little-endian. Aby se spolu domluvily počítače s libovolnou architekturou je potřeba převádět adresy do síťového pořadí bytů. K tomu slouží funkce htons()[2], htonl(), ntohs() a ntohl().

Pro převod adresy z textového tvaru (147.32.86.1) do binárního tvaru ve správném pořadí bytů se používá funkce inet_aton():

int inet_aton (const char *NAME, struct in_addr *ADDR)

Pokud chceme převést DNS jméno počítače (www.seznam.cz) na binární IP adresu, použijeme funkci gethostbyname():

struct hostent * gethostbyname (const char *NAME)

2.3. Práce se sockety

Socket se vytvoří voláním funkce socket(). Tato funkce vrací file-descriptor stejně jako například funkce open(). Pro čtení ze socketu a zapisování do něj můžeme použít nízkoúrovňové funkce jako třeba read() a write(). Navíc máme k dispozici několik funkcí speciálně pro sockety.

Programy můžeme rozdělit do dvou skupin podle toho, jak používají sockety (SOCK_STREAM). První skupinou jsou takzvané servery, což jsou programy, které vytvoří socket a pak čekají (poslouchají) až se k nim někdo připojí. Druhou skupinou jsou klienti, kteří po vytvoření socketu začnou aktivně navazovat spojení. Podle toho, do jaké skupiny daný program patří, volají se různé funkce uvedené níže.

socket()
int socket(int domain, int type, int protocol);

Vytvoří socket daného typu a vrátí file-descriptor.

Používá server i klient.

connect()
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen); 

Slouží k navazování spojení.

Používá klient.

bind()
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

Sváže socket se jménem. Nejčastěji se používá v kombinaci s listen() pro určení čísla portu na kterém server poslouchá.

Používá server.

listen()
int listen(int sockfd, int backlog);

Volá se po bind() a slouží k nastavení socketu do stavu čekání na příchozí spojení. listen() se okamžitě vrací. Vlastní čekání se děje až ve funkci accept().

Používá server.

accept()
int accept(int sockfd, void *addr, int
            *addrlen);

Je-li socket v čekacím stavu, funkce accept() čeká na příchozí spojení. Funkce vrátí parametry prvního spojení ve frontě a nový socket, který slouží ke komunikaci s druhou stranou. Původní socket sockfd se tak může opět použít pro čekání na další spojení.

Používá server.

Obrázek 1. Princip funkce accept()

Princip funkce accept()

close(), shutdown()

Zavírá socket.

Používá server i klient.

Další funkce, které pracují se sockety jsou send(), recv(); sendto(), recvfrom() (pro SOCK_DGRAM) a mnohé další.

3. Servery

Jak už bylo řečeno, servery jsou programy, které čekají až se k nim někdo bude chtít připojit. Když se připojí, obslouží jeho požadavky a pak zase čekají na dalšího. Pokud je server hodně vytížen, toho čekání si moc neužije a spíš musí řešit to, jak obsloužit víc klientů najednou. Řešení je hned několik:

  • Každé příchozí spojení se obslouží jiným procesem, který vznikne po zavolání funkce fork().

  • Pro každého klienta se spustí samostatné vlákno. Tato možnost je v podstatě stejná jako předchozí, ale trochu šetrnější k systémovým zdrojům a často také pro programátory výhodnější, protože nemusí složitě řešit komunikaci mezi různými procesy. Viz obrázek 2 – „Vícevláknový server s několika připojenými klienty“.

    Obrázek 2. Vícevláknový server s několika připojenými klienty

    Vícevláknový server s několika připojenými klienty

  • Všichni klienti jsou obsluhováni v jednom vlákně. K tomu je ale potřeba mít možnost čekání na více událostí současně. K tomu slouží systémové volání select().

3.1. Systémové volání select()

Funkce select() umožňuje procesu čekat na několik událostí najednou. Příklad může být buď výše zmíněný server nebo třeba program, který čeká jak na příkazy z klávesnice (standardní vstup), tak na příkazy po síti. Kdyby program zavolal read() nebo fgets() na standardní vstup, tak po sítí by mohlo chodit co by chtělo a program nereagoval, protože by "visel" v čekání na klávesnici. V takovýchto případech se právě hodí funkce select().

Příklad serveru realizovaného pomocí select() je uveden v příkladu 4. Podrobnosti o čekání na více událostí najednou si můžete přečíst v článku na root.cz.

4. Příklady

  1. Jednoduchý HTTP klient

  2. Jednoduchý server – spusťte server a pak v jiném okně příkaz telnet localhost 5555.

  3. Příklad na select()

  4. Server + select

5. Odkazy



[1] Application Programming Interface

[2] host to network, short

Všechny připomínky k předmětu, obsahu stránek, objevené chyby v ukázkových programech apod. adresujte na autory: