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 transportowej są
protokół 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:
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:
#include <netinet/in.h>
struct sockaddr_in
{
short sin_family;
unsigned short sin_port;
struct in_addr sin_addr;
char sin_zero[ 8 ];
};
struct sin_addr
{
unsigned long s_addr;
}
Zaczynamy od wypełnienia struktury sockaddr_in (zalecanym jest przed każdym użyciem wyzerowanie struktury):
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:
#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:
#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:
#include <sys/types.h>
#include <sys/socket.h>
int socket( int domain, int type, int protocol );
Opis argumentów:
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:
#include <sys/socket.h>
int shutdown( int sockfd, int flag );
Argumenty:
Wartość zwracana:
Przykład użycia:
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:
#include <sys/types.h>
#include <sys/socket.h>
int connect( int sockfd, const struct sockaddr * addr, socklen_t addrlen );
Opis argumentów:
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ę:
#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:
Wartość zwracana:
Przykład użycia:
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:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
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, "^]" );
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:
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:
#include <sys/types.h>
#include <sys/socket.h>
int bind( int sockfd, struct sockaddr * my_addr, int addrlen );
Argumenty:
Wartość zwracana:
Przykład użycia:
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:
#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:
Wartość zwracana:
Przykład użycia:
#define MAX_CONNECTION 10
listen( gniazdo, MAX_CONNECTION );
Wszystko gotowe, klienci mogą się łaczyć, do połączenia klientów musimy jeszcze zawołać funkcję:
#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:
Wartość zwracana:
Przykład użycia:
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:
#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 );
}
Oto analogiczny klient TCP dla tego serwera:
#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 );
}
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:
#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:
if( fcntl( gniazdo, F_SETFL, O_NONBLOCK ) < 0 )
{
perror( "fcntl() ERROR" );
exit( - 1 );
}
Również zawartość pętli while() ulegnie minimalnej zmianie:
#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:
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:
#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:
struct timeval
{
int tv_sec;
int tv_usec;
};
FD_SET( int fd, fd_set * set );
FD_CLR( int fd, fd_set * set );
FD_ISSET( int fd, fd_set * set );
FD_ZERO( fd_set * set );
Argumenty funkcji select():
Wartość zwracana:
Przykład użycia, w którym demonstrujemy oczekiwanie na podanie tekstu od użytkownika na standardowe wejście (które też jest deskryptorem):
#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:
for( int ds = 0; ds < dsMax + 1; ++ds
{
if( FD_ISSET( ds, & readfds ) )
{
}
}
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
#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:
Przykład użycia:
struct sockaddr_in from;
char bufor_ip[ 128 ];
inet_ntop( AF_INET, & from.sin_addr, bufor_ip, sizeof( bufor_ip ) );
uint16_t ntohs( uint16_t netshort );
- konwersja portu z reprezentacji sieciowej do reprezentacji na danej maszynie
struct hostent * gethostbyname( const char * nazwa );
- konwersja nazwy hosta do adresu IP (tylko IPv4)
Argumenty:
Wartość zwracana:
struct hostent * gethostbyaddr( const char * adres, int dlug, int typ );
- konwersja adresu IP (tylko IPv4) na nazwę hosta
Argumenty:
Wartość zwracana:
Nasza struktura hostent wygląda następująco:
struct hostent
{
char * h_name;
char ** h_aliases;
int h_addrtype;
int h_length;
char ** h_addr_list;
}
Przykład użycia:
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:
Przykłady użycia funkcji pobierających informacje o hoście
Pobranie adresu IP w oparciu o URL:
#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:
#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";
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:
#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:
Wartość zwracana:
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#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 );
}
Serwer UDP
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#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 );
}
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