0Pricing
C Academy · 课时

事件循环

响应可读套接字。

事件循环 是 CoddyKit 上的免费 C Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 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 fd、一个用于输出 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 时将数据完全读尽。接下来,我们将组装一个完整的回显服务器。

常见问题解答

「事件循环」课时是免费的吗?

是的 — 「事件循环」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C Academy 课程的其余内容,请升级到 CoddyKit PRO。 C Academy 课程共包含 4 节课。

「事件循环」这节课中我会学到什么?

响应可读套接字。 你通过在浏览器中直接运行的动手代码来练习 C Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「事件循环」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C Academy 课中编写并运行代码吗?

能。每节 C Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 阻塞与非阻塞 I/O
  2. 设置 epoll
  3. 事件循环
  4. 简单的回显服务器
← 返回 C Academy