网络编程(5)------ Reactor实现(v3)
回顾
前面两个版本我们做了两件事:
- v1 :把
socket / bind / listen / accept / read / write这些系统调用用 C++ 的类封装了一遍(Socket、InetAddress、Acceptor、SocketIO、TcpConnection); - v2 :封装了 epoll 的三个接口,写了
EventLoop这个"事件循环 + 事件分发"的核心,并给每条连接注册了三个回调------连接建立 、消息到达 、连接断开(TCP 的三个半事件里的三个,剩下半个是"消息发送完毕")。
v2 最后的测试代码长这样:
cpp
void test0()
{
Acceptor acceptor("0.0.0.0",8080);
acceptor.ready();
EventLoop eloop(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 + loop) |
stop() |
让事件循环退出 |
调用方从此只需要三行。
类图与设计思路
【图 1:Reactor_v3 类图】------ 在 v2 的类图上加一个
TcpServer,它组合(持有)Acceptor和EventLoop两个成员即可。
懒得画图的话,下面这段 mermaid 可以直接粘进 CSDN 的 mermaid 代码块:
#mermaid-svg-kPhDbdqnzjm6HB7H{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kPhDbdqnzjm6HB7H .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kPhDbdqnzjm6HB7H .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kPhDbdqnzjm6HB7H .error-icon{fill:#552222;}#mermaid-svg-kPhDbdqnzjm6HB7H .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kPhDbdqnzjm6HB7H .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kPhDbdqnzjm6HB7H .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kPhDbdqnzjm6HB7H .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kPhDbdqnzjm6HB7H .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kPhDbdqnzjm6HB7H .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kPhDbdqnzjm6HB7H .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kPhDbdqnzjm6HB7H .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kPhDbdqnzjm6HB7H .marker.cross{stroke:#333333;}#mermaid-svg-kPhDbdqnzjm6HB7H svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kPhDbdqnzjm6HB7H p{margin:0;}#mermaid-svg-kPhDbdqnzjm6HB7H g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-kPhDbdqnzjm6HB7H g.classGroup text .title{font-weight:bolder;}#mermaid-svg-kPhDbdqnzjm6HB7H .cluster-label text{fill:#333;}#mermaid-svg-kPhDbdqnzjm6HB7H .cluster-label span{color:#333;}#mermaid-svg-kPhDbdqnzjm6HB7H .cluster-label span p{background-color:transparent;}#mermaid-svg-kPhDbdqnzjm6HB7H .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kPhDbdqnzjm6HB7H .cluster text{fill:#333;}#mermaid-svg-kPhDbdqnzjm6HB7H .cluster span{color:#333;}#mermaid-svg-kPhDbdqnzjm6HB7H .nodeLabel,#mermaid-svg-kPhDbdqnzjm6HB7H .edgeLabel{color:#131300;}#mermaid-svg-kPhDbdqnzjm6HB7H .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-kPhDbdqnzjm6HB7H .label text{fill:#131300;}#mermaid-svg-kPhDbdqnzjm6HB7H .labelBkg{background:#ECECFF;}#mermaid-svg-kPhDbdqnzjm6HB7H .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-kPhDbdqnzjm6HB7H .classTitle{font-weight:bolder;}#mermaid-svg-kPhDbdqnzjm6HB7H .node rect,#mermaid-svg-kPhDbdqnzjm6HB7H .node circle,#mermaid-svg-kPhDbdqnzjm6HB7H .node ellipse,#mermaid-svg-kPhDbdqnzjm6HB7H .node polygon,#mermaid-svg-kPhDbdqnzjm6HB7H .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kPhDbdqnzjm6HB7H .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-kPhDbdqnzjm6HB7H g.clickable{cursor:pointer;}#mermaid-svg-kPhDbdqnzjm6HB7H g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-kPhDbdqnzjm6HB7H g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-kPhDbdqnzjm6HB7H .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-kPhDbdqnzjm6HB7H .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-kPhDbdqnzjm6HB7H .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-kPhDbdqnzjm6HB7H .dashed-line{stroke-dasharray:3;}#mermaid-svg-kPhDbdqnzjm6HB7H .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-kPhDbdqnzjm6HB7H #compositionStart,#mermaid-svg-kPhDbdqnzjm6HB7H .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kPhDbdqnzjm6HB7H #compositionEnd,#mermaid-svg-kPhDbdqnzjm6HB7H .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kPhDbdqnzjm6HB7H #dependencyStart,#mermaid-svg-kPhDbdqnzjm6HB7H .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kPhDbdqnzjm6HB7H #dependencyStart,#mermaid-svg-kPhDbdqnzjm6HB7H .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kPhDbdqnzjm6HB7H #extensionStart,#mermaid-svg-kPhDbdqnzjm6HB7H .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kPhDbdqnzjm6HB7H #extensionEnd,#mermaid-svg-kPhDbdqnzjm6HB7H .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kPhDbdqnzjm6HB7H #aggregationStart,#mermaid-svg-kPhDbdqnzjm6HB7H .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kPhDbdqnzjm6HB7H #aggregationEnd,#mermaid-svg-kPhDbdqnzjm6HB7H .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kPhDbdqnzjm6HB7H #lollipopStart,#mermaid-svg-kPhDbdqnzjm6HB7H .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kPhDbdqnzjm6HB7H #lollipopEnd,#mermaid-svg-kPhDbdqnzjm6HB7H .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-kPhDbdqnzjm6HB7H .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-kPhDbdqnzjm6HB7H .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kPhDbdqnzjm6HB7H .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kPhDbdqnzjm6HB7H .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kPhDbdqnzjm6HB7H :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 组合
组合
引用
TcpServer
-Acceptor _acceptor
-EventLoop _loop
+TcpServer(ip, port)
+start()
+stop()
+setAllCallback(cb1, cb2, cb3)
Acceptor
-Socket _sock
-InetAddress _addr
+ready()
+accept() : int
+fd() : int
EventLoop
-int _epfd
-vector<epoll_event> _evtList
-bool _isLooping
-Acceptor& _acceptor
-map<int,TcpConnectionPtr> _conns
+loop()
+unloop()
+setNewConnectionCallback(cb)
+setMessageCallback(cb)
+setCloseCallback(cb)
几个需要想清楚的点:
1)为什么 TcpServer 里持的是 Acceptor/EventLoop 的"对象",而 EventLoop 里持的是 Acceptor 的"引用"?
EventLoop 只是要借用 Acceptor 的 fd() 和 accept() 两个函数,它不负责 Acceptor 的生死,所以用引用(v2 里就是这么写的,也没必要改)。而 TcpServer 是这两个东西的拥有者 ,用值成员,靠 TcpServer 的生命周期去兜住它们,不需要自己写 new/delete。
2)成员声明顺序有讲究。
cpp
private:
Acceptor _acceptor; // 必须写在前面
EventLoop _loop; // _loop(_acceptor) 依赖它
C++ 的成员初始化顺序由声明顺序决定,跟初始化列表里写的顺序无关 。EventLoop 的构造函数体里要用 _acceptor.fd() 去注册 listenfd,如果 _loop 声明在前面,它就会先构造,此时 _acceptor 还没构造完,拿到的是未初始化的 fd------这是那种"偶尔能跑、换个编译器就炸"的 bug。写初始化列表的时候顺手对齐声明顺序,是个好习惯。
3)ready() 为什么放进 start() 里?
因为 ready() 决定的是"监听套接字什么时候真正可用",而"服务器启动"这件事的语义就是它。调用方不用再关心 bind/listen 的时机。
这里有个细节值得说一下:Acceptor 里的 Socket 是在构造时 就调 ::socket() 创建好 fd 的,bind/listen 是后面 ready() 才做的。所以 EventLoop 构造时把这个还没 listen 的 fd 注册进 epoll 是合法 的(epoll 对普通套接字一视同仁,只是它暂时不会可读),等 start() 里 ready() 执行完并 listen() 之后,新连接一来这个 fd 就会可读。整个顺序是安全的。
4)回调用右值引用传递。
setAllCallback 的参数是 Callback &&(也就是 function<void(const TcpConnectionPtr&)> &&),内部 std::move 转交给 EventLoop 的三个 setter,EventLoop 的 setter 也是右值引用------一路移动 ,不拷贝 std::function 内部的那块内存,也不把 EventLoop 自己的回调"搬空"。
代码实现
TcpServer.h
cpp
#ifndef __TCP_SERVER_H
#define __TCP_SERVER_H
#include "EventLoop.h"
using Callback = TcpConnectionCallback;
class TcpServer{
public:
TcpServer(const string & ip, unsigned short port);
~TcpServer();
void start();
void stop();
void setAllCallback(Callback && cb1, Callback && cb2, Callback && cb3);
private:
Acceptor _acceptor;
EventLoop _loop;
};
#endif
using Callback = TcpConnectionCallback; 只是给上层一个好念的名字------对使用者来说,回调就是"回调",不需要知道它内部是 std::function<void(const TcpConnectionPtr&)>。
TcpServer.cpp
cpp
/* ************************************************************************
> File Name: TcpServer.cpp
> Author: hyj
> mail: 1683958261@qq.com
> Created Time: Mon 03 Aug 2026 03:42:40 PM CST
> Description:
************************************************************************/
#include"TcpServer.h"
TcpServer::TcpServer(const string & ip, unsigned short port)
:_acceptor(ip,port)
,_loop(_acceptor){
}
TcpServer::~TcpServer(){
}
void TcpServer::start(){
_acceptor.ready();
_loop.loop();
}
void TcpServer::stop(){
_loop.unloop();
}
void TcpServer::setAllCallback(Callback && cb1, Callback && cb2, Callback && cb3){
_loop.setNewConnectionCallback(std::move(cb1));
_loop.setMessageCallback(std::move(cb2));
_loop.setCloseCallback(std::move(cb3));
}
start() 里就两步:先把监听套接字准备好,再进事件循环。loop() 是阻塞的,所以 start() 之后的代码要等服务器停下来才会执行------这一点在下面说 stop() 的时候会用到。
Test.cpp(测试文件)
cpp
#include"TcpServer.h"
#include<iostream>
using std::cout;
using std::endl;
void onNewConnection(const TcpConnectionPtr & con){
cout << con->toString() << " connected!" << endl;
}
void onMessage(const TcpConnectionPtr & con){
string msg = con->receive();
cout << "recv msg from client:" << msg <<endl;
msg = "msg:"+msg;
con->send(msg);
}
void onClose(const TcpConnectionPtr & con){
cout << con->toString() << " closed " << endl;
}
void test0()
{
TcpServer server("0.0.0.0",8080);
server.setAllCallback(onNewConnection,onMessage,onClose);
server.start();
}
int main()
{
test0();
return 0;
}
跟 v2 的测试代码对比一下:少了一行 accept、少了一次 ready()、少了三次 set,而且再也不可能写错顺序了。这就是封装的意义------不是少打字,是让错误变得不可能。
编译与运行
bash
g++ *.cpp -o server -std=c++11 -llog4cpp -lpthread
./server
另开两个终端,用 nc(或者前几篇文章里自己写的客户端)连上来看:
bash
nc 127.0.0.1 8080
hello
msg:hello
服务器端:
>>recv msg from client: hello
>>recv msg from client: world
【图 2:两个客户端同时连接、服务器日志截图】
功能上跟 v2 完全一样------v3 是重构,不是加功能。别指望这里有性能提升,它只是把代码的"边界"划清楚了。
关于 stop() 的一个说明
cpp
void TcpServer::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 里做的是"读数据库""算一个大矩阵""写文件"这类耗时操作,那么在它执行完之前,整个反应堆是停摆的------所有其它连接的读写都要排队等它。
cpp
void onMessage(const TcpConnectionPtr & con){
string msg = con->receive();
// 如果这里 sleep(1) 或者查一次数据库......
// 这 1 秒里,所有其它客户端都是"卡住"的
con->send("msg:" + msg);
}
这就极大的降低了我们服务器的性能,所以下一版要解决的是:把"计算"从事件循环里剥出去,交给线程池。
【图 3:v3 串行处理 vs v4 计算/IO 分离的示意图】
但事情没那么简单------业务线程算完要回发数据,如果它直接 send(),就会和事件循环线程同时往同一个 fd 上写;而且它还得知道"这条连接是不是已经被关掉了"。所以 v4 除了线程池,还要给 EventLoop 加一套跨线程投递任务 + 唤醒 的机制(runInLoop + eventfd)。这些就是下一篇文章的内容。
