epoll与IO多路复用详解:从阻塞IO到高并发服务器的完整实战

发布时间:2026/10/13 4:09:36
epoll与IO多路复用详解:从阻塞IO到高并发服务器的完整实战 不卖关子直接聊正事。网络编程这块如果让新手列一个最头疼的清单多路复用和IO模型肯定排前几。上一弹我们讲完了socket的基础流程socket()、bind()、listen()、accept()、send()/recv()这一套下来能跑通一个客户端连服务端的简单Demo。但那只算入门真到线上环境阻塞IO和高并发这两个坎儿直接卡死一大半人。这一弹的内容核心就三件事怎么控制IO的等待时间、怎么用多路复用处理大量连接、以及一套拿得出手的epoll实战写法。适合刚把socket流程跑通、想进一步接触高并发的人也适合项目里被连接数困扰、想重构网络层的同学。1. 先想清楚为什么阻塞IO撑不住高并发很多人写网络程序第一个能跑通的模型就是“一次accept然后recv等数据”。这种方式在连接数少、交互不频繁的场景下没问题但一旦连接多了立刻原形毕露。1.1 阻塞IO的致命缺陷阻塞IO的核心问题是一个线程在一个时刻只能等一个文件描述符。当程序执行到recv()时如果对面没发数据整个线程就挂在那里啥也干不了。如果你用“一个连接一个线程”的模型那1000个连接就是1000个线程这还不算完——线程切换的时间、内存占用、锁竞争都会把服务器拖垮。我做过一个模拟项目X早期版本就是这种模型。压测到200个连接时CPU使用率冲到90%以上但吞吐量低得可怜。用top看大部分CPU都耗在线程切换上真正干活的没多少。后来我深刻明白一个道理高并发不是靠多线程堆出来的而是让有限的线程去服务尽可能多的连接。1.2 非阻塞IO的引入非阻塞IO就是解决这个问题的初步手段。你把socket设为非阻塞recv()调用后如果没数据不会傻等而是直接返回错误码通常是EAGAIN或EWOULDBLOCK。这样线程不会被卡住可以去处理其他事情。但这就带来一个新问题你什么时候知道某个连接上有数据可读你不能凭空猜也不能一轮一轮地轮询所有连接那样CPU消耗依然很大。于是就有了多路复用select、poll、epoll。它们做的事情本质上是一样的——帮你盯着所有关注的fd当某个fd就绪时告诉你“可以处理了”。2. 超时控制别让程序无限期傻等在进入多路复用之前先说一个看似简单但坑很多的问题超时控制。阻塞IO下recv()可能一直等不到数据如果对端异常断连程序就永久卡死。所以网络编程里任何可能导致无限等待的操作都必须加超时。2.1 通过socket选项设置超时Linux提供了两个socket选项SO_RCVTIMEO和SO_SNDTIMEO分别控制接收和发送超时。使用方式struct timeval tv; tv.tv_sec 5; tv.tv_usec 0; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, tv, sizeof(tv));设置之后recv()如果在5秒内没有收到数据就会返回-1errno是EAGAIN。这个方案的好处是简单、粒度细针对单个fd缺点是你必须为每个连接单独设置而且超时后你只能自己处理后续逻辑。注意EAGAIN和EWOULDBLOCK在Linux上通常是同一个值但代码里两个宏都判断一下比较稳妥毕竟不同平台的处理不完全一致。2.2 更常用的多路复用的超时参数多路复用函数本身就带超时参数这也是我实际项目中更偏爱的方案。以select为例struct timeval timeout {5, 0}; // 5秒 int ret select(fd 1, readfds, NULL, NULL, timeout);select和poll的超时精度到毫秒级epoll_wait的timeout参数单位是毫秒。用多路复用的超时参数好处是当超时发生时你可以统一处理所有连接的状态比如发送心跳包、清理空闲连接而不用在每个连接上分别设置socket超时。2.3 超时时间设置的几个经验值超时值的具体设定跟业务场景强相关。这里分享我的几个实践参考连接建立超时一般3~5秒。太短容易在老网络环境下误判太长会堆积大量半连接。读超时如果协议是“请求-响应”模式读超时可设15~30秒。如果数据量大或者链路慢适当放宽。写超时写超时通常比读超时小5~10秒足够。写操作超时多半意味着对端接收窗口满了或者连接已经异常。注意超时时间不是越小越好。设置过短正常处理稍慢的业务会被迫中断设置过长故障检测的速度跟不上用户就会卡在等待里。建议根据业务接口的P99耗时来推算给接口耗时上限留出至少2~3倍的余量。3. 多路复用三兄弟select、poll、epoll怎么选多路复用这个词听着高大上理解它的本质只需一句话我们让内核帮我们检查一堆fd是否有事件发生有的话告诉我。三兄弟的效率差异和适用场景差别很大选错了后面会很难受。3.1 select简单但上限太低select是资历最老的多路复用函数核心是把关注的可读、可写、异常fd集合传给内核内核检测后返回就绪的fd。问题也很明显fd上限被FD_SETSIZE限制默认是1024。1024对现代服务器来说太小了动辄上万连接的需求直接不满足。每次调用都要把fd集合从用户态拷贝到内核态调用频率越高拷贝开销越大。内核检测fd就绪是线性扫描复杂度O(n)fd一多哪怕只有几个就绪也要遍历完整个集合。select适合的场景连接数不多几十个、逻辑足够简单的小工具。说句实话我工作这些年在生产环境已经很少见到select作为主力了更多是教学场景或嵌入式环境下使用。3.2 poll突破了上限但问题依旧poll可以看作select的改进版用pollfd数组代替fd集合突破了1024的限制。但O(n)扫描的低效、每次调用重新传数组的拷贝开销本质上没解决。poll的额外好处是水平触发LT模式上处理起来比较直观只要fd就绪每次poll都会返回不会丢事件。poll适合连接数在几千左右的场景如果数量不大、没有超高性能要求poll简单粗暴反而容易维护。3.3 epoll高并发下的主流答案epoll是Linux专属的高性能IO多路复用方案。跟select/poll最大的差异在于事件通知机制不是每次调用都要把fd列表传进去而是先用epoll_ctl注册fd之后epoll_wait专门等事件。epoll的核心优势有三点复杂度从O(n)降到O(1)注册进去的fd由内核红黑树管理就绪的fd通过回调机制挂到就绪链表上。epoll_wait只把就绪链表里的fd取出来跟总fd数量无关。拷贝开销大幅降低fd的信息在注册时就已经进入内核空间不需要每次调用都重复拷贝。两种触发模式水平触发LT和边沿触发ET。LT模式下只要fd还有数据没读完epoll_wait就会持续返回该fdET模式下只有状态发生变化比如从无数据变为有数据时才通知一次。这个区别是很多bug的源头后面我会详细展开。3.4 三兄弟对比速查表特性selectpollepoll支持系统Linux/Windows等通用Linux/Windows部分版本Linux专用fd上限FD_SETSIZE(默认1024)受系统内存影响受系统文件句柄限制触发模式水平触发水平触发水平边沿复杂度O(n)O(n)O(1)是否支持注册fd否每次重新传否每次重新传是红黑树常驻内核生产环境使用建议小型工具/教学几千连接级服务高并发核心服务4. epoll实战从接口到完整代码前面讲了一堆原理真正动手才是硬道理。下面这份代码是从某个跨平台系统的网络模块简化过来的展示了epoll的核心使用方法。4.1 epoll三件套create、ctl、waitepoll的使用就是三个函数epoll_create1、epoll_ctl、epoll_wait。极简示例// 1. 创建epoll实例 int epfd epoll_create1(EPOLL_CLOEXEC); if (epfd -1) { perror(epoll_create1); return -1; } // 2. 注册一个fd struct epoll_event ev; ev.events EPOLLIN; // 关注可读事件 ev.data.fd listen_fd; // 需要携带的私有数据这里先用fd本身 epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, ev); // 3. 等待事件 #define MAX_EVENTS 64 struct epoll_event events[MAX_EVENTS]; int n epoll_wait(epfd, events, MAX_EVENTS, -1); // -1表示永久阻塞 for (int i 0; i n; i) { // 处理events[i] }这里有几点要注意都是以前踩过的坑epoll_create1(EPOLL_CLOEXEC)比老式的epoll_create(0)更推荐多了一个CLOEXEC标志避免fork子进程后意外继承epoll fd。epoll_ctl有三个操作码EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL。重复ADD会出错删除之后再ADD又得重新填结构体。events数组的大小决定了单次epoll_wait最多能取回多少就绪事件一般设置成与连接数相近的值但也不必过大64~256在多数场景够用。如果单批处理不过来剩下的下次继续取。4.2 完整的事件循环骨架一个典型的epoll服务器长这样int main() { int listen_fd make_socket_and_listen(8080); // 非阻塞 set_nonblocking(listen_fd); int epfd epoll_create1(EPOLL_CLOEXEC); struct epoll_event ev, events[256]; ev.events EPOLLIN; ev.data.fd listen_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, ev); while (1) { int n epoll_wait(epfd, events, 256, -1); for (int i 0; i n; i) { int fd events[i].data.fd; if (fd listen_fd) { // 处理新连接 int conn_fd accept(listen_fd, NULL, NULL); if (conn_fd 0) continue; set_nonblocking(conn_fd); ev.events EPOLLIN | EPOLLET; // 边沿触发 ev.data.fd conn_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, ev); } else { // 处理普通连接的可读事件 handle_read(epfd, events[i]); } } } }4.3 非阻塞socket的标准设置epoll配合非阻塞IO是标配听我一句劝哪怕你打算用水平触发也请把socket设为非阻塞。原因很简单——万一某个事件触发了但你的业务逻辑里又调用了一个阻塞接口整个事件循环就堵死了后面所有fd都别想处理。设置非阻塞的标准姿势void set_nonblocking(int fd) { int flags fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK); }先用F_GETFL取现有标志再叠加O_NONBLOCK不要直接赋值覆盖否则会丢掉之前设置的标志位。4.4 边沿触发ET的正确处理姿势ET模式是epoll性能最好的模式但也是最容易写bug的模式。核心原因是ET模式下fd状态发生变化时才通知一次如果一次没把数据读完剩余数据不会再次触发通知。所以ET模式的接收循环必须一次性把数据读干净void handle_read(int epfd, struct epoll_event ev) { int fd ev.data.fd; char buf[4096]; ssize_t n; while (1) { n read(fd, buf, sizeof(buf)); if (n 0) { // 处理数据 } else if (n -1 errno EAGAIN) { // 数据读完了正常退出循环 break; } else if (n 0) { // 对端关闭清理fd epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); close(fd); break; } else { // 其他错误按连接异常处理 break; } } }关键点就在EAGAIN的处理。read()返回-1且errno为EAGAIN时说明socket缓冲区里的数据已经读空了这是退出循环的正常信号。判断n 0表示对端优雅关闭记得从epoll里删除并close。注意read()返回0和对端正常关闭并不完全是一回事但处理逻辑一样——关闭连接、释放资源。还有一种容易忽略的情况是EOF之前还有未读数据此时read返回0但缓冲区里可能有残留要结合协议层的数据完整性来判断是否需要处理。4.5 LT和ET应该怎么选这是很多人在选型上纠结的问题。我的建议非常直接第一次接触epoll或者业务逻辑复杂、不想趟坑用LT。它是默认模式数据没读完就会持续通知代码写起来省心不容易漏数据。追求性能、连接数巨大比如上万级别、并且你能保证每次事件触发都把数据处理完用ET。实际上性能差异在连接数不多时几乎感知不到。ET多出来的那点性能提升往往不如它带来的编码复杂度多。我见过太多人一上来就大张旗鼓上ET结果数据没读完导致分包错乱回头查半天才意识到是对模式理解有误。5. 高并发单线程模型一个线程如何扛住上万连接有一个很经典的疑问epoll明明只搞定了“监听多个fd”这一步但真正处理数据还是在单个线程里一个线程处理上万连接真的不会卡死吗5.1 事件循环的秘密快进快出关键思路是在事件循环里尽量不做耗时操作。每个fd的事件处理都应该像流水线一样能直接处理的就直接处理处理不了的就缓存起来等条件满足再继续。比如一个HTTP请求解析完请求头发现还没收完包体就把半截数据放到这个连接对应的缓冲里等下次可读事件来了接着解析。只要事件处理函数能在微秒级别内返回即使一个循环里同时有几百个就绪事件处理完一轮也不会太慢。这就是为什么单线程epoll能扛住高并发——它不靠并行靠的是切换速度快。5.2 计算密集型业务怎么办如果一个事件处理中需要执行耗时任务比如压缩图片、加解密、跑算法直接在事件循环里执行就会把后续所有连接堵住。这时候需要把任务丢到一个线程池里异步处理。线程池方案下epoll线程只做收包、解析协议、分发任务真正干重活的在线程池中。处理完成后线程池通过管道或eventfd向epoll发送一个唤醒信号把结果塞回写缓冲区让epoll线程统一执行发送。5.3 eventfd线程间通信与事件唤醒在线程池和epoll线程协作时eventfd是个好东西。它本质上是一个内核维护的计数器fd可以写入数据来唤醒epoll_waitint wake_fd eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); // 线程池完成处理后 uint64_t one 1; write(wake_fd, one, sizeof(one)); // 唤醒epoll_waitepoll主循环把wake_fd注册进去一旦线程池写入epoll_wait立即返回主线程就能去检查任务队列、执行后续发送。这种设计的巧妙之处在于避免了锁竞争和忙等唤醒成本极低。6. 高频踩坑点连接处理、EOF、惊群与fd耗尽下面这部分是真正值钱的都是我调试过的真实问题和方法论。每一条都有血泪教训在里面。6.1 accept之后必须判断是否该立即注册读事件新手常见的错误accept拿到新连接立刻注册EPOLLIN就等数据来。但有些连接上来后根本不发数据或者很久才发这个连接就一直挂在epoll里。更稳妥的做法是accept后先设置非阻塞然后注册一个EPOLLOUT或EPOLLIN事件根据业务决定合适的时机。比如有些服务会主动发送欢迎语那就在注册EPOLLOUT时顺手发送如果完全没有数据要下发就不必急着注册读事件。6.2 正确处理accept返回的EAGAIN在非阻塞模式下accept也可能返回EAGAIN。典型场景有连接到达但还没等你的代码accept另一个线程已经把它处理了。此时accept()会返回-1errno是EAGAIN或ECONNABORTED。正确姿势while (1) { int conn_fd accept(listen_fd, NULL, NULL); if (conn_fd 0) { if (errno EAGAIN || errno ECONNABORTED) { break; // 没有新连接了 } // 其他错误需要处理 continue; } // 处理这个conn_fd... }忽略EAGAIN可能导致accept陷入无限空转这点在多线程同时listen的场景下尤其突出。6.3 惊群问题多个线程/进程同时epoll_wait如果你用多个线程或进程同时epoll_wait同一个epoll fd一个连接到达时可能多个等待者同时被唤醒但只有一个能accept成功其余人白醒一场这就是惊群thundering herd。Linux 2.6之后通过内核机制已经解决了大部分场景但为了绝对稳妥还是建议只有一个线程负责listen和accept其他线程通过任务队列获取已连接的fd。如果要多进程分摊accept压力可以用SO_REUSEPORT让内核在多个监听socket之间做负载均衡。6.4 fd耗尽处理连接数上限默认情况下进程的文件描述符上限可能只有1024这就是为什么你明明注册了成千上万的连接程序却报“Too many open files”。需要调整两个地方# 调整系统级限制 sysctl -w fs.file-max100000 # 调整进程级限制临时 ulimit -n 100000需要永久生效就改/etc/security/limits.conf把nofile的硬限制和软限制都调大。调完之后记得重启进程因为ulimit对已启动的进程无效。还有一个容易忽略的点net.core.somaxconn控制listen队列的长度。如果并发连接数大但accept处理速度跟不上连接就会在等待队列里积压给客户端表现为连接超时。需要调大sysctl -w net.core.somaxconn1024同时listen()的backlog参数也要相应增大这个参数真正决定了单个socket的等待队列长度。6.5 半关闭状态与TIME_WAIT如果一个连接上收到了EOF但你还想往对端发送数据就需要用到shutdown(fd, SHUT_RD)只关闭读方向保留写能力。注意TCP协议下主动关闭连接的一方在连接close后socket会进入TIME_WAIT状态持续2*MSL通常约2分钟。服务器作为主动关闭方时大量TIME_WAIT会导致端口资源紧张。解决方法是代码层面尽量由客户端主动关闭连接或者设置SO_LINGER主动丢弃。系统层面设置net.ipv4.tcp_tw_reuse 1允许复用TIME_WAIT状态的连接只对主动发起连接一方有效。6.6 EPOLLIN和EPOLLOUT同时注册的副作用有些人为了减少epoll_ctl调用次数把EPOLLIN和EPOLLOUT一起注册。但实际运行时如果socket缓冲区一直有空闲EPOLLOUT会一直触发导致你的循环凭空多出大量无用唤醒。正确的做法是平时只关注EPOLLIN需要发送数据时再采用EPOLL_CTL_MOD临时加上EPOLLOUT数据发完再改回去。7. 一个完整的回显服务器可直接抄的作业说了这么多用一个完整的回显服务器把前面知识点串起来。这个Demo用单线程epollLT模式数据读多少回多少代码粒度小、逻辑清晰。#define _GNU_SOURCE #include stdio.h #include stdlib.h #include string.h #include unistd.h #include fcntl.h #include errno.h #include sys/epoll.h #include sys/socket.h #include netinet/in.h #include netinet/tcp.h #define PORT 8080 #define MAX_EVENTS 256 static void set_nonblocking(int fd) { int flags fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK); } int main(void) { int listen_fd socket(AF_INET, SOCK_STREAM, 0); if (listen_fd 0) { perror(socket); return 1; } int opt 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); struct sockaddr_in addr; 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)) 0) { perror(bind); return 1; } if (listen(listen_fd, 64) 0) { perror(listen); return 1; } set_nonblocking(listen_fd); int epfd epoll_create1(EPOLL_CLOEXEC); if (epfd 0) { perror(epoll_create1); return 1; } struct epoll_event ev, events[MAX_EVENTS]; ev.events EPOLLIN; ev.data.fd listen_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, ev); printf(echo server listening on port %d\n, PORT); while (1) { int n epoll_wait(epfd, events, MAX_EVENTS, -1); if (n 0) { if (errno EINTR) continue; // 信号中断重新等待 perror(epoll_wait); break; } for (int i 0; i n; i) { int fd events[i].data.fd; if (fd listen_fd) { // 处理新连接 while (1) { int conn_fd accept(listen_fd, NULL, NULL); if (conn_fd 0) { if (errno EAGAIN || errno ECONNABORTED) break; continue; } set_nonblocking(conn_fd); ev.events EPOLLIN; ev.data.fd conn_fd; epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, ev); } } else { // 可读读出所有数据再原样写回 char buf[4096]; ssize_t rn, wn, off 0; while (1) { rn read(fd, buf off, sizeof(buf) - off); if (rn 0) { off rn; continue; } if (rn 0) { // EOF关闭连接 epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); close(fd); break; } if (errno EAGAIN) { // 数据读完了写入返回 while (off 0) { wn write(fd, buf off, off); if (wn 0) { memmove(buf, buf wn, off - wn); off - wn; } else if (wn 0 errno EAGAIN) { continue; // 写缓冲区满等下次 } else { // 写失败异常关闭 epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); close(fd); off 0; break; } } off 0; break; } // 其他读错误 epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL); close(fd); break; } } } } return 0; }这份代码虽然简化了一些错误分支但在实际环境中跑起来是没问题的。你可以用nc或ab压测工具去连接测试。我调试这份代码时还发现一个有意思的坑如果发送的数据量远超接收缓冲区read会持续返回EAGAIN吗不会。它只会在缓冲区的数据快读完时才返回空闲。所以在写事件里稍微处理了一下避免缓冲区写满时死等。8. 调试技巧与工具如何肉眼定位问题代码写完之后真正痛苦的是调试。这里分享几个我常用的调试思路和工具。8.1 strace网络程序调试的第一选择strace能追踪进程的系统调用让你看到每一次read/write/epoll_wait的确切行为。使用方式strace -f -e tracenetwork,epoll,read,write -p 1234-f跟踪子进程-e trace指定要追踪的系统调用类型-p指定进程号。当你怀疑数据没有被正确读取、或者epoll没有触发时strace的输出能让你一眼看出问题所在。比如你发现epoll_wait一直返回但没有对应的accept调用那八成是epoll_ctl注册时出了错。8.2 ss和lsof检查连接状态排查连接问题最常用的两个命令ss -antp | grep 8080 lsof -i :8080ss显示当前连接状态lsof展示哪些进程占用了端口。如果你看到大量ESTABLISHED连接但程序没反应往往是accept速度跟不上或者事件循环里的处理逻辑阻塞住了。8.3 tcpdump Wireshark当问题在应用层排查不清时用tcpdump抓包看TCP层行为tcpdump -i any port 8080 -w echo.pcap抓到包之后用Wireshark打开能直接看到SYN、ACK、PSH、FIN这些标志位的时序。如果客户端发了数据但服务端一直不回ACK说明内核协议栈那一层就有问题如果ACK正常但应用层没有输出那个问题一定出在你的event loop里。8.4 善用日志连接生命周期记录法线上调试最有效的方法之一是给每个连接维护一个简单的状态机在关键节点打日志连接accept记录对端IP、端口、fd编号数据到达记录当前socket缓冲区里累积的数据量数据发送完成记录本次发送了多大数据连接关闭记录关闭原因、连接生命周期总长有了这些日志你再配合前端展示连接状态就能快速定位“数据哪去了”的问题。9. 性能调优进阶从能用走到好用最后聊聊性能调优的进阶方向。这部分没有标准答案但有一些经过验证的思路。9.1 一个线程真的够吗单线程epoll能扛住大量并发连接但扛不住多核CPU的计算能力。现代服务器动辄几十核单线程模型相当于只用了一颗核。如果你的服务是纯IO密集型的单线程也许够用但一旦涉及协议解析、业务计算建议采用线程池epoll混合模型。一个稳妥的架构是1个主线程负责accept、分发连接N个工作线程各自维护独立epoll实例处理被分发的连接这种模式下每个连接固定归某个线程管理天然避免锁竞争。连接按fd取模或哈希分配到线程实现很直接。9.2 SO_REUSEPORT多进程负载均衡如果嫌线程不够可以用多进程模型每个进程独立listen同一个端口。设置SO_REUSEPORT后内核会把新连接平分给各个进程。这样能充分利用多核而且某个进程崩溃不会导致整个服务挂掉。int reuse 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEPORT, reuse, sizeof(reuse));注意所有进程必须在listen之前就设置这个选项否则第二次bind会失败。9.3 零拷贝sendfile与splice如果你有大量静态文件需要发送比如文件服务器每次从内核缓冲区复制到用户态再写回等于白做两次拷贝。Linux提供sendfile()系统调用数据直接在内核空间从文件拷贝到socket不在用户态停留。ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);splice则能实现两个fd之间的数据重定向不经过用户态。这些零拷贝手段对性能提升非常显著尤其在大规模文件传输场景下CPU占用可能直接降一个数量级。9.4 读写缓冲区的合理设置socket内核缓冲区大小对性能影响也很大。默认的接收缓冲区可能才几十KB高吞吐场景下会造成频繁的通知和拷贝。可以通过SO_RCVBUF和SO_SNDBUF调大int rcvbuf 1024 * 1024; // 1MB setsockopt(fd, SOL_SOCKET, SO_RCVBUF, rcvbuf, sizeof(rcvbuf));注意内核会把这个值翻倍因为结构体开销并且有上下限不是你填多少就一定是多少。设置完后用getsockopt回读一下确认实际生效值。结尾的个人经验这一弹写到这里核心的知识点都覆盖了。我个人在实际项目里踩过一次大坑就是一开始太迷信ET模式结果数据没读完就退出循环导致客户端收到的响应被截断排查了一整天才发现是对ET语义理解不到位。后来我学乖了没有足够必要绝不轻易在核心链路上用ET模式性能差异远不像理论分析里那么大但编码复杂度却是实打实的。另外想提醒一点学习网络编程最好的方式是复现改造。找一份小而精的epoll服务端代码就像上面那份回显服务器先把它跑通然后逐步往里面加协议解析、连接池、超时机制、日志输出。当你把它改造成一个完整的业务服务时你对事件驱动模型的理解就真正到位了。最后小技巧分享在调试网络程序时先保证系统调用这一层不犯错再去看业务逻辑。我排查问题的顺序永远是strace看系统调用行为ss看连接状态tcpdump看数据包最后才去看业务代码。按这个套路来绝大多数疑难杂症都能在30分钟内定位到根因。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询