0Pricing
C Academy · Lekcja

Pętla zdarzeń

Zareaguje Pan/Pani na gniazda gotowe do odczytu.

Pętla zdarzeń to bezpłatna lekcja C Academy na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej C Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs C Academy zawiera 4 lekcji w sumie.

Serce serwera

Pętla zdarzeń jest silnikiem serwera epoll. Wielokrotnie oczekuje na gotowe deskryptory i przekazuje pracę odpowiednim procedurom obsługi.

Jej schemat jest zawsze taki sam: wywołanie epoll_wait(), przejście po zwróconych zdarzeniach, obsłużenie każdego z nich, a następnie ponowne wykonanie pętli. Cała reszta opiera się na tym szkielecie.

for (;;) {
    int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
    for (int i = 0; i < n; i++) handle(&events[i]);
}

Wywoływanie epoll_wait

epoll_wait() przyjmuje deskryptor epoll, tablicę wynikową elementów epoll_event, jej pojemność oraz limit czasu w milisekundach.

Blokuje wykonanie do momentu, gdy co najmniej jeden deskryptor będzie gotowy albo upłynie limit czasu, po czym zwraca liczbę zdarzeń zapisanych w tablicy. Tylko te elementy tablicy są prawidłowe.

struct epoll_event events[MAX_EVENTS];
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (n == -1) { perror("epoll_wait"); }

Argument limitu czasu

Limit czasu równy -1 blokuje wykonanie bezterminowo, aż do nadejścia zdarzenia. Wartość 0 powoduje natychmiastowy powrót i sprawdzenie stanu bez uśpienia.

Wartość dodatnia ogranicza czas oczekiwania, co jest przydatne, gdy trzeba również wykonywać zadania okresowe, takie jak opróżnianie logów lub usuwanie bezczynnych połączeń.

int n = epoll_wait(epfd, events, MAX_EVENTS, 1000); /* up to 1s */
if (n == 0) run_periodic_tasks();

Dobieranie rozmiaru tablicy zdarzeń

Pojemność tablicy ogranicza liczbę gotowych zdarzeń, które może zgłosić jedno wywołanie epoll_wait(). Jeśli gotowych jest więcej zdarzeń, pozostałe zostaną zwrócone przy następnym wywołaniu.

Wystarczający będzie umiarkowany rozmiar, na przykład 64 lub 1024; epoll sprawiedliwie przechodzi w kolejnych wywołaniach przez oczekujące zdarzenia, więc żadne z nich nie będzie pomijane.

#define MAX_EVENTS 64
struct epoll_event events[MAX_EVENTS];

Przekazywanie obsługi na gnieździe nasłuchującym

Wewnątrz pętli należy najpierw sprawdzić, czy gotowy deskryptor jest gniazdem nasłuchującym. Jeśli tak, oznacza to, że oczekują nowe połączenia.

Należy je zaakceptować, ustawić gniazda w tryb nieblokujący i zarejestrować je w epoll. Wszystkie pozostałe deskryptory należą do istniejących klientów, od których można odczytywać dane lub do których można je zapisywać.

if (events[i].data.fd == listen_fd) {
    accept_new_connections(epfd, listen_fd);
} else {
    handle_client(epfd, &events[i]);
}

Akceptowanie połączeń w pętli

Jedno zdarzenie gotowości gniazda nasłuchującego może oznaczać kilka oczekujących połączeń. Należy wywoływać accept() w pętli, aż zwróci EAGAIN.

Jest to niezbędne w trybie krawędziowym i stanowi dobrą praktykę w każdym trybie, ponieważ klienci nie muszą czekać na kolejne wybudzenie.

for (;;) {
    int c = accept(listen_fd, NULL, NULL);
    if (c == -1) { if (errno == EAGAIN) break; else break; }
    set_nonblocking(c);
    add_to_epoll(epfd, c);
}

Obsługa zdarzeń gotowości do odczytu

Gdy dla klienta wystąpi EPOLLIN, należy odczytać z niego dane. W trybie poziomowym pojedyncze wywołanie recv() na jedno wybudzenie jest dopuszczalne.

Wartość zwrotna 0 oznacza, że klient zamknął połączenie — należy zwolnić związane z nim zasoby. Ujemna wartość zwrotna z błędem EAGAIN oznacza, że na tę chwilę odczytano wszystkie dostępne dane.

ssize_t r = recv(fd, buf, sizeof buf, 0);
if (r == 0) { close_conn(epfd, fd); }
else if (r > 0) { process(buf, r); }

Opróżnianie w trybie krawędziowym

W przypadku EPOLLET należy odczytywać dane, aż recv() zwróci EAGAIN. Jądro powiadamia tylko o przejściu do stanu gotowości do odczytu, więc pozostawione bajty nie zostaną obsłużone aż do kolejnej zmiany stanu.

Odczyt należy umieścić w pętli i przerwać ją dopiero po otrzymaniu EAGAIN lub po osiągnięciu końca strumienia.

for (;;) {
    ssize_t r = recv(fd, buf, sizeof buf, 0);
    if (r > 0) process(buf, r);
    else if (r == 0) { close_conn(epfd, fd); break; }
    else { if (errno == EAGAIN) break; else { close_conn(epfd, fd); break; } }
}

Obsługa zdarzeń gotowości do zapisu

EPOLLOUT występuje, gdy gniazdo może przyjąć więcej danych wychodzących. Należy używać tej flagi tylko wtedy, gdy w buforze znajdują się bajty, których wcześniej nie udało się w całości wysłać.

Należy opróżnić bufor, a gdy stanie się pusty, przełączyć monitorowanie z powrotem wyłącznie na EPOLLIN za pomocą EPOLL_CTL_MOD, aby uniknąć nieustannej lawiny zdarzeń gotowości do zapisu.

if (events[i].events & EPOLLOUT) {
    flush_pending(fd);
    if (buffer_empty(fd)) watch_read_only(epfd, fd);
}

Obsługa przerwanych oczekiwań

epoll_wait() może zwrócić -1 z wartością errno == EINTR, jeśli sygnał przerwie oczekiwanie. Nie jest to rzeczywisty błąd — należy po prostu ponowić próbę.

Solidne pętle kontynuują działanie po EINTR, a za krytyczne uznają dopiero inne kody błędów. Pominięcie tego przypadku może spowodować awarię serwera przy pierwszym odebranym sygnale.

int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (n == -1) {
    if (errno == EINTR) continue;
    perror("epoll_wait"); break;
}

Jedna pętla, wielu klientów

Po połączeniu wszystkich elementów pojedynczy wątek bez końca przechodzi przez gotowe deskryptory: akceptuje połączenia, odczytuje dane, zapisuje dane, zamyka połączenia i powtarza te czynności.

Ponieważ wątek podczas bezczynności śpi w epoll_wait(), a podczas pracy przetwarza tylko gotowe zadania, taka konstrukcja bez problemu obsługuje dziesiątki tysięcy jednoczesnych połączeń.

Szybkie sprawdzenie

Sprawdź swoje zrozumienie działania pętli zdarzeń.

Podsumowanie

Pętla zdarzeń wywołuje epoll_wait(), przechodzi po gotowych zdarzeniach i przekazuje ich obsługę: akceptuje połączenia na gnieździe nasłuchującym, odczytuje dane przy EPOLLIN, opróżnia bufory przy EPOLLOUT i sprząta zasoby po rozłączeniu.

Akceptowanie połączeń i odczyty należy wykonywać w pętli, po EINTR ponawiać próbę, a w przypadku EPOLLET całkowicie opróżniać bufory. Następnie złożymy kompletny serwer echo.

Często zadawane pytania

Czy lekcja „Pętla zdarzeń” jest bezpłatna?

Tak — pełny tekst „Pętla zdarzeń” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu C Academy, przejdź na CoddyKit PRO. Kurs C Academy zawiera 4 lekcji w sumie.

Co nauczysz się w „Pętla zdarzeń”?

Zareaguje Pan/Pani na gniazda gotowe do odczytu. Ćwiczysz C Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć C Academy?

Nie wymagamy żadnego doświadczenia. C Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „Pętla zdarzeń”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji C Academy?

Tak. Każda lekcja C Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Blokujące a nieblokujące I/O
  2. Konfiguracja epoll
  3. Pętla zdarzeń
  4. Prosty serwer echo
← Powrót do C Academy