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.