Sokety a C/C++: datagramy a PF_UNIX

6. 10. 2003
Doba čtení: 6 minut

Sdílet

Dnes se podíváme na datagramový soket v rodině protokolů PF_UNIX. Dále si ukážeme funkci socketpair a na příkladu si ukážeme komunikaci dvou procesů pomocí anonymního datagramového soketu.

V minulém dílu (Sokety a C/C++ – rodina protokolů PF_UNIX) jsme si ukázali práci s rodinou protokolů PF_UNIX. V příkladu jsme vytvořili soket v doméně PF_UNIX, jehož typ byl SOCK_STREAM (proud dat). Stejně jako v doméně PF_INET je i v doméně PF_UNIX k dispozici datagramová služba.

Doposud jsme v souvislosti s datagramy používali v doméně PF_INET protokol UDP. V doméně PF_UNIX nemá smysl mluvit o komunikačním protokolu. Spíše je potřeba odlišit vlastnosti typu soketu.

Typ SOCK_STREAM

Na tento typ soketu lze pohlížet jako na proud dat. Při čtení ze soketu nejsme schopni rozeznat jak velké bloky dat byly na druhé straně do soketu zapisovány. My máme k dispozici posloupnost bytů. V jakých celcích došlo k zápisu dat na druhé straně, nejsme schopni zjistit. Data čtená ze soketu typu „stream“ jsou čtena ve stejném pořadí, v jakém byla na druhé straně vložena. Data se nemohou předbíhat ani ztratit.

Typ SOCK_DGRAM

Obě strany spolu komunikují pomocí datagramů (zpráv). Přijímající strana může od sebe odlišit jednotlivé datagramy. Tedy každé volání recv mělo odpovídající volání send. Obecně u datagramové služby nelze předpokládat, že datagram byl doručen a že jednotlivé datagramy budou doručeny ve stejném pořadí, v jakém byly odeslány. U protokolu UDP v rodině protokolů PF_INET tomu tak i bylo. Doména rodiny protokolů PF_UNIX, o které hovoříme nyní, je trochu jiná. Obecně sice platí o datagramech, co jsem napsal, ale v případě domény PF_UNIX můžeme předpokládat, že datagram bude vždy doručen a že se jednotlivé datagramy nebudou předbíhat.

Typ soketu je druhým parametrem funkce socket. Nyní bychom mohli jako příklad na datagramový soket uvést příklad z minulého dílu (Sokety a C/C++ – rodina protokolů PF_UNIX). Stačilo by pouze změnit vytváření soketu. Řádek by vypadal asi takto:

if ((sock = socket(PF_UNIX, SOCK_DGRAM, 0)) == -1)

A příklad by byl s touto změnou funkční. Samozřejmě by ještě bylo nutné trochu upravit server. Oba procesy by používaly datagramový soket. Jiná možnost by byla bez volání funkce connect používat na obou stranách bind a potom recvfrom resp. sendto. Vše by v podstatě bylo v principu totožné jako v článku Protokol UDP 1. část nebo Protokol UDP 2. část. My si ale dnes ukážeme ještě něco navíc, takzvaný anonymní soket.

Anonymní soket

Anonymní soket je soket, který není svázán s žádným pseudosouborem (tak jako byl svázán soket v minulém dílu).

Funkce socketpair

  • int socketpair(int domain, int type, int protocol, int sv[2]);  – Prvním parametrem je rodina protokolů. V našem případě PF_UNIX. Mohou být ale i jiné. Druhým parametrem je typ soketu (např. SOCK_STREAM nebo SOCK_DGRAM). Třetím parametrem je protokol. Čtvrtý parametr je pole typu int o dvou prvcích. Nebo přesněji, jak už to v C bývá, ukazatel na první prvek tohoto pole. Funkce předpokládá, že ukazatel ukazuje na blok paměti velikosti 2 * sizeof(int). Funkce vytvoří dva navzájem propojené sokety ze zadané domény (rodiny protokolů), zadaného typu a použitého protokolu. Dva nové identifikátory soketů vloží do pole, které je dáno posledním parametrem. Funkce vrací 0 v případě úspěchu, v případě chyby –1.

V úvodu seriálu jsem pro představu napsal, že komunikace probíhá pomocí potrubí nebo hadice a soket je jeden konec tohoto potrubí. Funkce socketpair nám (z tohoto pohledu) vlastně vrací oba konce potrubí.

Příklad

Vytvoříme si jednoduchý příklad, kdy si dva procesy vymění několik informací. Nejprve pomocí socketpair vytvoříme pár propojených soketů, potom vytvoříme nový proces pomocí fork. Oba procesy spolu budou komunikovat pomocí těchto soketů.

Rodičovský proces bude ve funkci parent, synovský proces ve funkci child.

Formality

Vložíme hlavičkové soubory, deklarujeme funkce, začneme funkcí main a definujeme v ní lokální proměnné. Funkce main bude tentokrát malá a jednoduchá.

#include <sys/socket.h>
#include <sys/un.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <vector>
#include <algorithm>

#define BUFFER_LEN 200

void child(int sock);
void parent(int sock)

int main(int argc, char *argv[])
{
  int sockets[2];
  pid_t pid;

Vytvoření páru anonymních soketů

Sokety budou z domény PF_UNIX, typu SOCK_DGRAM a protokol v rodině protokolů PF_UNIX není podstatný, proto 0.

  if (socketpair(PF_UNIX, SOCK_DGRAM,
   0, sockets) == -1)
  {
    perror("Volání socketpair selhalo");
    return -1;
  }

Vytvoření nového procesu

Nový proces se vytváří pomocí fork. Jen připomenu, že fork vrací –1 v případě selhání, 0 vrací v synovském procesu a nenulovou kladnou hodnotu (identifikátor nového synovského procesu) vrací v původním rodičovském procesu.

V původním rodičovském procesu zavoláme funkci parent. Po jejím skončení zavřeme soket a počkáme pomocí wait na ukončení potomka.

V synovském procesu zavoláme funkci child a poté uzavřeme soket.

  if ((pid = fork()) == -1)
  {
    perror("Volání fork selhalo");
    return -1;
  }
  if (pid == 0)
  {
    child(sockets[0]);
    close(sockets[0]);
  }
  else
  {
    parent(sockets[1]);
    close(sockets[1]);
    wait(NULL);
  }

A můžeme ukončit funkci main.

bitcoin školení listopad 24

  return 0;
}

Synovský proces

Vše podstatné je ve funkci child. Její parametr je soket. Funkce odešle pomocí soketu úvodní řetězec a dále očekává příchozí data. Na každý příchozí datagram odpoví datagramem, kde sdělí, kolik měl příchozí datagram bytů, a svůj identifikátor procesu. Funkce se ukončí, přijde-li řetězec KONEC!.

void child(int sock)
{
  char firstMessage[] =
           "Ahoj rodiči, jsem připraven.";
  char endMessage[] = "Dobrá končím.";
  char pidMessage[BUFFER_LEN], buffer[BUFFER_LEN];
  fd_set mySet;

  int lenght;
  send(sock, firstMessage, strlen(firstMessage), 0);
  do
  {
    FD_ZERO(&mySet);
    FD_SET(sock, &mySet);
    if (select(sock + 1, &mySet, NULL, NULL, NULL)
           <= 0)
    {
      return;
    }
    if (FD_ISSET(sock, &mySet))
    {
      if ((lenght = recv(sock, buffer,
                 BUFFER_LEN - 1, 0)) <= 0)
      {
           return;
      }
      snprintf(pidMessage, BUFFER_LEN,
             "Přijal jsem %d bytů. Můj pid je %d.\n",
                 lenght, getpid());
      if (strcmp(buffer, "Konec!") == 0)
      {
           send(sock, endMessage,
              strlen(endMessage), 0);
      }
      else
      {
           if (send(sock, pidMessage,
                  strlen(pidMessage), 0) <= 0)
           {
                return;
           }
      }
    }
  } while (strcmp(buffer, "Konec!") != 0);
}

Rodičovský proces

Vše podstatné je ve funkci parent. Její parametr je soket. Rodičovský proces přijme data ze soketu, nějaká data mu pošle a nakonec odešle řetězec KONEC!

void parent(int sock)
{
  char buffer[BUFFER_LEN];
  fd_set mySet;
  int lenght, p;

  if ((lenght = recv(sock, buffer,
         BUFFER_LEN - 1, 0)) <= 0)
  {
    return;
  }
  buffer[lenght] = 0;
  printf(
   "Potomek poslal %d bytů.\nObsah zprávy:\n%s\n",
   lenght, buffer);
  printf("Můj pid je: %d\n", getpid());
  for(p = 0; p < 4; p++)
  {
    if (p == 3)
    {
      strncpy(buffer, "Konec!", 7);
    }
    else
    {
      buffer[0] = '0' + p;
      buffer[1] = '\0';
    }
    if (send(sock, buffer,
           strlen(buffer) + 1, 0) <= 0)
    {
      return;
    }
    FD_ZERO(&mySet);
    FD_SET(sock, &mySet);
    if (select(sock + 1, &mySet,
           NULL, NULL, NULL) <= 0)
    {
      return;
    }
    if (FD_ISSET(sock, &mySet))
    {
      if ((lenght = recv(sock,
                 buffer, BUFFER_LEN - 1, 0)) <= 0)
      {
            return;
      }
    }
    buffer[lenght] = 0;
    printf("Potomek poslal %d bytů.\nObsah
           zprávy:\n%s\n", lenght, buffer);
  }
}

Příklad ke stažení

Tabulka č. 500
Soubor OS
lin31.tgz Linux