Obsah
Poslední změna: pondělí 9. března 2009
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.
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).
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
aSOCK_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žePF_
znamená protocol family. Ke každéPF_
konstantě existuje odpovídající konstantaAF_
, 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) aPF_INET6
pro IPv6.
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)
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 funkciaccept()
.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í socketsockfd
se tak může opět použít pro čekání na další spojení.Používá server.
- 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ší.
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“.
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()
.
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.
Jednoduchý server – spusťte server a pak v jiném okně příkaz telnet localhost 5555.
Množiny socketů – článek ze seriálu na root.cz
Všechny připomínky k předmětu, obsahu stránek, objevené chyby v ukázkových programech apod. adresujte na autory: