【jsonRpc项目】基本的宏定义,抽象层和具象层的实现

目录

一.正文字段名称宏的定义

1.1.为什么需要进行宏定义

1.2.请求字段宏定义

1.3.响应码类型定义

1.4.RPC请求类型枚举

1.5.主题操作类型枚举

1.6.服务操作类型枚举

二.框架设计

2.1.抽象层

2.1.1.基础消息属性

2.1.2.输入缓冲区

2.1.3.应用层协议处理模块

2.1.4.通信连接

2.1.5.服务器

2.1.6.客户端

2.2.具象层1------BaseMessage类相关派生

2.2.1.第一次派生

2.2.2.第二次派生

2.2.3.第三次派生------请求的派生

2.2.4.第三次派生------响应的派生

2.2.5.工厂类设计

2.2.5.测试

2.3.具象层2------网络通信相关

2.3.1.MuduoBuffer类

2.3.2.LVProtocol类

2.3.3.MuduoConnection类

2.3.4.MuduoServer类

[2.3.5.BaseClient 类](#2.3.5.BaseClient 类)

2.3.6.服务端客户端通信测试


一.正文字段名称宏的定义

1.1.为什么需要进行宏定义

我们这个

cpp 复制代码
Json::Value msg;

msg["id"]="xxx-xxx-xxx";

我们不能直接使用id,这个键名来进行赋值,因为如果说这个键名后面改掉了,我们后面就需要满代码的去寻找这个id,进行替换,这个是非常危险的,我们必须使用一个宏来表示即可,后面我们只需要去修改这个宏即可实现整个项目的对应位置的替换。

但是注意,我们之前定义的宏都是字符串,而这里赋值的是Json::Value类型,所以直接使用字符串宏是没问题的。

所以,我们可以在这里增加一个宏定义:

cpp 复制代码
#define KEY_MSG_ID "id"

然后,在代码中这样使用:

cpp 复制代码
Json::Value msg;
msg[KEY_MSG_ID] = "xxx-xxx-xxx";

这样,当字段名需要改变时,我们只需要修改宏定义,而不用在整个代码库中搜索替换。


现在我们就开始编写我们的宏定义

1.2.请求字段宏定义

通信协议中所有消息采用统一的基础结构,包含消息头与消息体。消息头用于路由和识别,消息体则根据不同类型的请求携带具体参数。

1. 基础消息头

所有消息都必须包含以下两个基础字段:

  • 消息ID:全局唯一的标识符,用于请求与响应的配对追踪。

  • 消息类型:指明消息的具体类别,决定如何解析"消息正文"。

我们的消息类型都定义在下面

cpp 复制代码
enum class MType {
    REQ_RPC = 0,    // RPC 调用请求
    RSP_RPC,        // RPC 调用响应
    REQ_TOPIC,      // 发布/订阅操作请求
    RSP_TOPIC,      // 发布/订阅操作响应
    REQ_SERVICE,    // 服务治理操作请求
    RSP_SERVICE     // 服务治理操作响应
};

2. 消息正文

消息正文根据 消息类型 的不同,我们的消息正文的结构也完全不同。

以下是各种类型消息的详细结构。

a. RPC 请求

消息类型REQ_RPC 时,消息正文包含以下字段:

  • 方法名称:需要远程调用的接口或方法名。

  • 方法参数:调用该方法所需的参数列表。

cpp 复制代码
#define KEY_METHOD "method"
#define KEY_PARAMS "parameters"

b. 发布订阅相关请求

消息类型REQ_TOPIC/RSP_TOPIC 时,消息正文包含以下字段:

  • 主题名称

  • 主题消息

  • 操作类型

cpp 复制代码
#define KEY_TOPIC_KEY "topic_key"
#define KEY_TOPIC_MSG "topic_msg"
#define KEY_OPTYPE "optype"

c. 服务操作相关请求

消息类型REQ_SERVICE/RSP_SERVICE时,消息正文包含以下字段:

  • 方法名称

  • 操作类型

  • 主机信息:服务节点的网络地址信息。

    • IP地址:服务节点的IP地址。

    • PORT端口:服务节点监听的端口号。

cpp 复制代码
//因为操作类型和方法名称上面已经定义了
#define KEY_HOST "host"
#define KEY_HOST_IP "ip"
#define KEY_HOST_PORT "port"

d. RPC 响应

消息类型RSP_RPC时,消息正文包含以下字段:

  • 响应码:表示调用结果的状态码。这个其实就是我们下面的1.3小结里面的内容

  • 调用结果:远程方法执行后的返回数据。若调用失败,此处可包含错误描述信息。

cpp 复制代码
#define KEY_RCODE "rcode" //响应码
#define KEY_RESULT "result" //调用结果

1.3.响应码类型定义

RPC 响应里面

消息类型RSP_RPC时,消息正文包含以下字段:

  • 响应码:表示调用结果的状态码。这个其实就是我们这一小节的内容。

  • 调用结果:远程方法执行后的返回数据。若调用失败,此处可包含错误描述信息。

那么这里的响应码,就是我们这一个小节需要讲解的。

为统一系统中各类消息的标识与处理逻辑,定义消息类型枚举 MType。

该枚举将消息分为以下三类,每类均包含请求与响应两种对应类型,便于在通信过程中进行匹配与路由。

cpp 复制代码
/**
 * 响应码枚举
 * 用于标识系统处理结果状态,遵循 0 表示成功,非 0 表示失败的约定
 */
enum class RCode
{
    RCODE_OK = 0,                     ///< 操作成功
    RCODE_PARSE_FAILED,               ///< 消息解析失败
    RCODE_ERROR_MSGTYPE,              ///< 消息类型错误或不被支持
    RCODE_INVALID_MSG,                ///< 消息格式无效或不符合协议
    RCODE_DISCONNECTED,               ///< 连接已断开或不可用
    RCODE_INVALID_PARAMS,             ///< 请求参数无效或不合法
    RCODE_NOT_FOUND_SERVICE,          ///< 未找到请求的服务
    RCODE_INVALID_OPTYPE,             ///< 无效的操作类型
    RCODE_NOT_FOUND_TOPIC,            ///< 未找到指定的主题
    RCODE_INTERNAL_ERROR              ///< 系统内部错误
};

这些都是比较常见的错误。

为了根据错误码快速寻找到对应错误信息,我们编写了一个接口

cpp 复制代码
/**
 * 将响应码转换为对应的错误描述信息
 * 
 * @param code 响应码枚举值
 * @return std::string 对应的错误描述信息,如果传入未知响应码则返回"未知错误!"
 * 
 * @note 此函数内部使用静态哈希表存储响应码与错误描述的映射关系,
 *       首次调用时初始化该映射表,后续调用直接查询,保证性能与线程安全
 */
static std::string errReason(RCode code)
{
    // 使用静态局部变量保证线程安全的初始化(C++11及以上)
    static std::unordered_map<RCode, std::string> err_map = {
        {RCode::RCODE_OK,                   "成功处理!"},
        {RCode::RCODE_PARSE_FAILED,         "消息解析失败!"},
        {RCode::RCODE_ERROR_MSGTYPE,        "消息类型错误!"},
        {RCode::RCODE_INVALID_MSG,          "无效消息"},
        {RCode::RCODE_DISCONNECTED,         "连接已断开!"},
        {RCode::RCODE_INVALID_PARAMS,       "无效的Rpc参数!"},
        {RCode::RCODE_NOT_FOUND_SERVICE,    "没有找到对应的服务!"},
        {RCode::RCODE_INVALID_OPTYPE,       "无效的操作类型"},
        {RCode::RCODE_NOT_FOUND_TOPIC,      "没有找到对应的主题!"},
        {RCode::RCODE_INTERNAL_ERROR,       "内部错误!"}
    };
    
    // 在映射表中查找对应的错误描述
    auto it = err_map.find(code);
    
    // 如果未找到对应的响应码,返回默认的未知错误信息
    if (it == err_map.end())
    {
        return "未知错误!";
    }
    
    // 返回找到的错误描述信息
    return it->second;
}

1.4.RPC请求类型枚举

我们这里设定两种RPC请求类型

  1. 异步请求:返回异步对象,可在需要时通过该对象获取响应(若结果未到达则阻塞)
  2. 回调请求:通过设置回调函数异步处理响应,发送请求后立即返回,响应到达时自动触发
cpp 复制代码
/**
 * 请求类型枚举
 * 用于标识RPC请求的调用方式
 */
enum class RType
{
    REQ_ASYNC = 0,      ///< 异步请求:发送请求后不等待响应,立即返回
    REQ_CALLBACK        ///< 回调请求:发送请求后异步接收响应,通过回调函数处理结果
};

1.5.主题操作类型枚举

cpp 复制代码
/**
 * 主题操作类型枚举
 * 用于标识对主题的各种管理操作
 */
enum class TopicOptype
{
    TOPIC_CREATE = 0,   ///< 创建主题
    TOPIC_REMOVE,       ///< 删除/移除主题
    TOPIC_SUBSCRIBE,    ///< 订阅主题
    TOPIC_CANCEL,       ///< 取消订阅
    TOPIC_PUBLISH       ///< 向主题发布消息
};

1.6.服务操作类型枚举

cpp 复制代码
/**
 * 服务操作类型枚举
 * 用于标识对服务的各种管理操作
 */
enum class ServiceOptype
{
    SERVICE_REGISTRY = 0,   ///< 服务注册:将服务注册到服务发现中心
    SERVICE_DISCOVERY,      ///< 服务发现:查找可用的服务实例
    SERVICE_ONLINE,         ///< 服务上线:服务实例变为可用状态
    SERVICE_OFFLINE,        ///< 服务下线:服务实例变为不可用状态
    SERVICE_UNKNOW          ///< 未知操作:未定义或无效的操作类型
};

二.框架设计

在当前项⽬的实现中,我们将整个项⽬的实现划分为四层来进⾏实现

    1. 抽象层:将底层的⽹络通信以及应⽤层通信协议以及请求响应进⾏抽象,使项⽬更具扩展性和灵活 性。
    1. 具象层:针对抽象的功能进⾏具体的实现。
  • 3.Dispatcher模块:用于连接这个业务层和抽象层
    1. 业务层:基于抽象的框架在上层实现项⽬所需功能。

2.1.抽象层

在本项目的实现中,

  • 网络通信部分当前采用了第三方库 Muduo 来处理高并发网络 I/O
  • 同时使用基于 LV(Length-Value)格式的通信协议解决 TCP 粘包问题
  • 数据正文的序列化与反序列化则采用了 JSON 格式进行交换

然而,这几个技术选型在未来均存在进一步优化的空间------例如序列化方案不一定局限于 JSON,也可以根据性能需求切换为 Protobuf、MessagePack 等更高效的格式;网络库也可能根据部署环境或性能评测进行更换。

为此,我们在设计项目框架时,特别将底层通信相关的功能进行抽象,形成统一的通信抽象层(Abstraction Layer)

该抽象层对上提供一致的接口,而上层业务模块则基于这些接口完成功能调用。这样做的好处在于:

  1. 实现解耦:业务逻辑不直接依赖具体的网络库、协议或序列化实现,降低了模块间的耦合度。

  2. 可插拔替换:未来若要更换网络库、调整通信协议或升级序列化方式,只需在抽象层下方实现新的适配模块,无需改动上层业务代码。

  3. 提升灵活性与可扩展性:系统能够更容易地支持多种通信方案,适应不同的性能要求与业务场景,也为后续集成更高级的功能(如压缩、加密等)预留了结构空间。

  4. 便于测试与维护:抽象接口便于模拟实现,支持单元测试与集成测试;模块化结构也使代码更清晰,维护成本更低。

通过这一设计,我们不仅解决了当前技术选型可能带来的局限,也为项目后续演进奠定了良好的架构基础。

那么我们具体需要抽象什么东西?

首先是我们的通信协议,我们定义了很多基类

我们整个抽象层的代码,都存放在下面的abstract.hpp里面

2.1.1.基础消息属性

无论是请求还是响应的消息,它们里面一定包含了下面2个属性

  • 消息类型
  • 消息ID

那么为了方便起,我们就在这里进行封装这2个属性的操作,这样子后面子类就不再需要去再对这2个属性进行额外操作了

  • mtype():获取消息类型

  • setMType(MsgType):设置消息类型

  • id():获取消息ID

  • setId(const std::string &id):设置消息ID

然后其实不管我们的消息是LV格式还是其他什么格式,所有类型的消息都避不开序列化和反序列化操作。

  • serialize(std::string &body):消息的序列化,至于怎么序列化,是使用JSON的序列化还是其他什么样式的序列化,我们完全交给派生类去实现,这样子如果说以后我们不想使用JSON了,我们就可以去派生类里面修改一下里面序列化的方案即可。

  • unserialize(const std::string &body):消息的反序列化,同上。

  • check():消息的校验

那么我们很快就能写出这个来了

cpp 复制代码
// 抽象消息基类 - 定义RPC消息的通用接口
class BaseMessage
{
public:
    using ptr = std::shared_ptr<BaseMessage>;
    virtual ~BaseMessage() {}
    // 设置消息ID
    virtual void setId(const std::string &id)
    {
        _rid = id;
    }
    // 获取消息ID
    virtual std::string rid()
    {
        return _rid;
    }
    // 设置消息类型
    virtual void setMType(MType mtype)
    {
        _mtype = mtype;
    }
    // 获取消息类型
    virtual MType mtype()
    {
        return _mtype;
    }
    // 序列化消息到字符串
    virtual std::string serialize() = 0;
    // 从字符串反序列化消息
    virtual bool unserialize(const std::string &msg) = 0;
    // 检查消息有效性
    virtual bool check() = 0;

private:
    MType _mtype;     // 消息类型
    std::string _rid; // 请求ID
};

注意这里的消息类型MType其实我们在上面的宏定义里面实现了

cpp 复制代码
// 消息类型
    enum class MType
    {
        REQ_RPC = 0, // RPC 调用请求
        RSP_RPC,     // RPC 调用响应
        REQ_TOPIC,   // 发布/订阅操作请求
        RSP_TOPIC,   // 发布/订阅操作响应
        REQ_SERVICE, // 服务治理操作请求
        RSP_SERVICE  // 服务治理操作响应
    };

2.1.2.输入缓冲区

接下来我们定义了一个接收缓冲区的基类

注意我们的通信协议其实是LV格式的

那么我们在通信的过程中,我们收到了一个数据,我们的思想是先接受一个4字节的长度,再去接受后面的数据。

但是问题来了,如果说我们先接收了一个4字节长度之后,却发现后面的数据不足了,那怎么办?

那么我们这里就需要写一个接收缓冲区的基类

BaseBuffer 类成员函数

  • readableBytes():获取buffer里面可读字节数

  • peekInt32():从buffer里面尝试读取4字节,但是并不将数据从缓冲区里面移除这4字节长度的数据,主要目的就是看看后面的数据够不够组成一个完整的请求/响应。

  • retrieveInt32():从缓冲区移除4字节大小的数据

  • readInt32():从buffer里面尝试读取4字节,并将数据从缓冲区里面移除。其实就是peekInt32()和retrieveInt32()的组合体。

  • retrieveAsString(size_t len):从缓冲区读取指定长度的字符串数据,这个指定长度就是LV格式里面的L里面得到的。这个函数就是用于读取LV格式里面的V部分的。

我们的协议是LV格式的,我们这里就是输入缓冲区针对这个LV格式的通信协议必备的那些接口

cpp 复制代码
// 抽象缓冲区基类 - 定义网络数据缓冲区的通用接口
class BaseBuffer
{
public:
    using ptr = std::shared_ptr<BaseBuffer>;

    // 获取可读字节数
    virtual size_t readableSize() = 0;

    // 查看缓冲区中的int32值(不移动读指针)
    virtual int32_t peekInt32() = 0;

    // 从缓冲区移除int32大小的数据
    virtual void retrieveInt32() = 0;

    // 读取int32值并移动读指针
    virtual int32_t readInt32() = 0;

    // 从缓冲区读取指定长度的字符串数据
    virtual std::string retrieveAsString(size_t len) = 0;
};

有了这些操作接口,我们就能从输入缓冲区里面取出一条完整的数据了。

2.1.3.应用层协议处理模块

但是,我们上面仅仅只是提供了输入缓冲区的接口,但是我们没有去读取这个数据。

我们其实是在应用层协议处理模块里面来使用上面输入缓冲区的这些接口来 将这一整条数据从输入缓冲区里面读取出来。

BaseProtocol 类成员函数

  • onMessage(BaseBufferPtr &, BaseMessagePtr &):处理消息,也就是从缓冲区解析为消息对象

  • canProcessed(const BaseBufferPtr &):判断缓冲区中的数据是否足够我们进行一次处理,判断当前输入缓冲区里面的数据是不是一条完整的数据,如果足够,就去取出来进行出来,如果不足够,就再等新的数据到来

  • serialize(const BaseMessagePtr &):序列化消息对象为字符串,方便后续发送/存储

cpp 复制代码
 // 抽象协议基类 - 定义通信协议的通用接口
    class BaseProtocol
    {
    public:
        using ptr = std::shared_ptr<BaseProtocol>;

        //1.用于读取数据时使用
        // 判断缓冲区中的数据是否可以被处理
        virtual bool canProcessed(const BaseBuffer::ptr &buf) = 0;

        // 处理消息:从缓冲区解析为消息对象
        virtual bool onMessage(const BaseBuffer::ptr &buf, BaseMessage::ptr &msg) = 0;

        //2.用于发送数据时使用
        // 序列化消息对象为字符串
        virtual std::string serialize(const BaseMessage::ptr &msg) = 0;
    };

2.1.4.通信连接

这个在进行数据处理的时候,避免不了这个通信连接

BaseConnection 类成员函数

  • send(const BaseMessagePtr &):发送消息,这个时候我们只需要一个BaseMessage,我们将其进行JSON格式(或者其他)的序列化,然后通过这个应用层协议处理模块BaseProtocol 类里面的序列化组织成LV格式,然后将其发送出去

  • shutdown():关闭连接

  • connected():检查连接是否有效

这里我们没有提供recevice操作,因为现在大部分服务器都是异步操作,通过事件触发来接受数据,不需要我们上层来进行专门写这个接口,我们只需要去设置好这个数据到来的回调函数即可。

cpp 复制代码
// 抽象连接基类 - 定义网络连接的通用接口
class BaseConnection
{
public:
    using ptr = std::shared_ptr<BaseConnection>;

    // 发送消息
    virtual void send(const BaseMessage::ptr &msg) = 0;

    // 关闭连接
    virtual void shutdown() = 0;

    // 检查连接是否有效
    virtual bool connected() = 0;
};

2.1.5.服务器

基于上面几个模块,我们现在就能组成一个服务器

BaseServer 类成员函数

  • setConnectionCallback(const ConnectionCallback &):设置连接建立回调

  • setCloseCallback(const CloseCallback &):设置连接关闭回调

  • setMessageCallback(const MessageCallback &):设置消息接收回调

  • start():启动服务器

这几个是最最基本的,因为我们的服务器(例如Muduo库)一般都是异步IO了,那么我们就需要去设置一系列的回调函数,以保证整个服务器的正常运转。

cpp 复制代码
// 回调函数类型定义
using ConnectionCallback = std::function<void(const BaseConnection::ptr &)>;                  // 连接建立回调
using CloseCallback = std::function<void(const BaseConnection::ptr &)>;                       // 连接关闭回调
using MessageCallback = std::function<void(const BaseConnection::ptr &, BaseMessage::ptr &)>; // 消息接收回调

// 抽象服务器基类 - 定义RPC服务器的通用接口
class BaseServer
{
public:
    using ptr = std::shared_ptr<BaseServer>;

    // 设置连接建立回调
    virtual void setConnectionCallback(const ConnectionCallback &cb)
    {
        _cb_connection = cb;
    }

    // 设置连接关闭回调
    virtual void setCloseCallback(const CloseCallback &cb)
    {
        _cb_close = cb;
    }

    // 设置消息接收回调
    virtual void setMessageCallback(const MessageCallback &cb)
    {
        _cb_message = cb;
    }

    // 启动服务器
    virtual void start() = 0;

protected:
    ConnectionCallback _cb_connection; // 连接建立回调函数
    CloseCallback _cb_close;           // 连接关闭回调函数
    MessageCallback _cb_message;       // 消息接收回调函数
};

2.1.6.客户端

BaseClient 类成员函数

  • setConnectionCallback(const ConnectionCallback &):设置连接建立回调

  • setCloseCallback(const CloseCallback &):设置连接关闭回调

  • setMessageCallback(const MessageCallback &):设置消息接收回调

  • connect():连接到服务器

  • send(const BaseMessagePtr &message):发送消息

  • shutdown():关闭连接

  • connected():检查连接是否有效

  • BaseConnectionPtr connection():返回底层的连接对象

这些接口都是最最基本的,我们必须写

cpp 复制代码
// 抽象客户端基类 - 定义RPC客户端的通用接口
class BaseClient
{
public:
    using ptr = std::shared_ptr<BaseClient>;

    // 设置连接建立回调
    virtual void setConnectionCallback(const ConnectionCallback &cb)
    {
        _cb_connection = cb;
    }

    // 设置连接关闭回调
    virtual void setCloseCallback(const CloseCallback &cb)
    {
        _cb_close = cb;
    }

    // 设置消息接收回调
    virtual void setMessageCallback(const MessageCallback &cb)
    {
        _cb_message = cb;
    }

    // 连接到服务器
    virtual void connect() = 0;

    // 关闭连接
    virtual void shutdown() = 0;

    // 发送消息
    virtual bool send(const BaseMessage::ptr &) = 0;

    // 获取底层连接对象
    virtual BaseConnection::ptr connection() = 0;

    // 检查连接是否有效
    virtual bool connected() = 0;

protected:
    ConnectionCallback _cb_connection; // 连接建立回调函数
    CloseCallback _cb_close;           // 连接关闭回调函数
    MessageCallback _cb_message;       // 消息接收回调函数
};

2.2.具象层1------BaseMessage类相关派生

首先我们的这个具象层的关于消息部分的代码都存放在下面的message.hpp里面

具象层就是针对抽象层的具体实现。

⽽具体的实现也⽐较简单,从抽象类派⽣出具体功能的派⽣类,然后在内部实现各个接⼝功能即可。

  • 基于Muduo库实现⽹络通信部分抽象
  • 基于LV通信协议实现Protocol部分抽象

不过这⼀层中⽐较特殊的是,我们需要针对不同的请求,从BaseMessage中派⽣出不同的请求和响应类型,以便于在针对指定消息处理时,能够更加轻松的获取或设置请求及响应中的各项数据元素。

BaseMessage基类

cpp 复制代码
​
// 抽象消息基类 - 定义RPC消息的通用接口
class BaseMessage
{
public:
    using ptr = std::shared_ptr<BaseMessage>;
    virtual ~BaseMessage() {}
    // 设置消息ID
    virtual void setId(const std::string &id)
    {
        _rid = id;
    }
    // 获取消息ID
    virtual std::string rid()
    {
        return _rid;
    }
    // 设置消息类型
    virtual void setMType(MType mtype)
    {
        _mtype = mtype;
    }
    // 获取消息类型
    virtual MType mtype()
    {
        return _mtype;
    }
    // 序列化消息到字符串
    virtual std::string serialize() = 0;
    // 从字符串反序列化消息
    virtual bool unserialize(const std::string &msg) = 0;
    // 检查消息有效性
    virtual bool check() = 0;

private:
    MType _mtype;     // 消息类型
    std::string _rid; // 请求ID
};

​

这个类就是我们抽象层里面的第一个类。

2.2.1.第一次派生

首先我们先基于这个BaseMessage基类来派生出一个子类JsonMessage

子类JsonMessage相对于这个父类BaseMessage,就只是多出了一个成员body,注意这个body其实是一个Json::Value对象

然后我们在这个子类里面重新实现父类BaseMessage定义的3个虚函数

cpp 复制代码
    // 序列化消息到字符串
    virtual std::string serialize() = 0;
    // 从字符串反序列化消息
    virtual bool unserialize(const std::string &msg) = 0;
    // 检查消息有效性
    virtual bool check() = 0;

这3个虚函数实现起来其实很简单,我们就去借助我们之前写好的JSON工具类,在这里类里面,我们已经写好了JSON序列化和反序列化的接口

都存放于common/detail.hpp里面

然后我们就能编写我们的子类

cpp 复制代码
// JSON 消息基类,继承自 BaseMessage
    class JsonMessage : public BaseMessage
    {
    public:
        // 智能指针别名
        using ptr = std::shared_ptr<JsonMessage>;

        // 序列化方法,将内部 JSON 数据转换为字符串
        virtual std::string serialize() override
        {
            std::string body;
            bool ret = JSON::serialize(_body, body);
            if (ret == false)
            {
                return std::string();
            }
            return body;
        }

        // 反序列化方法,将字符串解析为内部 JSON 数据
        virtual bool unserialize(const std::string &msg) override
        {
            return JSON::unserialize(msg, _body);
        }

    protected:
        // 受保护的 JSON 数据成员
        Json::Value _body;
    };

注意这里使用了protected访问修饰符

在C++中,protected访问修饰符用于类的成员(数据成员和成员函数),它具有以下特点:

  1. 访问权限:

    • 在类内部:protected成员可以被该类内部的成员函数访问。

    • 在派生类内部:protected成员可以被派生类的成员函数访问。

    • 在类外部:protected成员不能被类的对象直接访问。

  2. 与private和public的区别:

    • private成员只能被该类内部的成员函数访问,派生类无法访问。

    • public成员在类内部、派生类和类外部都可以访问。

  3. 继承中的protected:

    • 当使用public继承时,基类的public成员在派生类中仍然是public,protected成员在派生类中仍然是protected,private成员不可访问。

    • 当使用protected继承时,基类的public和protected成员在派生类中都变为protected,private成员不可访问。

    • 当使用private继承时,基类的public和protected成员在派生类中都变为private,private成员不可访问。

注意:

_body 是 Json::Value 类型的对象,它来自 JsonCpp 库(或者类似的 JSON 库)。

以下是我们后续代码中出现的 Json::Value 的成员函数,并对其进行讲解:

  1. isNull()
  2. isString()
  3. isObject()
  4. isIntegral()
  5. isArray()
  6. asString()
  7. asInt()
  8. size()
  9. append()

另外,还使用了 Json::Value 的下标操作符 [] 来访问对象成员和数组元素。

下面分别进行讲解:

  1. isNull()
  • 功能:判断当前 Json::Value 是否为 null 值。
  • 示例:_body[KEY_RCODE].isNull()
  • 返回:如果当前值是 null,则返回 true,否则返回 false。
  1. isString()
  • 功能:判断当前 Json::Value 是否为字符串类型。
  • 示例:_body[KEY_METHOD].isString()
  • 返回:如果当前值是字符串类型,则返回 true,否则返回 false。
  1. isObject()
  • 功能:判断当前 Json::Value 是否为对象类型(即 JSON 对象)。
  • 示例:_body[KEY_PARAMS].isObject()
  • 返回:如果当前值是对象类型,则返回 true,否则返回 false。
  1. isIntegral()
  • 功能:判断当前 Json::Value 是否为整数类型(注意:在 JsonCpp 中,整数和浮点数都归为数字类型,但 isIntegral() 特指整数)。
  • 示例:_body[KEY_RCODE].isIntegral()
  • 返回:如果当前值是整数类型,则返回 true,否则返回 false。
  1. isArray()
  • 功能:判断当前 Json::Value 是否为数组类型。
  • 示例:_body[KEY_HOST].isArray()
  • 返回:如果当前值是数组类型,则返回 true,否则返回 false。
  1. asString()
  • 功能:将当前 Json::Value 转换为字符串类型。如果当前值不是字符串,则会尝试转换(例如,数字转换为字符串)。
  • 示例:_body[KEY_METHOD].asString()
  • 返回:返回当前值的字符串表示。
  1. asInt()
  • 功能:将当前 Json::Value 转换为整数类型。如果当前值不是整数,则会尝试转换(例如,字符串转换为整数,但注意可能会失败)。
  • 示例:_body[KEY_RCODE].asInt()
  • 返回:返回当前值的整数表示。
  1. size()
  • 功能:返回当前 Json::Value 的大小。对于数组,返回元素个数;对于对象,返回成员个数;对于字符串,返回字符串长度。
  • 示例:_body[KEY_HOST].size()
  • 返回:当前值的大小。
  1. append()
  • 功能:向当前 Json::Value(必须是数组)追加一个值。
  • 示例:_body[KEY_HOST].append(val)
  • 注意:调用此方法的 Json::Value 必须是数组类型,否则行为未定义。

下标操作符 []

  • 功能:访问当前 Json::Value 对象的成员(如果是对象类型)或数组元素(如果是数组类型)。
  • 示例:
  • 对象:_body[KEY_HOST] 返回键为 KEY_HOST 的值。
  • 数组:_body[KEY_HOST][i] 返回数组 KEY_HOST 的第 i 个元素。

2.2.2.第二次派生

基于这个JsonMessage类派生出两个子类 JsonRequest类和JsonResponse类

消息可以分为

  • 请求
  • 响应

因为响应和请求的正文里面的内容有点不一样,我们需要将请求和响应分离开。

这是因为我们进行消息检测的时候,我们的请求响应里面,它们的正文其实都是不一样的,比如我们的响应里面的正文全是状态码和处理结果,它和请求的处理方式是完全不一样的。为了方便起见,我们就派生出两个子类,JsonRequest类和JsonResponse类

每个子类里面都不添加新的成员,而是提供专门的接口。

响应

对于响应,其实也是有很多种响应的

事实上呢,请求有很多种对吧

  1. RPC请求
  2. 主题请求
  3. 服务请求

但是这3种响应的body里面没有其他什么相同的属性(注意:除了基类已经有的那些属性)

对于这个JsonRequest类里面,我们就什么也没有添加,因为没什么要做的

cpp 复制代码
// JSON 请求类,继承自 JsonMessage
    class JsonRequest : public JsonMessage
    {
    public:
        // 智能指针别名
        using ptr = std::shared_ptr<JsonRequest>;
    };

响应

事实上呢,响应有很多种对吧

  1. RPC响应
  2. 主题响应
  3. 服务响应

对于这3种响应,它们的body一定有下面这个属性(注意:除了基类已经有的那些属性)

  • 响应状态码

那么为了方便,我们就在这个类里面添加一些针对响应码的一些操作,这样子子类就没有必要再去针对这个响应码作出额外的操作了

像在JsonResponse类里面,我们相对与父类,我们添加了下面三个接口

  • 检查响应数据是否有效
  • 获取响应状态码
  • 设置响应状态码

这样子后续的继承自JsonResponse类的子类就无需再对这个响应码作出另外的处理了

cpp 复制代码
// JSON 响应类,继承自 JsonMessage
    class JsonResponse : public JsonMessage
    {
    public:
        // 智能指针别名
        using ptr = std::shared_ptr<JsonResponse>;

        // 检查响应数据是否有效
        virtual bool check() override
        {
            // 在响应中,大部分的响应都只有响应状态码
            // 因此只需要判断响应状态码字段是否存在,类型是否正确即可
            if (_body[KEY_RCODE].isNull() == true)
            {
                ELOG("响应中没有响应状态码!");
                return false;
            }
            if (_body[KEY_RCODE].isIntegral() == false)
            {
                ELOG("响应状态码类型错误!");
                return false;
            }
            return true;
        }

        // 获取响应状态码
        virtual RCode rcode()
        {
            return (RCode)_body[KEY_RCODE].asInt();
        }

        // 设置响应状态码
        virtual void setRCode(RCode rcode)
        {
            _body[KEY_RCODE] = (int)rcode;
        }
    };

注意:KEY_RCODE就是我们上面宏定义出来的宏

cpp 复制代码
//RPC响应相关
#define KEY_RCODE "rcode" //响应码
#define KEY_RESULT "result" //调用结果

RCODE也就就是我们上面的响应码类型

cpp 复制代码
/**
 * 响应码枚举
 * 用于标识系统处理结果状态,遵循 0 表示成功,非 0 表示失败的约定
 */
enum class RCode
{
    RCODE_OK = 0,                     ///< 操作成功
    RCODE_PARSE_FAILED,               ///< 消息解析失败
    RCODE_ERROR_MSGTYPE,              ///< 消息类型错误或不被支持
    RCODE_INVALID_MSG,                ///< 消息格式无效或不符合协议
    RCODE_DISCONNECTED,               ///< 连接已断开或不可用
    RCODE_INVALID_PARAMS,             ///< 请求参数无效或不合法
    RCODE_NOT_FOUND_SERVICE,          ///< 未找到请求的服务
    RCODE_INVALID_OPTYPE,             ///< 无效的操作类型
    RCODE_NOT_FOUND_TOPIC,            ///< 未找到指定的主题
    RCODE_INTERNAL_ERROR              ///< 系统内部错误
};

2.2.3.第三次派生------请求的派生

事实上呢,请求有很多种对吧

  1. RPC请求
  2. 主题请求
  3. 服务请求

那么对于每一种请求,事实上它的这个属性以及操作都是不太一样的。

为了方便起见,我们就基于JsonRequest类来派生出下面3个类

  • RPC 请求类 (RpcRequest)
  • 主题请求类 (TopicRequest)
  • 服务请求类 (ServiceRequest)

那么我们现在就分情况看看这3个类

1.RPC 请求类 (RpcRequest)

事实上呢,对于RPC请求,那么它的body里面就应该有下面2个信息(除了父类已经有的那些)

  1. 请求方法名称
  2. 请求方法参数

那么对于这两个成员,其实我们已经定义好了对应的宏

cpp 复制代码
​//RPC请求相关
#define KEY_METHOD "method" //方法名称
#define KEY_PARAMS "parameters" //方法参数

那么我们就需要针对这两个成员,专门定义出对应的操作

成员函数作用:

  1. check()

    • 验证RPC请求数据格式的有效性

    • 检查必须包含方法名称字段且为字符串类型

    • 检查必须包含参数字段且为对象类型

    • 验证失败时输出错误日志并返回false

  2. method()

    • 从请求数据中提取并返回请求的方法名称字符串
  3. setMethod()

    • 设置请求数据中的方法名称字段

    • 将指定的方法名称存储到JSON数据结构中

  4. params()

    • 获取请求中的参数部分

    • 返回包含所有参数的JSON对象

  5. setParams()

    • 设置请求的参数字段

    • 将指定的JSON参数对象存储到请求数据中

这些成员函数都是围绕

  1. 请求方法名称
  2. 请求方法参数

这2个来进行设置的

cpp 复制代码
// RPC 请求类,继承自 JsonRequest
    class RpcRequest : public JsonRequest
    {
    public:
        // 智能指针别名
        using ptr = std::shared_ptr<RpcRequest>;

        // 检查 RPC 请求数据是否有效
        virtual bool check() override
        {
            // rpc请求中,包含请求方法名称-字符串,参数字段-对象
            if (_body[KEY_METHOD].isNull() == true ||
                _body[KEY_METHOD].isString() == false)
            {
                ELOG("RPC请求中没有方法名称或方法名称类型错误!");
                return false;
            }
            if (_body[KEY_PARAMS].isNull() == true ||
                _body[KEY_PARAMS].isObject() == false)
            {
                ELOG("RPC请求中没有参数信息或参数信息类型错误!");
                return false;
            }
            return true;
        }

        // 获取方法名称
        std::string method()
        {
            return _body[KEY_METHOD].asString();//从解析后的JSON数据中提取KEY_METHOD字段的值,并作为String输出  
        }

        // 设置方法名称
        void setMethod(const std::string &method_name)
        {
            _body[KEY_METHOD] = method_name;
        }

        // 获取参数
        Json::Value params()
        {
            return _body[KEY_PARAMS];//从解析后的JSON数据中提取KEY_METHOD字段的值
        }

        // 设置参数
        void setParams(const Json::Value &params)
        {
            _body[KEY_PARAMS] = params;
        }
    };

大家注意这里的代码就涉及到两个宏,这些宏我们都是在上面已经定义好了的

cpp 复制代码
//RPC请求相关
#define KEY_METHOD "method" //方法名称
#define KEY_PARAMS "parameters" //方法参数

至于其他的就没什么好说的了

2.主题请求类 (TopicRequest)

对于这个主题请求,它的body里面就应该有下面3个信息(除了父类已经有的那些)

  • 主题名称
  • 主题消息
  • 操作类型

那么在之前的宏定义里面,我们已经这3个属性定义出来了

cpp 复制代码
//主题订阅相关
#define KEY_TOPIC_KEY "topic_key" //主题名称
#define KEY_TOPIC_MSG "topic_msg" //主题消息
#define KEY_OPTYPE "optype"  //操作类型

那么我们在这个派生类里面只需要针对这些属性定义出对应的操作即可

只不过注意:这个KEY_OPTYPE可是有很多种类型的,我们在之前的宏定义里面就早早的定义好了每一种主题操作类型,千万注意是主题操作类型

cpp 复制代码
/**
     * 主题操作类型枚举
     * 用于标识对主题的各种管理操作
     */
    enum class TopicOptype
    {
        TOPIC_CREATE = 0, ///< 创建主题
        TOPIC_REMOVE,     ///< 删除/移除主题
        TOPIC_SUBSCRIBE,  ///< 订阅主题
        TOPIC_CANCEL,     ///< 取消订阅
        TOPIC_PUBLISH     ///< 向主题发布消息
    };

好了,我们现在就来看看怎么进行操作吧

成员函数作用:

  1. check()

    • 验证主题请求数据格式的有效性

    • 检查必须包含主题名称字段且为字符串类型

    • 检查必须包含操作类型字段且为整数类型

    • 当操作为发布消息时,额外检查消息内容字段是否存在且为字符串类型

    • 验证失败时输出相应的错误日志

  2. topicKey()

    • 获取请求中的主题名称/标识符

    • 从JSON数据中提取主题键名字符串

  3. setTopicKey()

    • 设置请求中的主题名称/标识符

    • 将主题键名存储到JSON数据结构中

  4. optype()

    • 获取主题操作类型

    • 将JSON中的整数转换为TopicOptype枚举值

  5. setOptype()

    • 设置主题操作类型

    • 将TopicOptype枚举值转换为整数并存储到JSON中

  6. topicMsg()

    • 获取主题消息内容(仅发布操作时有效)

    • 从JSON数据中提取消息字符串

  7. setTopicMsg()

    • 设置主题消息内容

    • 将消息字符串存储到JSON数据结构中

那么很快,我们就能写出下面这个

cpp 复制代码
// 主题请求类,继承自 JsonRequest
    class TopicRequest : public JsonRequest
    {
    public:
        // 智能指针别名
        using ptr = std::shared_ptr<TopicRequest>;

        // 检查主题请求数据是否有效
        virtual bool check() override
        {
            // rpc请求中,包含请求方法名称-字符串,参数字段-对象
            if (_body[KEY_TOPIC_KEY].isNull() == true ||
                _body[KEY_TOPIC_KEY].isString() == false)
            {
                ELOG("主题请求中没有主题名称或主题名称类型错误!");
                return false;
            }
            if (_body[KEY_OPTYPE].isNull() == true ||
                _body[KEY_OPTYPE].isIntegral() == false)
            {
                ELOG("主题请求中没有操作类型或操作类型的类型错误!");
                return false;
            }
            if (_body[KEY_OPTYPE].asInt() == (int)TopicOptype::TOPIC_PUBLISH &&
                (_body[KEY_TOPIC_MSG].isNull() == true ||
                 _body[KEY_TOPIC_MSG].isString() == false))
            {
                ELOG("主题消息发布请求中没有消息内容字段或消息内容类型错误!");
                return false;
            }
            return true;
        }

        // 获取主题键名
        std::string topicKey()
        {
            //从解析后的JSON数据中提取KEY_TOPIC_KEY字段的值,并作为String输出  
            return _body[KEY_TOPIC_KEY].asString();
        }

        // 设置主题键名
        void setTopicKey(const std::string &key)
        {
            _body[KEY_TOPIC_KEY] = key;
        }

        // 获取操作类型
        TopicOptype optype()
        {
            //从解析后的JSON数据中提取KEY_TOPIC_KEY字段的值,并作为int输出,然后又强制转换为TopicOptype类型
            return (TopicOptype)_body[KEY_OPTYPE].asInt();
        }

        // 设置操作类型
        void setOptype(TopicOptype optype)
        {
            _body[KEY_OPTYPE] = (int)optype;
        }

        // 获取主题消息内容
        std::string topicMsg()
        {
            //从解析后的JSON数据中提取KEY_TOPIC_KEY字段的值,并作为String输出  
            return _body[KEY_TOPIC_MSG].asString();
        }

        // 设置主题消息内容
        void setTopicMsg(const std::string &msg)
        {
            _body[KEY_TOPIC_MSG] = msg;
        }
    };

3.服务请求类 (ServiceRequest)

那么对于这个服务请求,它的body里面就应该有下面3个信息(除了父类已经有的那些)

  • 方法
  • 操作类型
  • 主机端口,主机IP,主机信息

我们也已经定义好了对应的宏定义,那么这个方法和操作类型的宏定义,我们是借用了之前的,问题不大。

cpp 复制代码
// RPC请求相关
#define KEY_METHOD "method"     // 方法名称
// 主题订阅相关
#define KEY_OPTYPE "optype"       // 操作类型
// 服务操作相关请求
#define KEY_HOST "host"      // 主机信息
#define KEY_HOST_IP "ip"     // 主机地址
#define KEY_HOST_PORT "port" // 主机端口

但是,大家需要注意,我们这里的操作类型和上面的主题请求类 (TopicRequest)的操作类型是完全不一样的!!

我们这里的操作类型是专门的服务操作类型

cpp 复制代码
/**
     * 服务操作类型枚举
     * 用于标识对服务的各种管理操作
     */
    enum class ServiceOptype
    {
        SERVICE_REGISTRY = 0, ///< 服务注册:将服务注册到服务发现中心
        SERVICE_DISCOVERY,    ///< 服务发现:查找可用的服务实例
        SERVICE_ONLINE,       ///< 服务上线:服务实例变为可用状态
        SERVICE_OFFLINE,      ///< 服务下线:服务实例变为不可用状态
        SERVICE_UNKNOW        ///< 未知操作:未定义或无效的操作类型
    };

那么在我们的派生类里面,对于这个服务请求,我们这个类里面就应该专门针对body里面的这3个信息来进行操作

  • 方法
  • 操作类型
  • 主机端口,主机IP,主机信息

成员函数作用:

  1. check()

    • 验证服务请求数据格式的有效性

    • 检查必须包含方法名称字段且为字符串类型

    • 检查必须包含操作类型字段且为整数类型

    • 当操作不是服务发现时,检查主机地址信息字段是否完整且格式正确

    • 验证失败时输出相应的错误日志

  2. method()

    • 获取服务请求中的方法名称

    • 从JSON数据中提取方法名字符串

  3. setMethod()

    • 设置服务请求中的方法名称

    • 将方法名字符串存储到JSON数据结构中

  4. optype()

    • 获取服务操作类型

    • 将JSON中的整数转换为ServiceOptype枚举值

  5. setOptype()

    • 设置服务操作类型

    • 将ServiceOptype枚举值转换为整数并存储到JSON中

  6. host()

    • 获取请求中的主机地址信息

    • 从JSON数据中提取IP地址和端口,返回Address类型对象

  7. setHost()

    • 设置请求中的主机地址信息

    • 将Address对象转换为JSON格式并存储到请求数据中

注意:为了方便表示主机信息,我们这里将IP地址和端口号组成了一个键值对,这样子方便存储

那么就有问题了

cpp 复制代码
// 定义地址类型,pair<string, int> 表示 IP 地址和端口
    typedef std::pair<std::string, int> Address;

那么实际上在这个JSON的存储中,它们也就是下面这样子

cpp 复制代码
{
  "KEY_HOST": {
    "KEY_HOST_IP": "192.168.1.1",
    "KEY_HOST_PORT": 8080
  }
}

那么问题来了?我们怎么去操作这个Json::Value来保存这个呢?

提一下这个JSON对象啊

在 C++ 中,通常使用 JSON 库(如 jsoncpp)来解析和生成 JSON 数据。

在 jsoncpp 中,JSON 对象被表示为 Json::Value 类型,但 Json::Value 可以表示多种类型(对象、数组、字符串、数字等)。

那么我们这里就需要学习一下JSON对象

在 JSON 中,对象是一个无序的、键/值对的集合,一个对象以左花括号{开始,以右花括号}结束,左右花括号之间为对象中的若干键/值对。

键/值对中,键必须是字符串类型(即使用双引号将键包裹起来),而值可以是 JSON 中的任意类型,键和值之间需要使用冒号:分隔开,不同的键/值对之间需要使用逗号,分隔开。

下面来看一个 JSON 对象的例子:

javascript 复制代码
{ 
    "website": {
        "name" : "C语言中文网",
        "url" : "http://c.biancheng.net/"
    } 
}

通过上面的示例可以看出,整个 JSON 就是一个对象类型,在这个对象中包含一个名为"website"的键,与键所对应的值同样也是一个对象,对象中包含"name"、"url"等键,以及与键所对应的值。

下面详细解释 JSON 对象类型:

  1. 结构:JSON 对象由键值对组成,键必须是字符串,值可以是任意 JSON 类型(字符串、数字、对象、数组、布尔值、null)。

    例如:

    cpp 复制代码
    {
      "name": "Alice",
      "age": 30,
      "city": "New York"
    }
  2. 在 jsoncpp 中,Json::Value 可以表示一个对象。我们可以使用 operator[] 来访问对象的成员。例如,对于上面的 JSON,如果我们有一个 Json::Value 对象 v,那么 v["name"] 将返回一个表示 "Alice" 的 Json::Value。

  3. 嵌套对象:JSON 对象可以嵌套,即一个对象的值可以是另一个对象。

    例如:

    cpp 复制代码
    {
      "person": {
        "name": "Bob",
        "age": 25
      }
    }

在 jsoncpp 中,我们可以这样访问:v["person"]["name"]。

  1. 判断类型:**在访问对象成员之前,有时我们需要判断一个 Json::Value 是否是对象类型,可以使用 isObject() 方法。**同样,也可以使用 isMember() 方法检查某个键是否存在。

  2. 遍历对象:我们可以使用 getMemberNames() 方法获取对象的所有键,然后遍历这些键来访问值。

  3. 修改和添加:我们可以通过赋值来修改或添加对象的键值对。

如果还是不太了解的话,我们可以看看下面这个代码

cpp 复制代码
#include <iostream>
#include <json/json.h>

int main() {
    // 创建一个 JSON 对象
    Json::Value root;
    root["name"] = "Alice";
    root["age"] = 30;

    // 添加一个嵌套对象
    Json::Value address;
    address["city"] = "New York";
    address["zip"] = "10001";
    root["address"] = address;

    // 访问对象成员
    std::string name = root["name"].asString();
    int age = root["age"].asInt();
    std::string city = root["address"]["city"].asString();

    std::cout << "Name: " << name << std::endl;
    std::cout << "Age: " << age << std::endl;
    std::cout << "City: " << city << std::endl;

    // 检查root是不是一个对象
    if (root.isObject()) {
        std::cout << "root is an 对象" << std::endl;
    }

    // 遍历对象
    for (const auto& key : root.getMemberNames()) {
        std::cout << key << ": " << root[key] << std::endl;
    }

    return 0;
}

现在,我们就能写出我们这个服务请求类 (ServiceRequest)了

cpp 复制代码
// 定义地址类型,pair<string, int> 表示 IP 地址和端口
    typedef std::pair<std::string, int> Address;

    // 服务请求类,继承自 JsonRequest
    class ServiceRequest : public JsonRequest
    {
    public:
        // 智能指针别名
        using ptr = std::shared_ptr<ServiceRequest>;

        // 检查服务请求数据是否有效
        virtual bool check() override
        {
            // rpc请求中,包含请求方法名称-字符串,参数字段-对象
            if (_body[KEY_METHOD].isNull() == true ||
                _body[KEY_METHOD].isString() == false)
            {
                ELOG("服务请求中没有方法名称或方法名称类型错误!");
                return false;
            }
            if (_body[KEY_OPTYPE].isNull() == true ||
                _body[KEY_OPTYPE].isIntegral() == false)
            {
                ELOG("服务请求中没有操作类型或操作类型的类型错误!");
                return false;
            }
            if (_body[KEY_OPTYPE].asInt() != (int)(ServiceOptype::SERVICE_DISCOVERY) &&
                (_body[KEY_HOST].isNull() == true ||
                 _body[KEY_HOST].isObject() == false ||
                 _body[KEY_HOST][KEY_HOST_IP].isNull() == true ||
                 _body[KEY_HOST][KEY_HOST_IP].isString() == false ||
                 _body[KEY_HOST][KEY_HOST_PORT].isNull() == true ||
                 _body[KEY_HOST][KEY_HOST_PORT].isIntegral() == false))
            {
                ELOG("服务请求中主机地址信息错误!");
                return false;
            }
            return true;
        }

        // 获取方法名称
        std::string method()
        {
            //从解析后的JSON数据中提取KEY_METHO字段的值,并作为String输出  
            return _body[KEY_METHOD].asString();
        }

        // 设置方法名称
        void setMethod(const std::string &name)
        {
            _body[KEY_METHOD] = name;
        }

        // 获取操作类型
        ServiceOptype optype()
        {
            //从解析后的JSON数据中提取KEY_TOPIC_KEY字段的值,并作为int输出,然后又强制转换为 ServiceOptype类型
            return (ServiceOptype)_body[KEY_OPTYPE].asInt();
        }

        // 设置操作类型
        void setOptype(ServiceOptype optype)
        {
            _body[KEY_OPTYPE] = (int)optype;
        }

        // 获取主机地址
        Address host()
        {
            //从解析后的JSON数据中提取KEY_HOST字段里面的KEY_HOST_IP和KEY_HOST_PORT的值,并作为Address输出 
            Address addr;
            addr.first = _body[KEY_HOST][KEY_HOST_IP].asString();
            addr.second = _body[KEY_HOST][KEY_HOST_PORT].asInt();
            return addr;
        }

        // 设置主机地址
        void setHost(const Address &host)
        {
            Json::Value val;
            val[KEY_HOST_IP] = host.first;
            val[KEY_HOST_PORT] = host.second;
            _body[KEY_HOST] = val;
        }
    };

思路很简单。

2.2.4.第三次派生------响应的派生

事实上呢,响应有很多种对吧

  1. RPC响应
  2. 主题响应
  3. 服务响应

那么对于每一种请求,事实上它的这个成员以及操作都是不太一样的。为了方便起见,我们就基于JsonResponse类来派生出下面3个类

  • RPC 响应类 (RpcResponse)
  • 主题响应类 (TopicResponse)
  • 服务响应类 (ServiceResponse)

那么我们现在就分情况看看这3个类

1.RPC 响应类 (RpcResponse)

事实上呢,对于RPC响应,那么它的body里面就应该有下面2个信息

  1. 响应码
  2. RPC远程调用的处理结果

那么对于这两个成员,其实我们已经定义好了对应的宏

cpp 复制代码
// RPC响应相关
#define KEY_RCODE "rcode"   // 响应码
#define KEY_RESULT "result" // 调用结果

那么我们这个类的操作,都是针对这两个属性来进行操作的

事实上呢,其实这个响应码也没太过于必要去处理了,因为他是继承了JsonMessage类,而在JsonMessage类里面已经对这个响应码进行了下面两个操作

  1. 获取响应码
  2. 设置响应码

所以,在这个RPC 响应类 (RpcResponse)里面,我们只需要去专注于调用结果的处理。

成员函数作用:

  1. check()

    • 验证RPC响应数据格式的有效性

    • 检查必须包含响应状态码字段且为整数类型

    • 检查必须包含结果字段(不能为null,但可以是任意类型)

    • 验证失败时输出错误日志并返回false

  2. result()

    • 获取RPC调用的结果

    • 返回包含调用结果的JSON值

  3. setResult()

    • 设置RPC调用的结果

    • 将指定的JSON结果存储到响应数据中

cpp 复制代码
// RPC 响应类,继承自 JsonResponse
    class RpcResponse : public JsonResponse
    {
    public:
        // 智能指针别名
        using ptr = std::shared_ptr<RpcResponse>;

        // 检查 RPC 响应数据是否有效
        virtual bool check() override
        {
            if (_body[KEY_RCODE].isNull() == true ||
                _body[KEY_RCODE].isIntegral() == false)
            {
                ELOG("响应中没有响应状态码,或状态码类型错误!");
                return false;
            }
            if (_body[KEY_RESULT].isNull() == true)
            {
                ELOG("响应中没有Rpc调用结果,或结果类型错误!");
                return false;
            }
            return true;
        }

        // 获取结果
        Json::Value result()
        {
            return _body[KEY_RESULT];
        }

        // 设置结果
        void setResult(const Json::Value &result)
        {
            _body[KEY_RESULT] = result;
        }
    };

2.主题响应类 (TopicResponse)

事实上呢,对于主题响应,那么它的body里面就应该有下面1个信息

  1. 响应码

事实上呢,其实这个响应码也没太过于必要去处理了,因为他是继承了JsonMessage类,而在JsonMessage类里面已经对这个响应码进行了下面两个操作

  1. 获取响应码
  2. 设置响应码

所以,在这个主题响应类 (TopicResponse)里面,我们好像什么都没有必要去做

cpp 复制代码
// 主题响应类,继承自 JsonResponse
    class TopicResponse : public JsonResponse
    {
    public:
        // 智能指针别名
        using ptr = std::shared_ptr<TopicResponse>;
    };

3.服务响应类 (ServiceResponse)

事实上,针对注册+上线+下线,我们只需要一个响应码即可

但是对于服务发现,我们就必须需要以下信息

  1. 响应码
  2. 操作类型
  3. 方法
  4. 主机端口,主机IP,主机信息

事实上呢,对于服务响应,那么它的body里面就应该有下面4个信息

  1. 响应码
  2. 操作类型
  3. 方法
  4. 主机端口,主机IP,主机信息

那么对于这两个成员,其实我们之前已经定义好了对应的宏

cpp 复制代码
// RPC请求相关
#define KEY_METHOD "method"     // 方法名称
// 主题订阅相关
#define KEY_OPTYPE "optype"       // 操作类型
// 服务操作相关请求
#define KEY_HOST "host"      // 主机信息
#define KEY_HOST_IP "ip"     // 主机地址
#define KEY_HOST_PORT "port" // 主机端口
// RPC响应相关
#define KEY_RCODE "rcode"   // 响应码

注意啊:这个操作类型其实是服务操作类型

cpp 复制代码
/**
     * 服务操作类型枚举
     * 用于标识对服务的各种管理操作
     */
    enum class ServiceOptype
    {
        SERVICE_REGISTRY = 0, ///< 服务注册:将服务注册到服务发现中心
        SERVICE_DISCOVERY,    ///< 服务发现:查找可用的服务实例
        SERVICE_ONLINE,       ///< 服务上线:服务实例变为可用状态
        SERVICE_OFFLINE,      ///< 服务下线:服务实例变为不可用状态
        SERVICE_UNKNOW        ///< 未知操作:未定义或无效的操作类型
    };

事实上呢,其实这个响应码也没太过于必要去处理了,因为他是继承了JsonMessage类,而在JsonMessage类里面已经对这个响应码进行了下面两个操作

  1. 获取响应码
  2. 设置响应码

所以,在这个主题响应类 (TopicResponse)里面,我们只需要去专注于下面3个操作

  1. 操作类型
  2. 方法
  3. 主机端口,主机IP,主机信息

那么我们很快就能写出来

成员函数作用:

  1. check()

    • 验证服务响应数据格式的有效性

    • 检查必须包含响应状态码字段且为整数类型

    • 检查必须包含操作类型字段且为整数类型

    • 当操作类型为服务发现时,额外检查方法名称字段是否存在且为字符串类型,以及主机地址列表字段是否存在且为数组类型

    • 验证失败时输出相应的错误日志

  2. optype()

    • 获取服务响应中的操作类型

    • 将JSON中的整数转换为ServiceOptype枚举值

  3. setOptype()

    • 设置服务响应中的操作类型

    • 将ServiceOptype枚举值转换为整数并存储到JSON中

  4. method()

    • 获取服务响应中的方法名称

    • 从JSON数据中提取方法名字符串

  5. setMethod()

    • 设置服务响应中的方法名称

    • 将方法名字符串存储到JSON数据结构中

  6. setHost()

    • 设置服务响应中的主机地址列表

    • 将多个Address对象(IP和端口)转换为JSON数组并存储到响应数据中

  7. hosts()

    • 获取服务响应中的主机地址列表

    • 从JSON数组中提取多个Address对象(IP和端口),返回vector<Address>类型

注意:服务响应类主要用于服务发现操作,当操作类型为服务发现时,需要返回多个主机地址信息。

cpp 复制代码
// 服务响应类,继承自 JsonResponse
    class ServiceResponse : public JsonResponse
    {
    public:
        // 智能指针别名
        using ptr = std::shared_ptr<ServiceResponse>;

        // 检查服务响应数据是否有效
        virtual bool check() override
        {
            if (_body[KEY_RCODE].isNull() == true ||
                _body[KEY_RCODE].isIntegral() == false)
            {
                ELOG("响应中没有响应状态码,或状态码类型错误!");
                return false;
            }
            if (_body[KEY_OPTYPE].isNull() == true ||
                _body[KEY_OPTYPE].isIntegral() == false)
            {
                ELOG("响应中没有操作类型,或操作类型的类型错误!");
                return false;
            }
            if (_body[KEY_OPTYPE].asInt() == (int)(ServiceOptype::SERVICE_DISCOVERY) &&
                (_body[KEY_METHOD].isNull() == true ||
                 _body[KEY_METHOD].isString() == false ||
                 _body[KEY_HOST].isNull() == true ||
                 _body[KEY_HOST].isArray() == false))
            {
                ELOG("服务发现响应中响应信息字段错误!");
                return false;
            }
            return true;
        }

        // 获取操作类型
        ServiceOptype optype()
        {
            return (ServiceOptype)_body[KEY_OPTYPE].asInt();
        }

        // 设置操作类型
        void setOptype(ServiceOptype optype)
        {
            _body[KEY_OPTYPE] = (int)optype;
        }

        // 获取方法名称
        std::string method()
        {
            return _body[KEY_METHOD].asString();
        }

        // 设置方法名称
        void setMethod(const std::string &method)
        {
            _body[KEY_METHOD] = method;
        }

        // 设置主机地址列表
        void setHost(std::vector<Address> addrs)
        {
            for (auto &addr : addrs)
            {
                Json::Value val;
                val[KEY_HOST_IP] = addr.first;
                val[KEY_HOST_PORT] = addr.second;
                _body[KEY_HOST].append(val);
            }
        }

        // 获取主机地址列表
        std::vector<Address> hosts()
        {
            std::vector<Address> addrs;
            int sz = _body[KEY_HOST].size();
            for (int i = 0; i < sz; i++)
            {
                Address addr;
                addr.first = _body[KEY_HOST][i][KEY_HOST_IP].asString();
                addr.second = _body[KEY_HOST][i][KEY_HOST_PORT].asInt();
                addrs.push_back(addr);
            }
            return addrs;
        }
    };

2.2.5.工厂类设计

我们这里专门写了一个工厂类来构造上面那些第三次派生的类,也就是下面这些

  1. RPC 请求类 (RpcRequest)
  2. 主题请求类 (TopicRequest)
  3. RPC 服务请求类 (ServiceRequest)
  4. 响应类 (RpcResponse)
  5. 主题响应类 (TopicResponse)
  6. 服务响应类 (ServiceResponse)

其实这些类都是代表着不同的消息,我们在上面都已经定义了不同消息类型的宏定义,刚好和这里的每个类是一一对应的

cpp 复制代码
enum class MType {
    REQ_RPC = 0,    // RPC 调用请求
    RSP_RPC,        // RPC 调用响应
    REQ_TOPIC,      // 发布/订阅操作请求
    RSP_TOPIC,      // 发布/订阅操作响应
    REQ_SERVICE,    // 服务治理操作请求
    RSP_SERVICE     // 服务治理操作响应
};

那么我们就根据不同的消息类型,我们就能构造对应的类对象。

我们写的这个工厂类的思想很简单,就是提供一个create函数,然后根据传递进来的消息类型,然后构建出对应类型的对象。

cpp 复制代码
// 消息对象工厂类
    class MessageFactory
    {
    public:
        // 静态方法:根据消息类型创建对应的消息对象
        static BaseMessage::ptr create(MType mtype)
        {
            switch (mtype)
            {
            case MType::REQ_RPC:
                return std::make_shared<RpcRequest>();
            case MType::RSP_RPC:
                return std::make_shared<RpcResponse>();
            case MType::REQ_TOPIC:
                return std::make_shared<TopicRequest>();
            case MType::RSP_TOPIC:
                return std::make_shared<TopicResponse>();
            case MType::REQ_SERVICE:
                return std::make_shared<ServiceRequest>();
            case MType::RSP_SERVICE:
                return std::make_shared<ServiceResponse>();
            }
            return BaseMessage::ptr();
        }

        // 模板方法:创建指定类型的消息对象,支持传递构造参数
        template <typename T, typename... Args>
        static std::shared_ptr<T> create(Args &&...args)
        {
            return std::make_shared<T>(std::forward(args)...);
        }
    };

注意,我们这里其实是构建了一个子类对象,但是我们返回的却是一个父类的指针。这个是可以的

这里的模板函数使用了完美转发和一个模板可变参数整合到一起了,大家如果不太了解的,可以去:【消息队列项目】C++11 异步操作实现线程池_c++线程池std::future-CSDN博客

2.2.5.测试

针对这6种不同的消息类,我们必须先进行测试,然后再去写其他的部分

父类指针能否指向子类对象?子类指针能否指向父类对象?

在C++中,使用指针指向对象时,存在两种重要的关系:向上转型(upcast)和向下转型(downcast)。

父类指针指向子类对象(向上转型):

  • 这是安全且常见的做法,因为子类对象包含了父类的部分(继承关系)。父类指针可以指向子类对象,但通过这个指针只能访问父类中定义的成员(以及虚函数,如果是通过虚函数机制调用子类重写的函数)。
  • 例如:Base* ptr = new Derived(); 这是合法的。

子类指针指向父类对象(向下转型):

  • 直接这样做是不安全的,因为父类对象可能不包含子类特有的成员。如果试图通过子类指针访问子类特有的成员,而实际对象是父类,则会导致未定义行为。
  • 例如:Derived* ptr = new Base(); 这是不安全的,编译器会报错(除非使用强制类型转换,但如果不小心使用,会导致运行时错误)。

在C++中,dynamic_cast是一种安全的向下转型(downcast)操作符,用于在继承层次结构中将基类的指针或引用转换为派生类的指针或引用。它会在运行时检查转换的有效性,如果转换无效,则返回空指针(对于指针转换)或抛出std::bad_cast异常(对于引用转换)。

使用dynamic_cast的条件:

  1. 必须有虚函数:基类中至少有一个虚函数(即多态类型),因为dynamic_cast依赖于运行时类型信息(RTTI)。

  2. 转换的类型必须是多态类型的指针或引用。


那么我们这里就来编写代码

RPC请求类的测试

cpp 复制代码
#include"../../common/message.hpp"

int main()
{
    
    //jsonRpc::BaseMessage::ptr bmp=jsonRpc::MessageFactory::create(jsonRpc::MType::REQ_RPC);
    //jsonRpc::RpcRequest::ptr rrp=std::dynamic_pointer_cast<jsonRpc::RpcRequest>(bmp);//将父类指针转换为子类指针
    jsonRpc::RpcRequest::ptr rrp=jsonRpc::MessageFactory::create<jsonRpc::RpcRequest>();//等价于上面两句
    Json::Value param;
    param["num1"]=11;
    param["num2"]=22;
    rrp->setMethod("Add");
    rrp->setParams(param);
    std::string str=rrp->serialize();
    std::cout<<str<<std::endl;

    jsonRpc::BaseMessage::ptr bmp=jsonRpc::MessageFactory::create(jsonRpc::MType::REQ_RPC);//这个create里面构建的其实是一个子类,但是返回的却是父类指针
    bool ret=bmp->unserialize(str);
    if(ret==false)
    {
        return -1;
    }
    ret=bmp->check();//检查消息有效性
    jsonRpc::RpcRequest::ptr rrp2=std::dynamic_pointer_cast<jsonRpc::RpcRequest>(bmp);//将父类指针转换为子类指针
    std::cout<<rrp2->method()<<std::endl;
    std::cout<<rrp2->params()["num1"].asInt()<<std::endl;
    std::cout<<rrp2->params()["num2"].asInt()<<std::endl;
}

一点问题都没有

测试主题请求类 (TopicRequest)

cpp 复制代码
// test_topic_request.cpp
#include "../../common/message.hpp"
#include <iostream>

int main()
{
    // 创建主题请求
    jsonRpc::TopicRequest::ptr trp = jsonRpc::MessageFactory::create<jsonRpc::TopicRequest>();
    
    // 设置主题请求参数
    trp->setTopicKey("stock.prices");
    trp->setOptype(jsonRpc::TopicOptype::TOPIC_PUBLISH);
    trp->setTopicMsg("{\"AAPL\": 150.25, \"GOOGL\": 2800.75}");
    
    // 序列化并输出
    std::string str = trp->serialize();
    std::cout << "主题请求序列化结果:" << std::endl;
    std::cout << str << std::endl;
    std::cout << "----------------------------------------" << std::endl;
    
    // 反序列化测试
    jsonRpc::BaseMessage::ptr bmp = jsonRpc::MessageFactory::create(jsonRpc::MType::REQ_TOPIC);
    bool ret = bmp->unserialize(str);
    if (ret == false)
    {
        std::cerr << "反序列化失败!" << std::endl;
        return -1;
    }
    
    // 检查消息有效性
    ret = bmp->check();
    if (ret == false)
    {
        std::cerr << "消息检查失败!" << std::endl;
        return -2;
    }
    
    // 向下转型并验证数据
    jsonRpc::TopicRequest::ptr trp2 = std::dynamic_pointer_cast<jsonRpc::TopicRequest>(bmp);
    if (!trp2)
    {
        std::cerr << "向下转型失败!" << std::endl;
        return -3;
    }
    
    std::cout << "反序列化验证结果:" << std::endl;
    std::cout << "主题键名: " << trp2->topicKey() << std::endl;
    std::cout << "操作类型: " << (int)trp2->optype() << std::endl;
    std::cout << "主题消息: " << trp2->topicMsg() << std::endl;
    
    // 测试其他操作类型
    jsonRpc::TopicRequest::ptr trp3 = jsonRpc::MessageFactory::create<jsonRpc::TopicRequest>();
    trp3->setTopicKey("weather.updates");
    trp3->setOptype(jsonRpc::TopicOptype::TOPIC_SUBSCRIBE);  // 订阅操作
    
    std::string str2 = trp3->serialize();
    std::cout << "\n订阅操作序列化结果:" << std::endl;
    std::cout << str2 << std::endl;
    
    return 0;
}

测试服务请求类 (ServiceRequest)

cpp 复制代码
// test_service_request.cpp
#include "../../common/message.hpp"
#include <iostream>

int main()
{
    // 创建服务请求(服务注册)
    jsonRpc::ServiceRequest::ptr srp = jsonRpc::MessageFactory::create<jsonRpc::ServiceRequest>();
    
    // 设置服务注册请求参数
    srp->setMethod("UserService");
    srp->setOptype(jsonRpc::ServiceOptype::SERVICE_REGISTRY);
    srp->setHost(std::make_pair("192.168.1.100", 8080));
    
    // 序列化并输出
    std::string str = srp->serialize();
    std::cout << "服务注册请求序列化结果:" << std::endl;
    std::cout << str << std::endl;
    std::cout << "----------------------------------------" << std::endl;
    
    // 反序列化测试
    jsonRpc::BaseMessage::ptr bmp = jsonRpc::MessageFactory::create(jsonRpc::MType::REQ_SERVICE);
    bool ret = bmp->unserialize(str);
    if (ret == false)
    {
        std::cerr << "反序列化失败!" << std::endl;
        return -1;
    }
    
    // 检查消息有效性
    ret = bmp->check();
    if (ret == false)
    {
        std::cerr << "消息检查失败!" << std::endl;
        return -2;
    }
    
    // 向下转型并验证数据
    jsonRpc::ServiceRequest::ptr srp2 = std::dynamic_pointer_cast<jsonRpc::ServiceRequest>(bmp);
    if (!srp2)
    {
        std::cerr << "向下转型失败!" << std::endl;
        return -3;
    }
    
    std::cout << "反序列化验证结果:" << std::endl;
    std::cout << "服务方法: " << srp2->method() << std::endl;
    std::cout << "操作类型: " << (int)srp2->optype() << std::endl;
    
    jsonRpc::Address addr = srp2->host();
    std::cout << "主机地址: " << addr.first << ":" << addr.second << std::endl;
    
    // 测试服务发现请求
    jsonRpc::ServiceRequest::ptr srp3 = jsonRpc::MessageFactory::create<jsonRpc::ServiceRequest>();
    srp3->setMethod("UserService");
    srp3->setOptype(jsonRpc::ServiceOptype::SERVICE_DISCOVERY);
    // 注意:服务发现请求不需要 host 字段
    
    std::string str2 = srp3->serialize();
    std::cout << "\n服务发现请求序列化结果:" << std::endl;
    std::cout << str2 << std::endl;
    
    // 验证服务发现请求
    jsonRpc::BaseMessage::ptr bmp2 = jsonRpc::MessageFactory::create(jsonRpc::MType::REQ_SERVICE);
    bmp2->unserialize(str2);
    ret = bmp2->check();
    std::cout << "服务发现请求检查结果: " << (ret ? "通过" : "失败") << std::endl;
    
    return 0;
}

测试RPC响应类 (RpcResponse)

cpp 复制代码
// test_rpc_response.cpp
#include "../../common/message.hpp"
#include <iostream>

int main()
{
    // 创建RPC响应
    jsonRpc::RpcResponse::ptr rsp = jsonRpc::MessageFactory::create<jsonRpc::RpcResponse>();
    
    // 设置响应参数(成功响应)
    rsp->setRCode(jsonRpc::RCode::RCODE_OK);
    
    Json::Value result;
    result["sum"] = 33;
    result["average"] = 16.5;
    result["items"] = Json::arrayValue;
    result["items"].append(11);
    result["items"].append(22);
    result["items"].append(33);
    
    rsp->setResult(result);
    
    // 序列化并输出
    std::string str = rsp->serialize();
    std::cout << "RPC响应序列化结果(成功):" << std::endl;
    std::cout << str << std::endl;
    std::cout << "----------------------------------------" << std::endl;
    
    // 反序列化测试
    jsonRpc::BaseMessage::ptr bmp = jsonRpc::MessageFactory::create(jsonRpc::MType::RSP_RPC);
    bool ret = bmp->unserialize(str);
    if (ret == false)
    {
        std::cerr << "反序列化失败!" << std::endl;
        return -1;
    }
    
    // 检查消息有效性
    ret = bmp->check();
    if (ret == false)
    {
        std::cerr << "消息检查失败!" << std::endl;
        return -2;
    }
    
    // 向下转型并验证数据
    jsonRpc::RpcResponse::ptr rsp2 = std::dynamic_pointer_cast<jsonRpc::RpcResponse>(bmp);
    if (!rsp2)
    {
        std::cerr << "向下转型失败!" << std::endl;
        return -3;
    }
    
    std::cout << "反序列化验证结果:" << std::endl;
    std::cout << "响应状态码: " << (int)rsp2->rcode() << std::endl;
    
    Json::Value res = rsp2->result();
    std::cout << "结果 - sum: " << res["sum"].asInt() << std::endl;
    std::cout << "结果 - average: " << res["average"].asDouble() << std::endl;
    std::cout << "结果 - items 数组大小: " << res["items"].size() << std::endl;
    
    // 测试错误响应
    jsonRpc::RpcResponse::ptr rsp3 = jsonRpc::MessageFactory::create<jsonRpc::RpcResponse>();
    rsp3->setRCode(jsonRpc::RCode::RCODE_NOT_FOUND_SERVICE);
    
    Json::Value errorResult;
    errorResult["error_code"] = 404;
    errorResult["error_message"] = "Method 'Divide' not found";
    rsp3->setResult(errorResult);
    
    std::string str2 = rsp3->serialize();
    std::cout << "\nRPC响应序列化结果(错误):" << std::endl;
    std::cout << str2 << std::endl;
    
    return 0;
}

测试主题响应类 (TopicResponse)

cpp 复制代码
// test_topic_response.cpp
#include "../../common/message.hpp"
#include <iostream>

int main()
{
    // 创建主题响应
    jsonRpc::TopicResponse::ptr trsp = jsonRpc::MessageFactory::create<jsonRpc::TopicResponse>();
    
    // 设置响应参数(订阅成功)
    trsp->setRCode(jsonRpc::RCode::RCODE_OK);
    
    // 序列化并输出
    std::string str = trsp->serialize();
    std::cout << "主题响应序列化结果(成功):" << std::endl;
    std::cout << str << std::endl;
    std::cout << "----------------------------------------" << std::endl;
    
    // 反序列化测试
    jsonRpc::BaseMessage::ptr bmp = jsonRpc::MessageFactory::create(jsonRpc::MType::RSP_TOPIC);
    bool ret = bmp->unserialize(str);
    if (ret == false)
    {
        std::cerr << "反序列化失败!" << std::endl;
        return -1;
    }
    
    // 检查消息有效性
    ret = bmp->check();
    if (ret == false)
    {
        std::cerr << "消息检查失败!" << std::endl;
        return -2;
    }
    
    // 向下转型并验证数据
    jsonRpc::TopicResponse::ptr trsp2 = std::dynamic_pointer_cast<jsonRpc::TopicResponse>(bmp);
    if (!trsp2)
    {
        std::cerr << "向下转型失败!" << std::endl;
        return -3;
    }
    
    std::cout << "反序列化验证结果:" << std::endl;
    std::cout << "响应状态码: " << (int)trsp2->rcode() << std::endl;
    
    // 测试不同的响应状态码
    jsonRpc::TopicResponse::ptr trsp3 = jsonRpc::MessageFactory::create<jsonRpc::TopicResponse>();
    trsp3->setRCode(jsonRpc::RCode::RCODE_NOT_FOUND_TOPIC);
    
    std::string str2 = trsp3->serialize();
    std::cout << "\n主题响应序列化结果(订阅失败):" << std::endl;
    std::cout << str2 << std::endl;
    
    return 0;
}

测试服务响应类 (ServiceResponse)

cpp 复制代码
// test_service_response.cpp
#include "../../common/message.hpp"
#include <iostream>
#include <vector>

int main()
{
    // 创建服务响应(服务发现响应)
    jsonRpc::ServiceResponse::ptr srsp = jsonRpc::MessageFactory::create<jsonRpc::ServiceResponse>();
    
    // 设置服务发现响应参数
    srsp->setRCode(jsonRpc::RCode::RCODE_OK);
    srsp->setOptype(jsonRpc::ServiceOptype::SERVICE_DISCOVERY);
    srsp->setMethod("UserService");
    
    // 设置多个服务实例地址
    std::vector<jsonRpc::Address> addresses;
    addresses.push_back(std::make_pair("192.168.1.100", 8080));
    addresses.push_back(std::make_pair("192.168.1.101", 8081));
    addresses.push_back(std::make_pair("192.168.1.102", 8082));
    
    srsp->setHost(addresses);
    
    // 序列化并输出
    std::string str = srsp->serialize();
    std::cout << "服务发现响应序列化结果:" << std::endl;
    std::cout << str << std::endl;
    std::cout << "----------------------------------------" << std::endl;
    
    // 反序列化测试
    jsonRpc::BaseMessage::ptr bmp = jsonRpc::MessageFactory::create(jsonRpc::MType::RSP_SERVICE);
    bool ret = bmp->unserialize(str);
    if (ret == false)
    {
        std::cerr << "反序列化失败!" << std::endl;
        return -1;
    }
    
    // 检查消息有效性
    ret = bmp->check();
    if (ret == false)
    {
        std::cerr << "消息检查失败!" << std::endl;
        return -2;
    }
    
    // 向下转型并验证数据
    jsonRpc::ServiceResponse::ptr srsp2 = std::dynamic_pointer_cast<jsonRpc::ServiceResponse>(bmp);
    if (!srsp2)
    {
        std::cerr << "向下转型失败!" << std::endl;
        return -3;
    }
    
    std::cout << "反序列化验证结果:" << std::endl;
    std::cout << "响应状态码: " << (int)srsp2->rcode() << std::endl;
    std::cout << "操作类型: " << (int)srsp2->optype() << std::endl;
    std::cout << "服务方法: " << srsp2->method() << std::endl;
    
    std::vector<jsonRpc::Address> retrievedAddrs = srsp2->hosts();
    std::cout << "发现的服务实例数量: " << retrievedAddrs.size() << std::endl;
    
    for (size_t i = 0; i < retrievedAddrs.size(); ++i)
    {
        std::cout << "  实例 " << i + 1 << ": " 
                  << retrievedAddrs[i].first << ":" 
                  << retrievedAddrs[i].second << std::endl;
    }
    
    // 测试服务注册响应
    jsonRpc::ServiceResponse::ptr srsp3 = jsonRpc::MessageFactory::create<jsonRpc::ServiceResponse>();
    srsp3->setRCode(jsonRpc::RCode::RCODE_OK);
    srsp3->setOptype(jsonRpc::ServiceOptype::SERVICE_REGISTRY);
    
    std::string str2 = srsp3->serialize();
    std::cout << "\n服务注册响应序列化结果:" << std::endl;
    std::cout << str2 << std::endl;
    
    // 测试服务响应错误情况
    jsonRpc::ServiceResponse::ptr srsp4 = jsonRpc::MessageFactory::create<jsonRpc::ServiceResponse>();
    srsp4->setRCode(jsonRpc::RCode::RCODE_NOT_FOUND_SERVICE);
    srsp4->setOptype(jsonRpc::ServiceOptype::SERVICE_DISCOVERY);
    srsp4->setMethod("NonExistentService");
    
    std::string str3 = srsp4->serialize();
    std::cout << "\n服务发现失败响应:" << std::endl;
    std::cout << str3 << std::endl;
    
    return 0;
}

2.3.具象层2------网络通信相关

首先,我们这里的代码都是存放在下面的的net.hpp里面的

2.3.1.MuduoBuffer类

我们必须先基于这个BaseBuffer类来进行派生出对应的输入缓冲区的子类,因为所有的网络操作请求都离不开这个输入缓冲区的存在,那么我们先来复习一下这个BaseBuffer类

BaseBuffer 类成员函数

  • readableBytes():获取buffer里面可读字节数

  • peekInt32():从buffer里面尝试读取4字节,但是并不将数据从缓冲区里面移除这4字节长度的数据,主要目的就是看看后面的数据够不够组成一个完整的请求/响应。

  • retrieveInt32():从缓冲区移除4字节大小的数据

  • readInt32():从buffer里面尝试读取4字节,并将数据从缓冲区里面移除。其实就是peekInt32()和retrieveInt32()的组合体。

  • retrieveAsString(size_t len):从缓冲区读取指定长度的字符串数据,这个指定长度就是LV格式里面的L里面得到的。这个函数就是用于读取LV格式里面的V部分的。

我们的协议是LV格式的,我们这里就是输入缓冲区针对这个LV格式的通信协议必备的那些接口

cpp 复制代码
// 抽象缓冲区基类 - 定义网络数据缓冲区的通用接口
class BaseBuffer
{
public:
    using ptr = std::shared_ptr<BaseBuffer>;

    // 获取可读字节数
    virtual size_t readableSize() = 0;

    // 查看缓冲区中的int32值(不移动读指针)
    virtual int32_t peekInt32() = 0;

    // 从缓冲区移除int32大小的数据
    virtual void retrieveInt32() = 0;

    // 读取int32值并移动读指针
    virtual int32_t readInt32() = 0;

    // 从缓冲区读取指定长度的字符串数据
    virtual std::string retrieveAsString(size_t len) = 0;
};

有了这些操作接口,我们就能从输入缓冲区里面取出一条完整的数据了。

由于我们这里其实是使用Muduo网络库来实现的,而这个Muduo库里面就有一个输入缓冲区的概念------muduo::net::Buffer,所以我们这里的派生出的子类就全部使用muduo::net::Buffer来作为实际操作的实体。

实现起来就很简单

cpp 复制代码
// MuduoBuffer类:适配muduo库的Buffer,实现BaseBuffer接口
    // 功能:将muduo::net::Buffer适配为框架通用的缓冲区接口
    class MuduoBuffer : public BaseBuffer {
        public:
            // 智能指针类型别名
            using ptr = std::shared_ptr<MuduoBuffer>;
            
            // 构造函数:接收muduo::net::Buffer指针
            MuduoBuffer(muduo::net::Buffer *buf):_buf(buf){}
            
            // 获取可读数据大小
            virtual size_t readableSize() override 
            {
                return _buf->readableBytes();
            }
            
            // 查看缓冲区中的4字节整数(不移动读指针)
            virtual int32_t peekInt32() override 
            {
                // muduo库会自动处理网络字节序转换
                return _buf->peekInt32();
                //会从缓冲区里面读取4字节数据,然后从网络字节序转换成主机字节序
                //这就说明我们以后如果需要发送数据的时候,就需要先将数据从主机字节序转换成网络字节序,再去进行发送
            }
            
            // 从缓冲区中移除4字节数据(移动读指针)
            virtual void retrieveInt32() override 
            {
                return _buf->retrieveInt32();
            }
            
            // 读取并移除缓冲区中的4字节整数(相当于peekInt32()和retrieveInt32()的组合体)
            virtual int32_t readInt32() override 
            {
                return _buf->readInt32();
            }
            
            // 从缓冲区中读取指定长度的字符串并移除这些数据
            virtual std::string retrieveAsString(size_t len) override 
            {
                return _buf->retrieveAsString(len);
            }
        private:
            // 指向muduo库缓冲区的指针
            muduo::net::Buffer *_buf;
    };

为了方便构建,那么我们就需要一个工厂类

cpp 复制代码
// BufferFactory类:缓冲区工厂类
    // 功能:创建MuduoBuffer对象的工厂类
    class BufferFactory {
        public:
            // 静态创建方法:使用完美转发创建MuduoBuffer对象
            template<typename ...Args>
            static BaseBuffer::ptr create(Args&& ...args) {
                return std::make_shared<MuduoBuffer>(std::forward<Args>(args)...);
            }
    };

这里使用了完美转发和一个模板可变参数整合到一起了,大家如果不太了解的,可以去:【消息队列项目】C++11 异步操作实现线程池_c++线程池std::future-CSDN博客

2.3.2.LVProtocol类

有了输入缓冲区的具体实现,那么现在完美就能来完成我们的协议处理模块。

我们这里就是基于这个BaseProtocol 类来进行派生的了

BaseProtocol 类成员函数

  • onMessage(BaseBufferPtr &, BaseMessagePtr &):处理消息,也就是从缓冲区解析为消息对象

  • canProcessed(const BaseBufferPtr &):判断缓冲区中的数据是否足够我们进行一次处理,判断当前输入缓冲区里面的数据是不是一条完整的数据,如果足够,就去取出来进行出来,如果不足够,就再等新的数据到来

  • serialize(const BaseMessagePtr &):序列化消息对象为字符串,方便后续发送/存储

cpp 复制代码
 // 抽象协议基类 - 定义通信协议的通用接口
    class BaseProtocol
    {
    public:
        using ptr = std::shared_ptr<BaseProtocol>;

        //1.用于读取数据时使用
        // 判断缓冲区中的数据是否可以被处理
        virtual bool canProcessed(const BaseBuffer::ptr &buf) = 0;

        // 处理消息:从缓冲区解析为消息对象
        virtual bool onMessage(const BaseBuffer::ptr &buf, BaseMessage::ptr &msg) = 0;

        //2.用于发送数据时使用
        // 序列化消息对象为字符串
        virtual std::string serialize(const BaseMessage::ptr &msg) = 0;
    };

那么大家还记得我们的通信协议是什么样子的了吗?

这里带大家复习一下

Length字段(4字节固定长度): 这是协议的核心字段,固定占用4个字节(32位),采用大端字节序存储。它表示本条消息中Value部分的总字节长度,为接收方提供精确的数据读取边界。接收方首先读取这4个字节,即可准确知道后续需要接收多少数据才能构成一个完整的消息包。

MType(消息类型,4字节固定) :该字段为Value中的固定字段,固定4字节⻓度,⽤于表⽰该条消息的类型。

  • Rpc调⽤请求/响应类型消息
  • 发布/订阅/取消订阅/消息推送类型消息
  • 主题创建/删除类型消息
  • 服务注册/发现/上线/下线类型消息

IDLength(ID长度字段,4字节固定):

指示后续MID字段的实际字节长度。虽然MID长度可变,但通过这个固定长度的描述字段,接收方能够准确解析MID的边界。

MID(消息标识符,可变长度):

每条消息的唯一标识,用于消息追踪、去重和请求-响应对应。其长度由IDLength字段明确指定,确保灵活性与精确性的平衡。

Body(消息正文,可变长度):

承载消息的实际业务数据,是请求或响应的核心内容。

其长度可通过公式计算得出:
Body长度 = Length值 - (4 + 4 + IDLength值),这种设计既保证了协议的扩展性,又实现了内存的高效利用。


那么在子类里面重写我们父类的虚函数,是不是思路一下子就有了

canProcessed() 函数

  • 作用:判断缓冲区中的数据是否足够处理一条完整的消息
  • 流程:首先检查缓冲区中是否有至少4字节(长度字段),然后查看总长度值,最后判断整个消息体是否完整存在于缓冲区中,如果整个消息体都存在,那么返回true,不存在就返回false。

onMessage() 函数

  • 作用:从缓冲区中解析出一条完整的消息并转换为消息对象
  • 流程:按协议格式顺序读取总长度、消息类型、ID长度,然后计算消息体长度,提取ID和消息体数据,通过消息工厂创建对应类型的消息对象,最后进行反序列化并设置消息ID和类型

serialize() 函数

  • 作用:将消息对象序列化为符合协议格式的二进制字符串
  • 流程:获取消息体序列化结果和ID,将各长度字段转换为网络字节序,计算总长度,预分配内存后按协议格式拼接总长度、消息类型、ID长度、ID和消息体数据

类中定义的常量字段:

  • lenFieldsLength:总长度字段大小(4字节),这个其实是Length字段的长度(4字节固定长度)
  • mtypeFieldsLength:消息类型字段大小(4字节),这个其实是MType的长度(消息类型,4字节固定)
  • idlenFieldsLength:ID长度字段大小(4字节),这个其实是IDLength的长度(ID长度字段,4字节固定)
cpp 复制代码
// LVProtocol类:长度-值协议实现
    // 协议格式:|--总长度(4字节)--|--消息类型(4字节)--|--ID长度(4字节)--|--ID--|--消息体--|
    class LVProtocol : public BaseProtocol {
        public:
            // 智能指针类型别名
            using ptr = std::shared_ptr<LVProtocol>;
            
            // 判断缓冲区中的数据是否足够处理一条完整的消息
            virtual bool canProcessed(const BaseBuffer::ptr &buf) override {
                // 首先检查是否至少有一个长度字段的数据
                if (buf->readableSize() < lenFieldsLength) {
                    return false;
                }
                // 查看总长度(不移动读指针)
                int32_t total_len = buf->peekInt32();
                // 检查是否包含完整消息
                if (buf->readableSize() < (total_len + lenFieldsLength)) {
                    return false;
                }
                return true;
            }
            
            // 处理消息:从缓冲区解析出一条消息
            virtual bool onMessage(const BaseBuffer::ptr &buf, BaseMessage::ptr &msg) override {
                // 假设此时缓冲区中的数据足够一条完整的消息
                int32_t total_len = buf->readInt32();      // 读取Length字段里面的值(注意这个total_len是不包含Length字段自己这4个字节的)
                MType mtype = (MType)buf->readInt32();     // 读取MType字段------消息类型
                int32_t idlen = buf->readInt32();          // 读取IDLength字段里面的值,这个读取出来的就是MID字段的实际字节长度
                
                // 协议格式:|--总长度(4字节)--|--消息类型(4字节)--|--IDLength(4字节)--|--MID--|--消息体--|
                // 计算消息体长度 = 总长度 - 消息类型字段大小 - IDLength字段的长度 - MID字段的实际字节长度  
                int32_t body_len = total_len - mtypeFieldsLength - idlenFieldsLength - idlen;
                
                // 读取ID字符串
                std::string id = buf->retrieveAsString(idlen);//从缓冲区里面读取出idlen字节长度的数据,读取出来的就是MID
                // 读取消息体
                std::string body = buf->retrieveAsString(body_len);//从缓冲区里面读取出body_len字节长度的数据,读取出来的就是body
                
                // 根据消息类型创建对应的消息对象
                msg = MessageFactory::create(mtype);
                if (msg.get() == nullptr) {
                    // 消息类型错误,构造消息对象失败
                    ELOG("消息类型错误,构造消息对象失败!");
                    return false;
                }
                
                // 反序列化消息体
                bool ret = msg->unserialize(body);//将body数据进行反序列化存储到msg对象里面
                if (ret == false) {
                    // 消息体反序列化失败
                    ELOG("消息正文反序列化失败!");
                    return false;
                }
                
                // 设置消息ID和消息类型
                msg->setId(id);
                msg->setMType(mtype);
                return true;
            }
            
            // 序列化消息:将消息对象序列化为协议格式的字符串
            virtual std::string serialize(const BaseMessage::ptr &msg) override {
                // 序列化消息体
                std::string body = msg->serialize();//将msg对象里面的数据序列化成一个字符串保存到body里面,因为body是字符串,所以需要不转换为网络字节序
                std::string id = msg->rid();               // 获取消息ID,因为消息ID是字符串,所以需要不转换为网络字节序
                auto mtype = htonl((int32_t)msg->mtype()); // 转换消息类型为网络字节序,因为消息类型是4字节的整数,所以需要转换为网络字节序
                int32_t idlen = htonl(id.size());          // 转换ID长度为网络字节序,,因为idlen是4字节的整数,所以需要转换为网络字节序
                
                // 计算总长度(主机字节序)
                int32_t h_total_len = mtypeFieldsLength + idlenFieldsLength + id.size() + body.size();

                // 转换为网络字节序
                int32_t n_total_len = htonl(h_total_len);
                //在计算总长度n_total_len时,我们使用的是主机字节序的数值(h_total_len),但在将其放入协议中时,需要转换为网络字节序(n_total_len)。
                
                // 预分配内存,提高性能
                std::string result;
                result.reserve(h_total_len);
                
                // 拼接协议数据
                //使用(char*)&变量可以获取该变量内存起始地址的字符指针(即字节指针),然后指定长度(通常是4字节),将这些字节追加到字符串中。
                //例如,对于n_total_len,它是一个int32_t类型的变量,占用4字节。
                //我们通过&n_total_len获取它的地址,然后将其转换为char*指针,
                //这样我们就可以按字节来访问这个整数的内存表示。
                //然后,我们使用append方法将这个指针开始的lenFieldsLength个字节追加到字符串中。
                result.append((char*)&n_total_len, lenFieldsLength);      // 总长度
                result.append((char*)&mtype, mtypeFieldsLength);          // 消息类型
                result.append((char*)&idlen, idlenFieldsLength);          // ID长度
                result.append(id);                                        // ID
                result.append(body);                                      // 消息体
                
                return result;
            }
        private:
            // 协议各字段长度常量定义
            const size_t lenFieldsLength = 4;      // 总长度字段大小(4字节)
            const size_t mtypeFieldsLength = 4;    // 消息类型字段大小(4字节)
            const size_t idlenFieldsLength = 4;    // ID长度字段大小(4字节)
    };

为什么需要网络字节序的转换?

事实上,这里使用网络字节序的转换,主要是因为这个Muduo库在接受数据的时候,是会主动从网络字节序转换成主机字节序。

那么我们发送数据的时候,是不是就也需要这样子做!!

哪些数据需要网络字节序的转换?

注意我们这里的网络字节序的转换

在网络编程中,只有多字节的标量数据类型需要字节序转换 。具体来说:

  • 需要转换的数据 包括所有多字节整数(如int16、int32、int64)、浮点数(float、double)以及有底层整型类型的枚举**。这是因为这些数据在内存中由多个字节组成**,不同架构的机器存储这些字节的顺序可能相反(小端序或大端序),如果不统一为网络字节序(大端序),接收方会解析出完全错误的数值。
  • 不需要转换的数据主要是字节流和文本数据。字符串(std::string)、字符数组、JSON/XML文本,以及已经序列化的二进制数据(如Protocol Buffers输出),这些都已经是按字节顺序排列的数据流,没有字节序问题。
  • 单个字节(char、uint8_t)也没有字节序问题,因为字节序指的是多字节内部的排列顺序。

简单记忆:需要转换的是"数字"(多字节数值),不需要转换的是"文字"(字符串/文本)和"已经打包好的数据"(序列化结果)

在协议中,通常对固定长度的整数字段进行网络字节序转换,而对可变长度的字符串字段则不需要转换,因为字符串是按字节顺序传输的。

具体到这段代码:

需要转换的字段:

  1. 总长度(n_total_len):这是一个4字节的整数,使用htonl转换为主机字节序(如果主机是小端序则转换为大端序,如果主机是大端序则不变)。

  2. 消息类型(mtype):同样是4字节整数,使用htonl转换。

  3. ID长度(idlen):4字节整数,使用htonl转换。

不需要转换的字段:

  1. ID字符串(id):字符串是由一系列字节组成,每个字符占用一个字节,按顺序传输即可,没有字节序问题。

  2. 消息体(body):同样是一系列字节,按顺序传输。

注意:在计算总长度时,我们使用的是主机字节序的数值(h_total_len),但在将其放入协议中时,需要转换为网络字节序(n_total_len)。

另外,在拼接协议数据时,将整数指针转换为char*指针,然后按字节放入字符串中。由于已经转换为网络字节序,所以按顺序追加字节即可。


为了方便起见,我们专门写了一个工厂类来构造这个对象

cpp 复制代码
// ProtocolFactory类:协议工厂类
    // 功能:创建LVProtocol对象的工厂类
    class ProtocolFactory {
        public:
            // 静态创建方法:使用完美转发创建LVProtocol对象
            template<typename ...Args>
            static BaseProtocol::ptr create(Args&& ...args) {
                return std::make_shared<LVProtocol>(std::forward<Args>(args)...);
            }
    };

这个和上面可以说是一模一样的。

2.3.3.MuduoConnection类

当然了,我们还是需要针对连接来进行派生。还记得我们的通信连接的基类了吗?

BaseConnection 类成员函数

  • send(const BaseMessagePtr &):发送消息,这个时候我们只需要一个BaseMessage,我们将其进行JSON格式(或者其他)的序列化,然后通过这个应用层协议处理模块BaseProtocol 类里面的序列化组织成LV格式,然后将其发送出去

  • shutdown():关闭连接

  • connected():检查连接是否有效

这里我们没有提供recevice操作,因为现在大部分服务器都是异步操作,通过事件触发来接受数据,不需要我们上层来进行专门写这个接口,我们只需要去设置好这个数据到来的回调函数即可。

cpp 复制代码
// 抽象连接基类 - 定义网络连接的通用接口
class BaseConnection
{
public:
    using ptr = std::shared_ptr<BaseConnection>;

    // 发送消息
    virtual void send(const BaseMessage::ptr &msg) = 0;

    // 关闭连接
    virtual void shutdown() = 0;

    // 检查连接是否有效
    virtual bool connected() = 0;
};

那么现在我们就借助前面实现的通信协议模块,然后通过muduo::net::TcpConnection来实现真正的数据发送。

那么MuduoConnection类作为我们BaseConnection类的派生类,我们必须去重写这个父类的虚函数方法。

cpp 复制代码
// MuduoConnection类:适配muduo库的TcpConnection
    // 功能:将muduo::net::TcpConnection适配为框架通用的连接接口
    class MuduoConnection : public BaseConnection {
        public:
            // 智能指针类型别名
            using ptr = std::shared_ptr<MuduoConnection>;
            
            // 构造函数:接收muduo连接对象和协议对象
            MuduoConnection(const muduo::net::TcpConnectionPtr &conn,
                const BaseProtocol::ptr &protocol) : 
                _protocol(protocol), _conn(conn) {}
            
            // 发送消息:将消息序列化后通过muduo连接发送
            virtual void send(const BaseMessage::ptr &msg) override 
            {
                std::string body = _protocol->serialize(msg);//将这个消息对象里面的数据序列化为一个字符串存储到body里面
                _conn->send(body);//通过Muduo库的连接来进行发送
            }
            
            // 关闭连接
            virtual void shutdown() override 
            {
                _conn->shutdown();
            }
            
            // 检查连接状态
            virtual bool connected() override 
            {
                _conn->connected();
            }
        private:
            BaseProtocol::ptr _protocol;           // 协议对象
            muduo::net::TcpConnectionPtr _conn;    // muduo连接对象
    };

这个的思想非常简单,那么为了方便构造,我们还是需要来写一个工厂类来进行构造

cpp 复制代码
// ConnectionFactory类:连接工厂类
    // 功能:创建MuduoConnection对象的工厂类
    class ConnectionFactory {
        public:
            // 静态创建方法:使用完美转发创建MuduoConnection对象
            template<typename ...Args>
            static BaseConnection::ptr create(Args&& ...args) {
                return std::make_shared<MuduoConnection>(std::forward<Args>(args)...);
            }
    };

可以说一点难度都没有。

2.3.4.MuduoServer类

那么针对我们的服务器,我们也需要来实现我们的服务器的派生类了

那么服务器的基类其实是下面这个

BaseServer 类成员函数

  • setConnectionCallback(const ConnectionCallback &):设置连接建立回调

  • setCloseCallback(const CloseCallback &):设置连接关闭回调

  • setMessageCallback(const MessageCallback &):设置消息接收回调

  • start():启动服务器

这几个是最最基本的,因为我们的服务器(例如Muduo库)一般都是异步IO了,那么我们就需要去设置一系列的回调函数,以保证整个服务器的正常运转。

cpp 复制代码
// 回调函数类型定义
using ConnectionCallback = std::function<void(const BaseConnection::ptr &)>;                  // 连接建立回调
using CloseCallback = std::function<void(const BaseConnection::ptr &)>;                       // 连接关闭回调
using MessageCallback = std::function<void(const BaseConnection::ptr &, BaseMessage::ptr &)>; // 消息接收回调

// 抽象服务器基类 - 定义RPC服务器的通用接口
class BaseServer
{
public:
    using ptr = std::shared_ptr<BaseServer>;

    // 设置连接建立回调
    virtual void setConnectionCallback(const ConnectionCallback &cb)
    {
        _cb_connection = cb;
    }

    // 设置连接关闭回调
    virtual void setCloseCallback(const CloseCallback &cb)
    {
        _cb_close = cb;
    }

    // 设置消息接收回调
    virtual void setMessageCallback(const MessageCallback &cb)
    {
        _cb_message = cb;
    }

    // 启动服务器
    virtual void start() = 0;

protected:
    ConnectionCallback _cb_connection; // 连接建立回调函数
    CloseCallback _cb_close;           // 连接关闭回调函数
    MessageCallback _cb_message;       // 消息接收回调函数
};

大家注意了啊,我们这个BaseServer类里面就已经有了这个回调函数的相关处理啊,我们在派生类就没有必要再去搞这些回调函数的管理了,直接当成自己的成员函数去调用即可

那么MuduoServer类作为我们BaseServer 类的派生类,我们必须去重写这个父类的虚函数方法。

MuduoServer类函数整理:

1. 构造函数 MuduoServer(int port)

  • 作用:创建并初始化JSON-RPC服务器

  • 参数:port - 服务器监听的端口号

  • 内部操作

    • 初始化muduo TcpServer,绑定到所有IP地址的指定端口

    • 启用端口复用选项

    • 创建协议对象用于消息的序列化和反序列化

2. start() 函数

  • 作用:启动服务器并开始监听客户端连接

  • 设置回调

    • 连接建立/断开回调(onConnection函数)

    • 消息到达回调(onMessage函数)

  • 启动流程

    • 开始监听指定端口

    • 启动事件循环,进入服务器运行状态

3. onConnection(const muduo::net::TcpConnectionPtr &conn)

  • 作用:处理TCP连接建立和断开事件

  • 连接建立时

    • 创建框架内的连接对象

    • 线程安全地添加到连接映射表

    • 调用用户设置的新连接回调函数

  • 连接断开时

    • 从连接映射表中移除连接

    • 调用用户设置的连接关闭回调函数

  1. onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer buf, muduo::Timestamp)
  • 作用:处理从客户端接收到的网络数据

  • 处理流程

    1. 将muduo缓冲区适配为框架缓冲区

    2. 循环处理缓冲区中的所有完整消息

    3. 数据完整性检查

      • 使用协议对象检查是否有完整消息可处理

      • 如果数据不足,等待更多数据

      • 如果数据过大,关闭连接

    4. 消息解析

      • 使用协议对象反序列化消息

      • 如果解析失败,关闭连接

    5. 消息分发

      • 从连接映射表获取对应的连接对象

      • 调用用户设置的消息处理回调函数

cpp 复制代码
// MuduoServer类:基于muduo库的JSON-RPC服务器实现
    // 功能:管理TCP连接、处理网络事件、分发消息
    class MuduoServer : public BaseServer
    {
    public:
        // 智能指针类型别名
        using ptr = std::shared_ptr<MuduoServer>;

        // 构造函数:初始化服务器
        MuduoServer(int port) : _server(&_baseloop, muduo::net::InetAddress("0.0.0.0", port),
                                        "MuduoServer", muduo::net::TcpServer::kReusePort),
                                _protocol(ProtocolFactory::create()) // 通过工厂类来构建这个LVProtocol类对象
        {
        }

        // 启动服务器
        virtual void start()
        {
            // 设置连接建立/断开回调
            _server.setConnectionCallback(std::bind(&MuduoServer::onConnection, this, std::placeholders::_1));
            // 设置消息到达回调
            _server.setMessageCallback(std::bind(&MuduoServer::onMessage, this,
                                                 std::placeholders::_1,
                                                 std::placeholders::_2,
                                                 std::placeholders::_3));
            _server.start();  // 开始监听端口
            _baseloop.loop(); // 启动事件循环
        }

    private:
        // 连接事件回调函数------当连接建立或者断开的时候才会被调用
        void onConnection(const muduo::net::TcpConnectionPtr &conn)
        {
            if (conn->connected()) // 如果连接存在,说明是新连接建立
            {
                // 新连接建立
                std::cout << "连接建立!\n";
                auto muduo_conn = ConnectionFactory::create(conn, _protocol); // 创建一个MuduoConnection对象
                {
                    // 线程安全地添加连接到连接映射表
                    std::unique_lock<std::mutex> lock(_mutex);
                    _conns.insert(std::make_pair(conn, muduo_conn));
                }
                // 如果有连接建立回调函数,调用它
                if (_cb_connection)
                {
                    _cb_connection(muduo_conn);
                }
            }
            else
            {
                // 连接断开
                std::cout << "连接断开!\n";
                BaseConnection::ptr muduo_conn;
                {
                    // 线程安全地从连接映射表中移除连接
                    std::unique_lock<std::mutex> lock(_mutex);
                    auto it = _conns.find(conn);
                    if (it == _conns.end())
                    {
                        return;
                    }
                    muduo_conn = it->second;
                    _conns.erase(conn);
                }
                // 如果有连接关闭回调,调用它
                if (_cb_close)
                {
                    _cb_close(muduo_conn);
                }
            }
        }

        // 消息到达回调函数
        void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp)
        {
            DLOG("连接有数据到来,开始处理!");
            auto base_buf = BufferFactory::create(buf);//我们构建一个协议处理模块

            // 循环处理缓冲区中的所有完整消息
            while (1)
            {
                // 检查是否有完整的消息可处理
                if (_protocol->canProcessed(base_buf) == false)// 缓冲区中数据不足
                {
                     //有的人发了一个非常大的数据过来,超过了我们的缓冲区的默认大小,缓冲区会一直扩容,但是对面太恶心了,发了很大的数据,
                    //这会导致我们的缓冲区一直进行扩容,最后造成我们的服务器的瘫痪,因此,我们必须对这个接收的数据作出大小限制
                    if (base_buf->readableSize() > maxDataSize)// 数据过大,断开连接
                    {
                        conn->shutdown();
                        ELOG("缓冲区中数据过大!");
                        return;
                    }
                    break; // 等待更多数据
                }

                // 解析消息
                BaseMessage::ptr msg;
                bool ret = _protocol->onMessage(base_buf, msg);//将缓冲区里面的有效数据提取出来存放到msg对象里面
                if (ret == false)
                {
                    // 数据错误,断开连接
                    conn->shutdown();
                    ELOG("缓冲区中数据错误!");
                    return;
                }

                // 获取对应的连接对象------这一步只是为了下面调用我们的消息处理回调函数
                BaseConnection::ptr base_conn;
                {
                    std::unique_lock<std::mutex> lock(_mutex);
                    auto it = _conns.find(conn);//找找有没有对应的连接对象
                    if (it == _conns.end())//没找到
                    {
                        conn->shutdown();//关闭连接
                        return;
                    }
                    base_conn = it->second;
                }

                // 调用消息处理回调
                if (_cb_message)
                {
                    _cb_message(base_conn, msg);
                }
            }
        }

    private:
        const size_t maxDataSize = (1 << 16);                                         // 最大数据大小:64KB
        BaseProtocol::ptr _protocol;                                                  // 协议对象
        muduo::net::EventLoop _baseloop;                                              // 事件循环
        muduo::net::TcpServer _server;                                                // muduo服务器对象
        std::mutex _mutex;                                                            // 保护连接映射表的互斥锁
        std::unordered_map<muduo::net::TcpConnectionPtr, BaseConnection::ptr> _conns; // 连接映射表
    };

很简单,当然,为了方便我们的后续的构建,我们还是写了一个工厂类来构建这个项目

cpp 复制代码
// ServerFactory类:服务器工厂类
    // 功能:创建MuduoServer对象的工厂类
    class ServerFactory
    {
    public:
        // 静态创建方法:使用完美转发创建MuduoServer对象
        template <typename... Args>
        static BaseServer::ptr create(Args &&...args)
        {
            return std::make_shared<MuduoServer>(std::forward<Args>(args)...);
        }
    };

2.3.5.BaseClient 类

现在服务器有了,我们的客户端也不能少。

那么我们的客户端其实是基于下面这个来进行派生的

BaseClient 类成员函数

  • setConnectionCallback(const ConnectionCallback &):设置连接建立回调

  • setCloseCallback(const CloseCallback &):设置连接关闭回调

  • setMessageCallback(const MessageCallback &):设置消息接收回调

  • connect():连接到服务器

  • send(const BaseMessagePtr &message):发送消息

  • shutdown():关闭连接

  • connected():检查连接是否有效

  • BaseConnectionPtr connection():返回底层的连接对象

这些接口都是最最基本的,我们必须写

cpp 复制代码
// 抽象客户端基类 - 定义RPC客户端的通用接口
class BaseClient
{
public:
    using ptr = std::shared_ptr<BaseClient>;

    // 设置连接建立回调
    virtual void setConnectionCallback(const ConnectionCallback &cb)
    {
        _cb_connection = cb;
    }

    // 设置连接关闭回调
    virtual void setCloseCallback(const CloseCallback &cb)
    {
        _cb_close = cb;
    }

    // 设置消息接收回调
    virtual void setMessageCallback(const MessageCallback &cb)
    {
        _cb_message = cb;
    }

    // 连接到服务器
    virtual void connect() = 0;

    // 关闭连接
    virtual void shutdown() = 0;

    // 发送消息
    virtual bool send(const BaseMessage::ptr &) = 0;

    // 获取底层连接对象
    virtual BaseConnection::ptr connection() = 0;

    // 检查连接是否有效
    virtual bool connected() = 0;

protected:
    ConnectionCallback _cb_connection; // 连接建立回调函数
    CloseCallback _cb_close;           // 连接关闭回调函数
    MessageCallback _cb_message;       // 消息接收回调函数
};

大家注意了啊,我们这个BaseClient类里面就已经有了这个回调函数的相关处理啊,我们在派生类就没有必要再去搞这些回调函数的管理了,直接当成自己的成员函数去调用即可

那么MuduoClient类作为我们BaseClient类的派生类,我们必须去重写这个父类的虚函数方法。

MuduoClient类函数整理:

1. 构造函数 MuduoClient(const std::string &sip, int sport)

  • 作用:创建并初始化JSON-RPC客户端

  • 参数:sip - 服务器IP地址,sport - 服务器端口号

  • 内部操作

    • 创建协议对象用于消息的序列化和反序列化

    • 启动事件循环线程用于处理网络事件

    • 初始化倒计时锁,用于同步等待连接建立

    • 创建muduo TcpClient对象

2. connect() 函数

  • 作用:建立与服务器的TCP连接

  • 连接流程

    1. 设置连接建立/断开回调函数(onConnection)

    2. 设置消息到达回调函数(onMessage)

    3. 发起连接请求

    4. 使用倒计时锁等待连接建立完成

    5. 连接成功后继续执行

3. shutdown() 函数

  • 作用:断开与服务器的连接

  • 操作:调用muduo客户端的断开连接方法

4. send(const BaseMessage::ptr &msg) 函数

  • 作用:发送消息到服务器

  • 发送流程

    1. 检查连接状态,如果已断开则返回失败

    2. 通过连接对象发送消息

  • 返回值:发送成功返回true,失败返回false

5. connection() 函数

  • 作用:获取当前连接对象

  • 返回值:返回BaseConnection智能指针,可从中获取连接相关信息

6. connected() 函数

  • 作用:检查客户端与服务器的连接状态

  • 返回值:连接存在且有效返回true,否则返回false

7. onConnection(const muduo::net::TcpConnectionPtr &conn)

  • 作用:处理TCP连接建立和断开事件

  • 连接建立时

    1. 释放倒计时锁,唤醒等待线程

    2. 创建框架内的连接对象

  • 连接断开时

    1. 重置连接对象指针,清空连接
  1. onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer buf, muduo::Timestamp)
  • 作用:处理从服务器接收到的响应数据

  • 处理流程

    1. 将muduo缓冲区适配为框架缓冲区

    2. 循环处理缓冲区中的所有完整消息

    3. 数据完整性检查

      • 使用协议对象检查是否有完整消息可处理

      • 如果数据不足,等待更多数据

      • 如果数据过大,关闭连接

    4. 消息解析

      • 使用协议对象反序列化消息

      • 如果解析失败,关闭连接

    5. 消息分发:调用用户设置的消息处理回调函数

cpp 复制代码
// MuduoClient类:基于muduo库的JSON-RPC客户端实现
    // 功能:连接服务器、发送请求、接收响应
    class MuduoClient : public BaseClient
    {
    public:
        // 智能指针类型别名
        using ptr = std::shared_ptr<MuduoClient>;

        // 构造函数:初始化客户端
        MuduoClient(const std::string &sip, int sport) : _protocol(ProtocolFactory::create()),
                                                         _baseloop(_loopthread.startLoop()), // 启动事件循环线程
                                                         _downlatch(1),                      // 倒计时锁,用于等待连接建立
                                                         _client(_baseloop, muduo::net::InetAddress(sip, sport), "MuduoClient")
        {
        }

        // 连接服务器
        virtual void connect() override
        {
            DLOG("设置回调函数,连接服务器");
            // 设置连接回调
            _client.setConnectionCallback(std::bind(&MuduoClient::onConnection, this, std::placeholders::_1));
            // 设置消息回调
            _client.setMessageCallback(std::bind(&MuduoClient::onMessage, this,
                                                 std::placeholders::_1,
                                                 std::placeholders::_2,
                                                 std::placeholders::_3));

            // 连接服务器
            _client.connect();
            _downlatch.wait(); // 等待连接建立
            DLOG("连接服务器成功!");
        }

        // 断开连接
        virtual void shutdown() override
        {
            return _client.disconnect();
        }

        // 发送消息
        virtual bool send(const BaseMessage::ptr &msg) override
        {
            if (connected() == false)
            {
                ELOG("连接已断开!");
                return false;
            }
            _conn->send(msg);
        }

        // 获取连接对象
        virtual BaseConnection::ptr connection() override
        {
            return _conn;
        }

        // 检查连接状态
        virtual bool connected()
        {
            return (_conn && _conn->connected());
        }

    private:
        // 连接事件回调函数------只有连接建立或者连接断开的时候才会被调用
        void onConnection(const muduo::net::TcpConnectionPtr &conn)
        {
            if (conn->connected())
            {
                // 连接建立成功
                std::cout << "连接建立!\n";
                _conn = ConnectionFactory::create(conn, _protocol);
                _downlatch.countDown(); // 计数减一,唤醒等待线程
            }
            else
            {
                // 连接断开
                std::cout << "连接断开!\n";
                _conn.reset(); // 重置连接指针
            }
        }

        // 消息到达回调函数
        void onMessage(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buf, muduo::Timestamp)
        {
            DLOG("连接有数据到来,开始处理!");
            auto base_buf = BufferFactory::create(buf); // 通过工厂类来构建出一个协议处理对象

            // 循环处理缓冲区中的所有完整消息
            while (1)
            {
                // 检查是否有完整的消息可处理
                if (_protocol->canProcessed(base_buf) == false) // 数据不足一条完整的信息
                {
                    // 服务器返回的数据太大了
                    if (base_buf->readableSize() > maxDataSize)
                    {
                        // 数据过大,断开连接
                        conn->shutdown();
                        ELOG("缓冲区中数据过大!");
                        return;
                    }
                    break; // 等待更多数据
                }

                // 解析消息
                BaseMessage::ptr msg;
                bool ret = _protocol->onMessage(base_buf, msg);
                if (ret == false)
                {
                    // 数据错误,断开连接
                    conn->shutdown();
                    ELOG("缓冲区中数据错误!");
                    return;
                }

                // 调用消息处理回调
                if (_cb_message)
                {
                    _cb_message(_conn, msg);
                }
            }
        }

    private:
        const size_t maxDataSize = (1 << 16);    // 最大数据大小:64KB
        BaseProtocol::ptr _protocol;             // 协议对象
        BaseConnection::ptr _conn;               // 连接对象
        muduo::CountDownLatch _downlatch;        // 倒计时锁,用于同步连接建立
        muduo::net::EventLoopThread _loopthread; // 事件循环线程
        muduo::net::EventLoop *_baseloop;        // 事件循环指针
        muduo::net::TcpClient _client;           // muduo客户端对象
    };

事实上,这里存在一个小细节

让我们看看一个错误的 onConnection 回调函数:

cpp 复制代码
void onConnection(const muduo::net::TcpConnectionPtr &conn) {
    if (conn->connected()) {
        // 连接建立成功
        std::cout << "连接建立!\n";
        _downlatch.countDown(); // 计数减一,唤醒等待线程
        _conn = ConnectionFactory::create(conn, _protocol);  // 在这里赋值!
    } else {
        // 连接断开
        std::cout << "连接断开!\n";
        _conn.reset(); // 重置连接指针
    }
}

我们先去唤醒线程,再去更新连接的状态,这样子是不是有问题??

也就是说

cpp 复制代码
_downlatch.countDown(); // 计数减一,唤醒等待线程
_conn = ConnectionFactory::create(conn, _protocol);  // 在这里赋值!

问题

  • countDown() 被调用后,主线程立即从 _downlatch.wait() 中唤醒

  • 主线程开始执行 client->send(rpc_req)

  • 但是此时 _conn 可能还没有被赋值 (因为赋值操作在 countDown() 之后)

  • 这导致了竞争条件 :主线程尝试使用 _conn 时,它可能还是 nullptr

正确的顺序应该是:

cpp 复制代码
_conn = ConnectionFactory::create(conn, _protocol);  // 先赋值
_downlatch.countDown(); // 再唤醒等待线程

为什么这个顺序正确:

  • 先创建并赋值 _conn,确保连接对象已经完全初始化
  • 然后再唤醒等待的主线程
  • 主线程被唤醒时,_conn 已经准备好可以使用

那么为了我们我们的构建,那么我们就需要写一个工厂类来构建这个对象

cpp 复制代码
// ClientFactory类:客户端工厂类
    // 功能:创建MuduoClient对象的工厂类
    class ClientFactory
    {
    public:
        // 静态创建方法:使用完美转发创建MuduoClient对象
        template <typename... Args>
        static BaseClient::ptr create(Args &&...args)
        {
            return std::make_shared<MuduoClient>(std::forward<Args>(args)...);
        }
    };

至此,我们整个的具象层都写完了。

2.3.6.服务端客户端通信测试

server.hpp

cpp 复制代码
#include "../../common/message.hpp"
#include "../../common/net.hpp"

// onMessage函数:处理接收到的消息
// 参数:conn - 连接对象指针,msg - 接收到的消息对象指针
void onMessage(const jsonRpc::BaseConnection::ptr& conn, jsonRpc::BaseMessage::ptr& msg) {
    // 将消息对象序列化为字符串格式
    std::string body = msg->serialize();
    
    // 打印序列化后的消息内容到标准输出
    std::cout << body << std::endl;
    
    // 创建一个RPC响应消息对象
    // 使用MessageFactory工厂类创建指定类型的消息对象
    auto rpc_req = jsonRpc::MessageFactory::create<jsonRpc::RpcResponse>();
    
    // 设置响应消息的ID为"11111"
    rpc_req->setId("11111");
    
    // 设置消息类型为RPC响应类型
    rpc_req->setMType(jsonRpc::MType::RSP_RPC);
    
    // 设置响应码为成功
    rpc_req->setRCode(jsonRpc::RCode::RCODE_OK);
    
    // 设置响应结果为整数33
    rpc_req->setResult(33);
    
    // 通过连接对象将响应消息发送回客户端
    conn->send(rpc_req);
}

// main函数:程序入口点
int main()
{
    // 创建一个JSON-RPC服务器实例,监听端口9090
    auto server = jsonRpc::ServerFactory::create(9090);
    
    // 设置服务器的消息处理回调函数为上面定义的onMessage函数
    server->setMessageCallback(onMessage);
    
    // 启动服务器,开始监听客户端连接和处理请求
    server->start();
    
    // 程序正常结束,返回0
    return 0;
}

client.hpp

cpp 复制代码
#include "../../common/message.hpp"
#include "../../common/net.hpp"
#include<thread>

// onMessage函数:处理接收到的消息
// 参数:conn - 连接对象指针,msg - 接收到的消息对象指针
void onMessage(const jsonRpc::BaseConnection::ptr &conn, jsonRpc::BaseMessage::ptr &msg)
{
    // 将消息对象序列化为字符串格式
    std::string body = msg->serialize();
    std::cout << body << std::endl;
}

// main函数:程序入口点
int main()
{
    // 创建一个JSON-RPC服务器实例,监听端口9090
    auto client = jsonRpc::ClientFactory::create("127.0.0.1", 9090);

    client->setMessageCallback(onMessage);
    client->connect();
    
    auto rpc_req = jsonRpc::MessageFactory::create<jsonRpc::RpcRequest>();
    rpc_req->setId("11111");
    rpc_req->setMType(jsonRpc::MType::REQ_RPC);
    rpc_req->setMethod("Add");

    Json::Value param;
    param["num1"] = 11;
    param["num2"] = 22;
    rpc_req->setParams(param);

    if (client->send(rpc_req)) {
        std::cout << "发送成功!" << std::endl;
    } else {
        std::cout << "发送失败!" << std::endl;
    }
    std::this_thread::sleep_for(std::chrono::seconds(10));//休眠10秒
    client->shutdown();

    // 程序正常结束,返回0
    return 0;
}

比较的完美!!

由于我们文章篇幅比较大了,那么剩下的diapatcher和业务层,我们就留到后续的文章去跟大家详细讲解

相关推荐
步步为营DotNet2 小时前
深度解析.NET中IEnumerable<T>.SelectMany:数据扁平化与复杂映射的利器
java·开发语言·.net
Dreamy smile2 小时前
JavaScript 实现 HTTPS SSE 连接
开发语言·javascript·https
tqs_123452 小时前
Spring 框架中的 IoC (控制反转) 和 AOP (面向切面编程) 及其应用
java·开发语言·log4j
比昨天多敲两行2 小时前
C++ 类和对象(中)
开发语言·c++
hzb666662 小时前
basectf2024
开发语言·python·sql·学习·安全·web安全·php
深蓝海拓2 小时前
Qt(PySide/PyQt)的信号槽机制的比较深入的学习笔记
qt·学习·pyqt
superman超哥3 小时前
序列化性能优化:从微秒到纳秒的极致追求
开发语言·rust·开发工具·编程语言·rust序列化性能优化·rust序列化
Henry Zhu1233 小时前
Qt Model/View架构详解(一):基础理论
开发语言·qt
Swift社区3 小时前
Java 实战 - 字符编码问题解决方案
java·开发语言