Dydaktyka:
FeedbackZ doświadczeń lat ubiegłych wynika, że sprawne pisanie prostych programów stanowi problem dla części uczestników zajęć. Stąd w trakcie tych zajęć proszę korzystać z gotowych kodów źródłowych, analizując je przez uruchomieniem:
Aby ściągnąć i zbudować programy ręcznie, możesz wykonać:
Programy serwera i klienta można uruchamiać na jednym komputerze, łącząc się z klienta na adres 127.0.0.1 lub localhost.
Kolejne etapy "podróży" danych od karty sieciowej do aplikacji użytkownika:
W podobny sposób dane są buforowane przy wysyłaniu – podany przez użytkownika
bufor kopiowany jest do powiązanego z gniazdem bufora systemowego, z którego
dane są przenoszone do powiązanej z kartą kolejki danych do wysłania, skąd
trafiają do kolejki na karcie.
Więcej można doczytać np. tutaj: https://wiki.linuxfoundation.org/networking/kernel_flow
Funkcje
Funkcje
Dlatego zarówno przy odbieraniu jak i wysyłaniu trzeba sprawdzać zwracaną ilość
przetworzonych danych.
Zasadniczo funkcje które wymagają wysłania lub odebrania danych z sieci mogą
się zablokować, tj. po ich wywołaniu program zatrzymuje się do czasu
zakończenia żądanej operacji (por. z definicją
blocking z POSIX).
Można zmienić domyśle blokujące zachowanie przestawiając gniazdo w tryb nieblokujący.
Według standardu POSIX z 2024 roku tryb nieblokujący można ustawiać też
sumując ostatni argument funkcji
Zadanie 1 Stwórz serwer TCP, który w pętli akceptuje nowe połączenia
i je ignoruje (tzn. nie odbiera ani nie wysyła danych, nie zamyka połączeń).
Zadanie 2 Stwórz klienta TCP który:
Możesz sprawdzić poleceniem
Zadanie 3 Stwórz klienta TCP który:
Zadanie 4 Napisz serwer TCP który po odebraniu połączenia, do jego
zamknięcia, wykonuje w pętli:
Zadanie 5a Napisz klienta TCP który wysyła cały alfabet po jednej literze.
Podłącz się (wielokrotnie) do serwera z zadania 4.
Zadanie 6 Powtórz zadanie 4 dla UDP.
Zadanie 8 Napisz klienta TCP który w wielokrotnie:
Zadanie 9 Jak w strumieniu danych – czyli w połączeniu TCP – przesyłać dane
tak by serwer wiedział kiedy cała wiadomość dotrze?
Zadanie 10 Jak w protokole UDP zapewnić, że cała wiadomość została odebrana?
Zadanie 11 Wykonaj z roota poniższe polecenie, które spowoduje pomieszanie
kolejności pakietów wysłanych przez interfejs
Aby przywrócić domyślne zachowanie po wykonaniu ćwiczeń, możesz wpisać:
Zadanie 12 Przygotuj program, który wyśle 1000 pakietów UDP zawierających
kolejne liczby.
Zadanie 13 Wykonaj z roota poniższe polecenie, które spowoduje ograniczenie
prędkości wysyłania pakietów i przetestuj program z poprzedniego zadania:
Dla TCP możesz zobaczyć gromadzone przez system operacyjny informacje używane
do dobrania prędkości do możliwości łącza używając polecenia
Zadanie 14 Jak w protokole UDP radzić sobie z kolejnością pakietów?
Jak radzić sobie ze zgubieniem pakietów?
Funkcja
Funkcja wget -O - http://www.cs.put.poznan.pl/jkonczak/_media/sk2:l3.tar.xz | tar xvJ
cd l3
mkdir build
cd build
cmake ..
make
Bufory w komunikacji sieciowej
OS kopiuje dane do powiązanego z gniazdem systemowego bufora odbiorczego,
OS kopiuje dane z systemowego bufora odbiorczego do bufora podanego przez użytkownika.Blokowanie i wartości zwracane przez funkcje odbierające / wysyłające dane
Read i Write - ilość odczytanych / zapisanych bajtów
read
, recv
,… często zwracają mniej niż podana w argumentach
wielkość bufora. Powód? Mniej danych przyszło lub skończyło się miejsce w buforze
odbiorczym1).
Może się tak stać nawet pomimo ustawienia flagi MSG_WAITALL
w funkcji
recv
i podobnych (żądającej odebrania dokładnie tylu bajtów ile podano),
np. w przypadku zakończenia połączenia.
write
, send
,… mogą też w pewnych okolicznościach wysłać mniejszą
ilość bajtów niż zażądano.
Jest to spowodowane zapełnieniem systemowego buforu wysyłania (czyli jeśli dane
są dostarczane do wysłania szybciej niż można je wysyłać).
Normalnie funkcje zapisujące blokują się do momentu aż w buforze będzie dość
miejsca.
Jeśli jednak gniazdo pracuje w trybie nieblokującym (patrz niżej), to funkcje
takie jak write
, send
, … zapiszą tylko część danych jeśli w buforze
systemowym skończy się miejsce.
Funkcje blokujące / nieblokujące
Przykłady funkcji które mogą się zablokować to:
read
/ recv
/ recvfrom
(czeka aż przyjdą dane)write
/ send
(czeka aż zwolni się miejsce w buforze nadawczym)connect
(czeka aż uda się nawiązać połączenie)accept
(czeka aż przyjdzie nowe połączenie)gethostbyname
/ getaddrinfo
(czeka na odpowiedź od serwera nazw, o ile zaszła konieczność odpytania).
Przykłady takich funkcji (sieciowych) to: socket
, bind
, listen
, setsockopt
, gethostbyname
/ getaddrinfo
(o ile funkcja nie odpytywała serwera nazw, co można wymusić odpowiednimi flagami).
Funkcja zamykająca połączenie – shutdown
– jest nieblokująca, natomiast funkcja zamykająca gniazdo – close
– może się blokować dla gniazd połączeniowych dla których ustawiono na nim opcję SO_LINGER na czas wysłania danych z bufora systemowego, ale nie dłużej niż ilość sekund podana w opcji SO_LINGER[1].
Jeśli wykonanie żądanej funkcji na gnieździe w trybie nieblokującym nie jest
możliwe bez czekania, to wykonanie funkcji nie powodzi się (zwracany jest wynik
-1
) a zmienna errno
jest ustawiana na EAGAIN
lub EWOULDBLOCK
2).
Gniazdo przestawia się w tryb nieblokujący podobnie jak każdy inny deskryptor pliku:
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK)
recv
, send
) pozwalają na ustawienie w polu flag wartości MSG_DONTWAIT
, która wykonuje żądaną operację w trybie nieblokującym niezależnie od tego w jakim trybie pracuje gniazdo:
recv(fileDescriptor, buff, buffSize, MSG_DONTWAIT);
accept
na nasłuchującym gnieździe działającym w trybie nieblokującym.
socket
ze stałą SOCK_NONBLOCK
[1],
jak i
ustawiając flagę SOCK_NONBLOCK
w polu flag funkcji accept4
[2]
(wersja funkcji accept
z dodatkowym polem flag).
Linux od dawna wspiera stałą SOCK_NONBLOCK
.
Zadania
netstat
, np. netstat -tn
, ile bajtów jest w
buforze odbiorczym/nadawczym (kolumny Recv-Q i Send-Q).
Protokół strumieniowy / zorientowany na wiadomość
Zadanie 5b Napisz klienta TCP który wysyła wielokrotnie4) po
kilkanaście liter. Podłącz się (wielokrotnie) do serwera z zadania 4.
Zadanie 7 Powtórz zadanie 5a i 5b dla UDP.
.
, wstawia b
na jej początek, e
na koniec
Przed uruchomieniem programu, zmień ustawienia swojego interfejsu sieciowego poleceniami:
ip link set lo mtu 1500
tc qdisc add dev lo root netem delay 2ms 3ms 90% distribution pareto
ip link set lo mtu 65535
tc qdisc del dev lo root
Kolejność danych i (nie)zawodność
lo
.
Następnie uruchom ponownie programy z zadań 4÷7.
tc qdisc add dev lo root netem delay 5ms 5ms distribution normal loss 10%
tc qdisc del root dev lo
tc qdisc add dev lo root tbf rate 10kbps burst 1.5kb limit 10kb
ss
z
przełącznikiem i
, np: ss -atip
Opcje gniazd
setsockopt
pozwala dostosować zachowanie gniazd do potrzeb programisty. Bliźniacze getsockopt
pozwala na odczyt opcji gniazd.
Podobnie jak np. ioctl
, funkcja setsockopt
pozwala zmieniać zachowanie na wielu poziomach (np. gniazda / protokołu warstwy sieci / protokołu warstwy transportu). Drugi argument to właśnie wybór poziomu, trzeci - wybór opcji, czwarty to wskaźnik na wartość opcji, ostatni określa rozmiar przekazywanej opcji.
Parametry (opcje) które można ustawić są opisane w rozdziale 7 podręcznika systemowego. Przykłady opcji:
man 7 socket
, poziom SOL_SOCKET
):
SO_BROADCAST
– wymagane do UDP broadcastuSO_KEEPALIVE
– włącza mechanizm TCP keepalive http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.htmlSO_LINGER
– pozwala przed zamknięciem gniazda wysłać zakolejkowane daneSO_RCVBUF
i SO_SNDBUF
– ustawienie rozmiaru systemowych buforów odbiorczych / nadawczychSO_REUSEADDR
– pozwala ignorować połączenia w stanie TIME_WAIT przy ustalaniu lokalnego adresu gniazda (wywoływaniu bind) https://tools.ietf.org/html/rfc793#page-22man 7 ip
, poziom IPPROTO_IP
):
IP_ADD_MEMBERSHIP
oraz IP_DROP_MEMBERSHIP
– pozwala dodać siebie do grupy multicastowej man 7 tcp
, poziom IPPROTO_TCP
):
TCP_NODELAY
– wyłącza algorytm Nagle'a (wyłącza buforowanie wyjścia jeśli danych jest mało a poprzednie nie zostały potwierdzone)// typowe zastosowanie – reuseaddr
const int one = 1;
setsockopt(sockFd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
// przykład z innego poziomu - zmiana opcji TCP
setsockopt(sockFd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
// opcja której argumentem jest rozmiar
size_t bufsize = 4*1024*1024;
setsockopt(sockFd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
// opcja której argumentem jest struktura
struct sctp_event_subscribe events{.sctp_data_io_event=1, .sctp_association_event=1};
setsockopt(sockFe, IPPROTO_SCTP, SCTP_EVENTS, &events, sizeof(events));
// przykład odczytania opcji
uint32_t mtu;
socklen_t optSize = sizeof(mtu);
getsockopt(sockFd, IPPROTO_IP, IP_MTU, &mtu, &optSize);
fcntl
(man fcntl open
) pozwala na ustawienie (F_SETFL
) opcji O_NONBLOCK
potrzebnej do nieblokującej obsługi gniazd.
setsockopt(socket, SOL_SOCKET, SO_RCVBUF, …)
connect
, która rozpoczyna nawiązywanie połączenia w
tle, zwraca -1
i ustawia errno
na EINPROGRESS
.