Narzędzia użytkownika

Narzędzia witryny


Pasek boczny


O mnie


Dydaktyka:

Feedback


bio-psiec:poll

To jest stara wersja strony!


Ustandaryzowane funkcje

Do budowy aplikacji sieciowych opartych o pętlę zdarzeń potrzebna jest funkcja która określi na którym ze wskazanych gniazd jest możliwość odczytu/zapisu bez blokowania, jeśli trzeba czekając aż taka możliwość się pojawi.
Standard POSIX definiuje dwie takie funkcje: select i poll. Pozwalają one na monitorowanie dowolnych deskryptorów plików, w tym gniazd. Obie są też dostępne w systemach z rodziny Microsoft Windows, ale tam można nimi monitorować tylko gniazda.

Do budowy serwerów zwykle wybiera się funkcję poll (w Windowsie występującą pod zmienioną nazwą WSAPoll). W systemach uniksopodobnych funkcja select ma ograniczenia które limitują jej przydatność w programach sieciowych, np. zwykle nie obsługuje deskryptorów plików o numerach większych niż 1023. Implementacja w Windowsie pozwala dowolnie podnieść ten limit.

Poza ustandaryzowanymi funkcjami poll i select każdy system operacyjny dostarcza też własnych interfejsów programistycznych które pozwalają budować jednowątkowe aplikacje używające jednocześnie wielu gniazd (i te interfejsy często oferują wyższą wydajność).

Przykład użycia funkcji poll

Przed wykonaniem funkcji poll trzeba przygotować listę informacji na co program chce czekać.
Taka lista musi mieć postać tablicy struktur pollfd, w których trzeba wypełnić pole fd deskryptorem pliku, oraz pole events listą tego na jakie zdarzenia program chce czekać:

pollfd pfds[2];
 
pfds[0].fd = cli1;
pfds[0].events = POLLIN;
 
pfds[1].fd = cli2;
pfds[1].events = POLLIN;

POLLIN określa że nastąpiło coś, co pozwoli wykonać funkcje odbierające dane bez blokowania. Może to oznaczać że przyszła nowa wiadomość, ale może też przykładowo oznaczać że połączenie zostało zamknięte (wtedy funkcje czytające dane z sieci też zakończą się bez czekania).

Po przygotowaniu tych informacji, można rozpocząć oczekiwanie funkcją poll:

poll(pfds, 2, -1);

W momencie wywołania tej funkcji system operacyjny odczytuje z tablicy struktur pollfd przekazanej jako pierwszy argument tyle struktur ile podano w drugim argumencie.
Następnie zatrzymuje (blokuje) działanie wątku który wywołał poll do czasu aż którąś ze wskazanych w tych strukturach operacji będzie się dało wykonać bez blokowania.
Trzeci argument funkcji poll służy do ustawiania maksymalnego czas blokowania; ustawienie go na wartość -1 określi że poll ma czekać do skutku.

Kiedy można już wykonać którąś ze wskazanych operacji wejścia/wyjścia bez blokowania, system operacyjny nadpisuje pola revents struktur wskazanych w argumentach na te zdarzenia które wystąpiły (dla struktur opisujących deskryptory na których nie da się bez blokowania wykonać żadnej operacji ustawi 0).

Programista musi następnie przeanalizować ustawione wartości pól revents, np:

for (int i = 0; i < 2; ++i) {
    if (pfds[i].revents & POLLIN) {
        int cnt = recv(pfds[i].fd, buf, 256, 0);
        if (cnt <= 0) termianteConnection(pfds[i].fd);
        else handleMessage(pfds[i].fd, buf, cnt);
    }
}

Załóżmy że od klienta przyszła wiadomość od drugiego klienta z prośbą o wysłanie mu dużego pliku. Serwer próbuje więc wykonać:

int cnt = send(pfds[1].fd, bigFile, 16777216, MSG_DONTWAIT);

Zwróć uwagę że wysyłając ustawiono MSG_DONTWAIT. Pamiętaj że funkcje wysyłające w normalnym, blokującym trybie, czekają tak długo jak trzeba żeby wysłać całą wiadomość. W jednowątkowym serwerze to zatrzymałoby cały program na (potencjalnie bardzo długi) czas wysyłania tego pliku.

Załóżmy że funkcja send zwróciła wartość 2097152, czyli mniej niż żądano. Oznacza to że bez czekania nie da się wysłać więcej.
Program chce więc teraz też czekać na wysłanie danych, dlatego zmienia listę zdarzeń na którą czeka:

pfds[1].events = POLLIN | POLLOUT;

POLLOUT określa że albo można wysłać przynajmniej jeden bajt, albo gniazdo znalazło się w innym stanie w którym funkcje wysyłające dane wykonają się bez czekania (np. połączenie zostało zamknięte bądź zerwane).

Zauważ jak wewnątrz pliku nagłówkowego #include <poll.h> są zdefiniowane kolejne stałe określające zdarzenia:

#define POLLIN      0x001  //   0b00000001
#define POLLPRI     0x002  //   0b00000010
#define POLLOUT     0x004  //   0b00000100
#define POLLERR     0x008  //   0b00001000
#define POLLHUP     0x010  //   0b00010000

Jak widać, każdy bit określa inne zdarzenie. To pozwala w polu events czy revents (które jest np. typu short int) zapisać osobno informację o każdym zdarzeniu.
Dlatego, żeby sprawdzić czy dane zdarzenie jest pośród ustawionych, musisz użyć operatora &(np. if(pfds[i].revents & POLLIN)).
Dodanie zdarzenia np. POLLIN to pfd.events = pfd.events | POLLIN;, a usunięcie to pfd.events = pfd.events & ~POLLIN;

Następnie można ponownie wykonać funkcję poll (czekając już teraz na możliwość odczyty z obu gniazd i/lub możliwość zapisu do drugiego gniazda). Załóżmy że została teraz wykonana z argumentami:

int count = poll(pfds, 2, 5000);

Ustawienie ostatniego argumentu, maksymalnego czasu czekania, na 5000 oznacza 5000ms, czyli 5s.
Jako wynik poll zwraca na ilu deskryptorach ustawiono niezerową listę zdarzeń.

Dla przypomnienia, pfds[0].events ma wartość POLLIN, a pfds[1].events ma wartość POLLIN | POLLOUT.
Wybrane możliwe wyniki po wykonaniu funkcji to teraz:

count pfds[0].revents pfds[1].revents taki wynik wystąpi między innymi jeżeli:
0 0 0 przez 5s nic się nie stało
1 0 POLLIN przyszła wiadomość od drugiego klienta
1 0 POLLOUT systemowi operacyjnemu udało się coś wysłać do drugiego klienta i można nadać kolejna porcję danych
1 0 POLLIN | POLLOUT wystąpiły naraz dwa powyższe zdarzenia
2 POLLIN POLLIN przyszła wiadomość i od pierwszego, i od drugiego klienta
1 POLLIN | POLLERR | POLLHUP 0 połączenie z pierwszym klientem zostało zerwane1)

Zwróć uwagę że mimo tego że program nie czekał ani na zdarzenie POLLERR, ani na zdarzenie POLLHUP, to zostały mu one zwrócone – jeśli któreś z tych zdarzeń wystąpi, to funkcja poll zawsze je zgłosi.

Program musi teraz sprawdzić które zdarzenia nastąpiły. Zwróć uwagę, że jeżeli jest możliwość wysłania danych do klienta drugiego, to program potrzebuje teraz wykonać:

int cnt = send(fd, bigFile + 2097152, 16777216 - 2097152, MSG_DONTWAIT);

gdzie 16777216 to rozmiar pliku który ma być wysłany, a 2097152 to liczba już wysłanych bajtów.

Zwróć uwagę, że zwykle w programie trzeba dla każdego klienta przechowywać po stronie aplikacji częściowo odebrane logiczne wiadomości jak i dane które czekają na wysłanie (tj. przekazanie do wysłania do systemu operacyjnego).

Zwykle w programach pełniących rolę serwera trzeba dynamicznie tworzyć tablicę struktur pollfd – z każdym nowym klientem potrzeba przecież zwiększyć rozmiar tej tablicy. (W C++ można do tego wykorzystać np. std::vector, przy czym trzeba pamiętać o tym jak i które modyfikacje struktur danych można zrobić w trakcie przechodzenia po nich.)
Dla gniazda nasłuchującego POLLIN określa że pojawił się nowy klient.

Zadania

Zadanie 1 Weź z materiałów do poprzedniego tematu dwa pierwsze fragmenty kodu, a następnie dokończ opisaną tam grę "kto szybciej pisze na klawiaturze" tak żeby program działał na jednym wątku z użyciem funkcji poll.
Pamiętaj żeby dodać do kodu #include <poll.h>, które dołączy plik z deklaracją potrzebnych funkcji, struktur i stałych.

Zadanie 2 Napisz jednowątkowy program który połączy się, używając TCP, pod wskazany adres, a następnie będzie równocześnie:

  • odczytywał dane wpisywane z klawiatury, i po odczytaniu wysyłał je przez sieć,
  • odbierał dane z sieci, i po odebraniu wypisywał je na ekran.

Zadanie 3 Napisz jednowątkowy serwer TCP, który każdą otrzymaną wiadomość przekaże wszystkim połączonym klientom. Zauważ że serwer musi jednocześnie czekać na nowych klientów i jednocześnie odbierać wiadomości od każdego z już połączonych.
Pisząc serwer załóż, że jeżeli nieblokujące wysyłanie do kogoś wiadomości nie wyśle całej wiadomości bez czekania, to należy zakończyć połączenie z tym klientem uznając że zostało ono zerwane.

Zadanie 4 Protokół TCP nie gwarantuje że dane są odbierane w takich samych porcjach w jakich były wysłane. Zauważ że dla programu z poprzedniego zadania logiczna wiadomość to jedna linia – tekst kończący się znakiem '\n'.
Jeżeli jeden klient wysłał linię Hello world! a drugi wysłał linię Witaj świecie!, a pierwszy tekst zostanie odebrany w dwóch wywołaniach funkcji read, to pozostałe osoby mogą zobaczyć na swoim ekranie np. linię Hello Witaj świecie! oraz linię world!.
Zmień program z poprzedniego zadania tak, żeby logiczne wiadomości były przesyłane dalej poprawnie. Zauważ że musisz do tego zbierać dane od każdego klienta do osobnego bufora.
Do testów tego programu użyj jako klienta komendy socat tcp:adres:port stdio,ignoreeof która pozwoli tobie, wciskając Ctrl + d, wysłać dotychczas wpisane znaki.

Przykłady

Serwer wysyłający żądane pliki (Linux)

Serwer wysyłający żądane pliki (Windows)

1) Przy odczycie z tego gniazda kolejne wywołania read najpierw zwrócą wszystkie już odebrane dane, potem zakończą się wartością -1 ustawiając errno na wartość ECONNRESET (Połączenie zerwane przez drugą stronę).
bio-psiec/poll.1768488492.txt.gz · ostatnio zmienione: 2026/01/15 15:48 przez jkonczak