Narzędzia użytkownika

Narzędzia witryny


Pasek boczny


O mnie


Dydaktyka:

Feedback


bio-psiec:sockets_caveats

To jest stara wersja strony!


Dopuszczalne zachowanie się funkcji sieciowych

Bufory w komunikacji sieciowej

Kolejne etapy "podróży" danych od karty sieciowej do aplikacji użytkownika:

  1. pakiet odbierany przez kartę sieciową (NIC) jest najpierw zapisywany do wewnętrznego bufora NIC,
  2. NIC kopiuje dane ze swoich buforów (używając DMA) do pamięci głównej (RAM) i wywołuje przerwanie,
  3. system operacyjny (OS) w procedurze obsługi przerwania przetwarza kolejno każdy z odczytanych pakietów; dla pakietów które przenoszą dane już istniejącego połączenia,
    OS kopiuje dane do powiązanego z gniazdem systemowego bufora odbiorczego,
  4. kiedy program zażądał od systemu operacyjnego odbioru danych z sieci i są dane w buforze systemowym, OS kopiuje dane z systemowego bufora odbiorczego do bufora podanego przez użytkownika.

Zauważ że te kroki mogą dziać się niezależnie od siebie, a funkcja do odbioru danych z sieci tak naprawdę odczytuje dane z powiązanego z gniazdem bufora w systemie operacyjnym.

W podobny sposób dane są buforowane przy wysyłaniu – kiedy program odpowiednią funkcją żąda wysłania danych przez sieć, podany przez użytkownika bufor z danymi jest kopiowany do powiązanego z gniazdem bufora systemowego. Stamtąd, niekoniecznie w trakcie działania wspominanej funkcji, dane są przenoszone do powiązanej z kartą kolejki danych do wysłania, skąd ostatecznie trafiają do kolejki na karcie sieciowej.

Więcej można doczytać np. tutaj: https://wiki.linuxfoundation.org/networking/kernel_flow

Blokowanie i wartości zwracane przez funkcje odbierające / wysyłające dane

Funkcje blokujące / nieblokujące

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

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

Funkcje wykonujące tylko operacje lokalne są nieblokujące.
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 – closemoże się blokować jeżeli dla gniazda połączeniowego ustawiono opcję SO_LINGER, nakazującą przed zamknięciem połączenia wysłać zbuforowane w systemie danych, ale nie dłużej niż na czas podany w opcji SO_LINGER.

Domyśle blokujące zachowanie można zmienić przestawiając gniazdo w tryb nieblokujący.
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 EWOULDBLOCK1).

Gniazdo przestawia się w tryb nieblokujący zmieniając flagi (ustawienia) deskryptora pliku (kod wymaga pliku nagłówkowego fcntl.h):

fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);

Niektóre funkcje sieciowe (np. 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);

Uwaga: w BSD/POSIX socket API każde nowo utworzone gniazdo działa w trybie blokującym, nawet jeżeli gniazdo zostało utworzone w wyniku działania funkcji accept na nasłuchującym gnieździe działającym w trybie nieblokującym.

Read i Write – ilość odczytanych / zapisanych bajtów

Funkcje read, recv,… często odczytują mniej bajtów niż podana w argumentach wielkość bufora. Powód? Mniej danych przyszło lub mniej mieści się w systemowym buforze odbiorczym2).

Funkcja recv i podobne ma pole na flagi; można tam ustawić flagę MSG_WAITALL która żąda odebrania dokładnie tylu bajtów ile podano. Mimo to recv może odebrać mniej bajtów, np. w przypadku zakończenia połączenia.

Normalnie funkcje zapisujące blokują się do momentu aż całe dane zostaną wpisane do systemowego bufora nadawczego dla gniazda (być może dane będą tam umieszczane porcjami, np. jeśli nakazano wysłać więcej danych niż rozmiar bufora).

Jeśli gniazdo pracuje w trybie nieblokującym, to funkcje takie jak write, send, … zapiszą ("wyślą") tylko część danych jeśli w buforze systemowym skończy się miejsce.

Dlatego zarówno przy odbieraniu jak i nieblokującym wysyłaniu trzeba sprawdzać zwracaną ilość przetworzonych danych.

Kody źródłowe do zadań

Kody źródłowe do zadań z tych laboratoriów można pobrać stąd. Zadania należy wykonywać na Linuksie – do części z nich potrzeba zmienić ustawienia sieci na takie które symulują opóźnienia sieci lub gubienie pakietów.

Aby ściągnąć i zbudować w katalogu z plikami źródłowymi3) wszystkie programy ręcznie, możesz wykonać komendy:

wget -O - http://www.cs.put.poznan.pl/jkonczak/_media/sk2:l3.tar.xz | tar xvJ
cd l3

cmake .
make

Programy serwera i klienta można uruchamiać na jednym komputerze, łącząc się z klienta na adres 127.0.0.1 lub localhost.

Zadania

Zadanie 1 Przeczytaj kod serwera 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ń).
Uruchom taki serwer na wybranym porcie.

Zadanie 2 Przeczytaj kod klienta TCP który:

  • łączy się pod podany adres
  • w pętli:
    • wysyła porcję danych
    • wypisuje ile łącznie wysłał bajtów

Połącz klienta do serwera z zadania 1.

Możesz sprawdzić poleceniem netstat/ss, np. netstat -tnp/ss -tnp, ile bajtów jest w buforze odbiorczym/nadawczym (kolumny Recv-Q i Send-Q).

Zadanie 3 Przeczytaj kod klienta TCP który:

  • łączy się pod podany adres
  • ustawia tryb nieblokujący
  • w pętli:
    • wysyła porcję danych
    • jeśli wysłał całą porcję danych, wypisuje ile łącznie wysłał bajtów
    • jeśli wysłał mniej danych niż chciał, wypisuje stosowny komunikat i kończy program

Połącz klienta do serwera z zadania 1.4)

Protokół strumieniowy / zorientowany na wiadomość

Zadanie 4 Przeczytaj kod serwera TCP który po odebraniu połączenia, do jego zamknięcia, wykonuje w pętli:

  • odbiera do 10 bajtów
  • wypisuje ile bajtów odebrał i wypisuje odebrane dane

Uruchom taki serwer na wybranym porcie.

Zadanie 5a Przeczytaj kod klienta TCP który wysyła cały alfabet po jednej literze. Podłącz się (wielokrotnie) do serwera z zadania 4.
Zadanie 5b Przeczytaj kod klienta TCP który 20 razy wysyła kilkanaście znaków. Podłącz się (wielokrotnie) do serwera z zadania 4.

Zadanie 6 Powtórz zadanie 4 dla UDP.
Zadanie 7 Powtórz zadanie 5a i 5b dla UDP.

Zadanie 8 Przeczytaj kod klienta TCP, który w wielokrotnie:

  • losuje wielkość tablicy znaków do wysłania (w zakresie 1500 do 3500)
  • wypełnia tablicę znakiem ., wstawia b na jej początek, e na koniec
  • wysyła całą tablicę
  • czeka 10ms

Następnie przeczytaj kod serwera TCP który po odebraniu połączenia, do jego zamknięcia, w pętli odbiera do 1MB danych, a następnie sprawdza w każdej odebranej porcji na jakich pozycjach znajdują się znaki 'b' i 'e'.
Uruchom program serwera i połącz się do niego klientem. Jeżeli uruchamiasz serwer i klienta na tym samym komputerze, to przed uruchomieniem klienta 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

Aby wrócić do domyślnych ustawień, wpisz:

ip link set lo mtu 65535
tc qdisc del dev lo root

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?

Kolejność danych i (nie)zawodność

Zadanie 11 Wykonaj z roota poniższe polecenie, które spowoduje pomieszanie kolejności pakietów wysłanych przez interfejs lo.
Następnie uruchom ponownie programy z zadań 4÷7.

tc qdisc add dev lo root netem delay 5ms 5ms distribution normal loss 10%

Aby przywrócić domyślne zachowanie po wykonaniu zadania, wpisz:

tc qdisc del root dev lo

Zadanie 12 Przeczytaj kod programu, który wyśle 1000 pakietów UDP zawierających kolejne liczby (z 10µs przerwą przed wysłaniem każdego pakietu).
Połącz tego klienta z serwerem z zadania 4.

Zadanie 13 Wykonaj z roota poniższe polecenie, które spowoduje ograniczenie prędkości wysyłania pakietów i ponownie przetestuj program z poprzedniego zadania:

tc qdisc add dev lo root tbf rate 10kbps burst 1.5kb limit 10kb

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 ss z przełącznikiem i, np:      ss -atip

Zadanie 14 Jak w protokole UDP radzić sobie z kolejnością pakietów? Jak radzić sobie ze zgubieniem pakietów?

1) Wyjątkiem jest funkcja connect, która rozpoczyna nawiązywanie połączenia w tle, zwraca -1 i ustawia errno na EINPROGRESS.
2) Rozmiar bufora odbiorczego można ustawić funkcją: setsockopt(socket, SOL_SOCKET, SO_RCVBUF, …)
3) Co jest złą praktyką – kod należy budować w osobnym katalogu. Zainteresowanych odsyłam tutaj
4) W tym zadaniu warto porównać wynik dla klienta z lokalnego komputera i ze zdalnego komputera
bio-psiec/sockets_caveats.1764204138.txt.gz · ostatnio zmienione: 2025/11/27 01:42 przez jkonczak