Gniazda BSD

Gniazda BSD (BSD sockets): interfejs pomiędzy programami użytkowników a implementacją stosu TCP/IP.

API gniazd dostarcza funkcji bibliotecznych:

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:

  1. Stwórz gniazdo (socket)
  2. (Opcjonalnie: zwiąż gniazdo z adresem/portem klienta – bind)
  3. Odbieraj/wysyłaj dane do serwera (recvfrom/sendto)
  4. Zamknij gniazdo do komunikacji z serwerem (close)

Serwer:

  1. Stwórz gniazdo (socket)
  2. Zwiąż gniazdo z adresem/portem serwera (bind)
  3. Odbieraj/wysyłaj dane od klientów (recvfrom/sendto)
  4. Zamknij gniazdo do komunikacji z klientami (close)

Protokół TCP

Klient:

  1. Stwórz gniazdo (socket)
  2. Zwiąż gniazdo z adresem/portem serwera zdalnego (connect)
  3. Nawiąż połączenie z serwerem (connect)
  4. Odbieraj/wysyłaj dane do serwera (read/write)
  5. Zamknij gniazdo do komunikacji z serwerem (close)

Serwer:

  1. Stwórz gniazdo (socket)

  2. Zwiąż gniazdo z adresem/portem serwera (bind)

  3. Skonfiguruj kolejkę żądań gniazda (listen)

  4. Obsługuj żądania od klientów (pętla):

    1. Czekaj na połączenie od klienta (accept)
    2. Stwórz gniazdo do komunikacji z konkretnym klientem (accept)
    3. Odbieraj/wysyłaj dane do klienta (read/write)
    4. 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: rozmiar addr,
    • zwraca 0 lub -1 w przypadku błędu.

    W domenie protokołów internetowych używa się sockaddr_in zamiast sockaddr 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: rozmiar addr,
    • 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: rozmiar addr,
    • 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: rozmiar addr,
    • 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'

Socat tutorial

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).

  1. 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).

  1. 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));