TCP

Komunikace pomocí TCP/IP je nejčastější internetová komunikace. V této kapitole se ji naučíte v jazyku C. Díky přípravě v předchozích dvou kapitolách to bude poměrně snadné.

M$ Ve windows můžete najít knihovnu WinSock, která funguje velmi podobně jako linuxové sockety. Popisovat ji tu sice nebudu, ale pokud porozumíte linuxovým socketům, WinSocku porozumíte o mnoho snadněji. Viz třeba is.muni.cz/th/51885/fi_b/WinSock.pdf.

Úvod

V této kapitole naváži na kapitolu o Soketech. Použiji stejné kódy, jako v příkladu z této kapitoly. Všechny zdrojové soubory zůstanou stejné, jenom se změní implementace funkcí z hlavičkového souboru connection.h. Pro připomenutí jej znovu uvádím:

/*------------------------------------------------*/
/* 40socket/connection.h                          */
#include <stdbool.h>

typedef struct {
    int r;            /* read descriptor */
    int w;            /* write descriptor */
} descriptors;

/* server */
bool startServer(void);
void stopServer(void);
descriptors waitForClientConnection(void);
void closeClientConnection(descriptors * fd);
void serverWork(descriptors * fd);

/* client */
descriptors connectClientToServer(void);
void disconnectClientFromServer(descriptors * fd);
void clientWork(descriptors * fd);
/*------------------------------------------------*/

Funkce serverWork() a clientWork() zůstávají také beze změny. Mění se jen způsob připojení a odpojení.

IPv4 server

Tentokrát budu využívat sokety pro komunikaci pomocí TCP se starší, ale stále dominující IPv4 adresou.

Všechny výše deklarované funkce budou definované v souboru connection-inet.c.

Hlavní změnou oproti komunikaci skrze unixové sokety je použití jiné adresy. Namísto sockaddr_un se použije sockaddr_in.

struct sockaddr_in {
    short int sin_family;       /* AF_INET */
    unsigned short int sin_port; /* htons(cislo_portu) */
    struct in_addr sin_addr;     /* internetova adresa  */
}

Struktura in_addr obsahuje jen jednu položku:

#typedef uint32_t in_addr_t
struct in_addr {
    in_addr_t s_addr; /* = inet_addr('127.0.0.1'); nebo htonl(INADDR_ANY); */
}

Při návrhu této struktury se počítalo s tím, že bude časem položka obsahovat více položek, ale k tomu nikdy nedošlo.

Pro IPV4 je sin_family vždy AF_INET. sin_port se nastavuje na hodnotu portu pomocí funkce htons() (viz dále).
Položka s_addr je v případě serveru adresa, ze které bude server přijímat spojení. Hodnota INADDR_ANY znamená, že server bude přijímat spojení odkudkoliv.

Převody endianity

Port je 16 bitové číslo (4 bajty). Bajty tohoto portu musí být ve správném pořadí. Nikdy nevíte, jakou endianitu bude používat počítač na druhé straně sítě, proto musí existovat nějaká shoda. A k tomu vám dopomůže funkce htons(), která převádí číslo s bajty v pořadí, jaké podporuje váš počítač (h = host) do (to) pořadí používaného na netu (n = network). Konkrétně funkce htons() převádí short int (s). Kromě této funkce na převod short int (16 bitového) máte k dispozici i funkce na převod 32 bitového int.

#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

Funkce ntohl() a ntohs() převádějí formát čísla přijatého z internetu na formát přirozený pro váš počítač. Všiměte si datových typů, které konkrétně uvádějí počet bitů.

Problémy s endianitou se nevztahují jen na IP adresu a port, ale i na data, která posíláte. Když posíláte nějaká binární data tak je na vás, abyste se postarali o to, aby se na každém počítači, kde bude váš program přeložen, pracovalo se správnou endianitou a taky se správnou velikostí datového typu. Když budete posílat typ int a na jednom počítači bude mít int 32 bajtů a na druhém 64, nedopadne to dobře. Výše zmíněné funkce vám pomohou s endianitou, s velikostí bytů vám může pomoci používání nestandardních datových typů, jako je uint32_t. Já se o toto v příkladu nestarám, ale v reálné aplikaci byste měli.

Parametry serveru

Teď už máte k dispozici všechny potřebné informace pro spuštění serveru. Začne se funkcí socket(), která dostane jako argument AF_INET, SOCK_STREAM, a 0. První argument znamená IPv4, druhý argument, že chci spolehlivé spojení a třetí vybere defaultní protokol. Následuje nastavení adresy, na které server poslouchá, pomocí bind() a spuštění poslouchání pomocí listen().

Je tu ale ještě něco navíc, co jsem si pro vás připravil. Nastavení parametrů socketu. K tomu se používá funkce setsockopt().

int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

Tato funkce se používá k nastavování všemožných vlastností socketu. Bohužel není úplně jednoduché zjistit, co a jak se s tím dá nastavit.

První použití setsockopt() v příkladu nastavuje SO_REUSEADDR. To je vlastnost, která říká, zda se může použitá adresa otevřít po uzavření ihned. Konkrétně tedy jde o to, že když ukončíte server, tak port, na kterém poslouchal, zůstane několik (desítek) sekund ve stavu WAIT. V tomto stavu port nemůže nikdo jiný použít. Je to taková malá ochrana před tím, aby nemohl nějaký jiný server získat data přicházející od klienta k původně spuštěnému serveru. Pomocí setsockopt() se dá toto čekání zapnout nebo vypnout. Defaultně je SO_REUSEADDR vypunté, což při vývoji a stestování zdržuje. Proto jej zapnu:

int on = 1;
ret = setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, (const void *)&on, sizeof(on));

Prvním argumentem je socket. Druhým je tzv. level, který je při nastavování vlastností socketu vždy SOL_SOCKET. Třetí parametr říká, co chci v daném levelu nastavit, v tomto případě se tedy jedná o SO_REUSEADDR. Další parametr je odkaz na nastavovanou hodnotu. V tomto případě odkaz na int, který může být buď 1, nebo 0. A poslední parametr je velikost nastavované hodnoty.

Druhé použití setsockopt() nastaví, co se má stát v případě, že zavíráte socket pomocí close() ve chvíli, kdy má socket ve vyrovnávací paměti ještě nějaké neodeslané zprávy. Jde o vlastnost SO_LINGER, která se nastavuje pomocí struktury struct linger.

struct linger {
    int l_onoff;    /* Nonzero to linger on close.  */
    int l_linger;   /* Time to linger in seconds.  */
};

V příkladu nastavím socket tak, aby se při zavírání zablokoval až na 30 sekund, během kterých se pokusí odeslat zprávy z vyrovnávací paměti.

 struct linger linger;
 linger.l_onoff = 1;
 linger.l_linger = 30;
 ret = setsockopt(server_fd, SOL_SOCKET, SO_LINGER,(const char *)&linger, sizeof(linger));

Zdrojový kód serveru

A teď už se můžete podívat na první část souboru connection-socket.c, kde jsou definované funkce startServer()stopServer().

  1. /*------------------------------------------------*/
  2. /* 40socket/connection-socket.c                   */
  3. #include <sys/types.h>
  4. #include <sys/socket.h>
  5. #include <netinet/in.h>
  6. #include <arpa/inet.h>
  7. #include <stdbool.h>
  8. #include <string.h>
  9. #include <stdio.h>
  10. #include <errno.h>
  11. #include <unistd.h>
  12. #include "connection.h"
  13.  
  14. #define SERVER_IPv4 "127.0.0.1"
  15. #define SERVER_PORT 50505
  16.  
  17. static int server_fd = -1;
  18.  
  19. bool startServer(void)
  20. {
  21.     struct sockaddr_in server_address;
  22.     int ret;
  23.     server_fd = socket(AF_INET, SOCK_STREAM, 0);
  24.     if (server_fd < 0) {
  25.         perror("socket");
  26.         return false;
  27.     }
  28.  
  29.     int on = 1;
  30.     ret =
  31.         setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, (const void *)&on,
  32.                sizeof(on));
  33.  
  34.     struct linger linger;
  35.     linger.l_onoff = 1;
  36.     linger.l_linger = 30;
  37.     ret =
  38.         setsockopt(server_fd, SOL_SOCKET, SO_LINGER, (const char *)&linger,
  39.                sizeof(linger));
  40.     if (ret < 0)
  41.         perror("SO_LINGER");
  42.  
  43.     memset(&server_address, 0, sizeof(server_address));
  44.     server_address.sin_family = AF_INET;
  45.     server_address.sin_addr.s_addr = htonl(INADDR_ANY);
  46.     server_address.sin_port = htons(SERVER_PORT);
  47.     ret = bind(server_fd, (struct sockaddr *)&server_address,
  48.            sizeof(server_address));
  49.     if (ret < 0) {
  50.         perror("bind");
  51.         return false;
  52.     }
  53.     listen(server_fd, 5);
  54.     return true;
  55. }
  56.  
  57. void stopServer(void)
  58. {
  59.     close(server_fd);
  60. }
  61.  

Na začátku je definována adresa SERVER_IPv4, na které bude klient hledat server (viz dále). 127.0.0.1 je tzv. loopback, tedy IP adresa, která ukazuje na počítač, na kterém je použita.

Server port jsem si vymyslel 50505. Pokud by vám na tomto portu náhodou už něco běželo, můžete si ho změnit na nějaký jiný, dynamický port. Seznam aplikací poslouchajících na nějakém portu vám dá program netstat. Port 50505 je port, na kterém bude server poslouchat a ke kterému se bude klient připojovat.

Přijetí spojení

K přijetí spojení se používá funkce accept(). Tady se oproti kódu s unix socketovými soubory vlastně nic nezměnilo. Jen místo struktury struct sockaddr_un se ukládá adresa připojeného klienta do struct sockaddr_in. V příkladu s adresou klienta server nic nedělá, ale můžete z ní zjistit IP adresu klienta a jeho port. K tomu se používají funkce, které proberu až v další kapitole.

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

Vzpomeňte si, že client_fd je obousměrný deskriptor, který se používá pro čtení i zápis. Proto jej přiřazuji do fd.r i fd.w.

IPv4 client

Připojení klienta se také moc neliší od jeho verze s unix socketovými soubory. Rozdíl je jen v arugmentech funkce socket(), které jsem popsal u vytváření serveru a v použití adresy struct sockaddr_in.

Při nastavování této adresy je zajímavé nastavení address.sin_addr.s_addr.

POSIX norma k tomu pužívá funkci inet_addr(), která převede řetězcový zápis IP adresy, jako je "127.0.0.1" na číslo (se správným „internetovým“ pořadím bajtů).

Tahle funkce má ale jeden drobný nedostatek. K oznámení chyby používá návratovou hodnotu -1. Jenomže (platná) IP adresa "255.255.255.255" se také převede na číslo -1.

Kvůli tomu se používá funkce inet_aton(). Ta sice není v POSXI normě, ale je implementovaná na skoro každém Linuxu a Unixu.

int inet_aton(const char *cp, struct in_addr *inp);

Na její použití se podívejte v příkladu:

  1. descriptors connectClientToServer(void)
  2. {
  3.     struct sockaddr_in address;
  4.     int sockfd;
  5.     int ret;
  6.     descriptors fd = { -1, -1 };
  7.  
  8.     sockfd = socket(AF_INET, SOCK_STREAM, 0);
  9.     if (sockfd < 0) {
  10.         perror("socket");
  11.         return fd;
  12.     }
  13.  
  14.     memset(&address, 0, sizeof(address));
  15.     address.sin_family = AF_INET;
  16.     address.sin_port = htons(SERVER_PORT);
  17.     /*address.sin_addr.s_addr = inet_addr(SERVER_IPv4); */
  18.     if (inet_aton(SERVER_IPv4, &(address.sin_addr)) == 0) {
  19.         perror("inet_aton");
  20.         return fd;
  21.     }
  22.     ret = connect(sockfd, (struct sockaddr *)&address, sizeof(address));
  23.     if (ret < 0)
  24.         perror("connect");
  25.     fd.r = fd.w = sockfd;
  26.  
  27.     return fd;
  28. }
  29.  
  30. void disconnectClientFromServer(descriptors * fd)
  31. {
  32.     if (close(fd->r) < 0) {
  33.         perror("close r");
  34.     }
  35. }
  36. /*------------------------------------------------*/

Zkuste si přeložit příklad bez nastavení SO_REUSEADDR. Pak spusťe server, alespoň jedno připojení klienta, server ukončete a znovu ho zkuste spustit.

Server:

$ ./inet_server
Čekám na připojení od klienta ...

Klient:

$ ./inet_client
Connect ...
Write ...
Read ...
3 + 8 = 11

Znovu server:

$ ./inet_server
Čekám na připojení od klienta ...
Read ...
Sčítám 3 + 8.
Write ...
Čekám na připojení od klienta ...
^C
Closing server ...
$  ./inet_server
bind: Address already in use
$ netstat | egrep 50505
tcp        0      0 localhost:50505         localhost:38771         TIME_WAIT  

Jak vidíte, port 50505 je ve stavu TIME_WAIT. Takže sem vám nelhal :-). Port se uvolní až po několika vteřinách. 38771 je číslo portu, který použil pro komunikaci klient. Toto číslo bylo klientovi vybráno „náhodně“ z volných portů, které měl systém k dispozici. Netstat jej pořád zobrazuje, ikdyž klient už svou práci ukončil a spojení uzavřel.

Obsluha více klientů

K serveru se často připojuje mnoho klientů naráz. Díky funkci listen() zůstává jejich požadavek na připojení ve frontě, dokud není vyzvednut voláním accept(). To by mělo být voláno dostatečně rychle na to, aby se fronta nezaplnila.

Pokud zpracování požadavku zabere nějakou dobu, obvykle je dobré zpracovávat komunikaci s klientem v jiném procesu, nebo vlákně.

Hlavní vlákno/proces serveru jenom volá v cyklu accept() a vytváří pro obsluhu nový proces či vlákno.

Forkování vypadá nějak takto:

while (true)
{
    client_fd = accept(server_fd, (struct sockaddr *)&client_address, &client_len);
    pid = fork();
    if (pid < 0) { perror("fork"); exit(1); }
    if (pid == 0)  
    {
        /* Nový proces obslouží klienta */
        close(server_fd);
        serverWork(client_fd);
        exit(0);
    }
    else {
        /* Hlavní proces client_fd nepotřebuje. */
        close(client_fd);
    }
}

Při forkování si musíte dávat pozor, aby vám nevznikali zombie. Jednou z možností, jak zombie nedovolit, je ignorování signálu SIGCHLD. Tento signál se generuje, když proces skončí a doručí se rodičovskému procesu. Když jej rodičovský proces ignoruje, ukončený proces nečeká na přečtení své návratové hodnoty a je ihned odstraňen (nestane se z něj zombie).

signal(SIGCHLD, SIG_IGN);

Bohužel, ne na všech unixech/linuxech je to tak jednoduché. Někde prostě musíte volat wait(), k čemuž můžete použít tuto funkci:

void zombie_handler(int iSignal)
{
    signal(SIGCHLD, zombie_handler); //reset handler
    int status;
    (void) wait(&amp;status);
}

Funkce select()

Pokud je server velká aplikace, pak může být vytváření nového procesu forkováním poměrně zdlouhavé. Použití vláken je lepší, ale zase klade větší nároky na synchronizaci zdrojů. Určitým kompromisem může být použití funkce select().

int select(int nfds, fd_set *restrict readfds,
              fd_set *restrict writefds, fd_set *restrict errorfds,
              struct timeval *restrict timeout);

Tato funkce testuje sady deskriptorů, zda je některý z nich připraven na čteni, zápis, nebo obsahuje chybu. Toho se dá využít tak, že do sady deskriptorů přidáte deskriptor socketu a pak každý deskriptor získaný voláním funkce accept().
Když vám funkce select() vrátí deskriptor socketu že je připraven ke čtení, můžete zavolat accept() beze strachu ze zablokování čekáním na klienta. Získaný deskriptor voláním funkce accept() hned přidáte do sady testovaných deskriptorů a můžete zase spustit select().
Když je připraven ke čtení deksriptor klienta, můžete jej hned obsloužit (načíst data). Zase beze strachu ze zablokování čtením.

Přijímání nových spojení a jejich obsluha tak probíhá bez jakéhokoliv zbytečného blokování. Toto bude fungovat dobře ale jen v případě, že obsluha klientů je rychlá a požadavky od klientů chodí pomalu. Zkrátka, všechno se musí obsloužit dostatečně rychle na to, aby se stačili přijímat nový klienti.

Z toho důvodu si myslím, že je lepší investovat trochu námahy a používat vlákna, než funkci select(). Proto tady nenajdete její podrobnější popis ani příklad použití.

Komentář Hlášení chyby
Created: 6.10.2014
Last updated: 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..