Spis treści

Klient TCP (przypomnienie)

Do pracy programu w roli klienta TCP trzeba w kolejności użyć funkcji:

  1. socket do stworzenia gniazda, podając typ SOCK_STREAM i protokół IPPROTO_TCP lub 0
  2. connect żeby połączyć utworzone gniazdo pod adres wskazany w argumentach
  3. po połączeniu można wysyłać i odbierać dane, np. funkcjami write, send i read, recv
  4. shutdown żeby zakończyć połączenie na tym gnieździe
  5. close żeby zwolnić zasoby systemowe związane z gniazdem

Zwróć uwagę że socket tworzy gniazdo protokołu TCP, a connect wybiera żeby to gniazdo pełniło rolę klienta.

Serwer TCP

Wstęp

Aby oczekiwać na przychodzące połączenia TCP konieczne jest stworzenie gniazda do odbierania nowych połączeń.
Takie gniazdo, tzw. gniazdo nasłuchujące (listening), nie umożliwia odbierania ani wysyłania danych – pozwala tylko na odbieranie przychodzących połączeń i tworzy dla każdego nowego połączenia kolejne gniazdo reprezentujące to odebrane połączenie.

Do przygotowania nasłuchującego gniazda należy najpierw stworzyć gniazdo funkcją socket, a następnie wywołać na tym gnieździe funkcję listen, ale: jeśli wcześniej nie ustawi się adresu gniazda, system operacyjny wylosuje efemeryczny port i rozpocznie nasłuch na dowolnym adresie IP na tym porcie. Dlatego przed wywołaniem funkcji listen(…) należy funkcją bind ustawić lokalny adres gniazda. Funkcja bind przyjmuje jako argument wskaźnik na strukturę sockaddr, który musi wskazywać na strukturę opisująca adres z rodziny obsługiwanej przez gniazdo.

Lokalny adres IP nasłuchującego gniazda zwykle ustawia się na dowolny, czyli dla IPv4 adres 0.0.0.0, reprezentowany przez stałą INADDR_ANY.
Inny lokalny adres IP ustawia się, jeśli gniazdo ma odbierać połączenia kierowane tylko na ten właśnie adres – np. 127.0.0.1 (INADDR_LOOPBACK) jeśli połączenia mają być ograniczone do połączeń z tego komputera.

Wywołanie funkcji listen nakazuje systemowi operacyjnemu czekać na połączenia (wybiera że gniazdo ma pełnić rolę serwera). Wykonanie funkcji listen jest natychmiastowe – ta funkcja nie czeka na połączenie, tylko informuje system operacyjny że nowe połączenia przychodzące na ustawiony adres mają być kierowane na to gniazdo.
Argumentem funkcji listen jest ilość nowych połączeń które czekają w kolejce na odebranie (tj. połączeń dla których nie wykonano jeszcze funkcji accept)1).

Do odebrania nowych połączeń używa się funkcji accept(…). Funkcja accept zwraca nowe gniazdo reprezentujące nawiązane połączenie.

Tworzenie kolejnych deskryptorów plików oraz wyniki lsof, strace i ss są zaprezentowane tutaj

Zadania

Zadanie 1 Napisz program, który:

  1. stworzy gniazdo TCP funkcją socket,
  2. stworzy zmienną typu sockaddr_in i wypełni ją wpisując:
    • AF_INET w pole określające rodzinę adresów
    • stałą INADDR_ANY w pole określające adres IPv4
    • wybrany numer portu na którym serwer ma czekać na połączenia w pole określające port
      (pamiętaj o htons do zmiany kolejności bajtów)
  3. ustali lokalny adres gniazda funkcją bind
    uwaga: koniecznie sprawdzaj czy funkcja bind się powiodła
    jeśli zobaczysz błąd address already in use, przeczytaj tekst o stanie TIME_WAIT pod zadaniem
  4. rozpocznie oczekiwanie na połączenia wywołując funkcję listen
    drugi argument listen określa ile nieodebranych połączeń może naraz czekać w systemie; tutaj wystarczy 1
  5. zaakceptuje połączenie funkcją accept
    na razie drugi i trzeci argument funkcji accept ustaw na 0 lub nullptr
  6. wynik funkcji accept to deskryptor nowego gniazda, połączonego z klientem
    (w tym momencie możesz już zamknąć gniazdo nasłuchujące na połączenia)
  7. wyśle do gniazda połączonego z klientem stały ciąg znaków (np. hello)
  8. zakończy połączenie z klientem funkcją shutdown i zamknie deskryptor pliku funkcją close

Potrzebne pliki nagłówkowe:

#include <arpa/inet.h>
#include <cstdio>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
 
int main(int argc, char **argv) {
 
    return 0;
}

Stan TIME_WAIT

System operacyjny śledzi w jakim stanie jest połączenie TCP.
Poniższa ilustracja pokazuje kolejne stany połączenia po wykonaniu shutdown z argumentem SHUT_WR lub SHUT_RDWR:

Zwróć uwagę, że wywołanie shutdown wysyła segment TCP z informacją że ta strona nie będzie już więcej wysyłać danych (flaga FIN), a druga strona musi potwierdzić że odebrała tą informację (flaga ACK).

Jeżeli potwierdzenie zamknięcia połączenia nie dotrze (bo np. pakiet z tą informacją został zgubiony w sieci), to po upływie określonego czasu system operacyjny ponownie wyśle informację o zamknięciu połączenia:

Strona która pierwsza zamknęła połączenie po wysłaniu ostatniego ACK nie wie czy wystąpi sytuacja z pierwszej ilustracji (w której żaden segment z tego połączenie już nie przyjdzie) czy sytuacja z drugiej ilustracji (w której przyjdzie ponowiona informacja że druga strona zamyka połączenie). Dlatego przez określony czas (w Linuksie około minuty2)) system operacyjny pamięta o tym że połączenie istniało utrzymując je w stanie TIME_WAIT.
Więcej informacji o zamykaniu połączenia i stanach połączenia znajdziesz w odpowiednim RFC

Dla systemu operacyjnego port który był wykorzystany w tym połączeniu jest uznawany za zajęty, więc domyślnie system nie pozwoli uruchomić serwera na tym porcie. Można zmienić to zachowanie ustawiając dla gniazda opcję SO_REUSEADDR, która pozwala użyć adresu (IP i portu) mimo tego ze jakieś połączenie w stanie TIME_WAIT go jeszcze używa.
Poniższy kod ustawia opcję SO_REUSEADDR dla gniazda sockFd:

const int one = 1;
setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));

Oczywiście trzeba zmienić to zachowanie przed wywołaniem funkcji bind której działania ta opcja dotyczy.

Zadania

Zadanie 2 Zmodyfikuj program z poprzedniego zadania tak, by ustawiał opcję SO_REUSEADDR.
Sprawdź czy teraz możesz uruchomić serwer natychmiast po zakończeniu jego poprzedniego uruchomienia.

Zadanie 3 Zmodyfikuj program z poprzedniego zadania tak, by w pętli obsługiwał kolejne połączenia.

Odczytywanie informacji skąd przyszło połączenie

Funkcja accept może przekazać informację o adresie z którego nawiązano połączenie. W tym celu należy jej podać:

Przykład:

sockaddr_in cliAddr;
socklen_t cliAddrSize = sizeof(cliAddr);
client = accept(ssock, (sockaddr*)&cliAddr, &cliAddrSize);

Adresy gniazda (lokalny i zdalny) można też odczytać w każdej chwili funkcjami getsockname i getpeername.

Zadania

Zadanie 4 Dodaj do programu wyświetlanie z jakiego adresu IP i numeru portu nawiązano połączenie.
Funkcje zmieniające adres IP zapisany jako liczba na tekst to inet_ntoa i inet_ntop

Zadanie 5 Zmień logikę serwera tak, by zamiast wysyłać stały tekst, odbierał od klienta (do zamknięcia połączenia przez klienta) dane i jeżeli serwer odbierze tekst:

Uwaga: klient może wysłać na końcu tekstu znak nowej linii - weź to pod uwagę

Przykład minimalistycznego iteracyjnego serwera jednej gry w kółko i krzyżyk (bez sprawdzania warunku końca)

1) Argument "backlog" funkcji listen powinien się zawierać w zakresie 1÷SOMAXCONN (w tej chwili o wartości 4096 w Linuksie) i jest traktowany jako podpowiedź, tzn. system operacyjny może używać innego limitu niż podany w argumencie listen.