====== 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łem((choć są od tego wyjątki - np. [[https://en.wikipedia.org/wiki/Spinlock|spinlock]] czy [[https://www.dpdk.org/|DPDK]])). 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: * Iteracyjnie – kiedy współbieżność jest zbędna. * Pętla zdarzeń (event loop, [[https://en.wikipedia.org/wiki/Event_loop|[1]]], [[https://web.archive.org/web/20190730174916/https://pl.wikipedia.org/wiki/Programowanie_sterowane_zdarzeniami|[2]]]) – programista wpierw przygotowuje kod (funkcje) obsługi możliwych zdarzeń, następnie w pętli czeka na zdarzenie i wywołuje kod powiązany ze zdarzeniem. Aplikacja może być jedno- lub wielowątkowa, muszą być dostępne funkcje czekające na zdarzenie – dla I/O pod Linuksem to ''select'', ''poll'' i ''epoll''. \\ Praktycznie wszystkie programy z GUI wykorzystują pętlę zdarzeń przynajmniej do obsługi GUI. * Aplikacja wielowątkowa – każde źródło zdarzeń – np. gniazdo – jest obsługiwane w osobnym wątku. ====== Wiele wątków ====== [[sk2:cpp11_threads|Wątki w C++]] ~~Zadanie.#~~ 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 {{:sk2:tcp_client_template.cpp|przykładowego kodu klienta}}) ~~Zadanie.#~~ Czat – napisz serwer, który każdą otrzymaną wiadomość przekaże wszystkim połączonym klientom. \\ (Możesz skorzystać z {{:sk2:tcp_server_template.cpp|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: * ''select'' – "klasyczna" funkcja, ma kilka dziwnych ograniczeń. Dostaje zbiór deskryptorów, oczekuje na zdarzenie, modyfikuje przekazany zbiór deskryptorów zostawiając tylko te na których można wykonać read/write/lub na których wystąpił wyjątek. (POSIX) * ''poll'' – zbudowana podobnie jak ''select'', ale m. inn. nie ma ograniczenia na numery monitorowanych deskryptorów((Patrz [[https://man7.org/linux/man-pages/man2/select.2.html|man 2 select]].)), można użyć ponownie struktury opisującej deskryptory etc. (POSIX) * ''epoll'' – specyficzna dla Linuksa funkcja. Inny pomysł: program informuje jądro systemu na które deskryptory chce czekać, potem wywołuje funkcję czekającą na zdarzenie i dostaje informacje które zarzenia wystąpiły. 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 ''[[https://en.wikipedia.org/wiki/Io_uring|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:
- 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 - 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ść - 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 ''
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.#~~ Powtórz Zadanie 1, ale tym razem jako jednowątkowy program wykorzystujący ''poll''. \\ (Możesz skorzystać z {{:sk2:tcp_client_template.cpp|przykładowego kodu klienta}}) ===== epoll ===== Mechanizm działania ''epoll'' jest inny niż ''select'' czy ''poll''. - 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''. - 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''. - 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ść). \\ - ''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 ''
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''|
struct Handler {
    virtual void handle(epoll_event &ee) = 0;
    virtual ~Handler() {}
};

struct Client;
std::set<Client *> clients;
struct Client : public Handler {
    char *partialDataReceived, *dataQueuedToSend;
    int epollDescr, fd;
    Client(int epollDescr, int fd) : epollDescr(epollDescr), fd(fd) {
        epoll_event ee{.events = EPOLLIN, .data = {.ptr = this}};
        epoll_ctl(epollDescr, EPOLL_CTL_ADD, fd, &ee);
        clients.insert(this);
    }
    ~Client() {
        close(fd);
        ...
    }
    void receive(epoll_event &ee);
    void sendQueuedData();
    void handle(epoll_event &ee) {
        if (ee.events & EPOLLIN)
            receive(ee);
        if (ee.events & EPOLLOUT)
            sendQueuedData();
    }
    ...
};
struct Server : public Handler {
    int epollDescr, serverSock;
    Server(int epollDescr, uint16_t port) : epollDescr(epollDescr) {
        serverSock = ...
        ...
        epoll_event ee{.events = EPOLLIN, .data = {.ptr = this}};
        epoll_ctl(epollDescr, EPOLL_CTL_ADD, serverSock, &ee);
    }
    ~Server();
    void handle(epoll_event &) {
        int cliFd = accept(serverSock, 0, 0);
        new Client(epollDescr, cliFd);
    }
};

int main() {
    ...
    int epollDescr = epoll_create1(0);
    Server srv(epollDescr, port);
    while (1) {
        epoll_event ee;
        epoll_wait(epollDescr, &ee, 1, -1);
        ((Handler *)ee.data.ptr)->handle(ee);
    }
}
++++ ~~Zadanie.#~~ Powtórz Zadanie 1, ale tym razem jako jednowątkowy program wykorzystujący mechanizm epoll. \\ (Możesz skorzystać z {{:sk2:tcp_client_template.cpp|przykładowego kodu klienta}}) ~~Zadanie.#~~ Napisz jednowątkowy serwer czatu używając ''poll'' lub ''epoll_wait''. \\ (Możesz skorzystać z {{:sk2:tcp_server_template.cpp|przykładowego kodu serwera}})