网络编程(5)—— Reactor实现(v3)
网络编程5—— Reactor实现(v3)回顾前面两个版本我们做了两件事v1把socket / bind / listen / accept / read / write这些系统调用用 C 的类封装了一遍Socket、InetAddress、Acceptor、SocketIO、TcpConnectionv2封装了 epoll 的三个接口写了EventLoop这个事件循环 事件分发的核心并给每条连接注册了三个回调——连接建立、消息到达、连接断开TCP 的三个半事件里的三个剩下半个是消息发送完毕。v2 最后的测试代码长这样voidtest0(){Acceptoracceptor(0.0.0.0,8080);acceptor.ready();EventLoopeloop(acceptor);eloop.setNewConnectionCallback(onNewConnection);eloop.setMessageCallback(onMessage);eloop.setCloseCallback(onClose);eloop.loop();}能跑但有两个问题内部实现细节漏到了调用方。先ready()、再构造EventLoop、最后loop()这套顺序是服务器内部的约定使用者不该知道也不该有机会写错。注册回调太啰嗦。三个回调要一个一个 set而这三个回调在语义上是一组的都属于一条连接的生命周期。顺带说一句仓库里还有一个4.Reactor_V2.2那是加回调之前的中间版本——只有一个光秃秃的事件循环不做任何回调。有兴趣可以翻着看本系列不展开。所以v3 只做一件事把服务器的组装封装成一个TcpServer类。这也正是 v2 结尾留下的那句话——“为了更好的使用可以对当前进行一次封装”。v3 的目标把Acceptor监听、accept和EventLoop事件循环组装起来对外只暴露三个接口接口作用setAllCallback(cb1, cb2, cb3)一次性注册三个回调start()让服务器跑起来内部完成 ready loopstop()让事件循环退出调用方从此只需要三行。类图与设计思路【图 1Reactor_v3 类图】—— 在 v2 的类图上加一个TcpServer它组合持有Acceptor和EventLoop两个成员即可。懒得画图的话下面这段 mermaid 可以直接粘进 CSDN 的 mermaid 代码块组合组合引用TcpServer-Acceptor _acceptor-EventLoop _loopTcpServer(ip, port)start()stop()setAllCallback(cb1, cb2, cb3)Acceptor-Socket _sock-InetAddress _addrready()accept() : intfd() : intEventLoop-int _epfd-vectorepoll_event _evtList-bool _isLooping-Acceptor _acceptor-mapint,TcpConnectionPtr _connsloop()unloop()setNewConnectionCallback(cb)setMessageCallback(cb)setCloseCallback(cb)几个需要想清楚的点1为什么TcpServer里持的是Acceptor/EventLoop的对象而EventLoop里持的是Acceptor的引用EventLoop只是要借用Acceptor的fd()和accept()两个函数它不负责Acceptor的生死所以用引用v2 里就是这么写的也没必要改。而TcpServer是这两个东西的拥有者用值成员靠TcpServer的生命周期去兜住它们不需要自己写new/delete。2成员声明顺序有讲究。private:Acceptor _acceptor;// 必须写在前面EventLoop _loop;// _loop(_acceptor) 依赖它C 的成员初始化顺序由声明顺序决定跟初始化列表里写的顺序无关。EventLoop的构造函数体里要用_acceptor.fd()去注册 listenfd如果_loop声明在前面它就会先构造此时_acceptor还没构造完拿到的是未初始化的 fd——这是那种偶尔能跑、换个编译器就炸的 bug。写初始化列表的时候顺手对齐声明顺序是个好习惯。3ready()为什么放进start()里因为ready()决定的是监听套接字什么时候真正可用而服务器启动这件事的语义就是它。调用方不用再关心 bind/listen 的时机。这里有个细节值得说一下Acceptor里的Socket是在构造时就调::socket()创建好 fd 的bind/listen 是后面ready()才做的。所以EventLoop构造时把这个还没 listen 的 fd 注册进 epoll 是合法的epoll 对普通套接字一视同仁只是它暂时不会可读等start()里ready()执行完并listen()之后新连接一来这个 fd 就会可读。整个顺序是安全的。4回调用右值引用传递。setAllCallback的参数是Callback 也就是functionvoid(const TcpConnectionPtr) 内部std::move转交给EventLoop的三个 setterEventLoop的 setter 也是右值引用——一路移动不拷贝std::function内部的那块内存也不把EventLoop自己的回调搬空。代码实现TcpServer.h#ifndef__TCP_SERVER_H#define__TCP_SERVER_H#includeEventLoop.husingCallbackTcpConnectionCallback;classTcpServer{public:TcpServer(conststringip,unsignedshortport);~TcpServer();voidstart();voidstop();voidsetAllCallback(Callbackcb1,Callbackcb2,Callbackcb3);private:Acceptor _acceptor;EventLoop _loop;};#endifusing Callback TcpConnectionCallback;只是给上层一个好念的名字——对使用者来说回调就是回调不需要知道它内部是std::functionvoid(const TcpConnectionPtr)。TcpServer.cpp/* ************************************************************************ File Name: TcpServer.cpp Author: hyj mail: 1683958261qq.com Created Time: Mon 03 Aug 2026 03:42:40 PM CST Description: ************************************************************************/#includeTcpServer.hTcpServer::TcpServer(conststringip,unsignedshortport):_acceptor(ip,port),_loop(_acceptor){}TcpServer::~TcpServer(){}voidTcpServer::start(){_acceptor.ready();_loop.loop();}voidTcpServer::stop(){_loop.unloop();}voidTcpServer::setAllCallback(Callbackcb1,Callbackcb2,Callbackcb3){_loop.setNewConnectionCallback(std::move(cb1));_loop.setMessageCallback(std::move(cb2));_loop.setCloseCallback(std::move(cb3));}start()里就两步先把监听套接字准备好再进事件循环。loop()是阻塞的所以start()之后的代码要等服务器停下来才会执行——这一点在下面说stop()的时候会用到。Test.cpp测试文件#includeTcpServer.h#includeiostreamusingstd::cout;usingstd::endl;voidonNewConnection(constTcpConnectionPtrcon){coutcon-toString() connected!endl;}voidonMessage(constTcpConnectionPtrcon){string msgcon-receive();coutrecv msg from client:msgendl;msgmsg:msg;con-send(msg);}voidonClose(constTcpConnectionPtrcon){coutcon-toString() closed endl;}voidtest0(){TcpServerserver(0.0.0.0,8080);server.setAllCallback(onNewConnection,onMessage,onClose);server.start();}intmain(){test0();return0;}跟 v2 的测试代码对比一下少了一行accept、少了一次ready()、少了三次set而且再也不可能写错顺序了。这就是封装的意义——不是少打字是让错误变得不可能。编译与运行g *.cpp-oserver-stdc11-llog4cpp-lpthread./server另开两个终端用nc或者前几篇文章里自己写的客户端连上来看nc127.0.0.18080hello msg:hello服务器端recv msg from client: hello recv msg from client: world【图 2两个客户端同时连接、服务器日志截图】功能上跟 v2 完全一样——v3 是重构不是加功能。别指望这里有性能提升它只是把代码的边界划清楚了。关于 stop() 的一个说明voidTcpServer::stop(){_loop.unloop();// 只是把 _isLooping 置成 false}unloop()只是改了个标志位而loop()正阻塞在epoll_wait我们设的超时是 3000ms里所以stop()生效最多要等 3 秒。而且在单线程模型里start()已经把线程占住了你根本没机会在同一条线程里调stop()——只能从其它线程、或者从回调里调。这算是个半成品的退出机制。等 v4 把多线程引进来EventLoop有了唤醒的能力这个问题顺手就解决了unloop()里加一次wakeup()让epoll_wait立刻返回。小结与遗留问题到这里服务器的骨架算是搭完了v1 封装系统调用 → v2 事件驱动 → v3 把骨架打包成一个好用的壳。但核心问题一个都没解决我们的服务器是串行执行的。事件循环是单线程的onMessage回调也在同一个线程里执行。如果onMessage里做的是读数据库“算一个大矩阵”写文件这类耗时操作那么在它执行完之前整个反应堆是停摆的——所有其它连接的读写都要排队等它。voidonMessage(constTcpConnectionPtrcon){string msgcon-receive();// 如果这里 sleep(1) 或者查一次数据库……// 这 1 秒里所有其它客户端都是卡住的con-send(msg:msg);}这就极大的降低了我们服务器的性能所以下一版要解决的是把计算从事件循环里剥出去交给线程池。【图 3v3 串行处理 vs v4 计算/IO 分离的示意图】但事情没那么简单——业务线程算完要回发数据如果它直接send()就会和事件循环线程同时往同一个 fd 上写而且它还得知道这条连接是不是已经被关掉了。所以 v4 除了线程池还要给EventLoop加一套跨线程投递任务 唤醒的机制runInLoopeventfd。这些就是下一篇文章的内容。

相关新闻

【计算机毕设】基于Hadoop+Spark的草鱼价格大数据分析与预测系统 基于大数据的草鱼价格异常检测与行情预警可视化系统

【计算机毕设】基于Hadoop+Spark的草鱼价格大数据分析与预测系统 基于大数据的草鱼价格异常检测与行情预警可视化系统

💕💕作者:计算机源码社 💕💕个人简介:本人八年开发经验,擅长Java、Python、PHP、.NET、Node.js、Spark、hadoop、Android、微信小程序、爬虫、大数据、机器学习等,大家有这一块的问题…

2026/10/5 2:27:37 阅读更多 →
车间光线忽明忽暗?合米科技 AI SOP 视觉防错凭借强光照鲁棒性稳定核验工序.

车间光线忽明忽暗?合米科技 AI SOP 视觉防错凭借强光照鲁棒性稳定核验工序.

摘要车间灯光杂乱、反光、昼夜亮度差异,是 AI 视觉落地的一大阻碍。深圳合米科技 AI SOP 视觉防错系统,采集多场景光照样本训练模型,算法拥有光照鲁棒性,不受明暗变化干扰,稳定完成工序核验,适配工厂复杂现…

2026/10/5 2:26:37 阅读更多 →
海外版外卖加盟:总站与分站配送规则怎么分开管

海外版外卖加盟:总站与分站配送规则怎么分开管

海外版外卖加盟系统里,总站与分站对配送规则的权限边界若不清,要么分站完全不能改(失去本地灵活),要么各自改无版本(总部无法仲裁)。宜「分站编辑、总部审批或留痕只读汇总」,并把 r…

2026/10/5 2:26:37 阅读更多 →

最新新闻

多普勒效应公式推导:从波前间距到雷达测速与天文红移

多普勒效应公式推导:从波前间距到雷达测速与天文红移

在站台等车的时候,你多半留意过这样一个细节:列车从远处鸣笛驶来,音调明显比它静止时更尖锐;等车从身边呼啸而过之后,音调又一下子低沉下去。这个高低变化,就是多普勒效应最直观的体现。教材里的标准定义很…

2026/10/5 3:03:50 阅读更多 →
C#调用百度OCR全流程:从Token获取到批量识别避坑指南

C#调用百度OCR全流程:从Token获取到批量识别避坑指南

简介:这是一个基于C#实现的百度OCR图像文字识别示例项目,面向有一定C#基础、希望快速接入百度AI开放平台OCR能力的开发者。项目完整展示如何申请API密钥、构造HTTP请求上传图像(支持URL与Base64编码)、设置语言等可选参数&#xf…

2026/10/5 3:03:50 阅读更多 →
神经气体网络与GNG网络:从K-means缺陷到自组织拓扑骨架实战

神经气体网络与GNG网络:从K-means缺陷到自组织拓扑骨架实战

从K-means的缺陷说起,聊聊神经气体网络和GNG网络到底在解决什么问题。如果你只是想把一堆散点分成几个簇,那K-means和层次聚类已经够用;但当你面对的是——数据形状不规则、类别数量未知、还要求保留拓扑结构时,聚类那套就撑不住了…

2026/10/5 3:03:50 阅读更多 →
从零搭建后端:数据库建模与REST API实操全记录

从零搭建后端:数据库建模与REST API实操全记录

这是「30天全栈开发挑战」系列的第三天。前两天的内容分别完成了环境准备和一个极简前端页面,很多朋友私信问我“第三天到底干什么”,今天的答案很明确:把数据库表建明白,把接口写能跑起来。文章会围绕一个演示项目——团队任务管…

2026/10/5 3:03:50 阅读更多 →
工业数据中心一体化选型:连接、监控、供配电如何保障长期稳定运行

工业数据中心一体化选型:连接、监控、供配电如何保障长期稳定运行

1. 为什么“稳定不间断”会卡住绝大多数数据中心方案工业数据中心和普通企业机房有一个本质区别:普通机房追求的是“够用”,工业数据中心追求的是“不间断”。这两个词听着差不多,实际操作中完全是两套设计哲学。普通机房每年计划性停机维护几…

2026/10/5 3:03:50 阅读更多 →
微软驱动签名新规:Windows 11开发者与用户应对指南

微软驱动签名新规:Windows 11开发者与用户应对指南

最近这半年,微软针对驱动签名出了好几条新规,做驱动开发的和装驱动机器上遇到问题的普通用户,两边都被折腾得不轻。开发圈子最先感受到变化——以前“用 EV 证书签完名就能装”的老套路开始失灵,系统在加载驱动时不再只看签名有没…

2026/10/5 3:02:49 阅读更多 →

日新闻

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

马斯克杀回智能体战场,Grok 4.5万亿参数撑腰,Cursor接手数字白领项目:用TaoToken统一Key跑通多模型Agent工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 0:00:22 阅读更多 →
AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

AI编程工具插件机制详解:plugin.json配置与加载失败排查指南

1. 从“plugins”这个词说起:它到底在解决什么问题如果你最近在折腾 AI 编程工具,尤其是 Cursor、Codex CLI、Claude Code 这类带 CLI 的编辑器或命令行助手,那你大概率绕不开一个词——plugins。这个词本身不新鲜,从浏览器到 IDE…

2026/10/5 0:00:23 阅读更多 →
第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

第26课:OpenClaw|日志审计与问题诊断:把日志链路改到 TaoToken 的排查清单

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 0:00:23 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/5 1:10:22 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 11:40:45 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 9:43:54 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 20:14:29 阅读更多 →