Gniazda BSD¶
Gniazda BSD (BSD sockets): interfejs pomiędzy programami użytkowników a implementacją stosu TCP/IP.
API gniazd dostarcza funkcji bibliotecznych:
- funkcje ukrywają przed programistą warstwę transportową,
- dają możliwość prowadzenia komunikacji między węzłami sieci i/lub lokalnymi procesami.
Gniazdo jest plikiem specjalnym systemu operacyjnego (UNIX, GNU/Linux) tworzonym na życzenie programisty i identyfikowalnym przez deskryptor. Gniazdo obsługiwane jest przez programistę analogicznie do plików (everything is a file).
Gniazda BSD są identyfikowalne przez trójkę: <adres, nr
portu, tryb komunikacji>
.
Tryb komunikacji¶
Tryby komunikacji gniazd BSD:
- połączeniowy – protokół TCP (RFC 793),
- bezpołączeniowy – protokół UDP (RFC 768).
Model systemu¶
Komunikacja przez gniazda:
- jeden z procesów (serwer) oczekuje na zgłoszenia używając gniazda,
- inny proces (klient) nawiązuje połączenie z gniazdem serwera.
Klient musi mieć informacje o adresie, porcie i protokole na którym serwer nasłuchuje. Serwer nie musi mieć informacji o kliencie – dostaje informacje po nawiązaniu komunikacji.
Typy serwerów:
- serwer iteracyjny,
- serwer współbieżny.
Przypadki użycia gniazd BSD¶
Protokół UDP¶
Klient:
- Stwórz gniazdo (
socket
)- (Opcjonalnie: zwiąż gniazdo z adresem/portem klienta –
bind
)- Odbieraj/wysyłaj dane do serwera (
recvfrom
/sendto
)- Zamknij gniazdo do komunikacji z serwerem (
close
)
Serwer:
- Stwórz gniazdo (
socket
)- Zwiąż gniazdo z adresem/portem serwera (
bind
)- Odbieraj/wysyłaj dane od klientów (
recvfrom
/sendto
)- Zamknij gniazdo do komunikacji z klientami (
close
)
Protokół TCP¶
Klient:
- Stwórz gniazdo (
socket
)- Zwiąż gniazdo z adresem/portem serwera zdalnego (
connect
)- Nawiąż połączenie z serwerem (
connect
)- Odbieraj/wysyłaj dane do serwera (
read
/write
)- Zamknij gniazdo do komunikacji z serwerem (
close
)
Serwer:
Stwórz gniazdo (
socket
)Zwiąż gniazdo z adresem/portem serwera (
bind
)Skonfiguruj kolejkę żądań gniazda (
listen
)Obsługuj żądania od klientów (pętla):
- Czekaj na połączenie od klienta (
accept
)- Stwórz gniazdo do komunikacji z konkretnym klientem (
accept
)- Odbieraj/wysyłaj dane do klienta (
read
/write
)- Zamknij gniazdo do komunikacji z konkretnym klientem (
close
)
API gniazd BSD¶
Nagłówki¶
- gniazda
<sys/types.h>
,<sys/socket.h>
, - pozostałe funkcje
<netinet/in.h>
,<arpa/inet.h>
,<netdb.h>
.
Funkcje¶
Tworzenie gniazda:
int socket(int domain, int type, int protocol);
int domain
: domena komunikacji,AF_INET
(IPv4),AF_INET6
(IPv6), etc.int type
: semantyka komunikacji,SOCK_STREAM
(TCP),SOCK_DGRAM
(UDP), etc.int protocol
: wybór protokołu z dostępnych opcji (zazwyczaj 0).- zwraca deskryptor lub
-1
w wypadku błędu (nr błędu:errno
)
Związanie gniazda:
int bind(int sockfd, struct sockaddr *addr, socklen_t addrlen);
int sockfd
: deskryptor,struct sockaddr *addr
: adres gniazda,socklen_t addrlen
: rozmiaraddr
,- zwraca
0
lub-1
w przypadku błędu.
W domenie protokołów internetowych używa się
sockaddr_in
zamiastsockaddr
wykonując rzutowanie(struct sockaddr*)
:struct sockaddr_in { u_short sin_family; // AF_INET u_short sin_port; // numer portu: 0 losuje struct in_addr sin_addr; // adres węzła char sin_zero[8] } struct in_addr { u_long s_addr; // 32 bitowy adres }
Przygotowanie gniazda na obsługę połączeń i konfiguracja kolejki żądań (serwer):
int listen(int sockfd, int backlog);
int sockfd
: deskryptor,int backlog
: rozmiar kolejki żądań,- zwraca:
0
lub-1
w przypadku błędu.
Oczekiwanie i obsługa zgłoszenia (serwer):
int accept(int sockfd, struct sockaddr *addr, socklen_t * addrlen);
int sockfd
: deskryptor,struct sockaddr *addr
: adres gniazda,socklen_t addrlen
: rozmiaraddr
,- zwraca deskryptor gniazda do komunikacji z klientem lub
-1
w przypadku błędu.
Zwiażanie gniazda z serwerem (zdalnym) i nawiązanie połączenia (klient)
int connect(int sockfd, const struct sockaddr *srvaddr, socklen_t addrlen);
int sockfd
: deskryptor,struct sockaddr *addr
: adres gniazda,socklen_t addrlen
: rozmiaraddr
,- zwraca deskryptor gniazda do komunikacji z serwerem lub
-1
w przypadku błędu.
Odczyt/zapis (protokół połączeniowy):
int read(int fd, void *buf, size_t count); int write(int fd, void *buf, size_t count);
int fd
: deskryptor,void *buf
: bufor,size_t count
: rozmiar bufora.
Odczyt/zapis (protokół bezpołączeniowy):
sendto(int fd, void *buf, size_t count, int flags, const struct sockadds *dest_addr, socklen_t addrlen); recvfrom(int fd, void *buf, size_t count, int flags, const struct sockadds *dest_addr, socklen_t addrlen);
int fd
: deskryptor,void *buf
: bufor,size_t count
: rozmiar bufora,int flags
: flagi (np.MSG_DONTROUTE
,MSG_DONTWAIT
),struct sockaddr *addr
: adres gniazda,socklen_t addrlen
: rozmiaraddr
,- zwraca deskryptor gniazda do komunikacji z serwerem lub
-1
w przypadku błędu.
Zamknięcie połączenia/gniazda:
int close(int sockfd);
int sockfd
: deskryptor.
Funkcje pomocnicze¶
Dla protokołów TCP/IP przyjęto standard określający sieciowy porządek bajtów. Niektóre parametry funkcji interfejsu gniazd należy podawać zgodnie ze standardem. Funkcje do konwersji dla liczb 16 i 32 bitowych.
// host to network, long uint32_t htonl(uint32_t hostlong); // host to network, short uint16_t htons(uint16_t hostshort); // network to host, long uint32_t ntohl(uint32_t netlong); // network to host, short uint16_t ntohs(uint16_t netshort);
Zamiana adresu IP zapisanego w formacie dziesiętnym (XXX.YYY.ZZZ.VVV) jako string na jego reprezentację sieciową:
in_addr_t inet_addr(const char *cp:); int inet_aton(const char *cp, struct in_addr *inp);
Zmiana adresu IP na nazwę domenową i vice versa:
struct hostent *gethostbyaddr(const char *addr, int len, int type); struct hostent *gethostbyname(const char *name);
Adres zdalnego komputera (klienta) połączonego do gniazda:
int getpeername(int sockfd, struct sockaddr *name, socklen_t *namelen);
Informacje o protokole/usłudze na podstawie nazwy, korzystając z
/etc/protocols/
lub/etc/services
(odpowiednio).struct protent *getprotobyname(const char *name); struct servent *getservbyname(const char *name);
Inicjalizacja struktur¶
int my_fd = socket(PF_INET, SOCK_STREAM, 0);
struct sockaddr_in my_sockaddr;
my_sockaddr.sin_family = AF_INET;
my_sockaddr.sin_addr.s_addr = INADDR_ANY; // bind to all available interfaces
my_sockaddr.sin_port = htons(9001);
int result = bind (my_fd, (struct sockaddr *) &my_sockaddr, sizeof (my_sockaddr));
my_sockaddr.sin_addr.s_addr = inet_addr("150.254.130.150");
int result = inet_aton("150.254.130.150", &my_sockaddr.sin_addr);
Socat¶
# client
socat - udp:localhost:9001
socat - tcp:localhost:9001
# echo server
socat udp-listen:9001 pipe
socat tcp-listen:9001 pipe
# reply server
while 1
do
socat udp-listen:9001 system:'read msg; echo pong'
socat tcp-listen:9001 system:'real msg; echo pong'
done
# one-line reply server
socat tcp-listen:9001,fork,reuseaddr system:'read msg; echo pong'
Zadania¶
1. Napisz program-klient który wysyła na wskazany serwer napis i wypisuje na ekran odpowiedź serwera. Przesyłana wiadomość, adres serwera oraz port usługi na serwerze są podawane przez argumenty/parametry programu. Klient i serwer używają protokołu bezpołączeniowego (UDP).
- J/w, TCP.
3. Napisz program-serwer iteracyjny echo
który odczytuje wiadomości
wysyłane przez klientów. Adres serwera oraz port usługi są podawane przez
argumenty/parametry programu. Serwer używa protokołu bezpołączeniowego (UDP).
- J/w, TCP.
5. Napisz system złożony z klienta i serwera iteracyjnego maze
. Serwer
dysponuje mapą o wymiarach 64x64 jednostek, która zawiera 4 rodzaje pól: 1 pole
startowe, 1 pole końcowe, i dowolne liczby pól pustych i ścian. Mapa jest
wczytywana z pliku określonego przez argument/parametr serwera. Klient
nawiązuje połączenie z serwerem. Początkowo awatar klienta znajduje się na polu
startowym, jego celem jest przesunąć go na pole docelowe. Klient może wysłać do
serwera wiadomości up
, down
, left
, right
w celu przesunięcia
awatara w określonym kierunku. Serwer przesuwa awatara, jeśli jest to możliwe i
w odpowiedzi odsyła klientowi informacje o tym w jakich kierunkach awatar może
być przesunięty z nowej lokalizacji. Jeśli awatar klienta znajuje się na polu
końcowym, serwer informuje o tym klienta i kończy połączenie. Porty i adresy są
podawane przez argumenty/parametry programów.
6. Napisz system złożony z klienta i serwera iteracyjnego, gdzie klient wysyła do serwera informacje osobowe, a serwer dopisuje je na koniec pliku \(D\) w formacie YAML. Każdy rekord danych osobowych zawiera: imię, nazwisko, wiek, i adres zamieszkania. Opcjonalnie każdy rekord może zawierać także nr telefonu i adres email. Porty, adresy, oraz ścieżka do pliku \(D\) są podawane przez argumenty/parametry programów.
7. W serwerze TCP z zadania 4 zmień rozmiar bufora nadawczego jak poniżej i przestaw gniazdo w tryb nieblokujący. Zaobserwuj działanie serwera przy wysłaniu wiadomości o rozmiarze większym niż 4096 bajtów.
int buffsize = 1024;
setsockopt(socket, SOL_SOCKET, SO_SNDBUF, &buffsize, sizeof(buffsize));
Linki¶
- Skrypt SK2: Wprowadzenie do gniazd BSD
- Przykłady