☰
从零实现 C++ Json-Rpc(十):把消息交给真实连接——MuduoConnection 与 ConnectionFactory
2026/10/28 1:58:06 网站建设 项目流程

目录

前言

一、有了 LVProtocol,为什么还需要连接层?

1.1 协议解决“发什么”,连接解决“从哪里发”

1.2 第五篇的 BaseConnection 正是为这一层准备的

二、MuduoConnection:一条真实连接在框架中的表示

2.1 它是适配器,不是另一套 TCP 实现

2.2 构造函数为什么需要两个参数?

2.3 两个智能指针,是否意味着连接永远有效?

三、send():两行代码如何跨过三层?

3.1 先看真实实现

3.2 为什么不能省掉协议,直接发送 msg->serialize()?

3.3 调用 send(),是否代表远端已经收到?

四、shutdown() 与 connected():连接还有两个基本操作

4.1 shutdown() 只负责转交关闭请求

4.2 connected() 直接取底层连接状态

五、ConnectionFactory:统一创建入口,而不是连接管理器

5.1 工厂到底解决什么问题?

5.2 创建时输入什么,输出什么?

5.3 Factory 并不负责建立网络连接

六、把这一层放回工程:创建与发送怎样接起来?

6.1 先从一条已经存在的 Muduo 连接开始

6.2 上层拿到 BaseConnection 后就可以发送消息

6.3 在源码中,谁会真正调用工厂?

6.4 当前实现的边界还要说清楚

写在最后


前言

前面我们已经把“消息是什么”和“消息怎样编码”这两件事解决了。

第七篇完成了BaseMessage对应的具体消息对象,第八篇补充了 JSON 与请求 ID 工具,第九篇则让MuduoBuffer和LVProtocol真正工作起来:既能从接收字节中恢复一条完整消息,也能把BaseMessage编码为符合项目约定的 LV 报文。

可是,LVProtocol::serialize()最后交给我们的仍然只是一个std::string。这串字节虽然已经准备好,但它究竟要从哪一条 TCP 连接发送出去呢?

这就是本篇要解决的问题:让上层只拿着BaseConnection这样的统一接口,就能把消息交给底层的 Muduo 连接发送。这次我们只关注一条已经存在的连接,不急着展开服务端如何接受连接、客户端如何发起连接,以及网络回调怎样触发。那些是下一层的工作。


一、有了 LVProtocol,为什么还需要连接层?

1.1 协议解决“发什么”,连接解决“从哪里发”

先回想第九篇发送方向的结果:

std::string bytes = protocol->serialize(msg);

这里的msg是BaseMessage::ptr,而bytes是完整的网络报文。它包含的是:

| total_len | mtype | idlen | id | JSON body |

其中total_len、mtype、idlen等字段的意义和编解码方式,前一篇已经解释过,这里不再重新推一遍。只需要记住:serialize()负责准备字节,但不负责选择网络连接。

要真正把字节交给网络,Muduo 提供了muduo::net::TcpConnection。如果我们只是在写一个小型网络 Demo,直接拿着TcpConnectionPtr调用send(),当然可以。

但现在我们做的是一个 RPC 框架。后面 RPC 请求、服务注册发现、Topic 发布订阅等模块都会涉及向某条连接发送消息。如果它们都直接保存和使用muduo::net::TcpConnectionPtr,上层就会被 Muduo 的具体接口绑定。

更麻烦的是,上层拿到的通常是BaseMessage消息对象,而 Muduo 的TcpConnection::send()接收的是可以发送的字节数据。两者之间始终缺了一步协议编码。

因此,我们希望上层只需要写:

base_conn->send(msg);

至于“先编码成 LV 报文,再交给具体 TCP 连接”,由连接适配层内部完成。

1.2 第五篇的BaseConnection正是为这一层准备的

第五篇定义的抽象接口很简单:

class BaseConnection { public: using ptr = std::shared_ptr<BaseConnection>; virtual ~BaseConnection() {} virtual void send(const BaseMessage::ptr &msg) = 0; virtual void shutdown() = 0; virtual bool connected() = 0; };

这三个操作分别回答三个问题:

接口上层想做什么具体网络细节由谁处理
send(msg)通过当前连接发送框架消息连接实现 + 协议实现
shutdown()请求关闭当前连接底层 TCP 连接
connected()查询当前连接的连接状态底层 TCP 连接

注意,BaseConnection没有规定必须使用 Muduo,也没有规定消息必须采用哪一种协议。它只是把上层真正需要的能力固定下来。

这样整个关系就变成:

上层 RPC / Topic / Service 模块 ↓ BaseConnection ↓ MuduoConnection ↓ Muduo 的 TcpConnection

对于上层来说,发送消息不再需要知道“TcpConnection的send()怎样调用”,只需要认识BaseConnection::send()。


二、MuduoConnection:一条真实连接在框架中的表示

2.1 它是适配器,不是另一套 TCP 实现

在当前工程的source/common/net.hpp中,我们定义了MuduoConnection:

class MuduoConnection : public BaseConnection { public: using ptr = std::shared_ptr<MuduoConnection>; MuduoConnection(const muduo::net::TcpConnectionPtr &conn, const BaseProtocol::ptr &protocol) : _conn(conn), _protocol(protocol) {} // send() / shutdown() / connected() 的实现见后文 private: BaseProtocol::ptr _protocol; muduo::net::TcpConnectionPtr _conn; };

一眼看上去,它不过是一个继承自BaseConnection的类,构造函数保存了两个指针。

但这两个成员刚好组成了本篇最关键的职责分工:

  • _protocol:知道消息应该怎样编码。实际持有的可以是此前通过ProtocolFactory创建的LVProtocol,但这里通过BaseProtocol::ptr使用。

  • _conn:知道字节应该交给哪条真实连接。它指向 Muduo 已经提供的一条TcpConnection。

因此,MuduoConnection不需要重新实现 TCP,也不需要自己计算total_len、处理mtype,它要做的只是把框架消息、协议编码与真实连接接起来。

从命名上也能看出区别:BaseConnection是项目规定的通用能力,MuduoConnection是使用 Muduo 落地的具体实现。

2.2 构造函数为什么需要两个参数?

构造函数接收:

MuduoConnection(const muduo::net::TcpConnectionPtr &conn, const BaseProtocol::ptr &protocol)

conn必须是一条已经由 Muduo 创建的连接对象。这个构造函数本身不会连接服务器,也不会开启监听。它只是把现有连接包装成框架能使用的形式。

protocol则告诉连接对象:“以后通过你发送BaseMessage时,应该使用哪一个协议对象完成编码。”

这也解释了为什么发送过程不应该把 LV 头部的拼接逻辑直接写在MuduoConnection里。连接层依赖抽象协议接口,具体编码工作仍由LVProtocol承担;如果把两个职责混在一起,第五篇建立的BaseProtocol抽象就失去了意义。

2.3 两个智能指针,是否意味着连接永远有效?

这里的BaseProtocol::ptr和muduo::net::TcpConnectionPtr都是共享智能指针类型。只要MuduoConnection继续保存它们,就仍持有对应对象的共享所有权。

但对象仍然存在,与网络连接当前仍处于 connected 状态是两回事。底层连接可能已经断开,这也正是connected()必须查询真实连接状态的原因。

当前实现假定创建时传入的连接和协议对象可以正常使用,没有在这几个成员函数中额外做空指针检查。这是需要认识的代码前提,而不能把它描述成“自动处理了所有异常状态”。


三、send():两行代码如何跨过三层?

3.1 先看真实实现

MuduoConnection::send()的完整核心逻辑只有两行:

void send(const BaseMessage::ptr &msg) override { std::string body = _protocol->serialize(msg); _conn->send(body); }

别因为它短,就把它理解成普通的“把字符串发出去”。这两行分别跨越了协议层与网络层。

第一行:

std::string body = _protocol->serialize(msg);

调用的是BaseProtocol的虚接口。当前具体实现为LVProtocol::serialize(),所以返回的是完整的 LV 网络报文,而不是单独的 JSON Body。

这里局部变量虽然叫body,但不要把它与消息内部的 JSONbody混为一谈。该变量实际上包含:

| total_len | mtype | idlen | id | JSON body |

也就是说,前一篇介绍的报文编码结果,到这里已经真正有了使用位置。

第二行:

_conn->send(body);

把编码好的报文字节交给当前muduo::net::TcpConnection。真正的 socket 写入、缓冲和事件循环交互仍由 Muduo 的网络实现负责,MuduoConnection本身没有直接操作 socket。

3.2 为什么不能省掉协议,直接发送msg->serialize()?

这要区分两种不同层次的serialize():

msg->serialize();

这是消息对象自己的序列化接口,得到的是业务正文对应的 JSON 字符串。

而:

_protocol->serialize(msg);

是协议层的序列化接口。它在消息正文之外,还会按项目约定加入total_len、mtype、idlen和请求 ID。

因此两者不能混用。否则接收端虽然可能看到一段 JSON,却没有足够的信息按照第九篇的LVProtocol::canProcessed()与onMessage()去判断边界和恢复消息对象。

对比一下就很清楚:

调用得到什么本篇是否直接交给 TCP
msg->serialize()消息的 JSON 业务正文否
_protocol->serialize(msg)包含协议头与正文的完整 LV 报文是

3.3 调用send(),是否代表远端已经收到?

不能这样理解。

当前BaseConnection::send()的返回类型是void,MuduoConnection也只是把数据交给TcpConnection::send()的发送流程。它没有返回“对端已收到”的确认,更不会代表“对端 RPC 业务执行成功”。

尤其要分清三个阶段:

已经调用 MuduoConnection::send() != 对端已经完整收到这一帧 != 对端业务已经处理成功

当前这一层做的是提交发送数据。远端的业务执行结果,必须由后续请求、响应和相应的上层逻辑判断,而不是从这两行代码推断出来。


四、shutdown()与connected():连接还有两个基本操作

发送是主线,但统一连接接口还需要处理关闭请求和状态查询。

4.1shutdown()只负责转交关闭请求

实现如下:

void shutdown() override { _conn->shutdown(); }

这里没有自己实现 TCP 四次挥手,也没有保存一份新的“关闭状态”。它只是把上层对BaseConnection的关闭请求,委托给底层TcpConnection。

需要注意,调用shutdown()是请求底层按其机制关闭连接,不等于这一行执行完就已经完成全部网络断开过程。具体关闭过程、回调与连接资源管理要放到后续网络端点封装中理解。

4.2connected()直接取底层连接状态

源码同样简洁:

bool connected() override { return _conn->connected(); }

调用者拿到的是 Muduo 当前报告的连接状态:

  • true:底层连接当前处于 connected 状态;

  • false:当前不处于该状态。

这里最关键的其实是return。底层_conn->connected()虽然产生一个布尔结果,但只有把这个结果返回,上层通过BaseConnection::ptr调用时才真正能获得它。

而且connected() == true也不是一张“未来所有发送一定成功”的保证书。它只是查询这一刻底层连接的状态。

到这里,MuduoConnection已经完整兑现了第五篇BaseConnection留下的三个抽象接口,而且没有额外引入属于 Server / Client 的职责。


五、ConnectionFactory:统一创建入口,而不是连接管理器

5.1 工厂到底解决什么问题?

现在我们已经有了具体实现MuduoConnection。调用方当然可以直接写:

auto conn = std::make_shared<MuduoConnection>(tcp_conn, protocol);

但前面已经使用过MessageFactory、BufferFactory、ProtocolFactory。它们共同的思路是:把具体实现的创建细节集中起来,让需要使用对象的代码尽量面对抽象接口。

连接层沿用同样的方式:

class ConnectionFactory { public: template <typename... Args> static BaseConnection::ptr create(Args &&...args) { return std::make_shared<MuduoConnection>( std::forward<Args>(args)...); } };

工厂有两个值得注意的细节。

第一个是返回类型:

BaseConnection::ptr

它对外返回的是抽象连接指针,实际创建的是MuduoConnection对象。上层因此可以使用send()、shutdown()、connected(),而不需要把具体对象类型写在业务接口里。

第二个是:

template <typename... Args> std::forward<Args>(args)...

这是可变参数模板和转发。对于本篇,只需理解:工厂收到创建MuduoConnection所需的参数,再把它们传入构造函数。暂时不用把模板的值类别规则展开成另一篇 C++ 语法课。

5.2 创建时输入什么,输出什么?

在当前项目中,典型的使用方式是:

auto base_conn = ConnectionFactory::create(conn, _protocol);

其中:

  • conn:Muduo 已经建立的真实TcpConnectionPtr;

  • _protocol:当前网络端点保存的BaseProtocol::ptr;

  • base_conn:推导为BaseConnection::ptr,实际指向MuduoConnection。

5.3Factory并不负责建立网络连接

这个地方容易误会:既然名字叫ConnectionFactory,是不是调用create()就会连接服务器?

不是。

从源码可以看到,它只是:

std::make_shared<MuduoConnection>(...);

并没有自己执行TcpClient::connect(),也没有启动TcpServer或创建监听 socket。

它做的是包装一条已有的真实连接,而不是在网络上“建立”那条连接。

同样,工厂也不负责保存连接表、处理断开回调、接收 Buffer 或分发业务消息。这些事情属于之后要讲的服务端与客户端封装。


六、把这一层放回工程:创建与发送怎样接起来?

前面拆开讲了构造、发送、关闭和工厂,现在可以重新看一次完整流程。

6.1 先从一条已经存在的 Muduo 连接开始

假设网络端点已经取得了一条有效的TcpConnectionPtr,也准备好了协议对象。可以把它们交给一个统一的包装函数:

BaseConnection::ptr wrapExistingConnection( const muduo::net::TcpConnectionPtr &conn, const BaseProtocol::ptr &protocol) { return ConnectionFactory::create(conn, protocol); }

这里的代码只是展示已有依赖如何进入工厂,并不是创建真实 TCP 连接的函数。返回对象的静态类型是BaseConnection::ptr,底层实际持有的是MuduoConnection。这是标准的“通过基类接口使用派生类实现”。

6.2 上层拿到BaseConnection后就可以发送消息

假设上层已经构造好一条有效的BaseMessage,通过抽象连接发送可以写成:

void sendPreparedMessage(const BaseConnection::ptr &base_conn, const BaseMessage::ptr &msg) { base_conn->send(msg); }

这里的base_conn和msg均由调用方传入,前提是已经正确创建,不是要求读者发送一个空消息指针。

真正发生的调用顺序是:

base_conn->send(msg) ↓ MuduoConnection::send(msg) ↓ _protocol->serialize(msg) ↓ LVProtocol 编码完整报文 ↓ _conn->send(body) ↓ Muduo TcpConnection 接手发送

这里并没有绕回去依赖具体的 RPC 消息类。无论上层给的是 RPC、Topic 还是 Service 消息,只要它按框架的BaseMessage接口提供必要信息,连接层都按照同一条路径调用协议,再交给 TCP 发送。至于业务内容本身是否符合约定,不是MuduoConnection的职责。

6.3 在源码中,谁会真正调用工厂?

当前source/common/net.hpp后面的MuduoServer和MuduoClient,会在连接建立的回调中使用:

ConnectionFactory::create(conn, _protocol);

这也印证了我们前面强调的先后关系:先有网络连接,再把它包装为BaseConnection。这里先点到为止,不进入它们如何维护连接表、注册回调和在接收时循环解析报文的细节。

从项目架构看,本篇已经把两条线接在了一起:

消息线:BaseMessage → LVProtocol → 网络报文 连接线:已有 TcpConnection → MuduoConnection → BaseConnection ↓ 汇合 BaseConnection::send(msg)

6.4 当前实现的边界还要说清楚

为了不把“一个简单的连接适配器”理解成完整网络框架,我们最后再把范围划清楚:

当前这层已经做到还需要后续网络端点处理
保存一条已有TcpConnectionPtr如何建立 / 接受一条连接
保存用于编码的BaseProtocol::ptr协议对象怎样在客户端和服务端中配置
send()编码并交给 Muduo网络回调、接收 Buffer 与连续拆包
shutdown()请求关闭连接断开后的回调和资源清理
connected()查询状态连接表和多连接管理
工厂返回BaseConnection::ptr把连接和完整消息交给上层回调

这不是故意把文章写短,而是避免我们在理解一个send()之前,被EventLoop、线程、锁、连接表以及大量回调同时打断。


写在最后

回顾这篇,我们没有新增协议字段,也没有重新实现 TCP,而是给此前准备好的抽象找到了具体落点。

MuduoConnection保存一条真实的TcpConnection和一个BaseProtocol:收到BaseMessage时先让协议编码,再把完整报文交给 Muduo 发送;需要关闭或查询状态时,就把操作委托给底层连接。ConnectionFactory则负责通过统一入口创建这个适配器,并向上返回BaseConnection::ptr。

因此,现在我们已经能够沿着清晰的接口链完成:

BaseMessage ↓ BaseConnection::send() ↓ MuduoConnection ↓ BaseProtocol / LVProtocol::serialize() ↓ muduo::net::TcpConnection::send()

但一条真实连接究竟是怎样建立的?连接断开时怎样通知上层?接收缓冲区里可能粘着多条报文,又该由谁反复解析并交给消息回调?

这些问题正好引出下一步:把单条连接放进MuduoServer与MuduoClient,让协议层真正接入客户端、服务端的网络回调。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询