当前位置: 首页 > news >正文

非阻塞写的完整逻辑

第一部分:非阻塞写(Non-blocking Write)深度详解

1.1 为什么写操作会阻塞?

在网络编程中,当你调用write(sockfd, data, len)时,Linux 内核并不是直接把数据发到网络上,而是先把数据从用户空间拷贝到内核的 Socket 发送缓冲区(Send Buffer),然后由内核协议栈慢慢发出去。

阻塞场景:如果对端接收很慢,或者网络拥堵,导致发送缓冲区被填满了,此时再调用write(),默认行为是线程挂起(阻塞),直到缓冲区腾出空间。

非阻塞场景:如果 Socket 是非阻塞的,write()不会等,而是立刻返回-1,并设置errno = EAGAIN(或EWOULDBLOCK),意思是“缓冲区满了,你稍后再试”。

1.2 非阻塞写的核心难点:处理“部分写”

非阻塞写最麻烦的地方不是“写不进去”,而是“写了一半”。
比如你想发 10KB 数据,write()只成功写了 4KB 就返回了(因为缓冲区只剩 4KB 空间)。剩下的 6KB 怎么办?

解决方案

  1. 为每个 Socket 连接维护一个用户态发送缓冲区(比如std::stringstd::vector<char>)。
  2. 如果write()没写完,把剩下的数据存入这个缓冲区。
  3. 向 Epoll 注册EPOLLOUT事件(监听“可写”)。
  4. 当 Epoll 通知该 Socket 可写时,从缓冲区取出剩下的数据继续写。
  5. 如果全部写完,取消注册EPOLLOUT事件(否则 Epoll 会一直通知你可写,造成 CPU 浪费)。

第二部分:Epoll LT 与 ET 模式详解

Epoll 有两种工作模式,决定了它何时通知你以及通知你多少次

2.1 LT(Level Triggered,水平触发)—— 默认模式

特点:只要条件满足(缓冲区有数据可读 / 缓冲区有空可写),Epoll 就会不断地通知你,直到你处理完。

类比:就像门铃,只要你不把门打开,它就会一直响。

  • 读事件:只要输入缓冲区里有数据,每次epoll_wait都会返回这个事件。
  • 写事件:只要输出缓冲区有空余空间,每次epoll_wait都会返回这个事件。

优点:编程简单,不容易出错。
缺点:事件通知次数多,理论上效率比 ET 稍低(但在大多数场景下差异不大)。

2.2 ET(Edge Triggered,边缘触发)—— 高性能模式

特点:只有在状态发生变化的瞬间才通知你一次

  • 读事件:只有当新数据到来的那一刻(缓冲区从空变不空)才通知一次。如果你这次没把数据读完,Epoll 不会再通知你了,直到下次有新数据来。
  • 写事件:只有当发送缓冲区从满变不满的那一刻才通知一次。

类比:就像烟花,只响一次,错过了就没了。

强制要求:使用 ET 模式时,必须将文件描述符设置为非阻塞
编程要求

  • 读:收到通知后,必须循环read(),直到返回EAGAIN(确保把缓冲区读空)。
  • 写:如果有未发完的数据,收到通知后,必须循环write(),直到返回EAGAIN或全部发完。

第三部分:实战代码

我们将实现一个回显服务器(Echo Server):客户端发什么,服务器就发回什么。这是展示非阻塞读写和 Epoll 模式的最佳案例。

公共头文件与工具函数

首先,我们需要一些通用的辅助函数。

#include<iostream>#include<sys/epoll.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include<unistd.h>#include<fcntl.h>#include<errno.h>#include<string.h>#include<map>#include<string>#defineMAX_EVENTS1024#defineBUFFER_SIZE4096#defineSERVER_PORT8888// 1. 设置文件描述符为非阻塞intset_nonblocking(intfd){intflags=fcntl(fd,F_GETFL,0);if(flags==-1)return-1;flags|=O_NONBLOCK;returnfcntl(fd,F_SETFL,flags);}// 2. 创建并初始化监听 Socketintcreate_listen_socket(){intlisten_fd=socket(AF_INET,SOCK_STREAM,0);if(listen_fd==-1){perror("socket failed");return-1;}intopt=1;setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));structsockaddr_inaddr;bzero(&addr,sizeof(addr));addr.sin_family=AF_INET;addr.sin_addr.s_addr=htonl(INADDR_ANY);addr.sin_port=htons(SERVER_PORT);if(bind(listen_fd,(structsockaddr*)&addr,sizeof(addr))==-1){perror("bind failed");close(listen_fd);return-1;}if(listen(listen_fd,128)==-1){perror("listen failed");close(listen_fd);return-1;}set_nonblocking(listen_fd);// 监听 FD 也设为非阻塞returnlisten_fd;}// 3. 定义连接上下文:用于保存每个客户端的未发送完的数据structConnContext{intfd;std::string write_buffer;// 待发送数据的缓冲区};

示例一:Epoll LT 模式(水平触发)

这是最稳健、最容易写对的模式。

// ---------------- LT 模式主函数 ----------------voidrun_lt_server(){intlisten_fd=create_listen_socket();intepoll_fd=epoll_create1(0);// 注册监听 FD 到 Epoll (LT 模式,默认就是 LT)structepoll_eventevent;event.data.fd=listen_fd;event.events=EPOLLIN;epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_fd,&event);std::map<int,ConnContext*>contexts;// 管理所有连接的上下文structepoll_eventevents[MAX_EVENTS];std::cout<<"LT Server running on port "<<SERVER_PORT<<"..."<<std::endl;while(true){intn=epoll_wait(epoll_fd,events,MAX_EVENTS,-1);for(inti=0;i<n;++i){intsockfd=events[i].data.fd;// 1. 处理新连接if(sockfd==listen_fd){while(true){structsockaddr_inclient_addr;socklen_t len=sizeof(client_addr);intconn_fd=accept(listen_fd,(structsockaddr*)&client_addr,&len);if(conn_fd==-1){if(errno==EAGAIN||errno==EWOULDBLOCK)break;// 没有新连接了perror("accept failed");break;}std::cout<<"New client connected: "<<conn_fd<<std::endl;set_nonblocking(conn_fd);// 初始化上下文ConnContext*ctx=newConnContext();ctx->fd=conn_fd;contexts[conn_fd]=ctx;// 注册读事件 (LT 模式)event.data.ptr=ctx;// 这里用 ptr 携带上下文,比 fd 更方便event.events=EPOLLIN;epoll_ctl(epoll_fd,EPOLL_CTL_ADD,conn_fd,&event);}}// 2. 处理普通事件else{ConnContext*ctx=(ConnContext*)events[i].data.ptr;// 情况 A:可读事件if(events[i].events&EPOLLIN){charbuf[BUFFER_SIZE];// LT 模式下,这里可以不用 while 循环读(但建议还是读干净)// 因为如果没读完,epoll_wait 下次还会通知ssize_t bytes_read=read(ctx->fd,buf,sizeof(buf));if(bytes_read>0){// 收到数据,放入写缓冲区(模拟回显逻辑)ctx->write_buffer.append(buf,bytes_read);// 关键:因为有数据要发,我们需要监听 EPOLLOUT// 重新注册事件,同时加上 EPOLLOUTevent.data.ptr=ctx;event.events=EPOLLIN|EPOLLOUT;epoll_ctl(epoll_fd,EPOLL_CTL_MOD,ctx->fd,&event);}elseif(bytes_read==0){std::cout<<"Client disconnected: "<<ctx->fd<<std::endl;close(ctx->fd);deletectx;contexts.erase(sockfd);}else{if(errno!=EAGAIN){perror("read error");close(ctx->fd);deletectx;contexts.erase(sockfd);}}}// 情况 B:可写事件if(events[i].events&EPOLLOUT){if(!ctx->write_buffer.empty()){// 尝试发送数据ssize_t bytes_written=write(ctx->fd,ctx->write_buffer.c_str(),ctx->write_buffer.size());if(bytes_written>0){// 移除已发送的部分ctx->write_buffer.erase(0,bytes_written);}elseif(bytes_written==-1&&errno!=EAGAIN){perror("write error");}}// 如果缓冲区空了,取消监听 EPOLLOUT,避免空转if(ctx->write_buffer.empty()){event.data.ptr=ctx;event.events=EPOLLIN;// 只保留读事件epoll_ctl(epoll_fd,EPOLL_CTL_MOD,ctx->fd,&event);}}}}}close(listen_fd);}intmain(){run_lt_server();return0;}

示例二:Epoll ET 模式(边缘触发)

注意看代码中while循环的位置,这是 ET 模式的关键。

// ---------------- ET 模式主函数 ----------------voidrun_et_server(){intlisten_fd=create_listen_socket();intepoll_fd=epoll_create1(0);// 注册监听 FD 到 Epoll (注意加上 EPOLLET 标志)structepoll_eventevent;event.data.fd=listen_fd;event.events=EPOLLIN|EPOLLET;// <--- 关键:ET 模式epoll_ctl(epoll_fd,EPOLL_CTL_ADD,listen_fd,&event);std::map<int,ConnContext*>contexts;structepoll_eventevents[MAX_EVENTS];std::cout<<"ET Server running on port "<<SERVER_PORT<<"..."<<std::endl;while(true){intn=epoll_wait(epoll_fd,events,MAX_EVENTS,-1);for(inti=0;i<n;++i){intsockfd=events[i].data.fd;// 1. 处理新连接if(sockfd==listen_fd){// ET 模式下,accept 也必须用 while 循环!// 因为如果同时来 10 个连接,ET 只通知一次,必须一次性 accept 完while(true){structsockaddr_inclient_addr;socklen_t len=sizeof(client_addr);intconn_fd=accept(listen_fd,(structsockaddr*)&client_addr,&len);if(conn_fd==-1){if(errno==EAGAIN||errno==EWOULDBLOCK)break;perror("accept failed");break;}std::cout<<"New client connected: "<<conn_fd<<std::endl;set_nonblocking(conn_fd);ConnContext*ctx=newConnContext();ctx->fd=conn_fd;contexts[conn_fd]=ctx;// 注册读事件 (ET 模式)event.data.ptr=ctx;event.events=EPOLLIN|EPOLLET;// <--- ET 模式epoll_ctl(epoll_fd,EPOLL_CTL_ADD,conn_fd,&event);}}// 2. 处理普通事件else{ConnContext*ctx=(ConnContext*)events[i].data.ptr;boolerror=false;// 情况 A:可读事件if(events[i].events&EPOLLIN){// 【ET 核心】必须用 while 循环读,直到返回 EAGAIN// 因为只通知这一次,不读干净下次就没机会了(除非有新数据)while(true){charbuf[BUFFER_SIZE];ssize_t bytes_read=read(ctx->fd,buf,sizeof(buf));if(bytes_read>0){ctx->write_buffer.append(buf,bytes_read);}elseif(bytes_read==0){std::cout<<"Client disconnected: "<<ctx->fd<<std::endl;error=true;break;}else{if(errno==EAGAIN||errno==EWOULDBLOCK){break;// 数据读完了}perror("read error");error=true;break;}}// 如果有数据要发,注册 EPOLLOUTif(!ctx->write_buffer.empty()&&!error){event.data.ptr=ctx;// 注意:ET 模式下,EPOLLOUT 通常不需要一直注册// 只有当写缓冲区满了,没写完时,我们才需要注册它// 这里为了简化演示,我们先直接尝试写,写不完再注册// 但标准做法是:先尝试 write,如果返回 EAGAIN,再注册 EPOLLOUTevent.events=EPOLLIN|EPOLLOUT|EPOLLET;epoll_ctl(epoll_fd,EPOLL_CTL_MOD,ctx->fd,&event);}}// 情况 B:可写事件if(!error&&(events[i].events&EPOLLOUT)){// 【ET 核心】必须用 while 循环写,直到把缓冲区清空或返回 EAGAINwhile(!ctx->write_buffer.empty()){ssize_t bytes_written=write(ctx->fd,ctx->write_buffer.c_str(),ctx->write_buffer.size());if(bytes_written>0){ctx->write_buffer.erase(0,bytes_written);}elseif(bytes_written==-1){if(errno==EAGAIN||errno==EWOULDBLOCK){// 缓冲区又满了,这次写不完了,等下次 EPOLLOUT 通知break;}perror("write error");error=true;break;}}// 如果写完了,取消 EPOLLOUT (非常重要,节省资源)if(ctx->write_buffer.empty()&&!error){event.data.ptr=ctx;event.events=EPOLLIN|EPOLLET;epoll_ctl(epoll_fd,EPOLL_CTL_MOD,ctx->fd,&event);}}if(error){close(ctx->fd);deletectx;contexts.erase(sockfd);}}}}close(listen_fd);}intmain(){run_et_server();return0;}

总结与对比

特性LT (水平触发)ET (边缘触发)
通知机制只要数据没处理完,就一直通知只在状态变化时通知一次
编程复杂度简单,不易出错复杂,必须配合循环读写
性能稍低 (通知次数多)理论更高 (减少系统调用)
FD 要求阻塞/非阻塞均可必须非阻塞
适用场景通用,业务逻辑复杂高性能,连接数极多 (如 C10K)

测试方法

编译上述任一代码后,使用telnetnc(netcat) 进行测试:

nclocalhost8888
http://www.jsqmd.com/news/565662/

相关文章:

  • 2026年天津学面点/学西点蛋糕/学烹饪技术公司推荐:天津新东方职业培训学校,初中毕业学技术优选 - 品牌推荐官
  • 告别重复查询:用快马ai为solidworks工程师定制效率提升工具
  • site指令实战:精准定位与高效屏蔽的搜索艺术
  • A实验:AI人工智能T型迷宫 AI人工智能T迷宫组成资料。
  • 2026年电网储能系统厂家推荐:江苏阿诗特能源科技,工商业/户用储能及储能逆变器等全系产品解析 - 品牌推荐官
  • ARM Cortex-M嵌入式通用头文件sarmfsw深度解析
  • Qwen3-14B可信AI实践:输出可解释性分析、偏见检测与校准方法
  • 聊聊专业的酶标仪厂家,南京德铁的酶标仪价格贵不贵? - 工业推荐榜
  • 渗透测试信息收集指南,信息收集的关键步骤与技巧详解,网络安全渗透测试核心技巧你一定要知道!
  • 2026年酶标仪实力厂商排名,南京德铁以技术质量赢得市场 - 工业品牌热点
  • 2026年全国酶标仪供应商排名,南京德铁台式酶标仪靠谱又好用 - myqiye
  • 从数据转换到空间价值:togeojson的技术解构与实践指南
  • 2026最全 Java 面试题精选(附答案):Spring全家桶高频考点整理
  • Allegro PCB设计必备:3分钟搞定带钻孔数据的DXF文件导出(附常见错误排查)
  • 2026年杜兰小麦粉怎么选?资深面制品工厂教你3招避开采购坑 - 速递信息
  • DINO注意力可视化实战指南:3步掌握视觉Transformer内部机制
  • GodotPckTool 终极指南:轻松管理 Godot 游戏资源包的完整教程
  • ngx_http_create_locations_list
  • 佛山哪里能找到不会出现水纹烂斑的隔热条厂家 - 工业品牌热点
  • 用QMK固件打造你的专属宏键盘:从配置到实战案例
  • 2026年杜兰小麦粉排行:国产VS进口,谁更适合你的生产线? - 速递信息
  • Sonobuoy高级用例:工作负载调试与性能分析实战
  • 2026年铝镁锰板厂家排名,常州泰州靠谱的铝镁锰板制造商大盘点 - mypinpai
  • 洛谷 P2014:[CTSC1997] 选课 ← 有依赖的背包问题
  • PP-DocLayoutV3与.NET生态集成:开发C#桌面端文档处理工具
  • 旧Mac升级与macOS支持完全指南:开源系统优化工具实现老旧Mac焕新
  • Ubuntu系统资源监控实战:从命令行到图形化工具全解析
  • 2026年北京旅游服务公司Top10,含体育旅游活动的公司推荐 - mypinpai
  • 沈北汽车贴膜好去处:2026年口碑之选,汽车车衣/改色膜/汽车贴膜/隐形车衣/沈北车衣/车衣改色,汽车贴膜品牌联系方式 - 品牌推荐师
  • 如何用TradingAgents-CN构建AI驱动的智能投顾系统?从多智能体协作到实战交易决策