从零实现 C++ Json-Rpc(十):把消息交给真实连接——MuduoConnection 与 ConnectionFactory

目录

前言

[一、有了 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 两个智能指针,是否意味着连接永远有效?)

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

[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,让协议层真正接入客户端、服务端的网络回调。

相关推荐
青瓦梦滋8 小时前
WebSocket协议
linux·服务器·网络·c++·websocket·网络协议·http
CodeYp201913 小时前
第15讲:二叉搜索树
开发语言·c++·二叉搜索树
pipixia2003314 小时前
C++继承面试四连问:内存叠加、私有继承、sizeof、权限重映射
c++·面试·嵌入式·继承·面向对象·对象模型
牵猫散步的鱼儿16 小时前
C++ 内存分区
c++
choumin17 小时前
C++ 标准容器为什么不能存引用
c++·容器·引用类型
谙弆悕博士17 小时前
用 C 写了个 Windows 聊天室,踩了几个多线程和控制台的坑
c语言·开发语言·c++·windows·网络编程·聊天室
stolentime18 小时前
CF2277E Tree Guessing题解
c++·算法·贪心算法·交互·ad-hoc
syagain_zsx18 小时前
ODB 与 MySQL:C++ 里的 ORM 实践(类即表、事务铁律与查询封装)
开发语言·数据库·c++·mysql·工具库·1024程序员节
小鱼仙官18 小时前
使用Qt封装C/C++ 动态库 用于第三方调用
c语言·c++·qt