Spis treści

Obsługa wielu strumieni naraz

Typowo aplikacje (jakiekolwiek, włączając sieciowe) muszą jednocześnie obsługiwać wiele źródeł zdarzeń (np. dane gotowe do odbioru, użytkownik kliknął na menu, naciśnięto enter). Domyślnie operacje na gniazdach (np. connect, accept, read) blokują przetwarzanie. Można to zmienić (tak jak dla każdego innego pliku w Linuksie) na obsługę nieblokującą, ale oczywiście aktywne czekanie (https://en.wikipedia.org/wiki/Busy_waiting) jest bardzo głupim pomysłem1).

Do obsługi wielu źródeł zdarzeń stworzono dedykowane metody, można też używać typowych metod pisania aplikacji współbieżnych. Zwolennicy SE nazwą to "wzorcami projektowymi". Przegląd typowych metod tworzenia aplikacji sieciowych:

Wiele wątków

Wątki w C++

Zadanie 1 Napisz własną wersję programu netcat wspierającą tylko klienta TCP – program, który nawiąże połączenie TCP pod wskazany adres, następnie dane przychodzące na standardowe wejście będzie wysyłać przez to połączenie, a równocześnie dane przychodzące z sieci będzie wypisywać na standardowe wyjście.
(Możesz skorzystać z przykładowego kodu klienta)

Zadanie 2 Czat – napisz serwer, który każdą otrzymaną wiadomość przekaże wszystkim połączonym klientom.
(Możesz skorzystać z przykładowego kodu serwera)

Zdarzenia

Funkcje biblioteczne

System Linux zawiera 3 podstawowe funkcje pozwalające na czekanie na przychodzące zdarzenia na deskryptorach plików:

Dłuższe porównanie: http://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/

Od kilku lat trwają prace nad mechanizmem io_uring, pozwalającym na wykonywanie jednocześnie wielu operacji na plikach. Mechanizm pozwala na przekazanie do kernela żądań wykonania konkretnych operacji, oraz oferuje funkcję do czekania na wykonanie jakiejś ze zleconych wcześniej operacji.

poll

Aby stworzyć program korzystający z funkcji poll, należy:

  1. przygotować tablicę struktur pollfd i wypełnić:
    • .fd – deskryptor pliku do monitorowania,
    • .events – zbiór monitorowanych zdarzeń:
      • POLLIN – funkcja poll ma się przerwać, jeśli można wywołać bez czekania read()/… lub acccept()
      • POLLOUT – funkcja poll ma się przerwać, jeśli można wywołać bez czekania write()/…
        uwaga: na możliwość wysłania danych czeka się wtedy kiedy ma się dane do wysłania
        uwaga: próba zapisania większej liczby bajtów niż jest wolnego miejsca w buforze nadawczym i tak zablokuje write
      uwaga: jeśli na gnieździe wystąpił błąd, to read()/write()/… też można wywołać bez czekania
      reszta zdarzeń (POLLHUP, POLLERR, POLLPRI, …) opisana w man poll
    • zostawić w spokoju .revents – tam pojawi się informacja o tym co wystąpiło
  2. w pętli wywoływać funkcję poll(…).
    ostatni argument funkcji poll to maksymalny czas oczekiwania; aby funkcja czekała bez limitu, należy podać tam dowolną ujemną wartość
  3. sprawdzać który deskryptor jest gotowy przeglądając pola .revents

Funkcja poll, struktura pollfd i stałe POLL… są w pliku nagłówkowym #include <poll.h>

pollfd pfds[COUNT]{};                                          ─┐
pfds[0].fd = STDIN_FILENO;   pfds[2].fd = cliSock1;             
pfds[0].events = POLLIN;     pfds[2].events = POLLIN;           ├─ (1)
pfds[1].fd = serverSock;     pfds[3].fd = cliSock2;             
pfds[1].events = POLLIN;     pfds[3].events = POLLIN|POLLOUT;  ─┘
...                          ...

while(1){
    int ile_gotowych = poll(pfds, liczbaStruktur, -1);         ═╾─ (2)

    if(pfds[0].revents & POLLIN) readingFromStdin();           ─┐
    if(pfds[1].revents & POLLIN) acceptingNewClient();          
    for(int i = 2; i < liczbaStruktur; ++i){                    
        if(pfds[i].revents & POLLIN)                            ├─ (3)
            readFromClient(pfds[i].fd);                         
        if(pfds[i].revents & POLLOUT)                           
            writeToClient(pfds[i].fd);                         ─┘
    }
}

Zadanie 3 Powtórz Zadanie 1, ale tym razem jako jednowątkowy program wykorzystujący poll.
(Możesz skorzystać z przykładowego kodu klienta)

epoll

Mechanizm działania epoll jest inny niż select czy poll.

  1. Wpierw programista musi stworzyć interfejs do monitorowania plików funkcją epoll_create lub epoll_create1 – interfejs trafia do tablicy deskryptorów plików (analogicznie do open(), pipe() czy socket()). Po skończonej pracy zamyka się go podobnie jak każdy plik – funkcją close.
  2. Lista monitorowanych deskryptorów jest przechowywana w pamięci jądra systemu operacyjnego – aby ją uzupełnić, należy użyć funkcji epoll_ctl. Pozwala ona powiązać deskryptor z listą zdarzeń na które oczekuje program oraz dowolnymi danymi mieszczącymi się w unii epoll_data_t.
  3. Funkcja czekająca na zdarzenia – epoll_wait – przyjmuje jako argumenty tylko deskryptor utworzony przez epoll_create, tablicę zdarzeń do wypełnienia i czas oczekiwania (-1 = nieskończoność).
  4. epoll_wait nie przekazuje numerów gotowych deskryptorów – przekazuje tylko rodzaj zdarzenia i powiązane z nim wcześniej dane.

Funkcje epoll_…, struktura epoll_event i stałe EPOLL… są w pliku nagłówkowym #include <sys/epoll.h>

struct Client {
    int fd;
    char *partialDataReceived, *dataQueuedToSend;
} *clients;
void receiveFromCli(Client *c);
void sendQueuedData(Client *c);

...

  int epollDescr = epoll_create1(0);                                   ═╾─ (1)

  epoll_event ee[2];                                                   ─┐
  ee[0].events = EPOLLIN;                                               │
  ee[0].data.u32 = -1;                                                  ├─ (2)
  epoll_ctl(epollDescr, EPOLL_CTL_ADD, STDIN_FILENO, &ee[0]);           │
  ee[0].data.u32 = -2;                                                  │
  epoll_ctl(epollDescr, EPOLL_CTL_ADD, serverSock, &ee[0]);            ─┘ 

  while (1) {
      int ile_gotowych = epoll_wait(epollDescr, ee, 2, -1);            ═╾─ (3)

      for (int i = 0; i < ile_gotowych; ++i) {                         ─┐
          if (ee[i].data.u32 == -1)                                     │
              readingFromStdin();else if (ee[i].data.u32 == -2) {int cliFd = accept(serverSock, 0, 0);int cliId = getFreeClientId();                            │
              clients[cliId] = {.fd = cliFd};                           │
              ee[0].events = EPOLLIN;                               ┐   │
              ee[0].data.u32 = cliId;                               ├(2)├─ (4)
              epoll_ctl(epollDescr, EPOLL_CTL_ADD, cliFd, &ee[0]);  ┘   │
          } else {if (ee[i].events & EPOLLIN)                               │
                  receiveFromCli(&clients[ee[i].data.u32]);if (ee[i].events & EPOLLOUT)                              │
                  sendQueuedData(&clients[ee[i].data.u32]);}}                                                                ─┘
  }

Przykład użycia pola ''.data.ptr''

Zadanie 4 Powtórz Zadanie 1, ale tym razem jako jednowątkowy program wykorzystujący mechanizm epoll.
(Możesz skorzystać z przykładowego kodu klienta)

Zadanie 5 Napisz jednowątkowy serwer czatu używając poll lub epoll_wait.
(Możesz skorzystać z przykładowego kodu serwera)

1) choć są od tego wyjątki - np. spinlock czy DPDK