Sokety

O soketech

Soket (anglicky socket) je další z prostředků IPC (inter process communiation), podobně jako roura (pipe).

Soket ovšem nabízí mnohem více způsobů komunikace. To je jeho výhoda i nevýhoda. Nevýhoda je hlavně v tom, že kvůli své univerzálnosti je jeho rozhraní (deklarace funkcí) poměrně krkolomné. Však uvidíte sami.

Sokety se používají k tomu, na co se určitě už dlouho těšíte – ke komunikaci procesů skrze internet. V této kapitole vám ale nejdříve ukáži, jak použít soket podobně, jako pojmenované roury.

Většinu zde probraných informací a kusů kódu využijete i v další kapitole, která se už bude zabívat internetovou komunikací.

Příprava příkladu

Napsal jsem dva příklady, jeden na komunikaci pomocí roury a druhý pomocí soketu. Protože jsou si tyto dva typy komunikací tak podobné, většina zdrojového kódu je pro oba příklady stejná.

Sokety, stejně jako roury, jsou reprezentovány deskriptory souborů, tedy číslem typu int. Čtení a zápis se provádí funkcemi read() a write(). Deskriptory se uzavírají pomocí funkce close().

Hlavní rozdíl je v tom, jak tyto deskriptory získáte.

Oba příklady budou definovat funkce deklarované v souboru connection.h. Jeden příklad bude definovat těla těchto funkcí pro roury, druhý pro sokety.

Oba příklady se zkládají ze dvou programů, serveru a klienta.

První, co v connection.h najdete, je struktura descriptors, kterou používám pro předávání deskriptorů pro četení a zápis. Server bude číst data zapsaná klientem a zapisovat data, která bude číst klient. To je asi jasné :-). Klient a server tyto deskriptory získávají trochu jinak.

  1. /*------------------------------------------------*/
  2. /* 40socket/connection.h                          */
  3. #include <stdbool.h>
  4.  
  5.     int r;            /* read descriptor */
  6.     int w;            /* write descriptor */
  7. } descriptors;
  8.  
  9. /* server */
  10. bool startServer(void);
  11. void stopServer(void);
  12. descriptors waitForClientConnection(void);
  13. void closeClientConnection(descriptors * fd);
  14. void serverWork(descriptors * fd);
  15.  
  16. /* client */
  17. descriptors connectClientToServer(void);
  18. void disconnectClientFromServer(descriptors * fd);
  19. void clientWork(descriptors * fd);
  20. /*------------------------------------------------*/

Server funguje tak, že se nejdříve spustí (startServer()). Pak čeká, až se k němu připojí klient (waitForClientConnection()). Pak si server (ve funkci serverWork()) přečte co mu klient posílá (strukturu scitance) a odešle klientovi odpověď (strukturu vysledek). Pak znovu čeká, až se k němu připojí klient. A tohle dělá pořád dokola. Server se ukončí voláním funkce stopServer().

Funkce klienta je jednodušší. Pouze se připojí k serveru (connectClientToServer()), pošle serveru scitance (ve funkci clientWork()), přečte si vysledek a připojení ukončí pomocí disconnectClientFromServer().

Klient

Další zdroják, který budou příklady sdílet, je client.c. V něm je zapsaná logika klienta. Když si odmyslíte všechna ladící a chybová hlášení, jediné co program dělá je, že získá deskriptory, použije je s funkcemi write() a read() a deskriptory zase uzavře. A je mu jedno, jestli jsou deskriptory rourou, soketem, nebo třeba obyčejným souborem.

  1. /*------------------------------------------------*/
  2. /* 40socket/client.c                              */
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include "connection.h"
  6.  
  7. int main(void)
  8. {
  9.     printf("Connect ...\n");
  10.     descriptors fd = connectClientToServer();
  11.     if (fd.r < 0 || fd.w < 0) {
  12.         fprintf(stderr, "Connection failed.\n");
  13.         exit(EXIT_FAILURE);
  14.     }
  15.  
  16.     clientWork(&fd);
  17.     disconnectClientFromServer(&fd);
  18.  
  19.     return 0;
  20. }
  21.  
  22. /*------------------------------------------------*/

Kromě dříve deklarovaných funkcí connectClientToServer(), disconnectClientFromServer() a clientWork() v tomto zdrojáku nic zajímavého nenajdete.

Klient posílá serveru náhodně vygenerovaná čísla ve struktuře scitance a od serveru získává výsledek ve struktuře vysledek. A to vše ve funkci clientWork().

  1. /*------------------------------------------------*/
  2. /* 40socket/client-work.c                         */
  3. #include <stdio.h>
  4. #include <unistd.h>
  5. #include <stdlib.h>
  6. #include <time.h>
  7. #include "connection.h"
  8. #include "work.h"
  9.  
  10. void clientWork(descriptors * fd)
  11. {
  12.     scitance sc;
  13.     vysledek v;
  14.  
  15.     srand(time(NULL));
  16.     sc.x = rand() % 10;
  17.     sc.y = rand() % 10;
  18.  
  19.     printf("Write ...\n");
  20.     if (write(fd->w, &sc, sizeof(scitance)) != sizeof(scitance)) {
  21.         fprintf(stderr, "Write failed\n");
  22.         exit(EXIT_FAILURE);
  23.     }
  24.  
  25.     printf("Read ...\n");
  26.     if (read(fd->r, &v, sizeof(v)) != sizeof(v)) {
  27.         fprintf(stderr, "Reading failed\n");
  28.         exit(EXIT_FAILURE);
  29.     }
  30.  
  31.     printf("%i + %i = %i\n", sc.x, sc.y, v.z);
  32. }
  33. /*------------------------------------------------*/

Tato funkce nemusí vědět nic o tom, zda pracuje s deskriptory pro přenos dat po internetu, s rourami nebo s deskriptory souborů.

Server

Server je jen o málo složitější. Předně, protože je v něm nekonečná smyčka (while(true)), můžete jej ukončit jen pomocí CTRL+c. Takto kombinace kláves vyvolá signál SIGINT, který je ošetřen funkcí closeServerHandler(). V této funkci se nejdříve uzavřou spojení s klientem (funkce closeClientConnection()) a pak se ukončí server, který čekal na spojení (stopServer()).

Server v nekonečné smyčce čeká na spojení klienta, obslouží ho a spojení zase uzavře.

Všiměte si, že když dojde k nějaké chybě čtení nebo zápisu, server uzavře spojení s klientem a čeká na další spojení. K tomu může dojít, pokud se třeba klient předčasně odpojí, dojde k výpadku spojení s klientem atp. Na uzavření spojení s klientem se nesmí zapomenout, jinak by docházelo k únikům zdrojů. (Vzpomeňte si, že počet otevřených deskriptorů jedním procesem je operačním systémem limitován. Neuzavírání descriptorů souborů by vedlo rychle k jejich vyčerpání.)

  1. /*------------------------------------------------*/
  2. /* 40socket/server.c                              */
  3. #include <stdio.h>
  4. #include <stdlib.h>
  5. #include <signal.h>
  6. #include "connection.h"
  7.  
  8. static descriptors fd = { -1, -1 };
  9.  
  10. static void closeServerHandler()
  11. {
  12.     printf("\nClosing server ...\n");
  13.     closeClientConnection(&fd);
  14.     stopServer();
  15.     exit(EXIT_SUCCESS);
  16. }
  17.  
  18. int main(void)
  19. {
  20.     if (!startServer())
  21.         exit(EXIT_FAILURE);
  22.     signal(SIGINT, closeServerHandler);
  23.  
  24.     while (1) {
  25.         printf("Čekám na připojení od klienta ...\n");
  26.         fd = waitForClientConnection();
  27.         if (fd.r < 0 || fd.w < 0) {
  28.             fprintf(stderr, "Connection failed.\n");
  29.             stopServer();
  30.             exit(EXIT_FAILURE);
  31.         }
  32.         serverWork(&fd);
  33.         closeClientConnection(&fd);
  34.     }
  35.  
  36.     return 0;
  37. }
  38. /*------------------------------------------------*/

Komunikace s klientem probíhá ve funkci serverWork().

  1. /*------------------------------------------------*/
  2. /* 40socket/server-work.c                         */
  3. #include <stdio.h>
  4. #include <unistd.h>
  5. #include "connection.h"
  6. #include "work.h"
  7.  
  8. void serverWork(descriptors * fd)
  9. {
  10.     scitance sc;
  11.     vysledek v;
  12.  
  13.     printf("Read ...\n");
  14.     if (read(fd->r, &sc, sizeof(sc)) != sizeof(sc)) {
  15.         fprintf(stderr, "Read failed. Restart ...\n");
  16.         return;
  17.     }
  18.     printf("Sčítám %i + %i.\n", sc.x, sc.y);
  19.     v.z = sc.x + sc.y;
  20.     printf("Write ...\n");
  21.     if (write(fd->w, &v, sizeof(vysledek)) != sizeof(vysledek)) {
  22.         fprintf(stderr, "writing failed. Restart ...\n");
  23.         return;
  24.     }
  25. }
  26. /*------------------------------------------------*/

A to je ke společným kódum vše. V další části můžete porovnat, jak jsem definoval funkce pro navázání a uzavření spojení (deklarované v connection.h) pro roury a pro sokety.

Pojmenované roury

Začnu pojmenovanými rourami. Fungování rour už znáte, takže to nebude pro vás nic nového.

Soubor connection-pipe.c definuje funkce deklarované v connection.h. Na začátku souboru jsou nezbytné includy hlavičkových souborů a definice názvů pipe souborů. Roury jsou jednosměrné, to znamená, že jeden proces může do roury zapisovat a jeden z ní může číst. Proto je potřeba pro vzájemnou komunikaci procesů mit roury dvě.

Pipe soubory můžete vytvořit programem mkfifo na příkazové řádce, nebo funkcí mkfifo() přímo z programu v C. Zvolil jsem tu druhou možnost. Funkce pro vytvoření pipe souboru, vytvorRouru(), vrátí true, pokud mkfifo() rouru vytvoří, nebo pokud už roura existuje (chyba EEXIST). Jinak vrací false.

  1. /*------------------------------------------------*/
  2. /* 40socket/connection-pipe.c                     */
  3. #include <sys/types.h>
  4. #include <sys/stat.h>
  5. #include <fcntl.h>
  6. #include <unistd.h>
  7. #include <stdbool.h>
  8. #include <stdio.h>
  9. #include <errno.h>
  10. #include "connection.h"
  11.  
  12. #define PIPE_FILE_SERVER "/tmp/named_pipe_server"
  13. #define PIPE_FILE_CLIENT "/tmp/named_pipe_client"
  14.  
  15. static bool vytvorRouru(char *name)
  16. {
  17.     if (mkfifo(name, 0666) == -1) {
  18.         if (errno != EEXIST) {
  19.             perror("mkfifo");
  20.             return false;
  21.         }
  22.     }
  23.     return true;
  24. }
  25.  

Funkce startServer() nedělá nic jiného, než že se pokusí vytvořit obě roury (oba pipe soubory). Funkce stopServer() je zase smaže (ne že by jejich mazání bylo nutné).

  1. bool startServer(void)
  2. {
  3.     if (!vytvorRouru(PIPE_FILE_SERVER))
  4.         return false;
  5.     if (!vytvorRouru(PIPE_FILE_CLIENT))
  6.         return false;
  7.     return true;
  8. }
  9.  
  10. void stopServer(void)
  11. {
  12.     unlink(PIPE_FILE_SERVER);
  13.     unlink(PIPE_FILE_CLIENT);
  14. }
  15.  

Funkce waitForClientConnection() otevře pipe soubory, jeden pro čtení a jeden pro zápis. Klient pak otevře tytéž soubory, ale se čtením/zápisem obráceně.

  1. descriptors waitForClientConnection(void)
  2. {
  3.     descriptors fd = { -1, -1 };
  4.  
  5.     fd.r = open(PIPE_FILE_CLIENT, O_RDONLY);
  6.     if (fd.r < 1) {
  7.         perror("open r");
  8.         return fd;
  9.     }
  10.     fd.w = open(PIPE_FILE_SERVER, O_WRONLY);
  11.     if (fd.w < 1) {
  12.         perror("open w");
  13.         return fd;
  14.     }
  15.     return fd;
  16. }
  17.  

Při zavírání spojení si nejdříve zkontroluji, jestli jsem dostal platné deskriptory souborů (větší nebo rovné nule) a pak je uzavřu. Struktuře descriptors nastavím hodnoty na -1, aby bylo jasné, že neobsahuje platné deskriptory.

To je důležité kvůli tomu, kdybyste ukončili server stiskem CTLR+c poté, co se zrovna uzavřelo spojení s klientem, ale předtím, než se otevře nové. Funkce closeServerHanlder() by se totiž pokusila zavolat closeClientConnection() na stejné čísla deskriptorů znovu, což by byla chyba.

  1. void closeClientConnection(descriptors * fd)
  2. {
  3.     if (fd->r >= 0 && close(fd->r) < 0) {
  4.         perror("close r");
  5.     }
  6.     if (fd->w >= 0 && close(fd->w) < 0) {
  7.         perror("close w");
  8.     }
  9.     fd->r = fd->w = -1;
  10. }
  11.  

Zbylé dvě funkce z connection-pipe.c se týkají klienta. Otevírají a zavírají roury pomocí open() a close(), na čemž není žádná magie.

  1. descriptors connectClientToServer(void)
  2. {
  3.     descriptors fd = { -1, -1 };
  4.  
  5.     fd.w = open(PIPE_FILE_CLIENT, O_WRONLY);
  6.     if (fd.w < 1) {
  7.         perror("open w");
  8.         return fd;
  9.     }
  10.     fd.r = open(PIPE_FILE_SERVER, O_RDONLY);
  11.     if (fd.r < 1) {
  12.         perror("open r");
  13.         return fd;
  14.     }
  15.     return fd;
  16. }
  17.  
  18. void disconnectClientFromServer(descriptors * fd)
  19. {
  20.     if (close(fd->r) < 0) {
  21.         perror("close r");
  22.     }
  23.     if (close(fd->w) < 0) {
  24.         perror("close w");
  25.     }
  26. }
  27. /*------------------------------------------------*/

Takto může vypadat výstup ze serveru:

Čekám na připojení od klienta ...
Read ...
Sčítám 7 + 1.
Write ...
Čekám na připojení od klienta ...
Read ...
Read failed. Restart ...
Čekám na připojení od klienta ...
^C
Closing server ...

Co se stalo? Nejdříve se spustil server (vytvořili se roury). Pak se server pokusil otevřít roury voláním open(), které se zablokovalo, dokud jsem nespustil (na jiné konzoli) klienta.

Poté server načetl scitance z PIPE_FILE_CLIENT a zapsal výsledek do PIPE_FILE_SERVER.

Pak uzavřel spojení a pokusil se jej otevřít znovu. Což se mu povedlo ihned, protože se klient nestihnul ještě odpojit. Funkce open() proto ani chvilku server neblokovaly a server se pokusil hned načíst další scitance (od stejného klienta). Jenže tento klient další data neposlal, odpojil se a skončil. Proto druhý pokus o čtení v serveru skončilo s chybou (Read failed. Restart ...).

Server se tedy znovu restartoval a tentorkát už se open() zablokovalo a čekalo na dalšího klienta.

Načež jsem stiskl CTRL+c a server se ukončil.

Pro úplnost ještě uvedu výstup z klienta:

Connect ...
Write ...
Read ...
7 + 1 = 8

Sokety AF_UNIX

Teď je na řadě IPC pomocí soketů. Zprovoznění serveru pomocí soketů je o něco složitější právě proto, že sokety umožňují různé druhy komunikace. Nyní předvedu komunikaci pomocí lokálních soketů, které jsou reprezentovány soubory, podobně jako pojmenované roury. V tomto příkladu představím mnoho funkcí, které budete potřebovat i pro připojení po internetu.

Funkce socket()

Základem všeho je funkce socket().

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

První argument, domain, rozhoduje o výběru způsobu komunikace. Tento parametr ovlivňuje možnosti a smysl všech dalších parametrů. Nejdůležitější jsou tyto tři možnosti:

domainVýznam
AF_UNIXLokální soket reprezentovaný souborem. Na některých systémech můžete použít alias AF_LOCAL
AF_INETKomunikace přes internet pomocí IPv4
AF_INET6Komunikace přes internet pomocí IPv6

Druhý argument, type určuje typ spojení. Hlavní dva typy jsou tyto:

typeVýznam
SOCK_STREAMSpolehlivý přenos dat.
SOCK_DGRAMNespolehlivý přenos dat s omezenou velikostí.

Spolehlivý přenos neznamená, že bude vždy fungovat (když přenášíte data přes kabel, který vám soused překopne motikou, tak s tím ani ten nejlepší algoritmus nic nenadělá :-). Znamená to, že se vytvoří spojení, při kterém jste informováni o ne/doručení dat. Pokud nedostanete informaci o doručení dat, dostanete informaci o tom, že došlo k chybě.

Naproti tomu SOCK_DGRAM vyšle zprávu k cíli a už se nezajímá o to, jestli zpráva přišla nebo ne. Díky tomu je komunikace rychlejší (ale nespolehlivá). Velikost odeslané zprávy, tzv. datagramu, je omezená, můžete ale poslat datagramů kolik chcete. Nemáte bohužel zaručeno, že dojdou ve stejném pořadí, v jakém byly odeslány!
Tento způsob přenosu dat se používá například u ONLINE her, kde nějaká ta ztráta packetu způsobí maximálně malé trhnutí obrazu u hráče, ale rychlost přenosu/odezvy je zásadní.

Rozlišení mezi SOCK_STREAM a SOCK_DGRAM má smysl u přenášení dat po síti (AF_INET, AF_INET6 atp.). Pro AF_UNIX nemá SOCK_DGRAM smysl, takže se používá jen SOCK_STREAM.

Poslední argument, protocol, je číslo protokolu daného způsobu komunikace (domény). Obvykle se používá 0, což znamená defaultní protokol. Většina domén má jen jeden protokol, takže tento argument není moc zajímavý.

Funkce bind()

Vytvoření socketu je jen první krok k navázání komunikace. Druhý krok se liší podle toho, jestli chcete vytvořit server nebo klienta (což je další výrazný rozdíl oproti používání pipe).

Při vytváření serveru se musí nejdříve připojit k soketu adresa nebo jméno soketového souboru pomocí funkce bind().

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

První argument je deskriptor socketu, sockfd.

Druhý argument je odkaz na adresu a třetím argumentem je délka struktury, ve které je adresa uložena. Možná si říkáte, k čemu ta délka je, copak není jasné, jak je struktura struct sockaddr dlouhá?

Není. A to proto, že pro různé domény se používají různé struktury s adresou. Pro AF_UNIX musíte uvést jméno souboru, pro AF_INET IP adresu, pro AF_INET6 IP6 adresu atp.

Teď popíšu adresu pro AF_UNIX, IP adresy popíšu v další kapitole.

Pro adresu pro AF_UNIX se používá strutkura struct sockaddr_un definovaná v hlavičkovém souboru <sys/un.h>

struct sockaddr_un
{
    sa_family_t sun_family; /* = AF_UNIX */
    char sun_path[UNIX_PATH_MAX]; /* pathname (cesta), 108 max */
}

Do sun_family se ukládá vždy AF_UNIX, do sun_path se ukládá cesta k souboru, max. délky UNIX_MAX_PATH-1.

Když odkaz na tuto strukturu předáváte funkci bind(), musíte ji přetypovat na odkaz na struct sockaddr, aby si překladač nestěžoval (totéž se musí udělat i s ostatními typy adres, pro AF_INET atp).

Funkce listen()

Už máte otevřený soket a přiřadili jste mu nějakou adresu, adresu, na které má čekat na spojení od klienta. Teď je ještě potřeba toto poslouchání zapnout pomocí funkce listen().

int listen(int sockfd, int backlog);

Parametr sockfd je zase deskriptor socketu. Parametr backlog určuje maximální délku fronty čekajících klientů. Pokud se v jeden okamžik pokusí připojit více jak backlog klientů, začnou být klienti odmítáni.

Obvykle se nastavuje backlog na 5. Možná se vám to zdá málo, ale server obvykle funguje tak, že vezme připojení klienta a začne ho obsluhovat v nějakém novém vlákně či procesu. Toto „vzetí“ klienta jej uvolní z fronty. To je velice rychlé, takže se jen těžko stihne do fronty zapsat 5 klientů.
Komunikace s klientem v novém vlákně už může trvat třeba celý den, protože už není součástí fronty.

Maximální hodnotu backlog najdete v /proc/sys/net/core/somaxconn a obvykle je to 128. Dříve bylo „natvrdo“ definováno jako SOMAXCONN, ale dnes jej může měnit jádro linuxu za běhu (resp. uživatel root může říct jádru, aby somaxconn změnil).

Vytvoření serveru AF_UNIX

Už jsem posal všechny funkce potřebné pro spuštění socketového serveru. Slibuji, že to nejhorší už máte zasebou :-) Teď se podívejte na začátek socketové implementace connection.h, kde je vše předvedeno prakticky:

  1. /*------------------------------------------------*/
  2. /* 40socket/connection-socket.c                   */
  3. #include <sys/types.h>
  4. #include <sys/socket.h>
  5. #include <sys/un.h>
  6. #include <stdbool.h>
  7. #include <string.h>
  8. #include <stdio.h>
  9. #include <errno.h>
  10. #include <unistd.h>
  11. #include "connection.h"
  12.  
  13. #define SOCKET_FILE "/tmp/socket_file_example1"
  14.  
  15. static int server_fd = -1;
  16.  
  17. bool startServer(void)
  18. {
  19.     struct sockaddr_un server_address;
  20.     int ret;
  21.     server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
  22.     if (server_fd < 0) {
  23.         perror("socket");
  24.         return false;
  25.     }
  26.     server_address.sun_family = AF_UNIX;
  27.     strcpy(server_address.sun_path, SOCKET_FILE);
  28.     ret =
  29.         bind(server_fd, (struct sockaddr *)&server_address,
  30.          sizeof(server_address));
  31.     if (ret < 0) {
  32.         perror("bind");
  33.         return false;
  34.     }
  35.     listen(server_fd, 5);
  36.     return true;
  37. }
  38.  
  39. void stopServer(void)
  40. {
  41.     unlink(SOCKET_FILE);
  42. }
  43.  

Ve funkci startServer() jsou všechny výše popsané kroky. Funkce bind() se sama postará o vytvoření socketového souboru (SOCKET_FILE), ale o jeho smazání se už musíte postarat sami, viz funkce stopServer().

Pokud SOCKET_FILE nesmažete a pokusíte se spustit nový server s tímto souborem, selže bind() s chybou Address already in use. Jinak řečeno, soubor může použít jen jeden server. Když soubor smažete, vytvoří se nový soubor, který má sice stejné jméno, ale je to už jiný soubor (má v systému souborů jiné číslo i-node).
Toto podivné chování je jaká si ochrana před tím, aby došlo k nechtěné komunikaci nově spuštěného serveru s nějakým „starým“ klientem.

Čekání na klienta

Poslendí funkcí, kterou server využívá je funkce accept(). Ta se stará o ono „vyzvednutí“ klienta z fronty. Funkce accept() se zablokuje do okamžiku, kdy se připojí nějaký klient.

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

První argument je zase deskriptor socketu, druhý argument je ukazatel na strukturu, do které se uloží adresa klienta (odkud se připojuje) a třetí argument je ukazatel na délku této struktury. Délku musíte nastavit na délku předávané struktury (je to maximum bajtů, které si může funkce accept() dovolit zapsat). Funkce accept() hodnotu addrlen poté změní na skutečnou délku adresy klienta. Může se totiž teoreticky stát, že bude jiná, než jakou čekáte. V takovém případě je asi něco špatně a měli byste oznámit chybu (v příkladu to nedělám).

Pokud vás adresa klienta nezajímá, můžete jako druhý a třetí argument předat NULL.

Funkce accept() vrátí obousměrný deskriptor, což znamená, že jej můžete použít pro čtení i zápis. Funkce waitForClientConnection() je ale definovaná tak, že vrací strukturu descriptors, tak nastavuji oba členy této struktury na client_fd. Ve funkci closeClientConnection() pak stačí zavřít jen jeden člen, například fd->r.

  1. descriptors waitForClientConnection(void)
  2. {
  3.     descriptors fd = { -1, -1 };
  4.     struct sockaddr_un client_address;
  5.     int client_fd;
  6.     socklen_t client_len;
  7.     memset(&client_address, 0, sizeof(client_address));
  8.     client_len = sizeof(client_address);
  9.     client_fd =
  10.         accept(server_fd, (struct sockaddr *)&client_address, &client_len);
  11.     fd.r = fd.w = client_fd;
  12.     return fd;
  13. }
  14.  
  15. void closeClientConnection(descriptors * fd)
  16. {
  17.     if (fd->r >= 0) {
  18.         if (close(fd->r) < 0) {
  19.             perror("close r");
  20.         }
  21.     }
  22.     fd->r = fd->w = -1;
  23. }
  24.  

A to je vše k serveru.

Socket klient

Klient si vytvoří soket stejně jako server, ale pak už nemusí na nic čekat a rovnou se pokusí připojit k serveru pomocí funkce connect().

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

Funkce connect() funguje podobně jako funkce accept(), jenom s tím rozdílem, že se jí předává odkaz na strukturu s adresou serveru a délka této struktury se nepředává jako odkaz, ale jako hodnota.

Tentokrát se pro komunikaci používá přímo deskriptor socketu, sockfd, který je taky obousměrný (takže se použije pro čtení i zápis).

Dejte si pozor, pokud se omylem pokusíte použít pro čtení/zápis návratovou hodnotu funkce connect(), což je v případě úspěchu 0, pokusíte se vlastně číst / zapisovat ze standardního vstupu. Což je tedy platný deskriptor. Tak se klient zablokuje bez jakékoliv chyby a vy budete marně pátrat po tom, proč vám to nefunguje. Vím o čem mluvím :-).

  1. descriptors connectClientToServer(void)
  2. {
  3.     struct sockaddr_un address;
  4.     int sockfd;
  5.     int ret;
  6.     descriptors fd = { -1, -1 };
  7.  
  8.     sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
  9.     if (sockfd < 0) {
  10.         perror("socket");
  11.         return fd;
  12.     }
  13.  
  14.     memset(&address, 0, sizeof(address));
  15.     address.sun_family = AF_UNIX;
  16.     strcpy(address.sun_path, SOCKET_FILE);
  17.     ret = connect(sockfd, (struct sockaddr *)&address, sizeof(address));
  18.     if (ret < 0)
  19.         perror("connect");
  20.     fd.r = fd.w = sockfd;
  21.  
  22.     return fd;
  23. }
  24.  
  25. void disconnectClientFromServer(descriptors * fd)
  26. {
  27.     if (close(fd->r) < 0) {
  28.         perror("close r");
  29.     }
  30. }
  31. /*------------------------------------------------*/

A teď už stačí jen vše přeložit a vyzkoušet. Výstup ze serveru bude vypadat třeba takto:

Čekám na připojení od klienta ...
Read ...
Sčítám 4 + 0.
Write ...
Čekám na připojení od klienta ...
^C
Closing server ...

A z klienta:

Connect ...
Write ...
Read ...
4 + 0 = 4

Jak vidíte, oproti verzi s rourami je programování socketů o trochu náročnější, ale má své výhody. Jednak se sockety můžete komunikovat po internetu (o tom bude další kapitola). Druhak byly sockety lépe navrženy pro server – client aplikace. Díky funkcím listen() a accept() nemá problém server s obsluhou několika klientů naráz. Nestane se, jako ve verzi s rourami, že by se pokusil číst data od starého klienta v domění, že je to klient nový. Jakmile byl jednou klient acceptován, byl vytažen z fronty (vytvořené funkcí listen()) a další volání accept() už čeká na nové připojení.

Pokud jste se dočetli až sem, gratuluji, už skoro umíte přijímat a posílat data po internetu. Už vám chybí opravdu jen málo.

Komentář Hlášení chyby
Vytvořeno: 6.10.2014
Naposledy upraveno: 10.10.2014
Tato stánka používá ke svému běhu cookies, díky kterým je možné monitorovat, co tu provádíte (ne že bych to bez cookies nezvládl). Také vás tu bude špehovat google analytics. Jestli si myslíte, že je to problém, vypněte si cookies ve vašem prohlížeči, nebo odejděte a už se nevracejte :-). Prohlížením tohoto webu souhlasíte s používáním cookies. Dozvědět se více..