Hlavní navigace

Sokety a C/C++: rodina protokolů PF_UNIX

Radim Dostál 29. 9. 2003

Dnes se si ukážeme soket jako obecný komunikační nástroj, který nemusí mít s počítačovou sítí vlastně nic společného. Podíváme se na zvláštní druh soketu, který je určen pouze pro komunikaci s procesem na lokálním stroji. Dnešní článek není určen pro WinSock. Příklad je pouze pro Linux.

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_PAT­H_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í.

Tabulka č. 499
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.

Našli jste v článku chybu?

1. 10. 2003 8:28

Radim Dostál (neregistrovaný)

Myslím, že ne. Mě se nic takového nepodařilo rozchodit. V hlavičkových souborech pro WinSock není ani struktura sockadd_un. Zkoušel jsem ji dat stejnou, jako je v Linuxu a stejně ji vyplnit. Nepodařilo se mi to rozjet.

30. 9. 2003 22:21

bastlir (neregistrovaný)

Takovy stupidni dotaz, na WINSOCK nic takoveho nelze ani nahodou?

Root.cz: Telegram spustil anonymní blog Telegraph

Telegram spustil anonymní blog Telegraph

DigiZone.cz: ČT má dalšího zástupce v EBU

ČT má dalšího zástupce v EBU

DigiZone.cz: NG natáčí v Praze seriál o Einsteinovi

NG natáčí v Praze seriál o Einsteinovi

Podnikatel.cz: EET: Totálně nezvládli metodologii projektu

EET: Totálně nezvládli metodologii projektu

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Vitalia.cz: Znáte „černý detox“? Ani to nezkoušejte

Znáte „černý detox“? Ani to nezkoušejte

Podnikatel.cz: 1. den EET? Problémy s pokladnami

1. den EET? Problémy s pokladnami

Měšec.cz: Air Bank zruší TOP3 garanci a zdražuje kurzy

Air Bank zruší TOP3 garanci a zdražuje kurzy

120na80.cz: Jak oddálit Alzheimera?

Jak oddálit Alzheimera?

DigiZone.cz: Sat novinky: Fransat UHD Demo

Sat novinky: Fransat UHD Demo

Měšec.cz: Jak vymáhat výživné zadarmo?

Jak vymáhat výživné zadarmo?

Vitalia.cz: Spor o mortadelu: podle Lidlu falšovaná nebyla

Spor o mortadelu: podle Lidlu falšovaná nebyla

120na80.cz: Co všechno ovlivňuje ženskou plodnost?

Co všechno ovlivňuje ženskou plodnost?

Podnikatel.cz: Chtějte údaje k dani z nemovitostí do mailu

Chtějte údaje k dani z nemovitostí do mailu

Vitalia.cz: To není kašel! Správná diagnóza zachrání život

To není kašel! Správná diagnóza zachrání život

Vitalia.cz: Jmenuje se Janina a žije bez cukru

Jmenuje se Janina a žije bez cukru

Podnikatel.cz: Podnikatelům dorazí varování od BSA

Podnikatelům dorazí varování od BSA

Lupa.cz: Proč firmy málo chrání data? Chovají se logicky

Proč firmy málo chrání data? Chovají se logicky

Vitalia.cz: Mondelez stahuje rizikovou čokoládu Milka

Mondelez stahuje rizikovou čokoládu Milka

Podnikatel.cz: Zavře krám u #EET Malá pokladna a Teeta?

Zavře krám u #EET Malá pokladna a Teeta?