网络编程(5)—— Reactor实现(v3)

网络编程(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();
}

能跑,但有两个问题:

  1. 内部实现细节漏到了调用方 。"先 ready()、再构造 EventLoop、最后 loop()"这套顺序是服务器内部的约定,使用者不该知道,也不该有机会写错。
  2. 注册回调太啰嗦。三个回调要一个一个 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)。这些就是下一篇文章的内容。

相关推荐
枫叶丹41 小时前
从一次推理请求出发:模型、显存、网络与服务系统如何共同决定性能
网络·人工智能·chatgpt·开源·agent·codex
恒拓高科WorkPlus2 小时前
制造企业出现生产异常后,如何通过IM快速通知责任人?
网络·制造
程序猿老A2 小时前
从入门型到企业型:云服务器开放共享型到独享型规格升级
运维·服务器
Ruiery3 小时前
Linux 6.6内核 CPU 深度解析(九):时钟与 TSC — 内核怎么从 PIT/HPET 切到 TSC
linux·运维·服务器
打工仔折腾 AI4 小时前
用UU远程把家里电脑变成AI Agent常驻服务器:CLI、端口映射与代理实测
运维·服务器·人工智能·后端·python·电脑·ai agent 实战
熊文豪4 小时前
KingbaseES 数据加密全解:从 SSL 到全密态
网络·网络协议·ssl
91刘仁德4 小时前
Linux网络编程从入门到实战:UDP/TCP协议与socket编程全解析
linux·网络·笔记·tcp/ip·udp
guo_wen_qiang4 小时前
云服务器nacos搭建-集群&持久化
java·运维·服务器