W tym artykule zostaną opisane i pokazane podstawowe mechanizmy do komunikacji przy pomocy protokołów TCP i UDP na systemach linuxowych. (artykuł)
Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Zarejestruj się!
dział serwisuArtykuły
kategoriaInne artykuły
artykuł[C/C++] Programowanie sieciowe TCP i UDP pod Linuxem
Autorzy: Tomasz 'chondipo' Gruntowski & Grzegorz 'baziorek' Bazior

[C/C++] Programowanie sieciowe TCP i UDP pod Linuxem

[artykuł] W tym artykule zostaną opisane i pokazane podstawowe mechanizmy do komunikacji przy pomocy protokołów TCP i UDP na systemach linuxowych.

Wstęp

Artykuł jest poświęcony programowaniu przy użyciu gniazd na systemie Linux. Gniazda i porty są na tyle podstawowym elementem programowania sieciowego, że są stosowane w wielu językach programowania i systemach operacyjnych. Przedstawione tutaj funkcje są podobne do funkcji WinSocket dla systemu Windows (kurs WinSock na ławach niniejszego serwisu), niemniej jednak pewne różnice występują.

Zakres materiału

W tym artykule zostaną opisane i pokazane podstawowe mechanizmy do kumunikacji przy pomocy protokołów TCP i UDP na systemiach linuxowych.

Wstęp teoretyczny

Gniazdo (Socket)

Gniazdo jest jednym z końców komunikacji dwukierunkowej, podobnym jak wtyczka na kabel, np. ethernet. Dwukierunkowość oznacza możliwość wysyłania i przyjmowania danych. Pisząc programy komunikujące się przez sieć, będziemy programować na socketach. Wszystko stanie się jasne, gdy przejdziemy do pierwszego programu i zdefiniujemy pierwszy socket.

Warstwa transportowa i jej protokoły

Aby zrozumieć, co to są warstwy, polecam przeczytać o modelach obrazujących jak tak właściwie działa internet, najpopularniejszymi modelami są Model OSI oraz Model TCP/IP. Oczywiście te modele są tylko abstrakcją, która ma obrazować jak działa sieć. Dwoma najpopularniejszymi protokołami warstwy transportowejprotokół TCP i protokół UDP.

TCP (Transmission Control Protocol)

W TCP zaczynamy od połączenia z serwerem. Musimy z nim jawnie nawiązać połączenie. Tak więc wysyłamy do serwera żądanie połączenia i czekamy, aż serwer odpowie, wysyłając potwierdzenie (ACK) o połączeniu. W TCP mamy pewność, że pakiet dotrze do serwera, niekoniecznie szybko, ale dotrze. TCP/IP stosujemy w sytuacjach, gdy np. chcemy napisać komunikator. Tutaj nie jest ważna prędkość, pakiet dotrze i tak szybko, ale ważne jest to, żeby pakiet z wiadomością na pewno dotarł do adresata w odpowiedniej kolejności. Jeśli pakiet nie dotrze protokół samoczynnie dokonuje ponownego wysyłania, a jeśli mimo tego wszystkiego pakiet nie dojdzie -my się o tym dowiemy.

UDP (User datagram protocol)

Jest to protokół w warstwie transportowej, który charakteryzuję się prostotą i szybkością, jest zdecydowanie szybszy od TCP. Przede wszystkim dlatego, że nie otrzymujemy potwierdzenia ze strony serwera po wysłaniu pakietu. UDP nie nawiązuje połączenia z serwerem, tylko od razu wysyła pakiety. Wadą UDP jest to, że nie ma pewności, że pakiet dotrze do serwera. Tutaj pakiety wysyłane są od razu, bez buforowania. UDP stosujemy np. w grach komputerowych typu FPS. W tym momencie ważne jest to, żeby akcja działa się możliwie jak najszybciej, pakiety muszą być przesyłane z klienta do serwera tak szybko jak tylko to możliwe. Staramy się zawsze przesłać możliwie jak najmniejszą paczkę, po to by niepotrzebnie nie zajmować łącza. Jeżeli jakiś pakiet nie dojdzie to nie czekamy na niego.

Adres w sieci

Tutaj pierwszym skojarzeniem jest adres IP, który ma jednak dwie odmiany IPv4 i IPv6. Oczywiście proszę pamiętać, że nie jest to adres fizyczny komputera, fizyczny adres urządzenia to adres MAC. Na adres urządzenia składa się również port, który jest liczbą naturalną zakresu 2 bajtów, dzięki portom dane przychodzące na dane urządzenie się nie mieszają ze sobą i jest możliwe dostarczanie wielu usług sieciowych przez jedno urządzenie. Popularnymi portami są HTTP, HTTPS, protokół transferu plików FTP.
W temacie portów warto zwrócić uwagę, że nie powinniśmy sobie zarezerwować dowolnego portu, mamy następujący podział portów:
  • 0-1023 -porty ogólnie znane
  • 1024-49151 -porty zarejestrowane
  • 49152-65535(2do16-1) - dynamiczne/prywatne, które możemy swobodnie używać

Prosta komunikacja przez sieć -wstęp do praktyki

Adresacja serwera

Zaczniemy od implementacji klienta. Aby zaadresować musimy mieć adres IP i port. Na Linuxie tego typu informacje trzyma struktura sockaddr_in dla IPv4 lub sockaddr_in6 dla IPv6, my skupimy się na IPv4, dla której struktura adresu wygląda następująco:
C/C++
#include <netinet/in.h>

struct sockaddr_in
{
    short sin_family;
    unsigned short sin_port;
    struct in_addr sin_addr;
    char sin_zero[ 8 ]; // padding dla zachowania rozmiaru z struct sockaddr
};

struct sin_addr
{
    unsigned long s_addr;
}
Zaczynamy od wypełnienia struktury sockaddr_in (zalecanym jest przed każdym użyciem wyzerowanie struktury):
ArgumentOpis
sin_familyjest to rodzina adresów IP my zawsze podajemy AF_INET dla IPv4
sin_portport w reprezentacji sieciowej
sin_addradres IP w formie liczby
sin_zeronie będziemy wypełniać

Reprezentacja sieciowa liczb może być inna od reprezentacji liczb na naszym komputerze (patrz endianowość), dlatego musimy to zamienić, do tego celu mamy odpowiednią funkcję. Podobnie do zamiany adresu IP w wersji 4 rozumianego przez człowieka musimy zamienić go na liczbę rozumianą przez komputer, IPv4 ma 4 bajty. Zamieniamy przy użyciu funkcji:
C/C++
#include <arpa/inet.h>

int inet_pton( int af, const char * src, void * dst );

uint16_t htons( uint16_t hostshort );
Przykładowe wypełnienie struktury sockaddr_in:
C/C++
#include <netinet/in.h>
// ...
struct sockaddr_in address = { };
address.sin_family = AF_INET;
address.sin_port = htons( 50000 );
inet_pton( AF_INET, "127.0.0.1", & address.sin_addr );

Powyższe funkcje do zamiany mają funkcje odwrotne, które zamieniają ponownie na adres rozumiany przez człowieka na danej maszynie, ale do nich wrócimy przy okazji opisu serwera.

Tworzenie gniazda

Poza adresem do komunikacji potrzebujemy jeszcze gniazda, możemy utworzyć gniazdo różnych typów, my jednak będziemy tworzyć gniazda dla protokołów TCP i UDP.

Do utworzenia gniazda służy funkcja:
C/C++
#include <sys/types.h>
#include <sys/socket.h>

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

Opis argumentów:
ArgumentOpis
domainjest to domena komunikacji, możemy wybrać jak będziemy się komunikować, dla IPv4 będziemy używać AF_INET
typeoznacza semantykę komunikacji, będziemy używali SOCK_STREAM dla protokołu TCP lub SOCK_DGRAM dla komunikacji UDP
protocolokreśla szczegóły protokołu, np. możemy utworzyć gniazdo nieblokujące, będziemy używali najczęściej 0
Funkcja zwraca deskryptor gniazda, lub -1 w razie błędu. Oczywiście operując na funkcjach niskiego poziomu w C niestety błędy sprawdzać trzeba.

Zakończenie pracy z gniazdem

Po zakończeniu pracy z gniazdem należy pamiętać, żeby je zamknąć, służy do tego funkcja:
C/C++
#include <sys/socket.h>

int shutdown( int sockfd, int flag );

Argumenty:
ArgumentOpis
int sockfddeskryptor gniazda połączenia, które chcemy zamknąć
int flagsposób zamknięcia gniazda:
  • SHUT_RD - 0 (dalszy odbiór danych jest zabroniony)
  • SHUT_WR - 1 (dalszy zapis danych jest zabroniony)
  • SHUT_RDRW - 2 (dalszy odbiór i zapis danych jest zabroniony)

Wartość zwracana:
  • w przypadku powodzenia: 0
  • w przypadku błędu: -1

Przykład użycia:
C/C++
const int gniazdo =...;
shutdown( gniazdo, SHUT_RDWR );

Więcej informacji:
$ man socket

Protokuł TCP

Połączenie klienta z serwerem

Wiemy jak utworzyć gniazdo, wiemy jak podać adres serwera, a TCP jest protokołem połączeniowym, dlatego też potrzebna jest funkcja tworząca połączenie na gnieździe TCP:
C/C++
#include <sys/types.h>
#include <sys/socket.h>

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

Opis argumentów:
ArgumentOpis
sockfdgniazdo, utworzone przez funkcję socket()
addradres z którym chcemy się połączyć, czyli adres naszej struktury sockaddr_in, jest to funkcja ogólna która przyjmuje różne typy adresów, dlatego oczekuje wskaźnika na sockaddr
addrlenrozmiar struktury sockaddr_in
Funkcja w razie błędu połączania zwróci -1 i należy sprawdzać wartość zwracaną!

Funkcji connect() można użyć również na gnieździe UDP (tym bezpołączeniowym), to nie będzie błąd, ale wtedy zostanie ustawiony domyślny adres odbioru pakietów i domyślny adres wysyłania pakietów.

Więcej informacji:
$ man connect

Wysyłanie i odbieranie przez gniazdo TCP

Do wysyłania i odbierania przy pomocy protokołu TCP służą funkcję:
C/C++
#include <sys/types.h>
#include <sys/socket.h>

ssize_t send( int sockfd, const void * buf, size_t len, int flags );

ssize_t recv( int sockfd, void * buf, size_t len, int flags );

Argumenty:
ArgumentOpis
sockfddeskryptor gniazda, z którego chcemy odebrać dane
bufbufor, do którego zostaną zapisane/z którego zostaną odczytane dane
lenwielkość bufora
flagsflagi określające szczegółowe zachowanie, nam wystarczą ustawienia domyślne na 0

Wartość zwracana:
  • w przypadku powodzenia: ilość odebranych bajtów
  • gdy druga strona zamknęła połączenie: 0
  • w przypadku błędu -1

Przykład użycia:
C/C++
char bufor[ 256 ] = "...";
send( gniazdo, bufor, strlen( bufor ), 0 );

recv( gniazdo, bufor, sizeof( bufor ), 0 );

Zamiast send() i recv() możemy używać write() i read(), te drugie robią dokładnie to samo co te pierwsze z flags=0.

Więcej informacji:
$ man send
$ man recv

Przykładowy klient TCP

Mamy już wszystko do napisania przykładowego klienta TCP, poniżej zademonstruję przykładowego klienta HTTP, który pobierze stronę główną niniejszego serwisu (co należy wysłać aby pobrać stronę). Program jest prawie pozbawiony obsługi błędów, którą w faktycznym programie należałoby mieć. Celem zamiany adresu url rozumianego przez użytkownika na adres IP użyłem pewnej funkcji, którą opiszę później:

C/C++
#include <stdio.h>
#include <stdlib.h> // exit()
#include <string.h> // strlen()
#include <stdbool.h>

#include <sys/socket.h> // socket()
#include <netinet/in.h> // struct sockaddr_in
#include <arpa/inet.h> // inet_pton()
#include <netdb.h> // gethostbyname()


const short HTTP_PORT = 80;

bool entireWebsidedLoaded( const char * websitePart )
{
    return NULL != strstr( websitePart, "</html>" );
}

const char * getIpByName( const char * hostName )
{
    struct hostent * he = NULL;
   
    if(( he = gethostbyname( hostName ) ) == NULL )
    {
        herror( "gethostbyname" );
        exit( - 1 );
    }
   
    const char * ipAddress = inet_ntoa( **( struct in_addr ** ) he->h_addr_list );
    puts( ipAddress );
    return ipAddress;
}

int main( void )
{
    struct sockaddr_in serwer =
    {
        .sin_family = AF_INET,
        .sin_port = htons( HTTP_PORT )
    };
   
    const char * ipAddress = getIpByName( "cpp0x.pl" );
   
    inet_pton( serwer.sin_family, ipAddress, & serwer.sin_addr );
   
    const int s = socket( serwer.sin_family, SOCK_STREAM, 0 );
   
    connect( s,( struct sockaddr * ) & serwer, sizeof( serwer ) );
   
    char buffer[ 10000 ] = "GET / HTTP/1.1\nHOST: cpp0x.pl\n\n";
   
    send( s, buffer, strlen( buffer ), 0 );
   
    while( recv( s, buffer, sizeof( buffer ), 0 ) > 0 )
    {
        puts( buffer );
        if( entireWebsidedLoaded( buffer ) )
        {
            strcpy( buffer, "^]" ); /// exit character
            send( s, buffer, strlen( buffer ), 0 );
            break;
        }
    }
   
    shutdown( s, SHUT_RDWR );
   
    return 0;
}

Tworzenie serwera

Analogicznie jak wcześniej musimy utworzyć gniazdo oraz mieć nasz adres serwera (interesuje nas nie tyle adres IP, który jest localhostem, ale port), w tym celu ponownie używamy struktury sockaddr_in:
C/C++
const short myPort = 50000;

struct sockaddr_in serwer =
{
    .sin_family = AF_INET,
    .sin_port = htons( myPort )
};
server.sin_addr.s_addr = htons( INADDR_ANY );

Musimy również zarejestrować naszą usługę w systemie, w tym celu używamy funkcji:
C/C++
#include <sys/types.h>
#include <sys/socket.h>

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

Argumenty:
ArgumentOpis
int sockfddeskryptor gniazda, który chcemy powiązać ze strukturą
struct sockaddr *my_addrstruktura zawierająca adres IP oraz port
int addrlenwielkość struktury

Wartość zwracana:
  • w przypadku powodzenia: 0
  • w przypadku błędu: -1

Przykład użycia:
C/C++
bind( sockfd,( struct sockaddr * ) & server, sizeof( server ) );

Rejestrując usługę należy koniecznie sprawdzać wartość zwracaną przez funkcję bind(), gdyż usługa na danym porcie może już być zarejestrowana. Ponadto jest prawdopodobnym, że uruchamiając nasz program raz za razem system operacyjny nie zdąży odłączyć usługi, więc będziemy mieli port zablokowany przez poprzednie uruchomienie naszego programu.

Przyjmowanie połączeń TCP na serwerze

Po zarejestrowaniu usługi potrzebne jest jeszcze przejście w tryb nasłuchiwania, do tego służy funkcja:
C/C++
#include <sys/types.h>
#include <sys/socket.h>

int listen( int sockfd, int backlog );
Przełączenie gniazda w tryb nasłuchiwania na połączenia w przypadku protokołu TCP. W protokole UDP ten krok pomijamy.

Argumenty:
ArgumentOpis
sockfddeskryptor gniazda, na którym oczekujemy połączeń
backlogmaksymalna ilość równoczesnych połączeń oczekujących w kolejce do zaakceptowania

Wartość zwracana:
  • w przypadku powodzenia: 0
  • w przypadku błędu: -1

Przykład użycia:
C/C++
#define MAX_CONNECTION 10
listen( gniazdo, MAX_CONNECTION );

Wszystko gotowe, klienci mogą się łaczyć, do połączenia klientów musimy jeszcze zawołać funkcję:

C/C++
#include <sys/types.h>
#include <sys/socket.h>

int accept( int s, struct sockaddr * addr, socklen_t * addrlen );
Odbieranie połączeń z gniazda podobnie jak wyżej tylko w protokole TCP. Jest to funkcja domyślnie blokująca, która będzie oczekiwać aż połączenie nadejdzie.

Argumenty:
ArgumentOpis
sdeskryptor gniazda, na którym oczekujemy połączeń
addrwskaźnik do struktury sockaddr_in, w której zostaną zapisane informacje o odebranym połączeniu (adres IP i port)
addrlenadres pamięci, pod którą zapisany jest rozmiar struktury sockaddr_in, po wywołaniu funkcji będzie tutaj ustawiona faktyczna wielkość addr

Wartość zwracana:
  • w przypadku powodzenia: deskryptor gniazda odebranego połączenia
  • w przypadku błędu: -1

Przykład użycia:
C/C++
struct sockaddr_in server = { };
socklen_t len = sizeof( serwer );
struct sockaddr_in client = { };
int clientSocket = accept( socket_,( struct sockaddr * ) & client, & len );

Do komunikacji z połączonym klientem służy wyłącznie gniazdo zwrócone przez funkcję accept().
Dlatego po zakończonej obsłudze klienta gniazdo należy zamknąć.

Więcej informacji:
$ man 2 accept

Przykładowy serwer TCP

Zbierzmy wszystko do kupy, oto przykładowy serwer TCP z podstawową obsługą błędów:
C/C++
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAX_MSG_LEN 4096
#define SERWER_PORT 8888
#define SERWER_IP "127.0.0.1"
#define MAX_CONNECTION 10


int main()
{
    struct sockaddr_in serwer =
    {
        .sin_family = AF_INET,
        .sin_port = htons( SERWER_PORT )
    };
    if( inet_pton( AF_INET, SERWER_IP, & serwer.sin_addr ) <= 0 )
    {
        perror( "inet_pton() ERROR" );
        exit( 1 );
    }
   
    const int socket_ = socket( AF_INET, SOCK_STREAM, 0 );
    if( socket_ < 0 )
    {
        perror( "socket() ERROR" );
        exit( 2 );
    }
   
    socklen_t len = sizeof( serwer );
    if( bind( socket_,( struct sockaddr * ) & serwer, len ) < 0 )
    {
        perror( "bind() ERROR" );
        exit( 3 );
    }
   
    if( listen( socket_, MAX_CONNECTION ) < 0 )
    {
        perror( "listen() ERROR" );
        exit( 4 );
    }
   
   
    while( 1 )
    {
        printf( "Waiting for connection...\n" );
       
        struct sockaddr_in client = { };
       
        const int clientSocket = accept( socket_,( struct sockaddr * ) & client, & len );
        if( clientSocket < 0 )
        {
            perror( "accept() ERROR" );
            continue;
        }
       
        char buffer[ MAX_MSG_LEN ] = { };
       
        if( recv( clientSocket, buffer, sizeof( buffer ), 0 ) <= 0 )
        {
            perror( "recv() ERROR" );
            exit( 5 );
        }
        printf( "|Message from client|: %s \n", buffer );
       
        strcpy( buffer, "Message from server" );
        if( send( clientSocket, buffer, strlen( buffer ), 0 ) <= 0 )
        {
            perror( "send() ERROR" );
            exit( 6 );
        }
       
        shutdown( clientSocket, SHUT_RDWR );
    }
   
    shutdown( socket_, SHUT_RDWR );
}
//   gcc server.c -g -Wall -o server && ./server

Oto analogiczny klient TCP dla tego serwera:
C/C++
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define MAX_MSG_LEN 4096
#define SERWER_PORT 8888
#define SERWER_IP "127.0.0.1"
#define MAX_CONNECTION 10


int main()
{
    struct sockaddr_in serwer =
    {
        .sin_family = AF_INET,
        .sin_port = htons( SERWER_PORT )
    };
    if( inet_pton( AF_INET, SERWER_IP, & serwer.sin_addr ) <= 0 )
    {
        perror( "inet_pton() ERROR" );
        exit( 1 );
    }
   
    const int socket_ = socket( AF_INET, SOCK_STREAM, 0 );
    if( socket_ < 0 )
    {
        perror( "socket() ERROR" );
        exit( 2 );
    }
   
    socklen_t len = sizeof( serwer );
    if( bind( socket_,( struct sockaddr * ) & serwer, len ) < 0 )
    {
        perror( "bind() ERROR" );
        exit( 3 );
    }
   
    if( listen( socket_, MAX_CONNECTION ) < 0 )
    {
        perror( "listen() ERROR" );
        exit( 4 );
    }
   
   
    for( int i = 0; i < 3; ++i )
    {
        printf( "Waiting for connection...\n" );
       
        struct sockaddr_in client = { };
       
        const int clientSocket = accept( socket_,( struct sockaddr * ) & client, & len );
        if( clientSocket < 0 )
        {
            perror( "accept() ERROR" );
            continue;
        }
       
        char buffer[ MAX_MSG_LEN ] = { };
       
        if( recv( clientSocket, buffer, sizeof( buffer ), 0 ) <= 0 )
        {
            perror( "recv() ERROR" );
            exit( 5 );
        }
        printf( "|Message from client|: %s \n", buffer );
       
        strcpy( buffer, "Message from server" );
        if( send( clientSocket, buffer, strlen( buffer ), 0 ) <= 0 )
        {
            perror( "send() ERROR" );
            exit( 6 );
        }
       
        shutdown( clientSocket, SHUT_RDWR );
    }
   
    shutdown( socket_, SHUT_RDWR );
}
//   gcc server.c -g -Wall -o server && ./server

Gniazda nieblokujące

Do tej pory metody accept() oraz recv() blokowały program do czasu, aż nie otrzymały danych z gniazda. Istnieje jednak możliwość, aby gniazda połączeniowe (TCP) pracowały w trybie nieblokującym. Funkcje accept() oraz recv() zwrócą wartość -1 w przypadku braku danych, dlatego aby odróżnić faktyczny błąd od braku danych/połączeń musimy sprawdzić wartość zmiennej errno.
Aby utworzyć gniazda nieblokujące w protokole komunikacyjnym TCP będziemy potrzebowali zawołać pewną funkcję po utworzeniu gniazda:

C/C++
#include <sys/fcntl.h>

int fcntl( int fd, int cmd, struct flock * lock );
- funckja dokonuje jednej z wielu różnych operacji na deskryptorze (a gniazdo również jest deskryptorem). Dla naszych potrzeb funkcja ta będzie nam potrzebna do przełączenia gniazda w tryb nieblokujący.

Przykład użycia:
C/C++
if( fcntl( gniazdo, F_SETFL, O_NONBLOCK ) < 0 )
{
    perror( "fcntl() ERROR" );
    exit( - 1 );
}

Również zawartość pętli while() ulegnie minimalnej zmianie:
C/C++
#include <errno.h>
// ...
while( 1 )
{
    int new_sock = accept( listenfd,( struct sockaddr * ) & clientaddr, & le );
    if( new_sock < 0 )
    {
        if( errno == EWOULDBLOCK )
        {
            puts( "No connection yet" );
            sleep( 1 );
            continue;
        }
        else
        {
            perror( "accept() ERROR" );
            exit( - 1 );
        }
    }
    //...
    shutdown( gniazdo_clienta, SHUT_RDWR );
}
Jak widzimy w powyższym kodzie pętla odpala się raz za razem, a tym samym zajmuje procesor, dlatego przy gniazdach nieblokujacych warto dać pewien czas oczekiwania między iteracjami. W powyższym kodzie co pętlę śpimy 1 sekundę.

Więcej informacji:
$ man fcntl

Ponowne bindowanie

Uruchamiając nasz serwer ponownie tuż po jego poprawnym zakończeniu (wraz z pozamykaniem wszystkiego) czasami natrafiamy na problem z rejestrowaniem naszej usługi, na szczęście mamy możliwość wymusić ponowne bindowanie w następujący sposób:
C/C++
int opt = 1;
setsockopt( socket_, SOL_SOCKET, SO_REUSEADDR, & opt, sizeof( opt ) );

Problemy serwera TCP

Jak widzimy po podłączeniu się klienta kolejny klient musi czekać, rozwiązaniem tego problemu jest serwer wielowątkowy lub wieloprocesowy, ale pamiętajmy, że ilość procesów i ilość gniazd, nie wspominając o innych zasobach, jest ograniczona. Warto też uwzględnić klientów, którzy się podłączyli, ale zawiesiła się u nich aplikacja, więc warto im podziękować. Przykład takiego serwera byłby zbyt obszerny, aby go tutaj wrzucać, zamiast tego zademonstruję jak można na jednym wątku zasymulować serwer wielowątkowy.

Serwer TCP pseudo-wielowątkowy - funkcja select

Funkcja select() wybiera zmieniony ze zbioru deskryptorów:
C/C++
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select( int numfds, fd_set * readfds, fd_set * writefds, fd_set * exceptfds, struct timeval * timeout );
Poza samą funkcją należy uwzględnić struktury i makra pomocnicze:
C/C++
struct timeval
{
    int tv_sec; // ilosc sekund
    int tv_usec; // ilosc mikrosekund
};

FD_SET( int fd, fd_set * set ); // dodanie deskryptora fd do zbioru set
FD_CLR( int fd, fd_set * set ); // usuniecie deskrytpora fd ze zbioru set
FD_ISSET( int fd, fd_set * set ); // sprawdzenie czy deskrytpor fd nalezy do zbioru set
FD_ZERO( fd_set * set ); // usuniecie wszystkich deskryptorw ze zbioru set

Argumenty funkcji select():
ArgumentOpis
numfdsnajwyższa wartość deskryptora ze wszystkich+1, na których nasłuchujemy
readfdszbiór deskryptorów odczytu, jeśli któryś będzie miał dane do odczytu (przy próbie odczytu będzie nieblokujący) to funkcja zostawi go jako argument readfds. Podany argument może być NULL
writefdsjw., ale do zapisu
exceptfdsjw., ale deskryptory wyjątku
timeoutczas oczekiwania na połączenia, jeśli zerowy to funkcja zwraca momentalnie, jeśli NULL będzie czekać w sposób niezdefiniowany

Wartość zwracana:
  • w przypadku powodzenia: suma ilości zmienionych deskryptorów podczas trwania czasu zwłoki, ewentualnie 0, jeśli żaden nie został zmieniony
  • w przypadku błędu: -1

Przykład użycia, w którym demonstrujemy oczekiwanie na podanie tekstu od użytkownika na standardowe wejście (które też jest deskryptorem):
C/C++
#include <stdio.h>
#include <sys/select.h>

int main( void )
{
    const int stdinWalue = 0;
   
    while( 1 )
    {
        fd_set readfds;
       
        FD_ZERO( & readfds );
        FD_SET( stdinWalue, & readfds );
       
        struct timeval tv = { 2 };
       
        switch( select( stdinWalue + 1, & readfds, NULL, NULL, & tv ) )
        {
        case - 1:
            perror( "select" );
            return 1;
        case 0:
            puts( "Nic nie wpisano na standardowe wejscie" );
            break;
            default:
            if( FD_ISSET( stdinWalue, & readfds ) )
            {
                char buffer[ 100 ] = { };
                scanf( "%100s", buffer );
                printf( "Podano tekst \"%s\"!\n", buffer );
            }
            else
                 printf( "Zmieniono inny deskryptor\n" );
           
            break;
        }
    }
   
    return 0;
}
W powyższym kodzie w każdym obiegu pętli musimy na nowo dodać deskryptory do zbioru oraz ustawić czas oczekiwania, gdyż funkcja select() modyfikuje te wartości. W powyższym kodzie mamy 1 deskryptor, ale jeśli mielibyśmy ich więcej trzeba by iterować po każdym z nich i sprawdzać czy jest ustawiony:
C/C++
for( int ds = 0; ds < dsMax + 1; ++ds
{
    if( FD_ISSET( ds, & readfds ) )
    {
        // obsluga danego deskryptora
    }
}

Więcej informacji:
$ man select


Funkcja select() jest starą, POSIXową funkcją, prostą w użyciu. Niemniej jednak dla poważniejszych zastosowań istnieją lepsza funkcje oferujące tę funkcjonalność ale wydajniej, niestety funkcje te nie są przenośne między systemami. Są to m.in. linuxowa epoll() lub na FreeBSD kevent().

Odczytywanie informacji o podłączonym kliencie

C/C++
#include <arpa/inet.h>

const char * inet_ntop( int af, const void * src, char * dst, socklen_t size );
- konwersja adresu IP z reprezentacji sieciowej do tablicy znaków

Argumenty:
ArgumentOpis
int afrodzina adresów AF_INET (IPv4), AF_INET6 (IPv6)
const void *srcadres IP zródła
char *dstmiejsce przeznaczenia adresu IP
socklen_t sizerozmiar socketa

Przykład użycia:
C/C++
struct sockaddr_in from;
char bufor_ip[ 128 ];
inet_ntop( AF_INET, & from.sin_addr, bufor_ip, sizeof( bufor_ip ) );

C/C++
uint16_t ntohs( uint16_t netshort );
- konwersja portu z reprezentacji sieciowej do reprezentacji na danej maszynie

C/C++
struct hostent * gethostbyname( const char * nazwa );
- konwersja nazwy hosta do adresu IP (tylko IPv4)

Argumenty:
ArgumentOpis
nazwanazwa hosta do konwersji na adres IP

Wartość zwracana:
  • w przypadku powodzenia - wskaźnik do struktury zawierającej dane hosta
  • w przypadku błędu - wartość NULL

C/C++
struct hostent * gethostbyaddr( const char * adres, int dlug, int typ );
- konwersja adresu IP (tylko IPv4) na nazwę hosta

Argumenty:
ArgumentOpis
adresadres IP w reprezentacji sieciowej (inet_pton)
dlugwielkość adresu
typtyp adresu (AF_INET - IPv4, AF_INET6 - IPv6)

Wartość zwracana:
  • w przypadku powodzenia - wskaźnik do struktury zawierającej dane hosta
  • w przypadku błędu - wartość NULL

Nasza struktura hostent wygląda następująco:
C/C++
struct hostent
{
    char * h_name; // oficjalna nazwa hosta
    char ** h_aliases; // lista aliasów
    int h_addrtype; // typ adresu
    int h_length; // długość adresu
    char ** h_addr_list; // lista adresów
}

Przykład użycia:
C/C++
struct in_addr ipv4addr;
char buffer[ 100 ] = { };
inet_pton( AF_INET, buffer, & ipv4addr );
struct hostent * he = gethostbyaddr( & ipv4addr, sizeof ipv4addr, AF_INET );

Należy jednak pamiętać, że funkcje gethostbyaddr() oraz gethostbyname() są funkcjami przestarzałymi, ponieważ nie wspierają one rodziny adresów IP_v6. Nowe funkcje jakie należy stosować to:
  • getaddrinfo()
  • getnameinfo()

Przykłady użycia funkcji pobierających informacje o hoście

Pobranie adresu IP w oparciu o URL:
C/C++
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main()
{
    const char * url = "cpp0x.pl";
   
    struct hostent * he = gethostbyname( url );
   
    if( !he )
    {
        herror( "gethostbyname" );
        return - 1;
    }
   
    printf( "Official host name is: %s\n", he->h_name );
   
    printf( "IP addresses: " );
    struct in_addr ** addr_list =( struct in_addr ** ) he->h_addr_list;
    for( int i = 0; addr_list[ i ] != NULL; i++ )
    {
        printf( "%s\n", inet_ntoa( * addr_list[ i ] ) );
    }
   
    return 0;
}

Pobranie adresu URL w oparciu o IP:
C/C++
#include <stdio.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>

int main()
{
    struct in_addr ipv4addr;
   
    const char * ip = "62.129.232.55"; // adres cpp0x.pl na dzien pisania artykulu
   
    if( inet_pton( AF_INET, ip, & ipv4addr ) < 1 )
    {
        perror( "Wrong address:" );
        return - 1;
    }
   
    struct hostent * he = gethostbyaddr( & ipv4addr, sizeof ipv4addr, AF_INET );
    if( !he )
    {
        perror( "gethostbyaddr ERROR:" );
        return - 1;
    }
   
    printf( "Host name: %s\n", he->h_name );
    printf( "\n" );
   
    return 0;
}

Więcej informacji:
$ man gethostbyname
$ man gethostbyaddr
$ man getaddrinfo
$ man getnameinfo

Na zakończenie tematu gniazd TCP warto wspomnieć o jednej ciekawostce: protokół do wysyłania maili smtp używa protokołu TCP. Więc przy użyciu niektórych serwerów pocztowych można wysłać takiego maila, po zalogowaniu lub nawet bez zalogowania. Oczywiście do logowania i wysyłania wiadomości z ogonmaki potrzebujemy zakodować w Base64.

Protokół UDP

Nadszedł czas na przejście do protokołu UDP. Żeby wysłać bądź odebrać dane w UDP używamy metod sendto() oraz recvfrom(). Są to funkcje domyślnie NIE-blokujące:

Opis funkcji protokołu UDP

Do wysyłania przez gniazdo UDP, jak i odbierania przez nie służą funkcje:
C/C++
#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto( int sockfd, const void * buf, size_t len, int flags, const struct sockaddr * dest_addr, socklen_t addrlen );

ssize_t recvfrom( int sockfd, void * buf, size_t len, int flags, struct sockaddr * src_addr, socklen_t * addrlen );

Powyższych funkcji można użyć również do wysyłania/odbierania przez gniazdo TCP.

Argumenty funkcji:
ArgumentOpis
sockfddeskryptor gniazda, do którego chcemy wysłać/z którego chcemy odebrać
bufbufor z danymi, które będą wysłane/odebrane
lenrozmiar bufora
flagsspecjalne zachowanie, my będziemy ustawiać na 0
dest_addrwskaźnik do struktury, w której znajdują się informacje o przeznaczeniu    pakietu (adres IP oraz port), lub gdzie zostaną wpisane informacja o źródle pakietu
addrlenwielkość struktury sockaddr addrlen

Wartość zwracana:
  • w przypadku powodzenia: ilość wysłanych/odebranych bajtów
  • w przypadku błędu: -1

Przykładowy klient i serwer UDP

Teraz chciałbym przedstawić gotowe programy client-a oraz server-a, gdzie będziemy wykorzystywać przedstawione wyżej funkcje.

Klient UDP

C/C++
#include <stdio.h>
#include <stdlib.h> // exit()
#include <string.h> // memset()
#include <arpa/inet.h> // inet_pton()
#include <sys/socket.h>

#define SERWER_PORT 8888
#define SERWER_IP "127.0.0.1"


int main()
{
    struct sockaddr_in serwer =
    {
        .sin_family = AF_INET,
        .sin_port = htons( SERWER_PORT )
    };
    if( inet_pton( AF_INET, SERWER_IP, & serwer.sin_addr ) <= 0 )
    {
        perror( "inet_pton() ERROR" );
        exit( 1 );
    }
   
    const int socket_ = socket( AF_INET, SOCK_DGRAM, 0 );
    if( socket_ < 0 )
    {
        perror( "socket() ERROR" );
        exit( 1 );
    }
   
    char buffer[ 4096 ] = "Message from client";
    printf( "|Message for server|: %s \n", buffer );
   
    socklen_t len = sizeof( serwer );
   
    if( sendto( socket_, buffer, strlen( buffer ), 0,( struct sockaddr * ) & serwer, len ) < 0 )
    {
        perror( "sendto() ERROR" );
        exit( 1 );
    }
   
    struct sockaddr_in from = { };
   
    memset( buffer, 0, sizeof( buffer ) );
    if( recvfrom( socket_, buffer, sizeof( buffer ), 0,( struct sockaddr * ) & from, & len ) < 0 )
    {
        perror( "recvfrom() ERROR" );
        exit( 1 );
    }
    printf( "|Server's reply|: %s \n", buffer );
   
    shutdown( socket_, SHUT_RDWR );
}
// gcc client.c -g -Wall -o client && ./client

Serwer UDP

C/C++
#include <stdio.h>
#include <stdlib.h> // exit()
#include <string.h> // memset()
#include <arpa/inet.h> // inet_pton()
#include <sys/socket.h>

#define SERWER_PORT 8888
#define SERWER_IP "127.0.0.1"


int main()
{
    struct sockaddr_in serwer =
    {
        .sin_family = AF_INET,
        .sin_port = htons( SERWER_PORT )
    };
    if( inet_pton( AF_INET, SERWER_IP, & serwer.sin_addr ) <= 0 )
    {
        perror( "inet_pton() ERROR" );
        exit( 1 );
    }
   
    const int socket_ = socket( AF_INET, SOCK_DGRAM, 0 );
    if(( socket_ ) < 0 )
    {
        perror( "socket() ERROR" );
        exit( 2 );
    }
   
    char buffer[ 4096 ] = { };
   
    socklen_t len = sizeof( serwer );
    if( bind( socket_,( struct sockaddr * ) & serwer, len ) < 0 )
    {
        perror( "bind() ERROR" );
        exit( 3 );
    }
   
    while( 1 )
    {
        struct sockaddr_in client = { };
       
        memset( buffer, 0, sizeof( buffer ) );
       
        printf( "Waiting for connection...\n" );
        if( recvfrom( socket_, buffer, sizeof( buffer ), 0,( struct sockaddr * ) & client, & len ) < 0 )
        {
            perror( "recvfrom() ERROR" );
            exit( 4 );
        }
        printf( "|Message from client|: %s \n", buffer );
       
        char buffer_ip[ 128 ] = { };
        printf( "|Client ip: %s port: %d|\n", inet_ntop( AF_INET, & client.sin_addr, buffer_ip, sizeof( buffer_ip ) ), ntohs( client.sin_port ) );
       
        strncpy( buffer, "Message for client", sizeof( buffer ) );
       
        if( sendto( socket_, buffer, strlen( buffer ), 0,( struct sockaddr * ) & client, len ) < 0 )
        {
            perror( "sendto() ERROR" );
            exit( 5 );
        }
    }
   
    shutdown( socket_, SHUT_RDWR );
}
// gcc server.c -g -Wall -o server && ./server

Podsumowanie

Powyższe implementacje są tylko przykładowe, najprostsze. Wszystkie programy można napisać na wiele różnych sposób stosując znacznie lepsze zabezpieczenia.
Na zakończenie artykułu warto podać jeszcze jedną ciekawostkę -mimo iż protokół TCP przy pewnych zastosowaniach wydaje się lepszy od UDP, to i tak są dostarczone biblioteki, które przy pomocy protokołu UDP dostarczają gwarancje protokołu TCP, bibilioteki takie to: RakNet (uchodząca za najlepszą) lub ENet (prosta implementacja tego mechanizmu).

Bibliografia

Historia zmian

WersjaAutorZakres zmianAfiliacja
1.1Grzegorz BaziorSpora poprawa jakościowa treści artykułu.Doktorant AGH w Krakowie
Pracownik Politechniki Krakowskiej w Krakowie
Absolwent informatyki na Uniwersytecie Jagiellońskim
1.0Tomasz GruntowskiUtworzenie pierwszej wersji artykułu.