ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深入理解poll:从I/O多路复用到TCP服务器实战

深入理解poll:从I/O多路复用到TCP服务器实战 别急着上 epoll先把 poll 吃透接口原理与可复用的 TCP 服务器实战我最早接触网络编程时从select跳到poll并没有觉得多新鲜。真正让我觉得“这玩意值得认真研究”的是我在维护一个需要同时承载几千个连接的老项目时select的FD_SETSIZE上限把我卡得死死的。换poll之后代码重构量很小但系统瞬间稳了不少。如果你是刚接触Linux高级IO、准备学I/O多路转接的读者或者你写完了select版服务器正想往更高效的方向走那么poll就是绕不开的一座桥——它既不像select那样简陋也不像epoll那样需要理解内核红黑树和回调机制是理解事件驱动模型的最佳中间形态。这篇文章我不会空谈原理会直接拆poll的接口细节、工作流程再给你一份完整可跑的poll版 TCP 服务器代码最后把我踩过的坑连汤带水地倒出来。保证你看完能立刻在自己的机器上复现。1. 为什么 select 不够用poll 诞生的背景1.1 select 的三大痛点上限、拷贝、线性扫描要说清楚poll的价值必须先聊聊select。它是最常见的 I/O 多路转接接口几乎所有初级网络编程课程都会讲。但select有三件事做得相当难受监听 fd 数量受FD_SETSIZE限制默认 1024。你可以重新编译内核时调整FD_SETSIZE但在生产环境里你需要承载上万连接时改内核参数成本极高而且每出一个版本都要记得同步改动很容易翻车。每次调用都要重新构造fd_setselect会修改传入的fd_set参数所以每次循环你都必须重新把关心的 fd 加入集合。这是一次不折不扣的全量拷贝操作。连接越多这个拷贝成本越高。返回后你不知道具体哪个 fd 就绪select返回的是一个集合你需要把 0 到FD_SETSIZE整个范围全部扫描一遍再调用FD_ISSET检查每个 bit 是否置位。如果 1024 个 fd 中只有 3 个活跃你也得扫描 1024 次。select的时间复杂度是 O(n)这 n 是监听的最大 fd 数字加一而不是活跃 fd 数量。想象一下你的服务器维护着 5000 个连接其中只有 5 个有数据到达select依然要遍历整个 bitmap 才能找到这 5 个。这不是某一种实现的小瑕疵而是select接口设计本身的缺陷。1.2 poll 的突破与传承没了上限但保留了线性扫描poll的出现就是为了干掉第一个和第二个痛点。它不再用 bitmap而是用了一个struct pollfd数组这个数组的长度你说了算不再受FD_SETSIZE限制。你可以在堆上分配一个能容纳 10 万个元素的数组只要内存够。同时poll不再修改你传入的参数内核只会把结果写回每个元素的revents字段。这意味着你不需要每次调用前重新构造数组——这比select要清爽得多。但注意poll并没有解决第三个痛点返回后你依然需要线性扫描整个pollfd数组来找出哪些 fd 有事件。也就是说poll的时间复杂度依然是 O(n)这里的 n 是你的 pollfd 数组长度。很多初学者看到这里就笑了那poll有什么大进步其实不然——它解决的是数量和构造开销的问题而不仅仅是扫描开销的问题。在连接数 1 万以内的场景poll的线性扫描开销完全可接受但select的 bitmap 扫描和重新构造开销更加致命。所以当时很多项目从select迁到poll获得的提升非常明显。2. poll 的核心接口与数据结构不只是换个名字2.1 struct pollfd 的三个字段事情的关键都在这里poll的接口非常简洁核心就一个结构体加一个函数#include poll.h struct pollfd { int fd; /* 你要监听的 fd */ short events; /* 你关心的事件掩码 */ short revents; /* 内核返回的实测事件掩码由内核填充 */ }; int poll(struct pollfd *fds, nfds_t nfds, int timeout);这里的fds是指向struct pollfd数组首元素的指针nfds是数组长度timeout是超时时间单位毫秒。三个字段中fd和events是你设置的revents是内核写回的。一个常见坑是每次进入轮询循环前你不需要重设 fd 和 events但 revents 是内核覆盖写的你读完后不用管它下一轮 poll 调用时内核会重新填充。这一点和select完全不同也是我喜欢poll的原因。还有一点fd可以设置为负数表示对该 fd 不感兴趣。内核会直接忽略这个元素的事件并把revents置为 0。这是实现“动态移除连接”的一种巧妙手段后面代码里我会用到。2.2 事件掩码POLLIN、POLLOUT 和它们的难兄难弟events和revents都是短整型的位掩码通过按位或组合。最常用的是事件掩码含义说明POLLIN有数据可读包括普通数据和紧急数据带外数据POLLOUT可以写表明 socket 的发送缓冲区有空间不阻塞POLLERR发生错误只会在revents中返回不用于设置eventsPOLLHUP对方挂断只会在revents中返回不用于设置eventsPOLLNVALfd 未打开或非法只会在revents中返回不用于设置events初学者最容易迷惑的是POLLERR和POLLHUP的区别。POLLERR表示 socket 出现异步错误比如 TCP 的 RST 包到达POLLHUP表示对方正常关闭发送了 FIN。正常情况下对方调用close()或进程退出时你会收到POLLIN因为 FIN 也算一种“可读”事件read()会返回 0随后再收到POLLHUP。但有些场景下你会直接收到POLLHUP而不先看到POLLIN——比如对端使用shutdown(SHUT_RDWR)时。所以revents的判读务必要包含这些异常位。2.3 返回值与 revents 判读别把总数当成事件类型poll的返回值有三类情况返回大于 0有revents非零的 fd 数量。注意这个数量是“事件数量”不是“有数据的字节数”。返回 0超时没有任何 fd 就绪。返回 -1出错errno被设置。常见的是EINTR被信号打断和ENOMEM内存不足。你拿到返回值后需要遍历整个fds数组检查每个元素的revents是否非零。这是poll的固有操作无法避免。但你可以做个小优化用一个变量记录上一次处理到的最大下标或者把要监听的事件集中在数组开头减少有效遍历长度。后面服务器代码中我会展示这种方法。3. poll 的工作原理内核到底帮你做了什么3.1 从调用到底层三步走如果你只想知道“怎么用”看完上一节就够了。但如果面试官问“poll 的原理是什么”你至少要说出这三步拷贝用户态把fds数组拷贝到内核空间。等待内核根据你设置的events把当前进程挂到每个 fd 对应的等待队列上。当有事件发生时对应的驱动会唤醒等待队列中的进程。返回内核把每个 fd 的实际事件填充到revents并把整个数组拷贝回用户态然后返回就绪个数。这三步看懂后你就明白poll性能的关键瓶颈在哪了每次调用的两次全量拷贝进内核、出内核以及返回后用户的线性扫描。这也是后来epoll主要改进的方向——epoll 通过在内核维护一个事件表避免了每次调用都全量拷贝同时通过就绪链表直接返回就绪事件省掉了线性扫描。3.2 fd 的就绪判定与阻塞关系很多初学者会问poll 本身会阻塞吗答案是如果 timeout 是 -1poll 会一直阻塞直到有至少一个 fd 就绪如果 timeout 大于 0则最多阻塞 timeout 毫秒。但注意poll 阻塞的是“等待事件”这个过程而你对每个 fd 进行read()或write()时fd 本身所处模式阻塞或非阻塞也很关键。对于poll监听的 fd强烈建议设置为非阻塞。原因很简单poll返回某个 fd 可读并不代表你read()一定不会阻塞。比如一个字节的数据到达后你read()时又来了更多数据但如果你设置的读缓冲区太小最后一次read()可能恰好读完当前数据而缓冲区仍有空间这时不会阻塞但假如你调用了read()而数据被另一个线程抢走了多线程模型下你的read()就会阻塞在空 socket 上。更常见的是POLLOUT事件——即使write()一般不会阻塞但一旦发送缓冲区满write()也会阻塞。所以poll搭配非阻塞 fd 才是标准做法。3.3 时间复杂度对比select、poll、epoll我画不了图用表格展示模型添加 fd 复杂度等待事件复杂度返回后处理复杂度selectO(1)但受 FD_SETSIZE 限制O(n)内核轮询扫描O(n)遍历 bitmappollO(1)数组添加O(n)内核轮询扫描O(n)遍历 pollfd 数组epollO(1)红黑树插入O(1)回调机制就绪链表O(k)k 为就绪事件数直接从链表取可以看出poll 的 O(n) 相比之下没有质变但它去掉上限使大规模连接成为可能。而 epoll 的目标是把时间复杂度的常数降下来并且在活跃连接很少时仍然能快速返回。4. 手写一个 poll 版本的 TCP 服务器完整代码拆解4.1 设计思路一个线程搞定所有我们先不聊高并发先解决“能跑”的问题。服务器要干的活其实只有四类新连接到达已连接 socket 可读已连接 socket 可写连接异常断开我采用单线程事件循环模式一个pollfd数组监听 socket 和所有客户端 socket循环调用poll然后分发事件。这比多线程模型简单太多了而且完全符合poll的设计意图。整个服务器逻辑可以用伪代码表达初始化监听 socket 设置非阻塞 创建 pollfd 数组填入监听 fd while (1) { poll 等待事件 for 数组中的每个元素: 若发生错误、挂断 - 关闭连接 若可读: 是监听 fd - accept 新连接加入 pollfd 数组 否则 - 读取客户端数据 }4.2 完整代码可直接编译我提供一个最小但完整的实现。它做了三件事监听端口、接受连接、把收到的数据原样回发给客户端echo 服务器。同时打印日志方便观察事件变化。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include errno.h #include fcntl.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #include poll.h #define MAX_FDS 1024 #define BUFFER_SIZE 4096 // 设置非阻塞模式 static int set_nonblock(int fd) { int flags fcntl(fd, F_GETFL, 0); if (flags -1) return -1; flags | O_NONBLOCK; return fcntl(fd, F_SETFL, flags); } int main(int argc, char *argv[]) { int listen_fd; struct sockaddr_in addr; int port atoi(argv[1] ? argv[1] : 8080); // 创建监听 socket listen_fd socket(AF_INET, SOCK_STREAM, 0); if (listen_fd -1) { perror(socket); exit(EXIT_FAILURE); } // 允许端口复用防止 TIME_WAIT 导致的 bind 失败 int opt 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_addr.s_addr htonl(INADDR_ANY); addr.sin_port htons(port); if (bind(listen_fd, (struct sockaddr*)addr, sizeof(addr)) -1) { perror(bind); exit(EXIT_FAILURE); } if (listen(listen_fd, 128) -1) { perror(listen); exit(EXIT_FAILURE); } if (set_nonblock(listen_fd) -1) { perror(set_nonblock listen); exit(EXIT_FAILURE); } printf(服务器已启动监听端口 %d\n, port); // pollfd 数组先预留 MAX_FDS 个元素 struct pollfd fds[MAX_FDS]; // fds 数组中fd-1 表示空闲槽位 // 初始化所有槽位为 -1方便后续线性扫描跳过 for (int i 0; i MAX_FDS; i) { fds[i].fd -1; fds[i].events 0; fds[i].revents 0; } // 把监听 fd 放入第 0 个槽位 fds[0].fd listen_fd; fds[0].events POLLIN; // 只关心可读新连接到达 int max_fd_index 0; // 当前有效元素最大下标用于遍历优化 char buffer[BUFFER_SIZE]; while (1) { // 等待事件超时设为 -1无限等待 int ret poll(fds, max_fd_index 1, -1); if (ret 0) { if (errno EINTR) continue; // 被信号打断重新 poll perror(poll); break; } if (ret 0) { // 超时实际上 timeout-1 不会发生但为了完整性保留 printf(poll 超时\n); continue; } // 有事件就绪遍历数组 for (int i 0; i max_fd_index; i) { if (fds[i].fd -1) continue; // 空闲槽位 if (fds[i].revents 0) continue; // 无事件 // 先处理错误、挂断、无效 fd if (fds[i].revents (POLLERR | POLLHUP | POLLNVAL)) { printf(连接 %d 发生错误关闭\n, fds[i].fd); close(fds[i].fd); fds[i].fd -1; // 维护 max_fd_index 的优化可以放在统一整理时做这里先简单跳过 continue; } // 监听 fd 可读 - 新连接 if (fds[i].fd listen_fd) { while (1) { struct sockaddr_in client_addr; socklen_t len sizeof(client_addr); int client_fd accept(listen_fd, (struct sockaddr*)client_addr, len); if (client_fd -1) { if (errno EAGAIN || errno EWOULDBLOCK) break; // 已经接完所有待处理连接 perror(accept); break; } set_nonblock(client_fd); // 找一个空闲槽位放入新连接 int placed -1; for (int j 0; j MAX_FDS; j) { if (fds[j].fd -1) { fds[j].fd client_fd; fds[j].events POLLIN | POLLOUT; // 初始要读也要写实际可只加 POLLIN fds[j].revents 0; placed j; if (j max_fd_index) max_fd_index j; break; } } if (placed -1) { printf(连接池已满拒绝新连接\n); close(client_fd); continue; } printf(新连接 %d来自 %s:%d\n, client_fd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); } continue; } // 普通客户端可读 if (fds[i].revents POLLIN) { ssize_t n read(fds[i].fd, buffer, sizeof(buffer)); if (n 0) { if (n 0 (errno EAGAIN || errno EWOULDBLOCK)) { // 非阻塞下无数据可读忽略 } else { // 对端关闭或出错 printf(客户端 %d 断开\n, fds[i].fd); close(fds[i].fd); fds[i].fd -1; } } else { printf(收到 %zd 字节%s\n, n, buffer); // 回显直接写回客户端 write(fds[i].fd, buffer, n); } } // 可写事件这里只在回显后立即写入一般不手动关注 if (fds[i].revents POLLOUT) { // 可以在这里处理发送缓冲本范例暂不额外处理 } } } close(listen_fd); return 0; }4.3 代码中的一些设计细节fd 为 -1 表示空闲槽位这样 poll 会忽略该元素同时也方便我们自己遍历跳过比维护一个“有效列表”简单。事件回归化处理每次把新客户端加入时events设为POLLIN | POLLOUT。这里我故意这么做是为了展示POLLOUT的陷阱——POLLOUT表示发送缓冲区可写对于空数据未发送的 socket发送缓冲区基本上是满的所以POLLOUT会持续就绪导致poll每次都会返回大量“可写”事件占用 CPU。在实际生产代码中除非你确实有数据要发送且发送缓冲区已满否则不要监听POLLOUT。这个范例里注释也标了你运行时如果发现 CPU 占用高多半就是这个原因。accept 循环监听 fd 是非阻塞的一次accept可能只完成一个连接如果同时来了多个连接需要循环accept直到返回EAGAIN。这是非阻塞 socket 的标准处理方式可以防止连接数多时“惊群”式地只处理一个。max_fd_index 优化遍历时只遍历到max_fd_index避免遍历整个数组。当某个高下标槽位被关闭时我们没有缩减max_fd_index这会导致继续遍历几个空闲槽位但代价很小。如果想极致优化可以在遍历时顺便把最后的有效元素搬移但代码会复杂。我建议新手先这样能跑通再说优化。4.4 测试方法与预期输出编译运行gcc poll_server.c -o poll_server ./poll_server 8080另开终端测试# 测试连接 1 nc localhost 8080 # 或 telnet localhost 8080输入几行字符服务器会回显。再开几个终端多开几个连接观察服务器日志能看到新连接打印。测试POLLHUP直接按 CtrlD 或输入quit关闭 nc服务器应打印“客户端断开”。测试峰值可以写个脚本并发发起几万个连接观察服务器是否稳定、pollCPU 占用情况。但注意MAX_FDS1024限制了最大连接数实际生产环境这个值应根据需求设置高一些比如 65535。5. poll 实战中的坑与对策5.1 水平触发LT下的事件粘连问题poll是水平触发的Level Triggered。意味着只要某个 fd 还处于可读状态内核缓冲区有数据每次poll返回时它都会上报POLLIN。这不是 bug而是一个需要你去适配的行为。带来的问题如果你处理事件时没有把数据全部读完比如你只读了一个字节那么下次poll还会继续上报可读。对 echo 服务器来说我们可以一次性读完但如果应用层协议需要半包处理比如要攒满一条消息才能处理那么你读了一部分留下的部分下一次依然能读到——这其实是好事保证了不会丢数据。反之POLLOUT的水平触发特性更让人头疼只要发送缓冲区有空闲POLLOUT一直触发。所以绝不能一上来就监听POLLOUT而应该在需要发送但write()返回EAGAIN时才临时把POLLOUT加入events等数据发送完毕再移除。5.2 revents 判读顺序先处理错误再处理普通事件poll返回的revents可能同时包含POLLIN | POLLERR所以判断顺序很关键。我的经验是先看POLLNVAL—— fd 非法直接关闭没有任何读取的必要。再看POLLERR—— socket 出错了Read 大概率也会返回 -1你把错误流程走一遍。再看POLLHUP—— 对方关闭这时read()会返回 0EOF你可以安全关闭。最后看POLLIN和POLLOUT。如果你顺序反了先处理POLLIN可能read()返回 -1 或 0逻辑上也能走通但遇到某些边缘情况比如POLLNVALread()会报EBADF处理起来很不优雅。统一先处理异常事件代码更稳。5.3 关闭 fd 与数组清理别留下悬挂指针当你关闭一个 fd 并把它对应的fds[i].fd设为 -1 时可以保证后续poll忽略它。但如果某次事件处理中你在for循环内关闭了当前 fd 并continue没问题可如果你在循环中提前返回了比如某个函数内部出错了就可能漏掉清理导致 fd 泄漏。我建议封装一个remove_fd(int i)函数专门做关闭和清槽static void remove_fd(int i, struct pollfd *fds) { if (fds[i].fd ! -1) { close(fds[i].fd); fds[i].fd -1; fds[i].events 0; fds[i].revents 0; } }这样所有关闭行为统一走一个入口不容易漏。还有一个细节close()只是使 fd 不再有效但之前可能还有数据残留在内核 buffer 中。如果你希望发送完剩余数据再关闭需要调用shutdown(fd, SHUT_WR)并等待POLLIN返回 0 或超时后再 close。这个对于大流量高可靠服务尤其重要我吃过亏直接 close 导致客户端收到了 RST 而不是 FIN数据被丢弃。5.4 扩容问题别再吃 FD_SETSIZE 的亏了但你得有更大的数组虽然poll不需要FD_SETSIZE了但你自己定义的MAX_FDS仍然是个上限。如果连接数超过这个值服务器会拒绝新连接。在生产项目中我建议用动态数组存储struct pollfd当空闲槽位不足时realloc扩容到两倍。这里要注意扩容之后之前拿到的pfd指针如果把数组地址传给别处保存了会失效所以任何时候都只应该通过当前数组变量去访问元素不要事先保存一个指向数组中间元素的指针。也可以用链表数组混合但纯数组就够了。pollfd本身很小10 万个也就不到 2MB 内存一次分配到位往往更简单。6. 下一代选择从 poll 到 epoll为什么我不建议超人一上来就 epoll6.1 poll 的短板拷贝与扫描前面原理部分提到过poll 每次调用都要把整个数组在用户态和内核态之间拷一遍。连接数 1 万时数组大小是struct pollfd× 10000大约 16 万字节拷贝成本不算大但积少成多。更致命的是无论活跃多少连接poll返回后你都要线性扫描整个数组。如果 1 万个连接只有 3 个活跃你也要扫 1 万个。这在大规模网关服务器上会被放大成明显的 CPU 开销。6.2 epoll 的改进思路epoll在核心层面做了三件事内核维护事件表通过epoll_ctl动态注册或修改 fd而不是每次调用时重新传入全套 fd。红黑树管理 fd插入、删除、更新都是 O(log n) 或 O(1) 级操作。就绪链表只有发生事件的 fd 会被放入就绪链表内核返回时直接把链表拷贝到用户态不再需要线性扫描。所以 epoll 非常适合“连接很多活跃很少”的场景。如果你的服务器连接数只有几百用 poll 和 epoll 性能差异不大不必盲目追求 epoll 的复杂接口。6.3 学习 poll 的最终意义我见过不少人直接跳过 poll 从 select 跳到 epoll结果连“水平触发”和“边缘触发”都分不清也不知道内核回调是干什么的。这些概念在 poll 里体现得很朴素学完 poll 再看 epoll你会理解 epoll 究竟优化了什么而不是照葫芦画瓢地调用 API。事实上很多高性能框架内部还会实现自己的事件驱动抽象层在不同平台选不同后端Linux 上用 epollBSD 上用 kqueueWindows 上用 IOCP。而 poll 作为 POSIX 标准接口在嵌入式、老系统、以及某些不支持 epoll 的环境中依然是主力。理解了 poll你拿到任何新的事件模型都能快速上手。7. 把 poll 玩出花多进程/多线程 poll 的注意点虽然我这篇文章讲的是单线程 poll但实际项目里你可能需要多进程或多线程来利用多核 CPU。多线程与 poll 搭配时有几个问题必须想清楚多个线程同时 poll 同一个 fd如果两个线程同时调poll且等待同一 fd事件到达时两个线程都会被唤醒都会去read()就会产生数据竞争。常规做法是“一个 fd 只归一个线程管”要么用线程池加锁要么用主线程 accept 后分发给子线程的队列。poll 返回后处理事件要防止阻塞事件循环单线程 poll 中如果某个回调函数执行了耗费大量时间的操作比如磁盘 IO、数据库查询会直接阻塞整个事件循环导致其他连接饥饿。解决思路是把耗时操作拆成异步任务、延后处理或者干脆用多线程去处理业务逻辑poll 只管网络事件的分发。信号与 poll如果程序里用到了signal请务必知道信号中断会让 poll 返回-1且errno EINTR。很多网络服务会定期用定时器信号所以EINTR处理是必须的。在我上面的示例代码里就做了continue处理实际项目中你还可以用self-pipe trick来统一处理信号和 I/O 事件把信号写入管道让 poll 去监听管道可读。这样信号发生时 poll 可以即时返回而不必依赖EINTR。8. 别忘了监控与调优poll 服务器的容量评估有人代码写好了却不知道自己的服务器能扛多少连接。我分享一个粗略的估算思路。内存角度每个 TCP 连接在内核都会占用一定的 socket 缓冲区默认接收缓冲区约为几十 KB发送缓冲区几十 KB。如果你连接数 5 万每个按 60KB 算光内核缓冲就要 3GB。所以调低 socket 缓冲往往是必要的在setsockopt里设置SO_RCVBUF、SO_SNDBUF到一个合理值。fd 数量ulimit -n默认常见值是 1024。要承载上千连接必须调大ulimit -n 65535这是软限制可以临时设置。永久修改要在/etc/security/limits.conf中写入或用 systemd 服务时在 unit 里加LimitNOFILE65535。很多人代码明明没问题但跑不到一半就 hang第一反应是 poll 出 bug其实是 fd 数超限。poll 的返回频率监听多连接时即使没有事件poll 也会因为被信号打断或超时而返回。如果你发现 CPU 占用高先检查是不是有大量POLLOUT被监听。这是最最常见的隐蔽坑你events里加了POLLOUT发送缓冲区一直有空poll 自然不停返回。我调试过一个项目最初以为是内核 bug结果就是这行引起的。验证手段strace -e poll -p pid可以实时看 poll 调用的返回和参数。如果每毫秒返回一次说明有某个 fd 一直就绪去查它的revents。再用ss -nt看连接状态排除一堆CLOSE_WAIT的连接占住 fd。8.1 一个小工具检查 pollfd 数组就绪事件分布如果你的服务器已经上了生产又不想重新编译可以临时用strace和gdb组合分析。比如strace中看到poll([{fd3, eventsPOLLIN}, {fd7, eventsPOLLIN}], 2, 1000)返回时括号里有reventsPOLLIN的信息。但 strace 输出对大型数组很冗长建议在排错时只保留关键连接。我习惯在代码里加条件日志当就绪事件数超过一个阈值时打印哪些 fd 有POLLERR或POLLHUP。这比事后排查要快太多。初学者一开始就要养成打日志的习惯不要等踩了坑再补。9. 从范例走向生产给你的 poll 服务器封一层壳回显服务器只是演示。真实项目中你需要协议解析在read()数据后按照你的应用协议比如 HTTP、自定义 TCP解析和处理。你可以把读到的数据存在每个连接独立的input_buffer中而不是每次用完就丢。这一步直接决定你的服务器是不是能处理半包、粘包。写缓冲把write()改成先写到用户态发送队列然后一次性在POLLOUT事件中 flush。这样避免一次write可能写不完的问题并且你真的需要动态把POLLOUT加进去。发送完毕后记得把events里的POLLOUT去掉。超时管理poll 的 timeout 可以设为 1000 毫秒每次循环检查所有连接的最后活跃时间超过 60 秒没活动就关闭。这个功能很简单但能防住大量的僵死连接。这些延伸功能每一块都能单开一篇文章但核心的事件分发框架你已经有了poll 数组维护 事件遍历 事件处理。最后再分享一个小技巧我的pollfd数组在accept新连接时是按顺序填充的为了处理连接关闭后数组出现空洞我会在max_fd_index附近做“后补前挪”让新连接尽量用低位槽位。这样遍历时数组中即使有空槽也能很快跳过。代码量不大但对长连接服务器体验提升明显。我在实际项目里见过太多人动不动就上 epoll结果连 poll 的revents和POLLHUP这种基础都说不清。如果你想在 Linux 网络编程这条路上走得稳poll 是一道绕不过去的分水岭。把这个章节的代码用起来自己加几个连接跑一跑触发几个断连场景你才能真正理解它。到时候再看 epoll你会觉得豁然开朗。
返回列表