Soket je velice abstraktní pojem. Nejprve jsme (v seriálu) používali soket (typu SOCK_STREAM s použitým protokolem IPPROTO_TCP) pro komunikaci pomocí protokolů TCP/IP. Později jsme typ protokolu změnili na SOCK_DGRAM a používali protokol IPPROTO_UDP. Tím jsme komunikovali s opačnou stranou pomocí protokolů UDP/IP. Nakonec jsme typ protokolu změnili na SOCK_RAW a začali používat protokol ICMP (typ protokolu IPPROTO_ICMP).
Protokoly TCP, UDP a ICMP jsou velmi rozdílné protokoly (z pohledu vlastností, využití, vrstvy), přesto jsme pro práci s tak rozdílnými protokoly používali stále stejné funkce. Důležité bylo si jen uvědomit, jaká data posílat (zda s hlavičkou protokolu, nebo bez), jak rozumět příchozím datům a jaké čekat vlastnosti (se spojením, bez spojení atd…). Ale z pohledu API jsme stále volali stejné funkce pořád dokola.
Celé soketové API je navrženo tak, aby se se soketem pracovalo vždy stejně. Soket je velice abstraktní pojem, lze jej aplikovat na celou řadu komunikačních protokolů. A nejen to, soket je ještě obecnější, je to prostě nástroj pro komunikaci. Dosud jsme brali v úvahu pouze komunikaci po síti. Dnes se podíváme na komunikaci dvou procesů na jednom počítači.
„Lokální“ soket
Pomocí soketů mohou komunikovat dva procesy na jednom počítači. Bylo by možné, tak jak jsme dělali v seriálu otevřít TCP spojení na lokální počítač (localhost) a přenášet data mezi dvěma procesy (mezi aplikacemi). Tento způsob by byl velmi neefektivní, zbytečně by se mezi procesy přenášely nadbytečná data (hlavičky IP a TCP protokolů). Pokud máme jistotu, že vždy budeme komunikovat s procesem na stejném počítači, můžeme použít jiný „druh“ soketu, který je pro tyto účely určen a optimalizován.
Doména soketu
Prvním parametrem funkce socket je doména. Dosud jsme používali doménu danou hodnotou makra PF_INET. (Já jsem několikrát v seriálu zaměnil makra PF_INET a AF_INET, což není chyba, protože obě makra jsou definována jako stejné číslo. Po formální stránce je to ale nepřesnost.). Tím jsme definovali „rodinu“ protokolů pro komunikaci po internetu. Nyní se podíváme na „rodinu“ protokolů jménemPF_UNIX. Použitím rodiny protokolů PF_UNIX omezíme komunikaci pouze na lokální stroj, ale zato bude efektivnější, než by byla komunikace pomocí PF_INET. V rodině protokolů PF_UNIX lze použít pouze typy soketů SOCK_STREAM a SOCK_DGRAM. Hovořit o typu protokolu nyní nedává smysl, proto budeme jako poslední parametr funkce socket zadávat 0.
Pomocí takto vytvořeného soketu mohou obousměrně komunikovat dva procesy. Navíc lze využít architekturu klient/server, kde jeden proces je v roli serveru, ostatní v roli klientů. To jsou asi zásadní rozdíly mezi soketem používajícím rodinu protokolů PF_UNIX a „rourou“ (pipe).
V souvislosti s „lokálním“ soketem již nemá smysl mluvit o IP adrese příjemce a o portu příjemce. Přesto je potřeba příjemce nějak identifikovat (adresovat). Tak, jak rodina protokolů PF_INET měla svoji rodinu adres (AF_INET), tak i rodina protokolů PF_UNIX má svou rodinu adres AF_UNIX. Doteď jsme vyplňovali struktur sockaddr_in, nyní budeme vyplňovat strukturu sockaddr_un. Opět ji při použití přetypujeme na sockaddr.
Struktura sockaddr_un
Struktura má jen dva atributy. Prvním je jednobytová hodnota udávající rodinu adres, druhým je cesta k souboru.
- sa_family_t sun_family; – atribut bude vždy nabývat hodnoty makra AF_UNIX.
- char sun_path[UNIX_PATH_MAX]; – textový řetězec zakončený 0. Udává název souboru.
Co tam dělá ten soubor? Po dobu existence soketu je vytvořen „pseudosoubor“, pomocí kterého klient „adresuje“ (najde) soket serveru. Pokud při spuštění serveru z ukázkového příkladu prohlédnete adresář tmp, zjistíte, že obsahuje soubor pokus. A pokud dáte ls -la /tmp/pokus, zjistíte, že to není úplně obyčejný soubor. Má atribut s – je to socket. V Unixu se mnoho věcí někdy tváří jako soubor.
Funkce unlink
Ještě je dobré se seznámit s funkcí unlink, kterou v příkladu použiji. Funkce odstraní název souboru ze souborového systému. Jestliže na soubor neexistuje žádný link a soubor není otevřený, bude smazán. V našem případě se nejedná o soubor, ale o soket. U soketu bude odstraněn název a po oboustranném uzavření soketu bude odstraněn i on. Hlavička:
- int unlink(const char *pathname); – funkce je deklarovaná v hlavičkovém souboru unistd.h.
Příklad
V příkladu použijeme typ soketu SOCK_STREAM (proud dat). Soket bude mít stejné vlastnosti, jako měl proud dat v doméně PF_INET, tedy data se nebudou „předbíhat“ a příjemce při příjímání dat nebude schopen od sebe rozlišit bloky dat, které odesílatel poslal. Ukázkový příklad tvoří dva programy, server a klient. Server se velice podobá příkladu z článku Sokety a C/C++ – funkce select. Na jednom počítači si spusťte server a několik klientů. Server bude jen rozesílat data, která obdrží od nějakého klienta, všem klientům. Tím vlastně napíšeme velice jednoduchý program podobný programu talk. Klient se ukončí pro zadání prázdného řádku. Server se ukončí při odhlášení posledního klienta.
Server
Vytvoříme serverovou aplikaci, která bude v principu stejná, jako jsme vytvářeli TCP server v úvodu seriálu. Postupně bude volat socket, bind, listen, accept. Funkce accept nám vrátí nový soket, pomocí kterého budeme komunikovat s klientem. Před zavoláním funkce socket a po uzavření soketu funkcí close zavoláme funkci unlink.
Formality
Vložíme hlavičkové soubory, začneme funkci main a deklarujeme lokální proměnné.
#include <sys/socket.h> #include <sys/un.h> #include <sys/types.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <vector> #include <algorithm> #define BUFFER_LEN 200 using namespace std; int main(int argc, char *argv[]) { int sock, max; char *path = "/tmp/pokus"; sockaddr_un addr; fd_set mySet; vector<int> clients; vector<int>::iterator i;
Vytvoření soketu
Smažeme soubor pro případ, že by existoval od minulého spuštění. Funkci unlink stačilo zavolat až před bind. Vytvoříme soket. Rodina protokolů PF_UNIX, typ soketu je SOCK_STREAM.
unlink(path); if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) { perror("Volání socket selhalo"); return -1; } max = sock;
Struktura sockaddr_un
Zaplníme instanci struktury sockaddr_un. Atribut sun_family nabývá vždy hodnot AF_UNIX, atributaddr.sun_path je nulou zakončený textový řetězec s názvem souboru.
„Svážeme“ soket s adresou
Je dobré si všimnout, že zde se jako adresa chápe v podstatě cesta k souboru.
if (bind(sock, (sockaddr *)&addr, sizeof(addr)) == -1) { perror("Volání bind selhalo"); unlink(path); close(sock); return -1; }
Nastavíme velikost fronty
Velikost fronty příchozích požadavků na spojení. U PF_UNIX v podstatě formalita.
if (listen (sock, 10) == -1) { perror("Volání accept selhalo"); unlink(path); close(sock); return -1; }
Volání select
Všechny sokety, jež máme k dispozici, vložíme do množiny, kterou připravíme pro funkci select.
do { FD_ZERO(&mySet); FD_SET(sock, &mySet); for(i = clients.begin(); i != clients.end(); i++) { FD_SET(*i, &mySet); } if (select(max + 1, &mySet, NULL, NULL, NULL) == -1) { perror("Volání select selhalo"); close(sock); for_each(clients.begin(), clients.end(), close); unlink(path); return -1; }
Příjem žádosti o „spojení“
Vstupní událost na „hlavním“ soketu jménem sock je příchozí požadavek na komunikaci. U TCP/IP se jednalo o požadavek na spojení, zde se jedná o požadavek na komunikaci. Funkce accept vrací nový soket, který vložíme do kontejneru. V příkladu používám kontejner vector, iterátory a standardní algoritmus for_each.
if (FD_ISSET(sock, &mySet)) { printf("Někdo se připojil\n"); socklen_t size = sizeof(addr); int s = accept(sock, (sockaddr *)&addr, &size); if (s == -1) { perror("Volání accept selhalo"); close(sock); for_each(clients.begin(), clients.end(), close); unlink(path); return -1; } if (s > max) { max = s; } clients.push_back(s);
Příjem dat
Vstupní událost na ostatních soketech je příjem dat nebo uzavření soketu druhou stranou. V případě uzavření soketu jej uzavřeme na naší straně a odstraníme z kontejneru.
for (i = clients.begin(); i != clients.end(); i++) { if (FD_ISSET(*i, &mySet)) { char buffer[BUFFER_LEN]; int lenght; if ((lenght = recv(*i, buffer, BUFFER_LEN, 0)) <= 0) { printf("Někdo se odpojil\n"); close(*i); clients.erase(i); break; } else { for (vector<int>::iterator ii = clients.begin(); ii != clients.end(); ii++) { send(*ii, buffer, lenght, 0); buffer[lenght] = 0; } } } }
Konec programu
Program poběží, dokud bude existovat nějaký „připojený“ klient.
} while (!clients.empty()); close(sock); unlink(path); return 0; }
Klient
Ukážeme si jen podstatnou část klienta. V klientovi zavoláme funkce v pořadísocket a connect. Pak můžeme odesílat a přijímat data. Klient čte data ze standardního vstupu. Vložením prázdného řetězce (prázdného řádku) se klient ukončí. Formality jsou stejné jako u serveru. Vytvoření soketu také. Přejděme rovnou ke connect.
Funkce connect
Zavoláme funkci connect. Nejprve zaplníme strukturu sockaddr_un.
addr.sun_family = AF_UNIX; memcpy(addr.sun_path, path, strlen(path) + 1); if (connect(sock, (sockaddr *)&addr, sizeof(addr)) == -1) { perror("Volání connect selhalo"); close(sock); return -1; }
Příjem dat
Program přijímá data ze standardního vstupu nebo z otevřeného soketu. Data ze stdin pošleme soketem pryč. Data ze soketu vypíšeme na stdout.
do { FD_ZERO(&mySet); FD_SET(sock, &mySet); FD_SET(0, &mySet); if (select(sock + 1, &mySet, NULL, NULL, NULL) == -1) { perror("Volání select selhalo"); close(sock); return -1; } if (FD_ISSET(sock, &mySet)) { char buffer[BUFFER_LEN]; int lenght; if ((lenght = recv(sock, buffer, BUFFER_LEN - 1, 0)) <= 0) { perror("Selhalo volani recv"); close(sock); return -1; } buffer[lenght - 1] = 0; printf("%s\n", buffer); } if (FD_ISSET(0, &mySet)) { char buffer[BUFFER_LEN]; fgets(buffer, BUFFER_LEN, stdin); if (buffer[0] == '\n') { break; } send(sock, buffer, strlen(buffer), 0); } } while (1); close(sock); return 0; }
Jak je vidět, funkce connect, kterou jsem v úvodu seriálu používal pro navázání TCP spojení, ve skutečnosti s TCP protokolem nemá zas tam moc společného. Je daleko obecnější. Lze ji použít například i v našem dnešním příkladu. Navíc už jsme ji v seriálu použili i u protokolu UDP. Chtěl jsem ukázat, že vlastně funkce ze socket API jsou velmi obecné a použitelné na mnoho věcí.
Operační systém | Soubor |
---|---|
Linux | lin30.tgz |
Příště u PF_UNIX ještě zůstaneme. Ukážeme si na příkladu typ soketu SOCK_DGRAM a povíme si něco o funkci socketpair.