Цикл обработки событий
Реагируйте на доступные для чтения сокеты.
«Цикл обработки событий» — бесплатный урок C Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C Academy содержит 4 уроков всего.
Сердце сервера
Цикл обработки событий — это механизм, который приводит в действие сервер epoll. Он постоянно ожидает готовности дескрипторов и передаёт работу обработчикам.
Его структура всегда одинакова: вызвать epoll_wait(), перебрать возвращённые события, обработать каждое из них, а затем повторить цикл. Всё остальное строится вокруг этого каркаса.
for (;;) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) handle(&events[i]);
}Вызов epoll_wait
epoll_wait() принимает дескриптор epoll, выходной массив epoll_event, его вместимость и время ожидания в миллисекундах.
Функция блокируется, пока хотя бы один дескриптор не станет готов (или пока не истечёт время ожидания), и возвращает количество событий, записанных в Ваш массив. Допустимо использовать только эти элементы.
struct epoll_event events[MAX_EVENTS];
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (n == -1) { perror("epoll_wait"); }Аргумент времени ожидания
Время ожидания -1 блокирует выполнение на неопределённый срок, пока не поступит событие. Значение 0 немедленно возвращает управление, выполняя опрос без паузы.
Положительное значение ограничивает максимальное время ожидания. Это удобно, если Вам также нужно выполнять периодические задачи, например сбрасывать журналы или удалять неактивные соединения.
int n = epoll_wait(epfd, events, MAX_EVENTS, 1000); /* up to 1s */
if (n == 0) run_periodic_tasks();Выбор размера массива событий
Вместимость массива ограничивает количество готовых событий, о которых может сообщить один вызов epoll_wait(). Если готово больше событий, остальные будут возвращены при следующем вызове.
Подойдёт умеренный размер, например 64 или 1024: epoll справедливо чередует ожидающие события между вызовами, поэтому ни одно из них не будет постоянно оставаться без обработки.
#define MAX_EVENTS 64
struct epoll_event events[MAX_EVENTS];Передача обработки на сокете, принимающем соединения
Внутри цикла сначала проверьте, является ли готовый дескриптор сокетом, принимающим соединения. Если да, это означает, что ожидают новые соединения.
Примите их, переведите сокеты в неблокирующий режим и зарегистрируйте их в epoll. Все остальные дескрипторы принадлежат уже подключённым клиентам, у которых есть данные для чтения или место для записи.
if (events[i].data.fd == listen_fd) {
accept_new_connections(epfd, listen_fd);
} else {
handle_client(epfd, &events[i]);
}Приём соединений в цикле
Одно событие готовности на сокете, принимающем соединения, может означать наличие нескольких ожидающих соединений. Выполняйте accept() в цикле, пока он не вернёт EAGAIN.
Это необходимо в триггерном режиме и является хорошей практикой в любом режиме, чтобы не заставлять клиентов ждать до следующего пробуждения.
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);
}Обработка событий готовности к чтению
Когда у клиента срабатывает EPOLLIN, прочитайте данные из него. В уровневом режиме одного вызова recv() за одно пробуждение достаточно.
Возвращаемое значение 0 означает, что клиент закрыл соединение: освободите связанные с ним ресурсы. Отрицательное значение вместе с EAGAIN означает, что все доступные на данный момент данные прочитаны.
ssize_t r = recv(fd, buf, sizeof buf, 0);
if (r == 0) { close_conn(epfd, fd); }
else if (r > 0) { process(buf, r); }Полное чтение в триггерном режиме
При использовании EPOLLET необходимо читать данные, пока recv() не вернёт EAGAIN. Ядро уведомляет Вас только при переходе к состоянию готовности для чтения, поэтому оставшиеся байты не будут обработаны до следующего изменения состояния.
Оберните чтение в цикл и выходите из него только при получении EAGAIN или конца потока.
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; } }
}Обработка событий готовности к записи
EPOLLOUT срабатывает, когда сокет может принять дополнительные исходящие данные. Этот флаг нужен только при наличии буферизованных байтов, которые ранее не удалось отправить полностью.
Очистите буфер; как только он станет пустым, с помощью EPOLL_CTL_MOD верните отслеживание только EPOLLIN, чтобы избежать постоянного потока уведомлений о готовности к записи.
if (events[i].events & EPOLLOUT) {
flush_pending(fd);
if (buffer_empty(fd)) watch_read_only(epfd, fd);
}Обработка прерванного ожидания
epoll_wait() может вернуть -1 с условием errno == EINTR, если сигнал прервал ожидание. Это не настоящая ошибка — просто повторите вызов.
Надёжные циклы продолжают работу при EINTR и считают критическими только другие коды ошибок. Если об этом забыть, сервер может завершиться при первом же полученном сигнале.
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
if (n == -1) {
if (errno == EINTR) continue;
perror("epoll_wait"); break;
}Один цикл для множества клиентов
Объединив всё вместе, Вы получите один поток, который бесконечно перебирает готовые дескрипторы: принять, прочитать, записать, закрыть и повторить.
Поскольку в режиме простоя поток спит внутри epoll_wait(), а при загрузке обрабатывает только готовую работу, такая архитектура позволяет уверенно обслуживать десятки тысяч одновременных соединений.
Быстрая проверка
Проверьте, насколько хорошо Вы поняли цикл обработки событий.
Итоги
Цикл обработки событий вызывает epoll_wait(), перебирает готовые события и передаёт их обработчикам: принимает соединения на сокете, принимающем подключения, читает при EPOLLIN, очищает буфер при EPOLLOUT и освобождает ресурсы при разрыве соединения.
Принимайте соединения и выполняйте чтение в циклах, повторяйте вызов при EINTR и полностью считывайте данные при использовании EPOLLET. Далее мы соберём полноценный эхо-сервер.
Часто задаваемые вопросы
Урок «Цикл обработки событий» бесплатный?
Да — полный текст урока «Цикл обработки событий» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C Academy, подпишись на CoddyKit PRO. Курс C Academy содержит 4 уроков всего.
Чему я научусь в уроке «Цикл обработки событий»?
Реагируйте на доступные для чтения сокеты. Ты практикуешь C Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать C Academy?
Предыдущий опыт не требуется. C Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Цикл обработки событий»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке C Academy?
Да. Каждый урок C Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Блокирующий и неблокирующий ввод-вывод
- Настройка epoll
- Цикл обработки событий
- Простой эхо-сервер