【微服务即时通讯】消息转发子服务

目录

一.实现

1.1..hxx文件的定义

1.2.mysql操作类的实现

1.2.1.测试程序

1.3.RPC服务的定义

1.3.1.消息类型的定义(base.proto)

1.3.2.transmite.proto

1.3.3.RPC接口实现

1.4.TransmiteServer类的实现

1.5.TransmiteServerBuilder类

1.6.搭建消息转发子服务

二.测试


我们这个消息转发子服务的架构如下:

一.实现

1.1..hxx文件的定义

还记得我们之前的聊天设计思路吗?

多人聊天会话

在后台系统中,所有聊天消息都需要被持久化保存,通常通过数据库中的消息存储表来实现。

每条消息都必须明确记录其来源与去向,即消息由谁发出、最终要送达给谁。

在单聊场景中,这种设计是可行的,因为每一条消息只涉及发送方和接收方两个明确的用户ID。

然而,在群聊场景中,同一消息需要送达给多个用户,如果仍在单条消息中记录所有接收者,则面临存储结构上的难题------一条消息难以合理承载多个用户ID,这样的设计既不灵活,也不符合数据库规范。

为此,我们引入了"聊天会话"这一中间概念。**每个聊天会话在后台都有独立的元数据,其中记录了参与该会话的所有成员信息。**当一条群消息发送时,系统只需关联到对应的会话ID,即可通过会话成员表确定所有接收者,从而避免在单条消息中冗余存储多个用户ID。这样既保证了数据的规范化,也提升了系统的可扩展性与维护性。

那么我们在这里就实现了这部分

chat_session_member.hxx

cpp 复制代码
// 聊天会话成员表映射对象
#pragma once
#include <cstddef>
#include <odb/core.hxx>
#include <string>

namespace IMS
{
// 使用 ODB 映射到数据库中的 chat_session_member 表
#pragma db object table("chat_session_member")
    class ChatSessionMember
    {
    public:
        // 默认构造函数,用于 ODB 反序列化
        ChatSessionMember() {}

        // 构造函数:使用会话ID和用户ID创建成员对象
        ChatSessionMember(const std::string &ssid, const std::string &uid) : _session_id(ssid), _user_id(uid) {}

        // 析构函数
        ~ChatSessionMember() {}

        // 获取会话ID
        std::string session_id() const { return _session_id; }
        // 设置会话ID
        void session_id(std::string &ssid) { _session_id = ssid; }

        // 获取用户ID
        std::string user_id() const { return _user_id; }
        // 设置用户ID
        void user_id(std::string &uid) { _user_id = uid; }

    private:
        // 允许 ODB 访问私有成员
        friend class odb::access;

// 自增主键ID,数据库自动生成
#pragma db id auto
        unsigned long _id;

// 会话ID,类型为 varchar(64),创建索引以加速查询
#pragma db type("varchar(64)") index
        std::string _session_id;

// 用户ID,类型为 varchar(64)
#pragma db type("varchar(64)")
        std::string _user_id;
    };
} // namespace IMS

这个类映射的是聊天会话与用户的成员关系表,存储的是"某个会话中有哪些用户"的信息。从数据结构上看:

  • 一个 session_id 可以对应多条记录(表示该会话有多个成员)------比如群聊。
  • 一个 user_id 也可以对应多条记录(表示该用户加入了多个会话)------比如用户同时参与多个群聊和单聊。

因此它表示的是多对多关系,而不是一对一

所以该表不仅能保存一对一的成员关系,也能保存一对多、多对多等场景。

需要特别注意:我们在这个用户管理子服务说过,用户登录后会分配唯一的一个会话ID,那么那个会话ID和我们这里的会话ID有啥关系吗??

事实上它们没有任何关系!!!

  • 我们这里的会话ID代表聊天会话的ID,即一个具体的聊天对话(如单聊或群聊)的唯一标识。它用于标识用户之间交流的"房间"或"会话",与聊天业务直接相关。
  • 用户登录后服务器分配的"会话ID":通常是用于维护用户的登录状态、权限验证等。它属于系统层面的会话管理,与具体聊天内容无关。

1.2.mysql操作类的实现

我们在上面虽然定义出了mysql数据库里关于"某个会话中有哪些用户"的表的定义,但是我们会经常去对这个表进行数据插入,查询等相关操作,因此,我们就有必要去对这些操作进行二次封装,以便我们更方便的去调用

那么由于我们多个子服务都需要使用到这个数据库连接,那么我们就将这个数据库连接单独进行封装到这个mysql.hpp里了。有了这个mysql.hpp,我们就能专注于我们的这个user表的增删查改操作了,而不用去考虑数据库连接的问题。

我们直接编写出来这个mysql_chat_session_member.hpp

cpp 复制代码
#pragma once
#include "chat_session_member-odb.hxx"   // ODB 生成的查询类,用于数据库操作
#include "chat_session_member.hxx"       // ChatSessionMember 类定义
#include "mysql.hpp"                     // 数据库连接相关封装

namespace IMS
{
    // 聊天会话成员表操作类,提供对 chat_session_member 表的增删查操作
    class ChatSessionMemeberTable
    {
    public:
        using ptr = std::shared_ptr<ChatSessionMemeberTable>;

        // 构造函数,传入数据库连接对象
        ChatSessionMemeberTable(const std::shared_ptr<odb::core::database> &db) : _db(db) {}

        // 新增单个会话成员记录
        // 参数 csm: 包含会话ID和用户ID的 ChatSessionMember 对象
        // 返回值: true 表示成功,false 表示失败
        bool append(ChatSessionMember &csm)
        {
            try
            {
                odb::transaction trans(_db->begin());   // 开启数据库事务
                _db->persist(csm);                      // 持久化对象,插入数据库
                trans.commit();                         // 提交事务
            }
            catch (std::exception &e)
            {
                // 记录错误日志,包含会话ID、用户ID和异常信息
                LOG_ERROR("新增单会话成员失败 {}-{}:{}!",
                          csm.session_id(), csm.user_id(), e.what());
                return false;
            }
            return true;
        }

        // 批量新增多个会话成员记录
        // 参数 csm_lists: ChatSessionMember 对象的 vector 容器
        // 返回值: true 表示全部成功,false 表示任意失败
        bool append(std::vector<ChatSessionMember> &csm_lists)
        {
            try
            {
                odb::transaction trans(_db->begin());   // 开启事务
                for (auto &csm : csm_lists)             // 遍历每个成员对象
                {
                    _db->persist(csm);                  // 逐条插入
                }
                trans.commit();                         // 提交事务
            }
            catch (std::exception &e)
            {
                // 记录错误日志,包含第一个会话ID、插入数量和异常信息
                LOG_ERROR("新增多会话成员失败 {}-{}:{}!",
                          csm_lists[0].session_id(), csm_lists.size(), e.what());
                return false;
            }
            return true;
        }

        // 删除指定会话中的指定成员(通过会话ID和用户ID定位)
        // 参数 csm: 包含要删除的会话ID和用户ID的 ChatSessionMember 对象
        // 返回值: true 表示删除成功(即使没有匹配记录也返回成功),false 表示异常
        bool remove(ChatSessionMember &csm)
        {
            try
            {
                odb::transaction trans(_db->begin());
                // 使用 ODB 查询语法构造删除条件:session_id == 指定值 and user_id == 指定值
                typedef odb::query<ChatSessionMember> query;
                typedef odb::result<ChatSessionMember> result;
                _db->erase_query<ChatSessionMember>(query::session_id == csm.session_id() &&
                                                    query::user_id == csm.user_id());
                trans.commit();
            }
            catch (std::exception &e)
            {
                LOG_ERROR("删除单会话成员失败 {}-{}:{}!",
                          csm.session_id(), csm.user_id(), e.what());
                return false;
            }
            return true;
        }

        // 删除指定会话的所有成员(根据会话ID)
        // 参数 ssid: 会话ID字符串
        // 返回值: true 表示删除成功(即使没有记录也返回成功),false 表示异常
        bool remove(const std::string &ssid)
        {
            try
            {
                odb::transaction trans(_db->begin());
                typedef odb::query<ChatSessionMember> query;
                typedef odb::result<ChatSessionMember> result;
                // 删除所有 session_id 等于指定值的记录
                _db->erase_query<ChatSessionMember>(query::session_id == ssid);
                trans.commit();
            }
            catch (std::exception &e)
            {
                LOG_ERROR("删除会话所有成员失败 {}:{}!", ssid, e.what());
                return false;
            }
            return true;
        }

        // 获取指定会话的所有成员的用户ID列表
        // 参数 ssid: 会话ID字符串
        // 返回值: 包含该会话所有成员用户ID的 vector 容器(可能为空)
        std::vector<std::string> members(const std::string &ssid)
        {
            std::vector<std::string> res;
            try
            {
                odb::transaction trans(_db->begin());
                typedef odb::query<ChatSessionMember> query;
                typedef odb::result<ChatSessionMember> result;
                // 查询所有 session_id 等于指定值的记录
                result r(_db->query<ChatSessionMember>(query::session_id == ssid));
                for (result::iterator i(r.begin()); i != r.end(); ++i)
                {
                    res.push_back(i->user_id());   // 提取用户ID并加入结果列表
                }
                trans.commit();
            }
            catch (std::exception &e)
            {
                LOG_ERROR("获取会话成员失败:{}-{}!", ssid, e.what());
                // 发生异常时返回空列表
            }
            return res;
        }

    private:
        std::shared_ptr<odb::core::database> _db;   // 数据库连接对象(智能指针)
    };
}

那么这么容易编写出来的话,我们是不是有必要去进行测试一下呢??

1.2.1.测试程序

我们其实很容易就写出这个测试程序

cpp 复制代码
// 引入聊天会话成员表的封装类
#include "../../../../common/mysql_chat_session_member.hpp"
// 引入 ODB 生成的查询相关代码,用于数据库操作
#include "chat_session_member-odb.hxx"
// 引入 gflags 命令行参数解析库
#include <gflags/gflags.h>

// 定义命令行参数:运行模式,false-调试模式,true-发布模式
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
// 定义命令行参数:发布模式下的日志输出文件路径
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
// 定义命令行参数:发布模式下的日志输出等级
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");

// 测试单个插入功能:向会话中逐个添加成员
void append_test(IMS::ChatSessionMemeberTable &tb)
{
    // 创建成员对象:会话ID1,用户ID1
    IMS::ChatSessionMember csm1("会话ID1", "用户ID1");
    tb.append(csm1);  // 插入数据库
    // 创建成员对象:会话ID1,用户ID2
    IMS::ChatSessionMember csm2("会话ID1", "用户ID2");
    tb.append(csm2);  // 插入数据库
    // 创建成员对象:会话ID2,用户ID3
    IMS::ChatSessionMember csm3("会话ID2", "用户ID3");
    tb.append(csm3);  // 插入数据库
}

// 测试批量插入功能:向同一个会话批量添加多个成员
void multi_append_test(IMS::ChatSessionMemeberTable &tb)
{
    // 创建三个成员对象,都属于会话ID3
    IMS::ChatSessionMember csm1("会话ID3", "用户ID1");
    IMS::ChatSessionMember csm2("会话ID3", "用户ID2");
    IMS::ChatSessionMember csm3("会话ID3", "用户ID3");
    // 将三个成员放入vector中
    std::vector<IMS::ChatSessionMember> list = {csm1, csm2, csm3};
    // 批量插入
    tb.append(list);
}

// 测试删除单个成员功能
void remove_test(IMS::ChatSessionMemeberTable &tb)
{
    // 创建要删除的成员对象:会话ID2,用户ID3
    IMS::ChatSessionMember csm3("会话ID2", "用户ID3");
    // 从数据库中删除该成员记录
    tb.remove(csm3);
}

// 测试获取会话成员列表功能
void ss_members(IMS::ChatSessionMemeberTable &tb)
{
    // 获取会话ID1的所有成员用户ID
    auto res = tb.members("会话ID1");
    // 遍历并输出每个用户ID
    for (auto &id : res)
    {
        std::cout << id << std::endl;
    }
}

// 测试删除会话所有成员功能
void remove_all(IMS::ChatSessionMemeberTable &tb)
{
    // 删除会话ID3的所有成员记录
    tb.remove("会话ID3");
}

int main(int argc, char *argv[])
{
    // 解析命令行参数
    google::ParseCommandLineFlags(&argc, &argv, true);
    // 初始化日志系统
    IMS::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);

    // 创建数据库连接:用户名root,密码123456,主机127.0.0.1,数据库名IMS,字符集utf8,连接池最小0,最大1
    auto db = IMS::ODBFactory::create("root", "123456", "127.0.0.1", "IMS", "utf8", 0, 1);

    // 创建聊天会话成员表操作对象
    IMS::ChatSessionMemeberTable csmt(db);

    // 依次执行测试函数
    append_test(csmt);       // 插入测试数据
    multi_append_test(csmt); // 批量插入测试
    remove_test(csmt);       // 删除单个成员测试
    ss_members(csmt);        // 查询会话成员测试
    remove_all(csmt);        // 删除会话所有成员测试

    return 0;
}

我们编译出来会有一个.sql文件

我们需要先将这个sql文件导入进我们的mysql数据库里面

cpp 复制代码
mysql -u root -D IMS -p <chat_session_member.sql

注意这里需要我们提前创建好IMS数据库,不过我们之前在那个用户管理子服务已经创建了,这个问题不大

接下来我们就运行我们的程序

非常完美,直接给到夯。

注意:我们测试完之后,我们需要将这个表里面的数据给清空

cpp 复制代码
TRUNCATE TABLE chat_session_member;

清空完了之后,我们才能进行后续的测试

1.3.RPC服务的定义

1.3.1.消息的定义(base.proto)

我们这个服务首先是消息转发子服务。

那么消息转发,首先我们需要先将消息给定义出来吧

首先我们的消息是分成4大类型的

  • 纯文本消息
  • 图片消息
  • 文件消息
  • 语音消息
javascript 复制代码
// 消息类型枚举,定义不同类型的消息内容
enum MessageType
{
    STRING = 0;   // 纯文本消息
    IMAGE = 1;    // 图片消息
    FILE = 2;     // 文件消息
    SPEECH = 3;   // 语音消息
}

这4种消息的内容是需要不同的数据结构来进行存储的,所以我们就必须需要先将它们定义出来。

cpp 复制代码
// 纯文本消息的具体内容
message StringMessageInfo {
    string content = 1; // 文字聊天内容
}

// 图片消息的具体内容
message ImageMessageInfo {
    // 图片文件ID,客户端发送时不需要设置,由transmit服务器生成后交给storage服务时设置
    optional string file_id = 1;//optional表示该字段是可选的
    // 图片数据,在ES中存储消息时只需保留ID,不需要存储文件数据;服务端转发时需要原样转发
    optional bytes image_content = 2;//optional表示该字段是可选的
}

// 文件消息的具体内容
message FileMessageInfo {
    optional string file_id = 1;      // 文件ID,客户端发送时不用设置,由服务端生成
    optional int64 file_size = 2;     // 文件大小(字节)
    optional string file_name = 3;    // 文件名称
    // 文件数据,在ES中存储消息时只需保留ID和元信息,不需要存储文件数据;服务端转发时也不需要填充
    optional bytes file_contents = 4;
}

// 语音消息的具体内容
message SpeechMessageInfo {
    optional string file_id = 1;      // 语音文件ID,客户端发送时不用设置,由服务端生成
    // 文件数据,在ES中存储消息时只需保留ID,不需要存储文件数据;服务端转发时也不需要填充
    optional bytes file_contents = 2;
}

这样子,我们就类型+内容我们都定义出来了,那么我们现在就需要将这个 消息类型+消息内容 深度绑定,我们需要定义出一种通用的消息类型结构来表示一个消息的内容。

也就是下面这个

cpp 复制代码
// 消息内容的联合体,根据消息类型包含不同的具体消息内容
message MessageContent {
    MessageType message_type = 1; // 消息类型,指明具体是哪种消息
    oneof msg_content {           // 具体消息内容,oneof表明每次只能有一种类型
        StringMessageInfo string_message = 2; // 文字消息
        FileMessageInfo file_message = 3;     // 文件消息
        SpeechMessageInfo speech_message = 4; // 语音消息
        ImageMessageInfo image_message = 5;   // 图片消息
    };
}

消息类型+消息内容 都存放在这个MessageContent里面,但是作为一条消息,它还是缺少了一些其他的信息。

  • 消息ID
  • 这条消息属于哪个聊天会话
  • 这条消息是什么时候发出的
  • 这条消息的发送者信息

那么我们现在就能封装出一个完整的消息

cpp 复制代码
// 消息结构,表示一条完整的聊天消息
message MessageInfo {
    string message_id = 1;          // 消息ID,全局唯一
    string chat_session_id = 2;     // 消息所属聊天会话ID
    int64 timestamp = 3;             // 消息产生的时间戳(Unix时间戳,秒)
    UserInfo sender = 4;             // 消息发送者的用户信息
    MessageContent message = 5;      // 消息内容体
}

有的人可能好奇,为什么消息还需要chat_session_id??这个有啥用啊??为什么我只记录消息的发出者,却不记录消息的接收者??

这个就涉及到一个问题:

  • 1对1聊天
  • 多人聊天

在后台系统中,所有聊天消息都需要被持久化保存,通常通过数据库中的消息存储表来实现。

每条消息都必须明确记录其来源与去向,即消息由谁发出、最终要送达给谁。

1对1聊天场景中,这种设计是可行的,因为每一条消息只涉及发送方和接收方两个明确的用户ID。

然而,在群聊场景中,同一消息需要送达给多个用户,如果仍在单条消息中记录所有接收者,则面临存储结构上的难题------一条消息难以合理承载多个用户ID,这样的设计既不灵活,也不符合数据库规范。

为此,我们引入了"聊天会话"这一中间概念。**每个会话在后台都有独立的元数据,其中记录了参与该会话的所有成员信息。**当一条群消息发送时,系统只需关联到对应的会话ID,即可通过会话成员表确定所有接收者,从而避免在单条消息中冗余存储多个用户ID。这样既保证了数据的规范化,也提升了系统的可扩展性与维护性。

因此,我们就引入了这个chat_session_id来标记每条消息的属性。

1.3.2.transmite.proto

现在消息定义好了,那么我们现在就可以来定义出我们的消息转发RPC服务

javascript 复制代码
syntax = "proto3";
package IMS;
import "base.proto";

option cc_generic_services = true;

// 消息传输服务相关的 protobuf 定义
// 用于网关与消息传输服务之间的通信以及内部服务之间的消息转发

// 网关 -> 消息传输服务的请求消息
// 用于发送新消息的请求,包含发送者身份、会话信息和消息内容
message NewMessageReq 
{
    string request_id = 1;           // 请求ID,全链路唯一标识,用于追踪和调试
    optional string user_id = 2;     // 发送消息的用户ID(网关从会话中解析得出)
    optional string session_id = 3;  // 客户端身份识别信息(网关的会话标识),实际上是消息发送者的会话ID
    string chat_session_id = 4;      // 聊天会话ID,标识当前消息所属的聊天会话(单聊或群聊),用于确定消息转发范围
    MessageContent message = 5;      // 消息内容,包含消息类型(文本/图片/文件等)和具体数据
}

// 网关 <- 消息传输服务的响应消息
// 消息传输服务处理后的响应,用于告知网关请求结果
message NewMessageRsp 
{
    string request_id = 1;   // 请求ID,与请求中的 request_id 对应
    bool success = 2;        // 处理是否成功
    string errmsg = 3;       // 错误信息,成功时为空
}

// 消息传输服务内部用于组织完整消息并获取转发目标列表的响应
// 该消息会在消息传输服务调用后返回给调用方(通常也是消息传输服务内部或存储服务)
message GetTransmitTargetRsp 
{
    string request_id = 1;              // 请求ID,与请求中的 request_id 对应
    bool success = 2;                  // 操作是否成功
    string errmsg = 3;                // 错误信息,成功时为空
    MessageInfo message = 4;          // 组织好的完整消息结构,包含消息ID、时间戳、发送者信息等
    repeated string target_id_list = 5; // 消息需要转发的目标用户ID列表(会话中的所有其他成员)
}

// 消息传输服务定义
// 负责接收新消息请求,完善消息信息,获取转发目标,并将消息发布到消息队列供存储和分发
service MsgTransmitService {
    // 根据新消息请求,获取完整的消息信息以及需要转发的目标用户列表
    rpc GetTransmitTarget(NewMessageReq) returns (GetTransmitTargetRsp);
}

这个其实相当的简单。

1.3.3.RPC接口实现

有了上面的transmite.proto,我们就很容易就能写出我们的RPC服务。

那么第一个我们是消息转发子服务,它的核心功能就是将消息转发给应该受到这个消息的对象。

那么它需要的组件也就是下面这些

javascript 复制代码
// 消息传输服务实现类
    class TransmiteServiceImpl : public IMS::MsgTransmitService
    {
    public:
        // 构造函数:初始化依赖组件
        TransmiteServiceImpl(const std::string &user_service_name,
                             const ServiceManager::ptr &channels,
                             const std::shared_ptr<odb::core::database> &mysql_client,
                             const std::string &exchange_name,
                             const std::string &routing_key,
                             const MQClient::ptr &mq_client) : _user_service_name(user_service_name),
                                                               _mm_channels(channels),
                                                               _mysql_session_member_table(std::make_shared<ChatSessionMemeberTable>(mysql_client)),
                                                               _exchange_name(exchange_name),
                                                               _routing_key(routing_key),
                                                               _mq_client(mq_client) {}

        ~TransmiteServiceImpl() {}
......

    private:
        // 用户子服务相关信息
        std::string _user_service_name;   // 用户子服务名称
        ServiceManager::ptr _mm_channels; // 信道管理器,用于动态选择用户服务节点

        // 聊天会话成员表操作句柄
        ChatSessionMemeberTable::ptr _mysql_session_member_table;

        // 消息队列客户端相关
        std::string _exchange_name; // RabbitMQ 交换机名称
        std::string _routing_key;   // 路由键
        MQClient::ptr _mq_client;   // RabbitMQ 客户端
    };

那么我们的实现思路如下:

你可以把它想象成一个消息转发中心,当用户发送一条新消息时,它会负责处理这条消息,确保它被正确地记录并送达到该会话中的所有成员。

整个过程大致可以分为以下几个步骤:

1. 接收请求与身份确认

当收到一条新消息的转发请求时,这个服务首先会确认"是谁发的消息"。它不会盲目相信请求里的用户ID ,而是会通过一个专门管理用户信息的服务来核实发送者的身份。它会动态找到一个可用的用户服务节点,然后去查询发送者的详细信息(比如昵称、头像等),确保消息来源是真实有效的。

2. 组装完整的消息信息

在确认发送者身份后,它会将这条消息补充完整。原始的请求里可能只有消息内容和会话ID,而这里会给消息附加上一个全局唯一的消息ID、当前的时间戳,以及刚才查询到的发送者详细信息。这样,一条完整、独立的消息就构建好了。

3. 确定转发目标

接下来,它需要知道这条消息应该发给谁。它会根据请求中的"聊天会话ID",去MySQL数据库中查询这个聊天会话里包含了哪些成员。这个成员列表就是最终的转发目标列表。

4. 消息的可靠存储

在把消息发给其他人之前,它首先会确保这条消息被可靠地保存下来 。它会把刚刚组装好的完整消息发送到消息队列中,交给专门负责存储的服务去处理。这样做的好处是,即使后续的转发环节出现问题,消息也已经落盘了,不会丢失。这一步是异步处理的,保证了消息转发的效率。

5. 返回转发结果

最后,它会向发送者所在的客户端返回一个响应。这个响应里包含了:

  • 操作是否成功的标识。

  • 刚才组装好的那条完整消息(这样发送者那边可以立刻显示自己发出的消息)。

  • 以及从数据库查出来的目标用户列表

这个返回的目标列表,主要是为了让上游服务知道这条消息最终需要推送给哪些在线用户。

javascript 复制代码
// 消息传输服务实现类
// 负责处理新消息的转发目标获取,并将消息持久化到消息队列
class TransmiteServiceImpl : public IMS::MsgTransmitService
{
public:
    // 构造函数:初始化依赖组件
    // @param user_service_name 用户子服务的服务名称,用于服务发现
    // @param channels 信道管理器,用于动态获取用户子服务的可用通信信道
    // @param mysql_client MySQL 数据库客户端,用于操作聊天会话成员表
    // @param exchange_name RabbitMQ 交换机名称,用于消息发布
    // @param routing_key RabbitMQ 路由键,用于消息路由
    // @param mq_client RabbitMQ 客户端,用于消息队列操作
    TransmiteServiceImpl(const std::string &user_service_name,
                         const ServiceManager::ptr &channels,
                         const std::shared_ptr<odb::core::database> &mysql_client,
                         const std::string &exchange_name,
                         const std::string &routing_key,
                         const MQClient::ptr &mq_client) : _user_service_name(user_service_name),
                                                           _mm_channels(channels),
                                                           _mysql_session_member_table(std::make_shared<ChatSessionMemeberTable>(mysql_client)),
                                                           _exchange_name(exchange_name),
                                                           _routing_key(routing_key),
                                                           _mq_client(mq_client) {}

    // 析构函数:默认实现
    ~TransmiteServiceImpl() {}

    // RPC 方法:获取消息转发目标,并发布消息到队列
    // 该方法由 RPC 框架调用,根据请求中的消息内容,确定该消息应转发给哪些用户(会话成员),
    // 并将消息持久化到消息队列,最后返回消息信息和目标用户列表。
    // @param controller RPC 控制器,用于控制 RPC 调用过程
    // @param request 请求对象,包含新消息的元数据
    // @param response 响应对象,包含处理结果、消息信息和目标用户列表
    // @param done 完成回调,用于通知 RPC 框架调用完成
    void GetTransmitTarget(google::protobuf::RpcController *controller,
                           const ::IMS::NewMessageReq *request,
                           ::IMS::GetTransmitTargetRsp *response,
                           ::google::protobuf::Closure *done) override
    {
        brpc::ClosureGuard rpc_guard(done); // 确保 done 被调用(异常或正常返回时自动调用)

        // 定义错误响应辅助函数,用于统一构造错误响应
        auto err_response = [this, response](const std::string &rid,
                                             const std::string &errmsg) -> void
        {
            response->set_request_id(rid);
            response->set_success(false);
            response->set_errmsg(errmsg);
            return;
        };

        // 从请求中提取关键信息
        std::string rid = request->request_id();                 // 请求唯一标识
        std::string uid = request->user_id();                    // 发送者用户 ID
        std::string chat_ssid = request->chat_session_id();      // 聊天会话 ID
        const MessageContent &content = request->message();      // 消息内容

        // 获取用户子服务的一个可用信道(用于后续 RPC 调用)
        auto channel = _mm_channels->choose(_user_service_name);
        if (!channel)
        {
            LOG_ERROR("{}-{} 没有可供访问的用户子服务节点!", rid, _user_service_name);
            return err_response(rid, "没有可供访问的用户子服务节点!");
        }

        // 调用用户子服务获取发送者信息
        UserService_Stub stub(channel.get());
        GetUserInfoReq req;
        GetUserInfoRsp rsp;
        req.set_request_id(rid);
        req.set_user_id(uid);
        brpc::Controller cntl;
        stub.GetUserInfo(&cntl, &req, &rsp, nullptr);
        if (cntl.Failed() == true || rsp.success() == false) // 发送者不存在或调用失败
        {
            LOG_ERROR("{} - 用户子服务调用失败:{}!", request->request_id(), cntl.ErrorText());
            return err_response(request->request_id(), "用户子服务调用失败!");
        }

        // 构造完整的消息对象(包含消息ID、时间戳、发送者信息等)
        MessageInfo message;
        message.set_message_id(uuid());                      // 生成全局唯一消息ID
        message.set_chat_session_id(chat_ssid);              // 设置会话ID
        message.set_timestamp(time(nullptr));                // 设置当前时间戳
        message.mutable_sender()->CopyFrom(rsp.user_info()); // 设置发送者信息
        message.mutable_message()->CopyFrom(content);        // 设置消息内容

        // 从数据库获取该会话的所有成员用户ID(转发目标列表)
        auto target_list = _mysql_session_member_table->members(chat_ssid);

        // 将消息序列化后发布到消息队列,供消息存储服务处理
        bool ret = _mq_client->publish(_exchange_name, message.SerializeAsString(), _routing_key);
        if (ret == false)
        {
            LOG_ERROR("{} - 持久化消息发布失败:{}!", request->request_id(), cntl.ErrorText());
            return err_response(request->request_id(), "持久化消息发布失败:!");
        }

        // 组织响应,包含消息信息和目标用户ID列表
        response->set_request_id(rid);
        response->set_success(true);
        response->mutable_message()->CopyFrom(message);
        for (const auto &id : target_list)
        {
            response->add_target_id_list(id);
        }
    }

private:
    // 用户子服务相关信息
    std::string _user_service_name;   // 用户子服务名称,用于服务发现
    ServiceManager::ptr _mm_channels; // 信道管理器,用于动态选择用户服务节点

    // 聊天会话成员表操作句柄,用于查询会话成员
    ChatSessionMemeberTable::ptr _mysql_session_member_table;

    // 消息队列客户端相关
    std::string _exchange_name; // RabbitMQ 交换机名称
    std::string _routing_key;   // 路由键
    MQClient::ptr _mq_client;   // RabbitMQ 客户端
};

1.4.TransmiteServer类的实现

这个类是消息转发子服务的核心所在。

javascript 复制代码
// 消息传输服务服务器类
    class TransmiteServer
    {
    public:
        using ptr = std::shared_ptr<TransmiteServer>;

        // 构造函数:初始化服务器所需的各类组件
        TransmiteServer(
            const std::shared_ptr<odb::core::database> &mysql_client,
            const Discovery::ptr discovery_client,
            const Registry::ptr &registry_client,
            const std::shared_ptr<brpc::Server> &server) : _service_discoverer(discovery_client),
                                                           _registry_client(registry_client),
                                                           _mysql_client(mysql_client),
                                                           _rpc_server(server) {}

        ~TransmiteServer() {}

        // 启动 RPC 服务器,进入事件循环
        void start()
        {
            _rpc_server->RunUntilAskedToQuit();
        }

    private:
        Discovery::ptr _service_discoverer;                 // 服务发现客户端(用于发现其他服务)
        Registry::ptr _registry_client;                     // 服务注册客户端(向 etcd 注册本服务)
        std::shared_ptr<odb::core::database> _mysql_client; // MySQL 数据库客户端
        std::shared_ptr<brpc::Server> _rpc_server;          // brpc 服务器对象
    };

brpc::Server 的 RunUntilAskedToQuit() 方法用于阻塞当前线程,启动服务器的服务循环,使服务器持续处理 RPC 请求,直到收到明确的退出指令(如调用 Stop() 或接收到中断信号)。

简单来说,它让服务器开始运行并等待停止,通常在主线程中调用,以保证进程不会立即退出。

1.5.TransmiteServerBuilder类

和之前那几个子服务是类似的,我们这个TransmiteServer类的构造函数是一个拷贝构造函数,这意味着我们需要提前创建好这个构造函数需要的那些变量

javascript 复制代码
// 构造函数:初始化服务器所需的各类组件
        TransmiteServer(
            const std::shared_ptr<odb::core::database> &mysql_client,
            const Discovery::ptr discovery_client,
            const Registry::ptr &registry_client,
            const std::shared_ptr<brpc::Server> &server) : _service_discoverer(discovery_client),
                                                           _registry_client(registry_client),
                                                           _mysql_client(mysql_client),
                                                           _rpc_server(server) {}

而我们的这个TransmiteServer类就是专门干这个的,那我们闭着眼都能知道这个类的成员变量是啥了吧。

javascript 复制代码
// 消息传输服务构建器(建造者模式)
    class TransmiteServerBuilder
    {
......
    private:
        // 用户子服务相关
        std::string _user_service_name;
        ServiceManager::ptr _mm_channels;   // 信道管理器
        Discovery::ptr _service_discoverer; // 服务发现客户端

        // 消息队列相关
        std::string _routing_key;
        std::string _exchange_name;
        MQClient::ptr _mq_client;

        // 服务注册与数据库
        Registry::ptr _registry_client;                     // 服务注册客户端
        std::shared_ptr<odb::core::database> _mysql_client; // MySQL 数据库客户端
        std::shared_ptr<brpc::Server> _rpc_server;          // brpc 服务器对象
    };

整个的思路都是比较固定的

javascript 复制代码
// 消息传输服务构建器(建造者模式)
    class TransmiteServerBuilder
    {
    public:
        // 创建 MySQL 数据库客户端对象
        void make_mysql_object(
            const std::string &user,
            const std::string &pswd,
            const std::string &host,
            const std::string &db,
            const std::string &cset,
            int port,
            int conn_pool_count)
        {
            _mysql_client = ODBFactory::create(user, pswd, host, db, cset, port, conn_pool_count);
        }

        // 创建服务发现客户端和信道管理器
        void make_discovery_object(const std::string &reg_host,
                                   const std::string &base_service_name,
                                   const std::string &user_service_name)
        {
            _user_service_name = user_service_name;
            _mm_channels = std::make_shared<ServiceManager>();
            _mm_channels->declared(user_service_name); // 声明需要管理的服务名称,只有需要被管理的服务在服务上下线的时候才会去调用服务上下线回调函数
            LOG_DEBUG("设置用户子服务为需添加管理的子服务:{}", user_service_name);
            // 设置服务上线/下线的回调,用于更新信道管理器
            auto put_cb = std::bind(&ServiceManager::onServiceOnline, _mm_channels.get(), std::placeholders::_1, std::placeholders::_2);
            auto del_cb = std::bind(&ServiceManager::onServiceOffline, _mm_channels.get(), std::placeholders::_1, std::placeholders::_2);
            _service_discoverer = std::make_shared<Discovery>(reg_host, base_service_name, put_cb, del_cb);
        }

        // 创建服务注册客户端,向 etcd 注册本服务
        void make_registry_object(const std::string &reg_host,
                                  const std::string &service_name,
                                  const std::string &access_host)
        {
            _registry_client = std::make_shared<Registry>(reg_host);
            _registry_client->registry(service_name, access_host);
        }

        // 创建 RabbitMQ 客户端对象,并声明交换机和队列
        void make_mq_object(const std::string &user,
                            const std::string &passwd,
                            const std::string &host,
                            const std::string &exchange_name,
                            const std::string &queue_name,
                            const std::string &binding_key)
        {
            _routing_key = binding_key;
            _exchange_name = exchange_name;
            _mq_client = std::make_shared<MQClient>(user, passwd, host);
            _mq_client->declareComponents(exchange_name, queue_name, binding_key);
        }

        // 创建 RPC 服务器对象,并添加服务实现
        void make_rpc_server(uint16_t port, int32_t timeout, uint8_t num_threads)
        {
            // 检查必要组件是否已初始化
            if (!_mq_client)
            {
                LOG_ERROR("还未初始化消息队列客户端模块!");
                abort();
            }
            if (!_mm_channels)
            {
                LOG_ERROR("还未初始化信道管理模块!");
                abort();
            }
            if (!_mysql_client)
            {
                LOG_ERROR("还未初始化Mysql数据库模块!");
                abort();
            }

            _rpc_server = std::make_shared<brpc::Server>();

            // 创建服务实现对象(由 brpc 自动管理生命周期)
            TransmiteServiceImpl *transmite_service = new TransmiteServiceImpl(
                _user_service_name, _mm_channels, _mysql_client, _exchange_name, _routing_key, _mq_client);

            // 将服务实现添加到 brpc 服务器
            int ret = _rpc_server->AddService(transmite_service,
                                              brpc::ServiceOwnership::SERVER_OWNS_SERVICE);
            if (ret == -1)
            {
                LOG_ERROR("添加Rpc服务失败!");
                abort();
            }

            // 配置服务器选项并启动
            brpc::ServerOptions options;
            options.idle_timeout_sec = timeout;
            options.num_threads = num_threads;
            ret = _rpc_server->Start(port, &options);
            if (ret == -1)
            {
                LOG_ERROR("服务启动失败!");
                abort();
            }
        }

        // 构建最终的服务对象
        TransmiteServer::ptr build()
        {
            if (!_service_discoverer)
            {
                LOG_ERROR("还未初始化服务发现模块!");
                abort();
            }
            if (!_registry_client)
            {
                LOG_ERROR("还未初始化服务注册模块!");
                abort();
            }
            if (!_rpc_server)
            {
                LOG_ERROR("还未初始化RPC服务器模块!");
                abort();
            }
            TransmiteServer::ptr server = std::make_shared<TransmiteServer>(
                _mysql_client, _service_discoverer, _registry_client, _rpc_server);
            return server;
        }

    private:
        // 用户子服务相关
        std::string _user_service_name;
        ServiceManager::ptr _mm_channels;   // 信道管理器
        Discovery::ptr _service_discoverer; // 服务发现客户端

        // 消息队列相关
        std::string _routing_key;
        std::string _exchange_name;
        MQClient::ptr _mq_client;

        // 服务注册与数据库
        Registry::ptr _registry_client;                     // 服务注册客户端
        std::shared_ptr<odb::core::database> _mysql_client; // MySQL 数据库客户端
        std::shared_ptr<brpc::Server> _rpc_server;          // brpc 服务器对象
    };

1.6.搭建消息转发子服务

有了上面这个TransmiteServer.hpp,我们很快就能搭建出这个消息转发子服务的服务器

javascript 复制代码
//主要实现语音识别子服务的服务器的搭建
#include "transmite_server.hpp"

DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");

DEFINE_string(registry_host, "http://127.0.0.1:2379", "服务注册中心地址");
DEFINE_string(instance_name, "/transmite_service/instance", "当前实例名称");
DEFINE_string(access_host, "127.0.0.1:10004", "当前实例的外部访问地址");

DEFINE_int32(listen_port, 10004, "Rpc服务器监听端口");
DEFINE_int32(rpc_timeout, -1, "Rpc调用超时时间");
DEFINE_int32(rpc_threads, 1, "Rpc的IO线程数量");

DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(user_service, "/service/user_service", "用户管理子服务名称");

DEFINE_string(mysql_host, "127.0.0.1", "Mysql服务器访问地址");
DEFINE_string(mysql_user, "root", "Mysql服务器访问用户名");
DEFINE_string(mysql_pswd, "123456", "Mysql服务器访问密码");
DEFINE_string(mysql_db, "IMS", "Mysql默认库名称");
DEFINE_string(mysql_cset, "utf8", "Mysql客户端字符集");
DEFINE_int32(mysql_port, 0, "Mysql服务器访问端口");
DEFINE_int32(mysql_pool_count, 4, "Mysql连接池最大连接数量");

DEFINE_string(mq_user, "root", "消息队列服务器访问用户名");
DEFINE_string(mq_pswd, "123456", "消息队列服务器访问密码");
DEFINE_string(mq_host, "127.0.0.1:5672", "消息队列服务器访问地址");
DEFINE_string(mq_msg_exchange, "msg_exchange", "持久化消息的发布交换机名称");
DEFINE_string(mq_msg_queue, "msg_queue", "持久化消息的发布队列名称");
DEFINE_string(mq_msg_binding_key, "msg_queue", "持久化消息的发布队列名称");


int main(int argc, char *argv[])
{
    google::ParseCommandLineFlags(&argc, &argv, true);
    IMS::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);

    IMS::TransmiteServerBuilder tsb;
    tsb.make_mq_object(FLAGS_mq_user, FLAGS_mq_pswd, FLAGS_mq_host,
        FLAGS_mq_msg_exchange, FLAGS_mq_msg_queue, FLAGS_mq_msg_binding_key);
    tsb.make_mysql_object(FLAGS_mysql_user, FLAGS_mysql_pswd, FLAGS_mysql_host, 
        FLAGS_mysql_db, FLAGS_mysql_cset, FLAGS_mysql_port, FLAGS_mysql_pool_count);
    tsb.make_discovery_object(FLAGS_registry_host, FLAGS_base_service, FLAGS_user_service);
    tsb.make_rpc_server(FLAGS_listen_port, FLAGS_rpc_timeout, FLAGS_rpc_threads);
    tsb.make_registry_object(FLAGS_registry_host, FLAGS_base_service + FLAGS_instance_name, FLAGS_access_host);
    auto server = tsb.build();
    server->start();
    return 0;
}

二.测试

注意:做这个测试是需要提前运行我们的用户管理子服务的!!!

javascript 复制代码
// 测试客户端:演示如何通过 RPC 调用消息传输服务,发送不同类型的消息
// 该客户端使用 etcd 进行服务发现,动态获取消息传输服务的可用节点

#include "etcd.hpp"          // etcd 服务发现相关功能
#include "channel.hpp"       // RPC 信道管理相关功能
#include "utils.hpp"         // 工具函数,如 uuid 生成、日志初始化
#include <gflags/gflags.h>   // 命令行参数解析
#include <gtest/gtest.h>     // 测试框架断言
#include <thread>            // 线程支持(本示例未直接使用)
#include "transmite.pb.h"    // 消息传输服务的 protobuf 定义
#include "user.pb.h"
#include "base.pb.h"

// 命令行参数定义(确保无重复)
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");

DEFINE_string(etcd_host, "http://127.0.0.1:2379", "服务注册中心地址");
DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(transmite_service, "/service/transmite_service", "消息传输服务的 etcd 路径");
DEFINE_string(user_service, "/service/user_service", "用户服务的 etcd 路径");

// 全局服务管理器,用于动态获取消息传输服务信道
IMS::ServiceManager::ptr sm;
// 全局服务管理器,用于动态获取用户服务信道
IMS::ServiceManager::ptr user_channels;

// 全局用户 ID,从数据库查询后手动输入
std::string g_uid1, g_uid2, g_uid3;

// 用户注册函数
// @param nickname 用户昵称
// @param pswd 用户密码(建议实际传输时加密)
void reg_user(const std::string &nickname, const std::string &pswd) 
{
    // 从服务管理器获取用户服务的可用信道
    auto channel = user_channels->choose(FLAGS_user_service); // 获取通信信道
    ASSERT_TRUE(channel);  // 断言信道有效

    // 构造注册请求
    IMS::UserRegisterReq req;
    req.set_request_id(IMS::uuid());  // 设置唯一请求 ID
    req.set_nickname(nickname);           // 设置昵称
    req.set_password(pswd);               // 设置密码

    IMS::UserRegisterRsp rsp;         // 响应对象
    brpc::Controller cntl;                // RPC 控制器
    IMS::UserService_Stub stub(channel.get());  // 创建 RPC 存根
    stub.UserRegister(&cntl, &req, &rsp, nullptr);  // 发起 RPC 调用

    // 断言调用成功且业务响应成功
    ASSERT_FALSE(cntl.Failed());
    ASSERT_TRUE(rsp.success());
}

// 发送文本消息的测试函数
// @param uid 发送者用户 ID
// @param sid 聊天会话 ID
// @param msg 文本消息内容
void string_message(const std::string &uid, const std::string &sid, const std::string &msg) 
{
    // 从服务管理器获取消息传输服务的可用信道
    auto channel = sm->choose(FLAGS_transmite_service);
    if (!channel) {
        std::cout << "获取通信信道失败!" << std::endl;
        return;
    }
    // 创建 RPC 存根并构造请求
    IMS::MsgTransmitService_Stub stub(channel.get());
    IMS::NewMessageReq req;
    IMS::GetTransmitTargetRsp rsp;
    req.set_request_id(IMS::uuid());                    // 生成唯一请求 ID
    req.set_user_id(uid);                                   // 设置发送者 ID
    req.set_chat_session_id(sid);                           // 设置会话 ID
    req.mutable_message()->set_message_type(IMS::MessageType::STRING); // 消息类型为文本
    req.mutable_message()->mutable_string_message()->set_content(msg);     // 设置文本内容

    // 发起 RPC 调用
    brpc::Controller cntl;
    stub.GetTransmitTarget(&cntl, &req, &rsp, nullptr);
    // 断言调用成功
    ASSERT_FALSE(cntl.Failed());
    ASSERT_TRUE(rsp.success());

    // 验证响应中的消息内容与请求一致
    const IMS::MessageInfo& resp_msg = rsp.message();
    ASSERT_EQ(resp_msg.chat_session_id(), sid);
    ASSERT_EQ(resp_msg.sender().user_id(), uid);
    ASSERT_EQ(resp_msg.message().message_type(), IMS::MessageType::STRING);
    ASSERT_EQ(resp_msg.message().string_message().content(), msg);

    // 验证转发目标用户 ID 列表
    // 根据会话 ID 确定预期的成员用户 ID
    std::vector<std::string> expected_targets;
    //会话1里面有用户1,用户2;会话2里面有用户1,用户2,用户3
    if (sid == "会话ID1") {
        expected_targets = {g_uid1, g_uid2};
    } else if (sid == "会话ID2") {
        expected_targets = {g_uid1, g_uid2, g_uid3};
    } else {
        FAIL() << "未知的会话ID: " << sid;
    }

    // 验证响应中的 target_id_list 与预期一致
    ASSERT_EQ(rsp.target_id_list_size(), expected_targets.size());
    for (size_t i = 0; i < expected_targets.size(); ++i) {
        ASSERT_EQ(rsp.target_id_list(i), expected_targets[i]);
    }
}

// 发送图片消息的测试函数
// @param uid 发送者用户 ID
// @param sid 聊天会话 ID
// @param msg 图片内容(如 Base64 编码或文件路径)
void image_message(const std::string &uid, const std::string &sid, const std::string &msg) 
{
    auto channel = sm->choose(FLAGS_transmite_service);
    if (!channel) {
        std::cout << "获取通信信道失败!" << std::endl;
        return;
    }
    IMS::MsgTransmitService_Stub stub(channel.get());
    IMS::NewMessageReq req;
    IMS::GetTransmitTargetRsp rsp;
    req.set_request_id(IMS::uuid());
    req.set_user_id(uid);
    req.set_chat_session_id(sid);
    req.mutable_message()->set_message_type(IMS::MessageType::IMAGE);   // 消息类型为图片
    req.mutable_message()->mutable_image_message()->set_image_content(msg); // 设置图片内容

    brpc::Controller cntl;
    stub.GetTransmitTarget(&cntl, &req, &rsp, nullptr);
    ASSERT_FALSE(cntl.Failed());
    ASSERT_TRUE(rsp.success());

    // 验证响应内容
    const IMS::MessageInfo& resp_msg = rsp.message();
    ASSERT_EQ(resp_msg.chat_session_id(), sid);
    ASSERT_EQ(resp_msg.sender().user_id(), uid);
    ASSERT_EQ(resp_msg.message().message_type(), IMS::MessageType::IMAGE);
    ASSERT_EQ(resp_msg.message().image_message().image_content(), msg);

    // 验证根据会话 ID 确定预期的成员用户 ID
    std::vector<std::string> expected_targets;
    if (sid == "会话ID1") {
        expected_targets = {g_uid1, g_uid2};
    } else if (sid == "会话ID2") {
        expected_targets = {g_uid1, g_uid2, g_uid3};
    } else {
        FAIL() << "未知的会话ID: " << sid;
    }

    // 验证响应中的 target_id_list 与预期一致
    ASSERT_EQ(rsp.target_id_list_size(), expected_targets.size());
    for (size_t i = 0; i < expected_targets.size(); ++i) {
        ASSERT_EQ(rsp.target_id_list(i), expected_targets[i]);
    }
}

// 发送语音消息的测试函数
// @param uid 发送者用户 ID
// @param sid 聊天会话 ID
// @param msg 语音数据(如音频文件内容)
void speech_message(const std::string &uid, const std::string &sid, const std::string &msg) 
{
    auto channel = sm->choose(FLAGS_transmite_service);
    if (!channel) {
        std::cout << "获取通信信道失败!" << std::endl;
        return;
    }
    IMS::MsgTransmitService_Stub stub(channel.get());
    IMS::NewMessageReq req;
    IMS::GetTransmitTargetRsp rsp;
    req.set_request_id(IMS::uuid());
    req.set_user_id(uid);
    req.set_chat_session_id(sid);
    req.mutable_message()->set_message_type(IMS::MessageType::SPEECH);   // 消息类型为语音
    req.mutable_message()->mutable_speech_message()->set_file_contents(msg); // 设置语音文件内容

    brpc::Controller cntl;
    stub.GetTransmitTarget(&cntl, &req, &rsp, nullptr);
    ASSERT_FALSE(cntl.Failed());
    ASSERT_TRUE(rsp.success());

    // 验证响应内容
    const IMS::MessageInfo& resp_msg = rsp.message();
    ASSERT_EQ(resp_msg.chat_session_id(), sid);
    ASSERT_EQ(resp_msg.sender().user_id(), uid);
    ASSERT_EQ(resp_msg.message().message_type(), IMS::MessageType::SPEECH);
    ASSERT_EQ(resp_msg.message().speech_message().file_contents(), msg);

    // 验证根据会话 ID 确定预期的成员用户 ID
    std::vector<std::string> expected_targets;
    if (sid == "会话ID1") {
        expected_targets = {g_uid1, g_uid2};
    } else if (sid == "会话ID2") {
        expected_targets = {g_uid1, g_uid2, g_uid3};
    } else {
        FAIL() << "未知的会话ID: " << sid;
    }

    // 验证响应中的 target_id_list 与预期一致
    ASSERT_EQ(rsp.target_id_list_size(), expected_targets.size());
    for (size_t i = 0; i < expected_targets.size(); ++i) {
        ASSERT_EQ(rsp.target_id_list(i), expected_targets[i]);
    }
}

// 发送文件消息的测试函数
// @param uid 发送者用户 ID
// @param sid 聊天会话 ID
// @param filename 文件名
// @param content 文件内容(二进制数据)
void file_message(const std::string &uid, const std::string &sid, 
    const std::string &filename, const std::string &content) {
    auto channel = sm->choose(FLAGS_transmite_service);
    if (!channel) {
        std::cout << "获取通信信道失败!" << std::endl;
        return;
    }
    IMS::MsgTransmitService_Stub stub(channel.get());
    IMS::NewMessageReq req;
    IMS::GetTransmitTargetRsp rsp;
    req.set_request_id(IMS::uuid());
    req.set_user_id(uid);
    req.set_chat_session_id(sid);
    req.mutable_message()->set_message_type(IMS::MessageType::FILE);      // 消息类型为文件
    req.mutable_message()->mutable_file_message()->set_file_contents(content); // 设置文件内容
    req.mutable_message()->mutable_file_message()->set_file_name(filename);   // 设置文件名
    req.mutable_message()->mutable_file_message()->set_file_size(content.size()); // 设置文件大小

    brpc::Controller cntl;
    stub.GetTransmitTarget(&cntl, &req, &rsp, nullptr);
    ASSERT_FALSE(cntl.Failed());
    ASSERT_TRUE(rsp.success());

    // 验证响应内容
    const IMS::MessageInfo& resp_msg = rsp.message();
    ASSERT_EQ(resp_msg.chat_session_id(), sid);
    ASSERT_EQ(resp_msg.sender().user_id(), uid);
    ASSERT_EQ(resp_msg.message().message_type(), IMS::MessageType::FILE);
    ASSERT_EQ(resp_msg.message().file_message().file_name(), filename);
    ASSERT_EQ(resp_msg.message().file_message().file_contents(), content);
    ASSERT_EQ(resp_msg.message().file_message().file_size(), content.size());

    // 验证根据会话 ID 确定预期的成员用户 ID
    std::vector<std::string> expected_targets;
    if (sid == "会话ID1") {
        expected_targets = {g_uid1, g_uid2};
    } else if (sid == "会话ID2") {
        expected_targets = {g_uid1, g_uid2, g_uid3};
    } else {
        FAIL() << "未知的会话ID: " << sid;
    }

    // 验证响应中的 target_id_list 与预期一致
    ASSERT_EQ(rsp.target_id_list_size(), expected_targets.size());
    for (size_t i = 0; i < expected_targets.size(); ++i) {
        ASSERT_EQ(rsp.target_id_list(i), expected_targets[i]);
    }
}

// 测试用例:发送文本消息
TEST(TransmiteTest, StringMessage) {
    ASSERT_FALSE(g_uid1.empty()) << "用户1的UID未设置";
    string_message(g_uid1, "会话ID1", "吃饭了吗?");
    string_message(g_uid1, "会话ID2", "吃的盖浇饭!!");
}

// 测试用例:发送图片消息
TEST(TransmiteTest, ImageMessage) {
    ASSERT_FALSE(g_uid2.empty()) << "用户2的UID未设置";
    image_message(g_uid2, "会话ID1", "可爱表情图片数据");
}

// 测试用例:发送语音消息
TEST(TransmiteTest, SpeechMessage) {
    ASSERT_FALSE(g_uid2.empty()) << "用户2的UID未设置";
    speech_message(g_uid2, "会话ID2", "动听猪叫声数据");
}

// 测试用例:发送文件消息
TEST(TransmiteTest, FileMessage) {
    ASSERT_FALSE(g_uid2.empty()) << "用户2的UID未设置";
    file_message(g_uid2, "会话ID2", "文件名称", "文件数据");
}

int main(int argc, char *argv[])
{
    // 解析命令行参数
    google::ParseCommandLineFlags(&argc, &argv, true);
    // 初始化日志系统(根据运行模式和参数)
    IMS::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);

    // ------------------- 消息传输服务发现 -------------------
    sm = std::make_shared<IMS::ServiceManager>();
    sm->declared(FLAGS_transmite_service);  // 声明需要监控的服务名称

    // 设置服务上下线的回调函数(绑定到 sm 实例)
    auto transmite_put_cb = std::bind(&IMS::ServiceManager::onServiceOnline, sm.get(), std::placeholders::_1, std::placeholders::_2);
    auto transmite_del_cb = std::bind(&IMS::ServiceManager::onServiceOffline, sm.get(), std::placeholders::_1, std::placeholders::_2);

    // 构造消息传输服务的发现对象,连接 etcd 并监控指定前缀下的服务节点
    IMS::Discovery::ptr transmite_discovery = std::make_shared<IMS::Discovery>(FLAGS_etcd_host, FLAGS_base_service, transmite_put_cb, transmite_del_cb);

    // ------------------- 用户服务发现 -------------------
    user_channels = std::make_shared<IMS::ServiceManager>();
    user_channels->declared(FLAGS_user_service);  // 声明需要监控的服务名称

    // 设置服务上下线的回调函数(绑定到 user_channels 实例)
    auto user_put_cb = std::bind(&IMS::ServiceManager::onServiceOnline, user_channels.get(), std::placeholders::_1, std::placeholders::_2);
    auto user_del_cb = std::bind(&IMS::ServiceManager::onServiceOffline, user_channels.get(), std::placeholders::_1, std::placeholders::_2);

    // 构造用户服务的发现对象,连接 etcd 并监控指定前缀下的服务节点
    IMS::Discovery::ptr user_discovery = std::make_shared<IMS::Discovery>(FLAGS_etcd_host, FLAGS_base_service, user_put_cb, user_del_cb);

    // 注册用户
    reg_user("用户1", "123456");
    reg_user("用户2", "123456");
    reg_user("用户3", "123456");

    std::cout<<"接下来将要输入用户的uid,用户的uid需要我们自己去数据库里面查询"<<std::endl;
    std::cout<<"去user表里面查询到三个用户的uid后,我们还需要往数据库里面的chat_session_member表里面插入对应的数据,会话1里面有用户1,用户2;会话2里面有用户1,用户2,用户3"<<std::endl;
    std::cout << "输入用户1的用户ID:";
    std::cin >> g_uid1;
    std::cout << "输入用户2的用户ID:";
    std::cin >> g_uid2;
    std::cout << "输入用户3的用户ID:";
    std::cin >> g_uid3;

    // 初始化 Google Test 并运行所有测试用例
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

首先我们先去我们的mysql数据库看看

现在我们就编译运行,先运行用户管理子服务,后运行消息转发子服务,再运行我们的测试程序

这里卡在输入的时候,我们打开另外一个终端,去查看一下我们的mysql数据库,这个时候我们去查询一下user表

我们将表里面的uid给搞出来,然后往chat_session_member里面插入3条数据

为了实现合理的消息转发效果,建议为每个会话设置如下成员关系:

会话 ID 成员用户
会话ID1 uid1, uid2
会话ID2 uid1, uid2, uid3
javascript 复制代码
INSERT INTO chat_session_member (session_id, user_id) VALUES
('会话ID1', '0133-1d0c6a85-0003'),
('会话ID1', '4ca2-51132998-0004'),
('会话ID2', '0133-1d0c6a85-0003'),
('会话ID2', '4ca2-51132998-0004'),
('会话ID2', 'b2b4-c7fc9358-0005');

现在我们就可以去输入我们的用户ID了

非常完美,我们的测试完美通过。

注意:测试完之后需要我们去清理一下数据

对于Mysql数据库,我们执行下面这2句即可

javascript 复制代码
TRUNCATE TABLE chat_session_member;
TRUNCATE TABLE user;

对于RabbitMQ,我们需要去使用浏览器

javascript 复制代码
主机IP:15672

然后

往下面就能看到

点击这个就能删除这个队列

往下

点击这个就能删除交换机

相关推荐
喜欢喝果茶.1 小时前
SQL 预处理
数据库·sql
数据科学小丫3 小时前
Python 数据存储操作_数据存储、补充知识点:Python 与 MySQL交互
数据库·python·mysql
Knight_AL3 小时前
Nacos 启动问题 Failed to create database ’D:\nacos\nacos\data\derby-data’
开发语言·数据库·python
xianjian09124 小时前
MySQL 的 INSERT(插入数据)详解
android·数据库·mysql
知识分享小能手4 小时前
MongoDB入门学习教程,从入门到精通,MongoDB入门指南 —— 知识点详解(2)
数据库·学习·mongodb
what_20185 小时前
PostgreSQL 索引
数据库·postgresql
麦聪聊数据5 小时前
跨云与多区服游戏架构下的数据库运维:基于webSQL的访问实践
数据库·sql·低代码·游戏·restful
eggwyw5 小时前
MySQL 与 Redis 的数据一致性问题
数据库·redis·mysql
2401_879693875 小时前
使用Python控制Arduino或树莓派
jvm·数据库·python