目录
[一、有了 LVProtocol,为什么还需要连接层?](#一、有了 LVProtocol,为什么还需要连接层?)
[1.1 协议解决"发什么",连接解决"从哪里发"](#1.1 协议解决“发什么”,连接解决“从哪里发”)
[1.2 第五篇的 BaseConnection 正是为这一层准备的](#1.2 第五篇的 BaseConnection 正是为这一层准备的)
二、MuduoConnection:一条真实连接在框架中的表示
[2.1 它是适配器,不是另一套 TCP 实现](#2.1 它是适配器,不是另一套 TCP 实现)
[2.2 构造函数为什么需要两个参数?](#2.2 构造函数为什么需要两个参数?)
[2.3 两个智能指针,是否意味着连接永远有效?](#2.3 两个智能指针,是否意味着连接永远有效?)
[3.1 先看真实实现](#3.1 先看真实实现)
[3.2 为什么不能省掉协议,直接发送 msg->serialize()?](#3.2 为什么不能省掉协议,直接发送 msg->serialize()?)
[3.3 调用 send(),是否代表远端已经收到?](#3.3 调用 send(),是否代表远端已经收到?)
[四、shutdown() 与 connected():连接还有两个基本操作](#四、shutdown() 与 connected():连接还有两个基本操作)
[4.1 shutdown() 只负责转交关闭请求](#4.1 shutdown() 只负责转交关闭请求)
[4.2 connected() 直接取底层连接状态](#4.2 connected() 直接取底层连接状态)
五、ConnectionFactory:统一创建入口,而不是连接管理器
[5.1 工厂到底解决什么问题?](#5.1 工厂到底解决什么问题?)
[5.2 创建时输入什么,输出什么?](#5.2 创建时输入什么,输出什么?)
[5.3 Factory 并不负责建立网络连接](#5.3 Factory 并不负责建立网络连接)
[6.1 先从一条已经存在的 Muduo 连接开始](#6.1 先从一条已经存在的 Muduo 连接开始)
[6.2 上层拿到 BaseConnection 后就可以发送消息](#6.2 上层拿到 BaseConnection 后就可以发送消息)
[6.3 在源码中,谁会真正调用工厂?](#6.3 在源码中,谁会真正调用工厂?)
[6.4 当前实现的边界还要说清楚](#6.4 当前实现的边界还要说清楚)
前言
前面我们已经把"消息是什么"和"消息怎样编码"这两件事解决了。
第七篇完成了 BaseMessage 对应的具体消息对象,第八篇补充了 JSON 与请求 ID 工具,第九篇则让 MuduoBuffer 和 LVProtocol 真正工作起来:既能从接收字节中恢复一条完整消息,也能把 BaseMessage 编码为符合项目约定的 LV 报文。
可是,LVProtocol::serialize() 最后交给我们的仍然只是一个 std::string。这串字节虽然已经准备好,但它究竟要从哪一条 TCP 连接发送出去呢?

这就是本篇要解决的问题:让上层只拿着 BaseConnection 这样的统一接口,就能把消息交给底层的 Muduo 连接发送。 这次我们只关注一条已经存在的连接,不急着展开服务端如何接受连接、客户端如何发起连接,以及网络回调怎样触发。那些是下一层的工作。
一、有了 LVProtocol,为什么还需要连接层?
1.1 协议解决"发什么",连接解决"从哪里发"
先回想第九篇发送方向的结果:
cpp
std::string bytes = protocol->serialize(msg);
这里的 msg 是 BaseMessage::ptr,而 bytes 是完整的网络报文。它包含的是:
cpp
| 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() 接收的是可以发送的字节数据。两者之间始终缺了一步协议编码。
因此,我们希望上层只需要写:
cpp
base_conn->send(msg);
至于"先编码成 LV 报文,再交给具体 TCP 连接",由连接适配层内部完成。
1.2 第五篇的 BaseConnection 正是为这一层准备的
第五篇定义的抽象接口很简单:
cpp
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,也没有规定消息必须采用哪一种协议。它只是把上层真正需要的能力固定下来。
这样整个关系就变成:
cpp
上层 RPC / Topic / Service 模块
↓
BaseConnection
↓
MuduoConnection
↓
Muduo 的 TcpConnection
对于上层来说,发送消息不再需要知道"TcpConnection 的 send() 怎样调用",只需要认识 BaseConnection::send()。
二、MuduoConnection:一条真实连接在框架中的表示
2.1 它是适配器,不是另一套 TCP 实现
在当前工程的 source/common/net.hpp 中,我们定义了 MuduoConnection:
cpp
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 构造函数为什么需要两个参数?
构造函数接收:
cpp
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() 的完整核心逻辑只有两行:
cpp
void send(const BaseMessage::ptr &msg) override {
std::string body = _protocol->serialize(msg);
_conn->send(body);
}
别因为它短,就把它理解成普通的"把字符串发出去"。这两行分别跨越了协议层与网络层。
第一行:
cpp
std::string body = _protocol->serialize(msg);
调用的是 BaseProtocol 的虚接口。当前具体实现为 LVProtocol::serialize(),所以返回的是完整的 LV 网络报文,而不是单独的 JSON Body。
这里局部变量虽然叫 body,但不要把它与消息内部的 JSON body 混为一谈。该变量实际上包含:
cpp
| total_len | mtype | idlen | id | JSON body |
也就是说,前一篇介绍的报文编码结果,到这里已经真正有了使用位置。
第二行:
cpp
_conn->send(body);
把编码好的报文字节交给当前 muduo::net::TcpConnection。真正的 socket 写入、缓冲和事件循环交互仍由 Muduo 的网络实现负责,MuduoConnection 本身没有直接操作 socket。
3.2 为什么不能省掉协议,直接发送 msg->serialize()?
这要区分两种不同层次的 serialize():
cpp
msg->serialize();
这是消息对象自己的序列化接口,得到的是业务正文对应的 JSON 字符串。
而:
cpp
_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 业务执行成功"。
尤其要分清三个阶段:
cpp
已经调用 MuduoConnection::send()
!=
对端已经完整收到这一帧
!=
对端业务已经处理成功
当前这一层做的是提交发送数据。远端的业务执行结果,必须由后续请求、响应和相应的上层逻辑判断,而不是从这两行代码推断出来。
四、shutdown() 与 connected():连接还有两个基本操作
发送是主线,但统一连接接口还需要处理关闭请求和状态查询。
4.1 shutdown() 只负责转交关闭请求
实现如下:
cpp
void shutdown() override {
_conn->shutdown();
}
这里没有自己实现 TCP 四次挥手,也没有保存一份新的"关闭状态"。它只是把上层对 BaseConnection 的关闭请求,委托给底层 TcpConnection。
需要注意,调用 shutdown() 是请求底层按其机制关闭连接,不等于这一行执行完就已经完成全部网络断开过程。 具体关闭过程、回调与连接资源管理要放到后续网络端点封装中理解。
4.2 connected() 直接取底层连接状态
源码同样简洁:
cpp
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。调用方当然可以直接写:
cpp
auto conn = std::make_shared<MuduoConnection>(tcp_conn, protocol);
但前面已经使用过 MessageFactory、BufferFactory、ProtocolFactory。它们共同的思路是:把具体实现的创建细节集中起来,让需要使用对象的代码尽量面对抽象接口。
连接层沿用同样的方式:
cpp
class ConnectionFactory {
public:
template <typename... Args>
static BaseConnection::ptr create(Args &&...args) {
return std::make_shared<MuduoConnection>(
std::forward<Args>(args)...);
}
};
工厂有两个值得注意的细节。
第一个是返回类型:
cpp
BaseConnection::ptr
它对外返回的是抽象连接指针,实际创建的是 MuduoConnection 对象。上层因此可以使用 send()、shutdown()、connected(),而不需要把具体对象类型写在业务接口里。
第二个是:
cpp
template <typename... Args>
std::forward<Args>(args)...
这是可变参数模板和转发。对于本篇,只需理解:工厂收到创建 MuduoConnection 所需的参数,再把它们传入构造函数。 暂时不用把模板的值类别规则展开成另一篇 C++ 语法课。
5.2 创建时输入什么,输出什么?
在当前项目中,典型的使用方式是:
cpp
auto base_conn = ConnectionFactory::create(conn, _protocol);
其中:
-
conn:Muduo 已经建立的真实TcpConnectionPtr; -
_protocol:当前网络端点保存的BaseProtocol::ptr; -
base_conn:推导为BaseConnection::ptr,实际指向MuduoConnection。
5.3 Factory 并不负责建立网络连接
这个地方容易误会:既然名字叫 ConnectionFactory,是不是调用 create() 就会连接服务器?
不是。
从源码可以看到,它只是:
cpp
std::make_shared<MuduoConnection>(...);
并没有自己执行 TcpClient::connect(),也没有启动 TcpServer 或创建监听 socket。
它做的是包装一条已有的真实连接,而不是在网络上"建立"那条连接。
同样,工厂也不负责保存连接表、处理断开回调、接收 Buffer 或分发业务消息。这些事情属于之后要讲的服务端与客户端封装。
六、把这一层放回工程:创建与发送怎样接起来?
前面拆开讲了构造、发送、关闭和工厂,现在可以重新看一次完整流程。
6.1 先从一条已经存在的 Muduo 连接开始
假设网络端点已经取得了一条有效的 TcpConnectionPtr,也准备好了协议对象。可以把它们交给一个统一的包装函数:
cpp
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,通过抽象连接发送可以写成:
cpp
void sendPreparedMessage(const BaseConnection::ptr &base_conn,
const BaseMessage::ptr &msg) {
base_conn->send(msg);
}
这里的 base_conn 和 msg 均由调用方传入,前提是已经正确创建,不是要求读者发送一个空消息指针。
真正发生的调用顺序是:
cpp
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,会在连接建立的回调中使用:
cpp
ConnectionFactory::create(conn, _protocol);
这也印证了我们前面强调的先后关系:先有网络连接,再把它包装为 BaseConnection。 这里先点到为止,不进入它们如何维护连接表、注册回调和在接收时循环解析报文的细节。
从项目架构看,本篇已经把两条线接在了一起:
cpp
消息线: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。
因此,现在我们已经能够沿着清晰的接口链完成:
cpp
BaseMessage
↓
BaseConnection::send()
↓
MuduoConnection
↓
BaseProtocol / LVProtocol::serialize()
↓
muduo::net::TcpConnection::send()
但一条真实连接究竟是怎样建立的?连接断开时怎样通知上层?接收缓冲区里可能粘着多条报文,又该由谁反复解析并交给消息回调?
这些问题正好引出下一步:把单条连接放进 MuduoServer 与 MuduoClient,让协议层真正接入客户端、服务端的网络回调。

