目录
一.总体思想
发布订阅模式是一种高效的消息通信架构,通常包含三个核心角色:
-
消息发布客户端------负责产生并发送消息。
-
中转服务器------负责接收发布端的消息,并根据预定义的规则将消息转发给相应的订阅端。
-
消息处理客户端------订阅感兴趣的消息并进行业务处理。
典型应用场景示例
- 新闻发布系统
假设一位男歌手发生了绯闻,该新闻需要分发到多个不同的资讯板块,例如:歌手新闻 、花边新闻 、娱乐头条等。
-
一个新闻发布客户端将这条绯闻消息发送到中转服务器。
-
中转服务器根据消息的主题(Topic)或标签,将其转发至所有订阅了相关主题的客户端。
-
不同板块的处理客户端接收到消息后,可进行个性化编辑、推送或归档等操作。
中转服务器的路由决策 :通常基于主题订阅机制。每个处理客户端会提前向服务器订阅自己关心的主题(如"歌手新闻""花边新闻"),服务器根据消息所属主题,将其投递给所有订阅该主题的客户端。

- 电商平台用户请求路由
以拼多多为例,当用户上线并发起请求时,系统需要将请求导向相应的业务处理集群:
-
购物请求 → 购物业务处理节点
-
砍价请求 → 砍价业务处理节点
中转服务器(如网关或消息中间件)接收到用户请求后:
-
解析请求类型。
-
根据类型将其转发到对应的业务消息队列或主题。
-
该业务集群中的某个可用服务器(通过负载均衡策略,如轮询、最少连接等)从队列中获取请求并处理。
负载均衡机制:在同一类业务集群内,中转服务器可采用常见的负载均衡算法,确保请求均匀分配到多个处理节点,提高系统吞吐量与可靠性。

二.发布订阅服务端核心设计
发布订阅系统的核心功能模块主要包括对主题(Topic)的生命周期管理、订阅关系的维护以及消息的投递。
具体可分解为以下五个核心操作:
-
主题的创建
-
功能描述 :创建一个新的消息主题通道。采用强断言策略------即当请求创建的主题尚不存在时,系统执行创建并返回成功;如果主题已存在,则直接返回操作成功确认,保证操作的幂等性。
-
补充说明:此设计避免了重复创建的冗余操作,同时确保客户端无论调用多少次,结果状态都保持一致,是构建可靠系统的基础。
-
-
主题的删除
- 功能描述:删除一个指定的主题。执行此操作时,系统应同步清理所有与该主题关联的订阅关系,并可选地通知当前订阅者。
-
主题的订阅
-
服务端视角 :当客户端发起订阅请求时,服务端需将此订阅者 (通常以网络连接标识)与该主题的关联关系持久化地管理起来,以便后续进行消息路由。
-
客户端视角:客户端本地也应维护一份已订阅的主题列表,用于管理自身的订阅状态,并在必要时发起取消订阅操作。
-
-
主题的取消订阅
- 功能描述:解除某个订阅者与特定主题之间的关联。服务端需立即更新其订阅关系表,确保此后该主题的消息不再向此客户端投递。
-
主题消息的发布
- 功能描述 :发布者向指定主题发送一条消息。服务端在接收到消息后,需根据已维护的订阅关系,将该消息转发(或称为"广播")给所有订阅了此主题的客户端。
为实现上述功能,服务端内部需进行精心设计,主要包含以下关键部分:
-
对外接口回调函数
-
设计要点 :服务端需对外提供一个统一的主题操作类型的请求消息处理回调函数。所有客户端的创建、删除、订阅、取消订阅、发布等请求,都通过此函数入口进行分发和处理。
-
模块集成 :此回调函数通常设置给更上层的网络分发器(Dispatcher)模块。Dispatcher负责接收原始网络数据包,解析出基本指令后,调用此回调函数进入具体的业务逻辑处理。
-
-
核心数据管理(订阅关系维护)
-
高效管理订阅关系是服务端的核心。建议采用双哈希表结构以实现快速查询与更新:
- **hash_map<主题名称, 订阅者集合>:**此结构是消息路由的关键。当收到某个主题的消息时,服务端可立即通过此表查出所有订阅了该主题的客户端连接,进而进行批量消息投递。
- **hash_map<订阅者的通信连接, 订阅的主题集合>:**此结构用于维护连接的生命周期关联。当某个客户端连接异常断开时,服务端可迅速通过此表查出该连接订阅了哪些主题,并反向清理第一个哈希表中对应的订阅关系,确保数据一致性,防止内存泄漏。
-
-
连接断开的处理回调函数
-
设计要点 :服务端必须注册一个连接断开处理回调函数。当TCP连接断开或客户端心跳超时时,此函数被自动触发。
-
内部逻辑 :在该函数内部,系统需根据上述的hash_map<订阅者的通信连接, 订阅的主题集合> ,找到该连接对应的所有订阅主题,并遍历这些主题,从 hash_map<主题名称, 订阅者集合> 中移除该连接。完成清理后,再删除该连接自身的记录。
-
2.1.成员变量及其管理操作
设计思想看完了,那么我们现在就来看看这2个核心数据结构
- **hash_map<主题名称, 订阅者集合>:**此结构是消息路由的关键。当收到某个主题的消息时,服务端可立即通过此表查出所有订阅了该主题的客户端连接,进而进行批量消息投递。
- **hash_map<订阅者的通信连接, 订阅的主题集合>:**此结构用于维护连接的生命周期关联。当某个客户端连接异常断开时,服务端可迅速通过此表查出该连接订阅了哪些主题,并反向清理第一个哈希表中对应的订阅关系,确保数据一致性,防止内存泄漏。
嗯哼?主题是个什么鬼?我们没有这种数据类型!
订阅者又是什么鬼?我们也是没有这种数据类型!
那么我们就有必要去订阅出主题,订阅者两种数据结构。
- 主题类里面搞2个成员变量------主题名称,订阅者集合,那么我们不就实现了hash_map<主题名称, 订阅者集合>吗?
- 订阅者类里面搞2个成员变量------通信连接,订阅的主题集合,那么我们不就实现了这个hash_map<订阅者, 订阅的主题集合>了吗?
这样子很好,那么我们这2个成员变量肯定是私有的,那么我们必须提供一些公有的接口去
主题类
- 往订阅者集合里面添加订阅者
- 往订阅者集合里面移除订阅者
- 遍历订阅这个主题的订阅者集合,向它们发送消息(用于消息推送)
订阅者类
- 往订阅的主题集合里面添加主题
- 往订阅的主题集合里面移除主题
现在,我们就能封装出这么两个类了
cpp
// 订阅者结构体,表示一个订阅者(客户端连接)
struct Subscriber
{
using ptr = std::shared_ptr<Subscriber>;
std::mutex _mutex; // 互斥锁,保护成员变量
BaseConnection::ptr conn; // 客户端连接
std::unordered_set<std::string> topics; // 订阅者所订阅的主题名称集合
Subscriber(const BaseConnection::ptr &c) : conn(c) {}
// 添加订阅的主题(订阅主题时调用)
void appendTopic(const std::string &topic_name)
{
std::unique_lock<std::mutex> lock(_mutex);
topics.insert(topic_name);
}
// 移除订阅的主题(主题被删除或取消订阅时调用)
void removeTopic(const std::string &topic_name)
{
std::unique_lock<std::mutex> lock(_mutex);
topics.erase(topic_name);
}
};
// 主题结构体,表示一个主题
struct Topic
{
using ptr = std::shared_ptr<Topic>;
std::mutex _mutex; // 互斥锁,保护成员变量
std::string topic_name; // 主题名称
std::unordered_set<Subscriber::ptr> subscribers; // 当前主题的订阅者集合
Topic(const std::string &name) : topic_name(name) {}
// 添加订阅者(新增订阅时调用)
void appendSubscriber(const Subscriber::ptr &subscriber)
{
std::unique_lock<std::mutex> lock(_mutex);
subscribers.insert(subscriber);
}
// 移除订阅者(取消订阅或订阅者连接断开时调用)
void removeSubscriber(const Subscriber::ptr &subscriber)
{
std::unique_lock<std::mutex> lock(_mutex);
subscribers.erase(subscriber);
}
// 推送消息(收到消息发布请求时调用)
// 遍历当前主题的订阅者集合,并且往每个订阅者发送消息
void pushMessage(const BaseMessage::ptr &msg)
{
std::unique_lock<std::mutex> lock(_mutex);
for (auto &subscriber : subscribers)//当前主题的订阅者集合
{
// 向每个订阅者发送消息
subscriber->conn->send(msg);
}
}
};
但是问题又来了,我们主题可是不止有一个,我们的订阅者也不止一个啊。那么我们服务器就必须将这些管理起来,我们我们就又添加了下面两个数据结构
- hash_map<主题名称, 主题对象>
- hash_map<通信连接, 订阅者对象>
那么就正式引出我们的服务类对象的成员变量。
cpp
// 主题管理器类,负责管理发布订阅系统中的主题和订阅者
class TopicManager
{
public:
using ptr = std::shared_ptr<TopicManager>;
......
private:
std::mutex _mutex; // 保护整个TopicManager的互斥锁
std::unordered_map<std::string, Topic::ptr> _topics; // 主题名称到主题对象的映射
std::unordered_map<BaseConnection::ptr, Subscriber::ptr> _subscribers; // 连接对象到订阅者对象的映射
};
2.2.主题的5大核心操作
首先,我们的服务类需要实现下面5个核心操作
-
主题的创建
-
功能描述 :创建一个新的消息主题通道。采用强断言策略------即当请求创建的主题尚不存在时,系统执行创建并返回成功;如果主题已存在,则直接返回操作成功确认,保证操作的幂等性。
-
补充说明:此设计避免了重复创建的冗余操作,同时确保客户端无论调用多少次,结果状态都保持一致,是构建可靠系统的基础。
-
-
主题的删除
- 功能描述:删除一个指定的主题。执行此操作时,系统应同步清理所有与该主题关联的订阅关系,并可选地通知当前订阅者。
-
主题的订阅
-
服务端视角 :当客户端发起订阅请求时,服务端需将此订阅者 (通常以网络连接标识)与该主题的关联关系持久化地管理起来,以便后续进行消息路由。
-
客户端视角:客户端本地也应维护一份已订阅的主题列表,用于管理自身的订阅状态,并在必要时发起取消订阅操作。
-
-
主题的取消订阅
- 功能描述:解除某个订阅者与特定主题之间的关联。服务端需立即更新其订阅关系表,确保此后该主题的消息不再向此客户端投递。
-
主题消息的发布
- 功能描述 :发布者向指定主题发送一条消息。服务端在接收到消息后,需根据已维护的订阅关系,将该消息转发(或称为"广播")给所有订阅了此主题的客户端。
创建主题:
其实也很简单,就是获取请求里面的主题名称,然后根据主题名称创建一个主题对象
最后将<主题名称,主题对象>添加进我们的数据结构里面。
cpp
// 创建主题
void topicCreate(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
//传递一个通信连接和请求
std::unique_lock<std::mutex> lock(_mutex);//加锁
// 获取主题名称
std::string topic_name = msg->topicKey();
// 根据主题名称构造一个主题对象
auto topic = std::make_shared<Topic>(topic_name);
// 将主题添加到映射表中(如果已存在,会被覆盖,实现强断言)
_topics.insert(std::make_pair(topic_name, topic));
}
删除主题:
当主题被删除(即服务下线)时,我们需要处理两个方面:
-
清理主题本身的资源:将主题从系统中移除,释放主题占用的资源(如内存)。
-
处理依赖该主题的订阅者:每个订阅者可能订阅了多个主题,当某个主题被删除时,需要从所有订阅了该主题的订阅者中移除对该主题的引用,以防止后续无效的访问。
具体步骤如下:
a. 定位主题:首先根据主题名称找到对应的主题对象。如果主题不存在,则无需进行任何操作。
b. 获取订阅者列表:在删除主题之前,先获取当前主题的所有订阅者。因为一旦主题被删除,这些订阅者将不再需要接收该主题的消息,同时也需要更新他们的订阅列表。
c. 删除主题映射:从主题管理器的主题映射表(主题名称到主题对象的映射)中删除该主题的条目。这样,后续的任何操作(如发布消息、订阅、取消订阅)都无法再找到该主题。
d. 更新订阅者:遍历所有订阅了该主题的订阅者,从每个订阅者的订阅主题列表中移除该主题。这样,订阅者就知道自己不再订阅该主题,并且当订阅者断开连接时,清理工作也会更加简单。
cpp
// 删除主题
void topicRemove(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
// 1. 查看当前主题有哪些订阅者,然后从订阅者中将主题信息删除掉
// 2. 删除主题的数据 -- 主题名称与主题对象的映射关系
std::string topic_name = msg->topicKey();//获取主题名称
std::unordered_set<Subscriber::ptr> subscribers;//注意这里是一个集合set,代表着里面不会存储重复的订阅者结构体
{
std::unique_lock<std::mutex> lock(_mutex);
// 在删除主题之前,先找出会受到影响的订阅者
auto it = _topics.find(topic_name);//根据主题名称去 主题名称-主题对象的映射 里面获取主题对象
if (it == _topics.end())//没有找到
{
return; // 主题不存在,直接返回
}
//找到了对应主题对象
// 获取该主题的所有订阅者
subscribers = it->second->subscribers;
// 删除当前的主题映射关系
_topics.erase(it);//去 主题名称-主题对象的映射 里面删除这对键值对
}
// 通知所有订阅了该主题的订阅者,从他们的订阅列表中删除该主题
for (auto &subscriber : subscribers)//遍历被删除的这个主题的所有订阅者结构体
{
subscriber->removeTopic(topic_name);//删除这些订阅者里面订阅的指定名字的主题
}
}
订阅主题:
订阅主题的过程本质上是在服务端建立主题 与订阅者之间的双向关联关系。
这个过程类似于在社交平台上"关注"某个话题------系统需要记录谁关注了什么,同时也要记录每个话题被谁关注着。
核心目标:
建立客户端连接与主题之间的订阅关系,确保后续该主题的消息能正确推送给这个客户端。
详细步骤解析:
第一步:准备阶段
- 准备两个关键指针:主题指针和订阅者指针,用于后续操作
第二步:查找关键对象(线程安全区)
-
查找主题对象
-
根据请求中的主题名称,在"主题名称→主题对象"映射表中查找对应的主题
-
如果找不到,说明主题不存在,订阅失败直接返回
-
如果找到了,将主题对象保存起来备用
-
-
查找或创建订阅者对象
-
根据客户端连接对象,在"连接对象→订阅者对象"映射表中查找对应的订阅者
-
情况一:订阅者已存在
-
说明这个客户端之前已经订阅过其他主题
-
直接使用现有的订阅者对象
-
-
情况二:订阅者不存在
-
说明这是客户端第一次订阅任何主题
-
需要创建一个新的订阅者对象
-
将这个新创建的订阅者对象添加到映射表中,建立连接对象与订阅者对象的关联
-
-
第三步:建立双向关联
-
在主题中添加订阅者
-
将找到(或新建)的订阅者对象添加到主题的订阅者列表中
-
这样,当这个主题有消息时,就知道要推送给哪些订阅者
-
-
在订阅者中添加主题
-
将主题名称添加到订阅者的主题列表中
-
这样,当订阅者断开连接时,就知道需要清理哪些主题中的订阅关系
-
cpp
// 订阅主题
bool topicSubscribe(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
// 1. 先找出主题对象,以及订阅者对象
// 如果没有找到主题--就要报错;如果没有找到订阅者对象,那就要构造一个订阅者
Topic::ptr topic;//主题
Subscriber::ptr subscriber;//订阅者
{
std::unique_lock<std::mutex> lock(_mutex);
// 查找主题
auto topic_it = _topics.find(msg->topicKey());//根据主题名称去 主题名称-主题对象的映射 里面获取主题对象
if (topic_it == _topics.end())
{
return false; // 主题不存在
}
//找到了对应主题对象,就保存给topic
topic = topic_it->second;
// 查找订阅者
auto sub_it = _subscribers.find(conn);//根据连接对象称去 连接对象到订阅者对象的映射 里面获取订阅者对象
if (sub_it != _subscribers.end())//找到了
{
subscriber = sub_it->second; // 订阅者已存在
}
else//不存在
{
// 创建新的订阅者对象
subscriber = std::make_shared<Subscriber>(conn);
_subscribers.insert(std::make_pair(conn, subscriber));//添加进 连接对象到订阅者对象的映射
}
}
// 2. 在主题对象中,新增一个订阅者对象关联的连接;
// 在订阅者对象中新增一个订阅的主题
topic->appendSubscriber(subscriber);//添加订阅者
subscriber->appendTopic(msg->topicKey());//添加订阅的主题
return true;
}
取消订阅:
取消订阅是发布订阅系统中一个关键的反向操作,它类似于在社交平台上"取消关注"某个话题。这个过程需要精确地解除已建立的关联关系,确保系统的数据一致性和资源管理效率。
核心目标:
解除客户端连接与指定主题之间的订阅关系,确保后续该主题的消息不再推送给这个客户端。
详细步骤解析:
第一步:准备阶段
-
准备两个关键指针:主题指针和订阅者指针,初始为空
-
这两个指针将用于存储找到的关联对象
第二步:查找相关对象(线程安全区)
-
尝试查找主题对象
-
根据请求中的主题名称,在"主题名称→主题对象"映射表中查找对应的主题
-
设计特点: 这里使用
!= _topics.end()的判断方式 -
意义: 如果主题存在,就记录下来;如果不存在,主题指针保持为空
-
原因: 取消订阅操作具有幂等性------即使主题不存在,操作也视为成功(因为目标已经达到:客户端不订阅该主题)
-
-
尝试查找订阅者对象
-
根据客户端连接对象,在"连接对象→订阅者对象"映射表中查找对应的订阅者
-
设计特点: 同样使用
!= _subscribers.end()的判断方式 -
意义: 如果订阅者存在,就记录下来;如果不存在,订阅者指针保持为空
-
原因: 同样基于幂等性原则------如果客户端从未订阅过任何主题,取消订阅操作也视为成功
-
第三步:解除双向关联(条件性执行)
-
从订阅者中移除主题(如果订阅者存在)
-
检查订阅者指针是否有效(不为空)
-
如果有效,从订阅者的主题列表中移除指定的主题名称
-
作用: 更新订阅者本地的订阅状态,避免其继续保持对已取消订阅主题的引用
-
-
从主题中移除订阅者(如果主题和订阅者都存在)
-
只有当主题和订阅者同时存在时才执行此操作
-
设计考虑: 这个双重检查很重要,因为它确保我们只移除实际存在的关联关系
-
如果主题存在但订阅者不存在,说明客户端从未订阅过该主题
-
如果订阅者存在但主题不存在,说明主题可能已被删除
-
cpp
// 取消订阅主题
void topicCancel(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
// 1. 先找出主题对象,和订阅者对象
Topic::ptr topic; // 主题
Subscriber::ptr subscriber; // 订阅者
{
std::unique_lock<std::mutex> lock(_mutex);
// 查找主题
auto topic_it = _topics.find(msg->topicKey()); // 根据主题名称去 主题名称-主题对象的映射 里面获取主题对象
if (topic_it != _topics.end()) // 找到了,说明这个主题之前就存在
{
topic = topic_it->second; // 保存这个主题对象
}
// 查找订阅者
auto sub_it = _subscribers.find(conn); // 根据连接对象称去 连接对象到订阅者对象的映射 里面获取订阅者对象
if (sub_it != _subscribers.end()) // 找到了,说明这个订阅者之前订阅过其他主题
{
subscriber = sub_it->second; // 保存起来
}
}
// 2. 从主题对象中删除当前的订阅者连接;
// 从订阅者信息中删除所订阅的主题名称
if (subscriber)//如果订阅者已经在 连接对象到订阅者对象的映射 里面存在,说明订阅者之前订阅过其他主题
{
subscriber->removeTopic(msg->topicKey());//移除该订阅者订阅的这个主题
}
if (topic && subscriber)//如果主题和订阅者都已经存在于我们的服务器中
{
topic->removeSubscriber(subscriber);//移除主题对象的关注列表里面的对应订阅者
}
}
主题消息的发布
发布消息是发布订阅系统的核心功能,它实现了消息从发布者到多个订阅者的广播传递。这个过程类似于电视台向所有订阅了该频道的观众播放节目。
核心目标:
将消息高效、准确地推送给指定主题的所有订阅者。
详细步骤解析:
第一步:查找目标主题(线程安全区)
-
加锁保护关键数据
-
使用互斥锁确保在查找主题时,其他线程不会修改主题映射表
-
这是防止数据竞争的关键措施
-
-
定位主题对象
-
根据请求消息中的主题名称,在"主题名称→主题对象"映射表中查找对应的主题
-
这是通过哈希表实现的快速查找操作,时间复杂度接近O(1)
-
-
验证主题存在性
-
如果没找到主题(
topic_it == _topics.end()),说明主题不存在 -
此时直接返回false,表示发布失败
-
设计考虑: 这种快速失败机制避免了对不存在的主题进行无效操作
-
第二步:执行消息推送(无锁区)
-
释放锁后执行推送
-
在获取主题对象后立即释放锁
-
关键设计: 消息推送操作在锁外执行
-
原因: 消息推送涉及网络I/O,耗时可能较长,如果在锁内执行会严重阻塞其他操作
-
-
委托主题对象完成分发
-
调用主题对象的
pushMessage()方法 -
设计理念: 将具体实现细节封装在主题对象内部,遵循面向对象设计的封装原则
-
cpp
// 发布消息到主题
bool topicPublish(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
Topic::ptr topic;
{
std::unique_lock<std::mutex> lock(_mutex);
// 查找主题
auto topic_it = _topics.find(msg->topicKey());// 根据主题名称去 主题名称-主题对象的映射 里面获取主题对象
if (topic_it == _topics.end())//没找到
{
return false; // 主题不存在
}
//找到了
topic = topic_it->second;
}
// 向主题的所有订阅者推送消息
topic->pushMessage(msg);
return true;
}
2.3.响应接口
我们作为一个服务器,那么服务器肯定需要针对这个客户端发来的请求作出响应吧。
那么响应呢,我们这里写的比较简单
就是
- 错误响应
- 成功响应
cpp
// 发送错误响应给客户端
void errorResponse(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg, RCode rcode)
{
auto msg_rsp = MessageFactory::create<TopicResponse>();
msg_rsp->setId(msg->rid());
msg_rsp->setMType(MType::RSP_TOPIC);
msg_rsp->setRCode(rcode);
conn->send(msg_rsp);
}
// 发送成功响应给客户端
void topicResponse(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
auto msg_rsp = MessageFactory::create<TopicResponse>();
msg_rsp->setId(msg->rid());
msg_rsp->setMType(MType::RSP_TOPIC);
msg_rsp->setRCode(RCode::RCODE_OK);
conn->send(msg_rsp);
}
2.4.onTopicRequest函数
这个函数可以说是我们这个服务端的重中之重
还记得我们最开始设计这个服务端的时候说过一个函数:
对外接口回调函数
-
设计要点 :服务端需对外提供一个统一的主题操作类型的请求消息处理回调函数。所有客户端的创建、删除、订阅、取消订阅、发布等请求,都通过此函数入口进行分发和处理。
-
模块集成 :此回调函数通常设置给更上层的网络分发器(Dispatcher)模块。Dispatcher负责接收原始网络数据包,解析出基本指令后,调用此回调函数进入具体的业务逻辑处理。
那么我们这个onTopicRequest函数就提供给Dispatcher模块进行注册的
这个函数是整个主题管理系统的总调度中心,类似于公司的前台接待处,负责接收所有来访请求并进行初步分类,然后分发给对应的部门处理。
核心目标:
作为主题管理系统的统一入口,负责接收、分发各种主题操作请求,并统一返回处理结果。
详细步骤解析:
第一步:请求解析与分类
-
提取操作类型
-
从请求消息中获取操作类型(
topic_optype) -
这是决定请求去向的关键信息
-
-
准备状态变量
-
设置
ret变量记录操作结果,默认认为成功(true) -
这个变量将用于跟踪需要检查返回值的操作
-
第二步:请求路由分发(switch-case路由)
这是函数的核心部分,通过switch语句将请求精确路由到对应的处理函数:
-
主题创建(TOPIC_CREATE)
-
直接调用
topicCreate()函数 -
不检查返回值,因为创建操作设计为幂等操作(强断言)
-
主题存在时返回成功,不存在时创建并返回成功
-
-
主题删除(TOPIC_REMOVE)
-
直接调用
topicRemove()函数 -
同样不检查返回值,遵循幂等性原则
-
主题存在时删除,不存在时也视为成功
-
-
主题订阅(TOPIC_SUBSCRIBE)
-
调用
topicSubscribe()函数,并捕获返回值 -
需要检查返回值,因为订阅可能失败(如主题不存在)
-
返回值存储到
ret变量中供后续判断
-
-
主题取消订阅(TOPIC_CANCEL)
-
直接调用
topicCancel()函数 -
不检查返回值,遵循幂等性原则
-
无论主题或订阅者是否存在,都视为操作成功
-
-
主题消息发布(TOPIC_PUBLISH)
-
调用
topicPublish()函数,并捕获返回值 -
需要检查返回值,因为发布可能失败(如主题不存在)
-
返回值存储到
ret变量中供后续判断
-
-
无效操作类型(default)
-
处理未定义或不支持的操作类型
-
直接返回错误响应,不进行后续处理
-
第三步:结果处理与响应
-
检查操作结果
-
检查
ret变量,判断操作是否成功 -
注意: 只有订阅和发布操作会设置
ret值 -
其他操作即使失败也不会设置
ret为false
-
-
错误处理
-
如果
ret为false,返回"主题未找到"的错误响应 -
设计特点: 这里错误信息比较笼统,实际可以更细化
-
-
成功响应
- 如果操作成功,返回标准的成功响应
cpp
// 处理主题相关请求的入口函数
// 这个是需要注册给这个Dispatcher模块的
void onTopicRequest(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
// 获取请求中的操作类型
TopicOptype topic_optype = msg->optype();
bool ret = true;
// 根据操作类型调用相应的处理函数
switch (topic_optype)
{
// 主题的创建
case TopicOptype::TOPIC_CREATE:
topicCreate(conn, msg);
break;
// 主题的删除
case TopicOptype::TOPIC_REMOVE:
topicRemove(conn, msg);
break;
// 主题的订阅
case TopicOptype::TOPIC_SUBSCRIBE:
ret = topicSubscribe(conn, msg);
break;
// 主题的取消订阅
case TopicOptype::TOPIC_CANCEL:
topicCancel(conn, msg);
break;
// 主题消息的发布
case TopicOptype::TOPIC_PUBLISH:
ret = topicPublish(conn, msg);
break;
// 无效的操作类型
default:
return errorResponse(conn, msg, RCode::RCODE_INVALID_OPTYPE);
}
// 如果操作失败,返回错误响应
if (ret == false)
{
return errorResponse(conn, msg, RCode::RCODE_NOT_FOUND_TOPIC);
}
// 操作成功,返回成功响应
return topicResponse(conn, msg);
}
2.5.onShutdown
我们在最开始设计这个服务端模块的时候,我们就说过要有一个函数:
-
连接断开的处理回调函数
-
设计要点 :服务端必须注册一个连接断开处理回调函数。当TCP连接断开或客户端心跳超时时,此函数被自动触发。
-
内部逻辑 :在该函数内部,系统需根据上述的hash_map<订阅者的通信连接, 订阅的主题集合> ,找到该连接对应的所有订阅主题,并遍历这些主题,从 hash_map<主题名称, 订阅者集合> 中移除该连接。完成清理后,再删除该连接自身的记录。
-
如今我们的
- hash_map<订阅者的通信连接, 订阅的主题集合>:这个是一个订阅者对象
- hash_map<主题名称, 订阅者集合>:这是一个主题对象
那么我们就好处理了
这个函数是系统的"清道夫",负责在客户端连接异常断开时,清理相关资源,维护系统状态的完整性。这类似于大楼物业在租户搬走后,需要清理其留下的各种登记信息和权限记录。
核心目标:
当客户端连接异常断开时,自动清理该连接(如果是订阅者)在系统中留下的所有关联关系,避免出现"僵尸订阅"或内存泄漏。
详细步骤解析:
第一步:身份识别与快速过滤
-
查找订阅者身份
-
根据断开连接的
conn对象,在"连接对象→订阅者对象"映射表中查找 -
这个映射表记录了所有订阅者客户端的连接信息
-
-
区分角色处理
-
如果不是订阅者:说明该连接只是消息发布者或从未订阅过任何主题
-
这种情况下直接返回,无需任何清理操作
-
设计哲学:只处理真正需要清理的情况,避免无效操作
-
第二步:收集影响范围(线程安全区)
-
获取订阅者对象
-
从映射表中提取出对应的订阅者对象
-
这个对象包含了该订阅者的完整订阅信息
-
-
遍历订阅主题清单
-
遍历订阅者对象中的主题列表(
subscriber->topics) -
这个列表记录了该订阅者订阅的所有主题名称
-
-
定位相关主题对象
-
根据每个主题名称,在主题映射表中查找对应的主题对象
-
将找到的主题对象收集到
topics向量中 -
设计特点:使用临时向量存储,避免在锁内执行耗时的清理操作
-
-
移除订阅者登记信息
-
从"连接对象→订阅者对象"映射表中删除该条目
-
这个操作在锁的保护下进行,确保线程安全
-
第三步:清理关联关系(无锁区)
-
遍历受影响主题
-
对之前收集的所有主题对象逐一处理
-
这些主题对象中都包含这个订阅者的引用
-
-
解除订阅关系
-
在每个主题对象中调用
removeSubscriber(subscriber) -
从主题的订阅者列表中移除这个已经断开的订阅者
-
设计考虑:这个操作在锁外执行,因为主题对象内部有自己的锁机制
-
cpp
// 处理订阅者连接断开的情况,清理相关数据
void onShutdown(const BaseConnection::ptr &conn)
{
// 消息发布者断开连接,不需要任何操作;消息订阅者断开连接需要删除管理数据
// 1. 判断断开连接的是否是订阅者,不是的话则直接返回
std::vector<Topic::ptr> topics;//订阅者订阅的主题所对应的主题对象集合
Subscriber::ptr subscriber;//下线的订阅者
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _subscribers.find(conn); // 根据连接对象称去 连接对象到订阅者对象的映射 里面获取订阅者对象
// 如果连接不在订阅者列表中,说明是发布者或未订阅任何主题的客户端,直接返回
if (it == _subscribers.end())
{
return;
}
// 找到了对应的订阅者列表
// 获取订阅者对象
subscriber = it->second;
// 2. 获取订阅者断开连接会影响到的主题对象
for (auto &topic_name : subscriber->topics) // 遍历这个订阅者订阅的那些主题
{
auto topic_it = _topics.find(topic_name); // 根据主题名称去 主题名称-主题对象的映射 里面获取主题对象
if (topic_it == _topics.end()) // 没找到
{
continue;
}
//找到了对应主题
topics.push_back(topic_it->second);//添加进订阅者订阅的主题所对应的主题对象集合
}
// 3. 从订阅者映射信息中,删除订阅者
_subscribers.erase(it);//从 连接对象到订阅者对象的映射 里面删除订阅者对象
}
// 4. 从受影响的主题对象中,移除该订阅者
for (auto &topic : topics)//遍历 下线的这个订阅者订阅的主题所对应的主题对象集合
{
topic->removeSubscriber(subscriber);//从这些主题对象里面的订阅者集合里面移除下线的订阅者
}
}
2.6.完整代码

cpp
#pragma once
#include "../common/net.hpp"
#include "../common/message.hpp"
#include <unordered_set>
namespace jsonRpc
{
namespace server
{
// 主题管理器类,负责管理发布订阅系统中的主题和订阅者
class TopicManager
{
public:
using ptr = std::shared_ptr<TopicManager>;
// 构造函数
TopicManager() {}
// 处理主题相关请求的入口函数
// 这个是需要注册给这个Dispatcher模块的
void onTopicRequest(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
// 获取请求中的操作类型
TopicOptype topic_optype = msg->optype();
bool ret = true;
// 根据操作类型调用相应的处理函数
switch (topic_optype)
{
// 主题的创建
case TopicOptype::TOPIC_CREATE:
topicCreate(conn, msg);
break;
// 主题的删除
case TopicOptype::TOPIC_REMOVE:
topicRemove(conn, msg);
break;
// 主题的订阅
case TopicOptype::TOPIC_SUBSCRIBE:
ret = topicSubscribe(conn, msg);
break;
// 主题的取消订阅
case TopicOptype::TOPIC_CANCEL:
topicCancel(conn, msg);
break;
// 主题消息的发布
case TopicOptype::TOPIC_PUBLISH:
ret = topicPublish(conn, msg);
break;
// 无效的操作类型
default:
return errorResponse(conn, msg, RCode::RCODE_INVALID_OPTYPE);
}
// 如果操作失败,返回错误响应
if (ret == false)
{
return errorResponse(conn, msg, RCode::RCODE_NOT_FOUND_TOPIC);
}
// 操作成功,返回成功响应
return topicResponse(conn, msg);
}
// 处理订阅者连接断开的情况,清理相关数据
void onShutdown(const BaseConnection::ptr &conn)
{
// 消息发布者断开连接,不需要任何操作;消息订阅者断开连接需要删除管理数据
// 1. 判断断开连接的是否是订阅者,不是的话则直接返回
std::vector<Topic::ptr> topics;//订阅者订阅的主题所对应的主题对象集合
Subscriber::ptr subscriber;//下线的订阅者
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _subscribers.find(conn); // 根据连接对象称去 连接对象到订阅者对象的映射 里面获取订阅者对象
// 如果连接不在订阅者列表中,说明是发布者或未订阅任何主题的客户端,直接返回
if (it == _subscribers.end())
{
return;
}
// 找到了对应的订阅者列表
// 获取订阅者对象
subscriber = it->second;
// 2. 获取订阅者断开连接会影响到的主题对象
for (auto &topic_name : subscriber->topics) // 遍历这个订阅者订阅的那些主题
{
auto topic_it = _topics.find(topic_name); // 根据主题名称去 主题名称-主题对象的映射 里面获取主题对象
if (topic_it == _topics.end()) // 没找到
{
continue;
}
//找到了对应主题
topics.push_back(topic_it->second);//添加进订阅者订阅的主题所对应的主题对象集合
}
// 3. 从订阅者映射信息中,删除订阅者
_subscribers.erase(it);//从 连接对象到订阅者对象的映射 里面删除订阅者对象
}
// 4. 从受影响的主题对象中,移除该订阅者
for (auto &topic : topics)//遍历 下线的这个订阅者订阅的主题所对应的主题对象集合
{
topic->removeSubscriber(subscriber);//从这些主题对象里面的订阅者集合里面移除下线的订阅者
}
}
private:
// 发送错误响应给客户端
void errorResponse(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg, RCode rcode)
{
auto msg_rsp = MessageFactory::create<TopicResponse>();
msg_rsp->setId(msg->rid());
msg_rsp->setMType(MType::RSP_TOPIC);
msg_rsp->setRCode(rcode);
conn->send(msg_rsp);
}
// 发送成功响应给客户端
void topicResponse(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
auto msg_rsp = MessageFactory::create<TopicResponse>();
msg_rsp->setId(msg->rid());
msg_rsp->setMType(MType::RSP_TOPIC);
msg_rsp->setRCode(RCode::RCODE_OK);
conn->send(msg_rsp);
}
// 创建主题
void topicCreate(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
// 传递一个通信连接和请求
std::unique_lock<std::mutex> lock(_mutex); // 加锁
// 获取主题名称
std::string topic_name = msg->topicKey();
// 根据主题名称构造一个主题对象
auto topic = std::make_shared<Topic>(topic_name);
// 将主题添加到映射表中(如果已存在,会被覆盖,实现强断言)
_topics.insert(std::make_pair(topic_name, topic));
}
// 删除主题
void topicRemove(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
// 1. 查看当前主题有哪些订阅者,然后从订阅者中将主题信息删除掉
// 2. 删除主题的数据 -- 主题名称与主题对象的映射关系
std::string topic_name = msg->topicKey(); // 获取主题名称
std::unordered_set<Subscriber::ptr> subscribers; // 注意这里是一个集合set,代表着里面不会存储重复的订阅者结构体
{
std::unique_lock<std::mutex> lock(_mutex);
// 在删除主题之前,先找出会受到影响的订阅者
auto it = _topics.find(topic_name); // 根据主题名称去 主题名称-主题对象的映射 里面获取主题对象
if (it == _topics.end()) // 没有找到
{
return; // 主题不存在,直接返回
}
// 找到了对应主题对象
// 获取该主题的所有订阅者
subscribers = it->second->subscribers;
// 删除当前的主题映射关系
_topics.erase(it); // 去 主题名称-主题对象的映射 里面删除这对键值对
}
// 通知所有订阅了该主题的订阅者,从他们的订阅列表中删除该主题
for (auto &subscriber : subscribers) // 遍历被删除的这个主题的所有订阅者结构体
{
subscriber->removeTopic(topic_name); // 删除这些订阅者里面订阅的指定名字的主题
}
}
// 订阅主题
bool topicSubscribe(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
// 1. 先找出主题对象,以及订阅者对象
// 如果没有找到主题--就要报错;如果没有找到订阅者对象,那就要构造一个订阅者
Topic::ptr topic; // 主题
Subscriber::ptr subscriber; // 订阅者
{
std::unique_lock<std::mutex> lock(_mutex);
// 查找主题
auto topic_it = _topics.find(msg->topicKey()); // 根据主题名称去 主题名称-主题对象的映射 里面获取主题对象
if (topic_it == _topics.end())
{
return false; // 主题不存在
}
// 找到了对应主题对象,就保存给topic
topic = topic_it->second;
// 查找订阅者
auto sub_it = _subscribers.find(conn); // 根据连接对象称去 连接对象到订阅者对象的映射 里面获取订阅者对象
if (sub_it != _subscribers.end()) // 找到了
{
subscriber = sub_it->second; // 订阅者已存在
}
else // 不存在
{
// 创建新的订阅者对象
subscriber = std::make_shared<Subscriber>(conn);
_subscribers.insert(std::make_pair(conn, subscriber)); // 添加进 连接对象到订阅者对象的映射
}
}
// 2. 在主题对象中,新增一个订阅者对象关联的连接;
// 在订阅者对象中新增一个订阅的主题
topic->appendSubscriber(subscriber); // 添加订阅者
subscriber->appendTopic(msg->topicKey()); // 添加订阅的主题
return true;
}
// 取消订阅主题
void topicCancel(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
// 1. 先找出主题对象,和订阅者对象
Topic::ptr topic; // 主题
Subscriber::ptr subscriber; // 订阅者
{
std::unique_lock<std::mutex> lock(_mutex);
// 查找主题
auto topic_it = _topics.find(msg->topicKey()); // 根据主题名称去 主题名称-主题对象的映射 里面获取主题对象
if (topic_it != _topics.end()) // 找到了,说明这个主题之前就存在
{
topic = topic_it->second; // 保存这个主题对象
}
// 查找订阅者
auto sub_it = _subscribers.find(conn); // 根据连接对象称去 连接对象到订阅者对象的映射 里面获取订阅者对象
if (sub_it != _subscribers.end()) // 找到了,说明这个订阅者之前订阅过其他主题
{
subscriber = sub_it->second; // 保存起来
}
}
// 2. 从主题对象中删除当前的订阅者连接;
// 从订阅者信息中删除所订阅的主题名称
if (subscriber) // 如果订阅者已经在 连接对象到订阅者对象的映射 里面存在,说明订阅者之前订阅过其他主题
{
subscriber->removeTopic(msg->topicKey()); // 移除该订阅者订阅的这个主题
}
if (topic && subscriber) // 如果主题和订阅者都已经存在于我们的服务器中
{
topic->removeSubscriber(subscriber); // 移除主题对象的关注列表里面的对应订阅者
}
}
// 发布消息到主题
bool topicPublish(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
Topic::ptr topic;
{
std::unique_lock<std::mutex> lock(_mutex);
// 查找主题
auto topic_it = _topics.find(msg->topicKey()); // 根据主题名称去 主题名称-主题对象的映射 里面获取主题对象
if (topic_it == _topics.end()) // 没找到
{
return false; // 主题不存在
}
// 找到了
topic = topic_it->second;
}
// 向主题的所有订阅者推送消息
topic->pushMessage(msg);
return true;
}
private:
// 订阅者结构体,表示一个订阅者(客户端连接)
struct Subscriber
{
using ptr = std::shared_ptr<Subscriber>;
std::mutex _mutex; // 互斥锁,保护成员变量
BaseConnection::ptr conn; // 客户端连接
std::unordered_set<std::string> topics; // 订阅者所订阅的主题名称集合
Subscriber(const BaseConnection::ptr &c) : conn(c) {}
// 添加订阅的主题(订阅主题时调用)
void appendTopic(const std::string &topic_name)
{
std::unique_lock<std::mutex> lock(_mutex);
topics.insert(topic_name);
}
// 移除订阅的主题(主题被删除或取消订阅时调用)
void removeTopic(const std::string &topic_name)
{
std::unique_lock<std::mutex> lock(_mutex);
topics.erase(topic_name);
}
};
// 主题结构体,表示一个主题
struct Topic
{
using ptr = std::shared_ptr<Topic>;
std::mutex _mutex; // 互斥锁,保护成员变量
std::string topic_name; // 主题名称
std::unordered_set<Subscriber::ptr> subscribers; // 当前主题的订阅者集合
Topic(const std::string &name) : topic_name(name) {}
// 添加订阅者(新增订阅时调用)
void appendSubscriber(const Subscriber::ptr &subscriber)
{
std::unique_lock<std::mutex> lock(_mutex);
subscribers.insert(subscriber);
}
// 移除订阅者(取消订阅或订阅者连接断开时调用)
void removeSubscriber(const Subscriber::ptr &subscriber)
{
std::unique_lock<std::mutex> lock(_mutex);
subscribers.erase(subscriber);
}
// 推送消息(收到消息发布请求时调用)
// 遍历当前主题的订阅者集合,并且往每个订阅者发送消息
void pushMessage(const BaseMessage::ptr &msg)
{
std::unique_lock<std::mutex> lock(_mutex);
for (auto &subscriber : subscribers) // 当前主题的订阅者集合
{
// 向每个订阅者发送消息
subscriber->conn->send(msg);
}
}
};
private:
std::mutex _mutex; // 保护整个TopicManager的互斥锁
std::unordered_map<std::string, Topic::ptr> _topics; // 主题名称到主题对象的映射
std::unordered_map<BaseConnection::ptr, Subscriber::ptr> _subscribers; // 连接对象到订阅者对象的映射
};
}
}
2.7.封装主题服务端
还记得我们上面说的:
-
对外接口回调函数
-
设计要点 :服务端需对外提供一个统一的主题操作类型的请求消息处理回调函数。所有客户端的创建、删除、订阅、取消订阅、发布等请求,都通过此函数入口进行分发和处理。
-
模块集成 :此回调函数通常设置给更上层的网络分发器(Dispatcher)模块。Dispatcher负责接收原始网络数据包,解析出基本指令后,调用此回调函数进入具体的业务逻辑处理。
-
-
连接断开的处理回调函数 :服务端必须注册一个连接断开处理回调函数。当TCP连接断开或客户端心跳超时时,此函数被自动触发。
事实上呢,这个主题服务端其实是很简单的,我们只需要将我们上面写好的这两个函数给设置好
- TopicManager::onTopicRequest函数注册给这个Dispatcher模块
- 连接关闭回调给设置好
- 启动服务器
cpp
// 主题服务器类 - 提供发布-订阅功能
class TopicServer
{
public:
using ptr = std::shared_ptr<TopicServer>;
// 构造函数,监听指定端口
TopicServer(int port) : _topic_manager(std::make_shared<TopicManager>()), // 创建主题管理器
_dispatcher(std::make_shared<jsonRpc::Dispatcher>()) // 创建消息分发器
{
// 注册主题请求处理器 - 当收到主题相关请求时,由主题管理器处理
auto topic_cb = std::bind(&TopicManager::onTopicRequest, _topic_manager.get(),
std::placeholders::_1, std::placeholders::_2);
_dispatcher->registerHandler<TopicRequest>(MType::REQ_TOPIC, topic_cb);
// 创建TCP服务器,监听指定端口
_server = jsonRpc::ServerFactory::create(port);
// 设置消息回调 - 当服务器收到消息时,交给分发器处理
auto message_cb = std::bind(&jsonRpc::Dispatcher::onMessage, _dispatcher.get(),
std::placeholders::_1, std::placeholders::_2);
_server->setMessageCallback(message_cb);
// 设置连接关闭回调 - 当连接断开时,清理相关资源
auto close_cb = std::bind(&TopicServer::onConnShutdown, this, std::placeholders::_1);
_server->setCloseCallback(close_cb);
}
// 启动主题服务器
void start()
{
_server->start();
}
private:
// 连接断开时的处理函数
void onConnShutdown(const BaseConnection::ptr &conn)
{
// 交给主题管理器处理连接断开,清理订阅者信息
_topic_manager->onShutdown(conn);
}
private:
TopicManager::ptr _topic_manager; // 主题管理器
Dispatcher::ptr _dispatcher; // 消息分发器
BaseServer::ptr _server; // 网络服务器
};
三.发布订阅客户端核心设计
客户端根据其角色主要分为两类:消息发布客户端 与消息订阅客户端。它们共享基础通信能力,但在核心行为上各有侧重。
客户端核心职责
-
消息发布客户端
-
主要负责主题的生命周期管理与消息生产。其核心操作包括:
-
创建主题:请求服务端建立新的消息通道。
-
删除主题:请求服务端移除指定主题(通常需权限)。
-
发布消息:向指定主题发送消息内容。
-
-
-
消息订阅客户端
-
主要负责主题的订阅与消息消费。其核心操作包括:
-
创建/删除主题:同样可管理主题(取决于系统设计)。
-
订阅主题:向服务端注册,表明希望接收某主题的消息。
-
取消订阅:向服务端注销对某主题的兴趣。
-
处理推送消息:接收并处理由服务端主动推送过来的订阅消息。
-
-
统一客户端架构设计
尽管职责不同,两类客户端在架构上可采用统一框架,对外提供一致的接口,内部通过注册不同的处理逻辑来区分行为。
-
对外的主动操作接口
-
客户端封装了五个核心的主动请求接口,供上层业务调用:
-
CreateTopic(创建主题) -
DeleteTopic(删除主题) -
Subscribe(订阅主题) -
Unsubscribe(取消订阅) -
Publish(发布消息)
-
-
这些接口负责构造对应的网络请求,并通过底层通信模块发送至服务端。
-
-
对外的被动消息处理接口
-
客户端需要向底层的网络分发器(Dispatcher)模块 提供一个消息处理回调函数。
-
作用 :当Dispatcher从网络收到服务端发来的任何数据包(尤其是消息推送)时,都会调用此函数进行第一级分发。
-
特别说明 :对于订阅客户端而言,服务端主动发来的"消息推送"正对应其
Publish请求的响应,但处理逻辑完全不同。
-
-
内部数据管理:主题-回调映射表
-
核心设计 :客户端内部维护一个
hash_map<主题名称, 消息处理回调函数>。 -
工作流程:
-
当订阅客户端调用
Subscribe("news.sports")时,不仅在服务端注册,同时会在本地 为"news.sports"主题注册一个特定的回调函数(例如OnSportsNewsReceived)。 -
当服务端推送一条属于
"news.sports"主题的消息时,消息经由Dispatcher到达客户端的统一消息处理接口。 -
该接口解析消息头中的主题字段,并立刻在本地映射表中查找对应的回调函数。
-
找到后,异步地调用该回调函数,并将消息内容作为参数传入,完成业务逻辑处理。
-
-
关键设计思想:异步回调机制
订阅客户端无法预测也无法同步等待消息的到来,因为消息的产生完全取决于发布客户端的行为。因此,同步的请求-响应模式在此不适用。
-
解决方案 :采用异步回调(Callback) 或观察者(Observer) 模式。
-
优势:
-
解耦:消息接收逻辑与消息处理逻辑完全分离,业务代码无需阻塞等待。
-
高效:网络层可在收到消息后立即转发,无需等待业务处理完成。
-
灵活:可以为不同的主题动态注册不同的处理函数,实现精细化的消息路由与处理。
-
2.1.成员变量设计
首先,我们作为一个客户端,我们肯定是需要去向这个服务端发送请求的
那么我们客户端就必须需要一个Requestor对象来发送我们的请求
此外,服务器肯定会根据我们的请求来作出响应,那么我们客户端收到响应后也应该作出一些处理
由于我们这里的客户端其实是和主题相关的,我们发送的请求也是和主题相关的,那么我们收到的响应也是和主题相关的。
那么我们就干脆引入一个哈希表<主题名称,回调函数>,借助这个哈希表,当响应到来的时候,我们只需要去遍历这个哈希表,调用这个主题对应的回调函数即可
cpp
// 客户端主题管理器类,负责管理客户端的主题相关操作
class TopicManager
{
public:
// 定义订阅回调函数类型,当收到订阅消息时调用
using SubCallback = std::function<void(const std::string &key, const std::string &msg)>;
using ptr = std::shared_ptr<TopicManager>;
......
private:
std::mutex _mutex; // 保护_topic_callbacks的互斥锁
std::unordered_map<std::string, SubCallback> _topic_callbacks; // 主题名称到回调函数的映射表
Requestor::ptr _requestor; // 请求器对象,用于发送请求和接收响应
};
那么,由于这个哈希表里面的类型有自定义类型,我们必须提供一些接口来对这个哈希表进行管理
- 添加主题回调函数到本地映射表
- 从本地映射表中删除主题回调函数
- 根据主题名称获取对应的回调函数
这些思想还是很简单的
cpp
// 添加主题回调函数到本地映射表
void addSubscribe(const std::string &key, const SubCallback &cb)
{
std::unique_lock<std::mutex> lock(_mutex);
_topic_callbacks.insert(std::make_pair(key, cb));
}
// 从本地映射表中删除主题回调函数
void delSubscribe(const std::string &key)
{
std::unique_lock<std::mutex> lock(_mutex);
_topic_callbacks.erase(key);
}
// 根据主题名称获取对应的回调函数
const SubCallback getSubscribe(const std::string &key)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _topic_callbacks.find(key);
if (it == _topic_callbacks.end())
{
return SubCallback(); // 返回空回调函数
}
return it->second;
}
2.2.请求接口
我们是客户端,最常用的就是发送请求,为了让我们的核心代码更加直白简洁,我们就有必要去写一个通用的发送请求的接口。
cpp
// 通用请求函数,处理所有主题相关的请求
bool commonRequest(const BaseConnection::ptr &conn,
const std::string &key,
TopicOptype type,
const std::string &msg = "")
{
// 1. 构造请求对象,并填充数据
auto msg_req = MessageFactory::create<TopicRequest>();
msg_req->setId(UUID::uuid());
msg_req->setMType(MType::REQ_TOPIC);
msg_req->setOptype(type);
msg_req->setTopicKey(key);
// 如果是发布消息请求,还需要设置消息内容
if (type == TopicOptype::TOPIC_PUBLISH)
{
msg_req->setTopicMsg(msg);
}
// 2. 向服务端发送请求,等待响应
BaseMessage::ptr msg_rsp;
bool ret = _requestor->send(conn, msg_req, msg_rsp);//发送同步请求,会阻塞到响应到来,响应会填充进msg_rsp里面
if (ret == false)
{
ELOG("主题操作请求失败!");
return false;
}
// 3. 判断请求处理是否成功
auto topic_rsp_msg = std::dynamic_pointer_cast<TopicResponse>(msg_rsp);//将父类指针转换成子类指针,这是一定可以成功的,
//因为无论请求还是响应,我们都说通过工厂类来生成一个子类,但是却返回一个父类指针,现在我们在这里只不过是还原到最初的状态而已
if (!topic_rsp_msg)
{
ELOG("主题操作响应,向下类型转换失败!");
return false;
}
if (topic_rsp_msg->rcode() != RCode::RCODE_OK)//响应表示这次操作是失败的
{
ELOG("主题操作请求出错:%s", errReason(topic_rsp_msg->rcode()).c_str());
return false;
}
return true;
}
在这个通用的请求接口里面,我们既进行了请求的发送,也对这个响应进行了等待,然后判断操作是否成功。
有了这么一个通用请求,我们客户端发送主题的5大操作的响应就很简单了。
直接带进去即可。
cpp
// 创建主题
bool create(const BaseConnection::ptr &conn, const std::string &key)
{
return commonRequest(conn, key, TopicOptype::TOPIC_CREATE);
}
// 删除主题
bool remove(const BaseConnection::ptr &conn, const std::string &key)
{
return commonRequest(conn, key, TopicOptype::TOPIC_REMOVE);
}
// 订阅主题,需要指定回调函数来处理接收到的消息
bool subscribe(const BaseConnection::ptr &conn, const std::string &key, const SubCallback &cb)
{
// 先将回调函数添加到本地映射表中
addSubscribe(key, cb);//添加主题回调函数到本地映射表
// 发送订阅请求到服务端
bool ret = commonRequest(conn, key, TopicOptype::TOPIC_SUBSCRIBE);
// 如果订阅失败,从本地映射表中删除回调函数
if (ret == false)
{
delSubscribe(key);//从本地映射表中删除主题回调函数
return false;
}
return true;
}
// 取消订阅主题
bool cancel(const BaseConnection::ptr &conn, const std::string &key)
{
// 先从本地映射表中删除回调函数
delSubscribe(key);
// 发送取消订阅请求到服务端
return commonRequest(conn, key, TopicOptype::TOPIC_CANCEL);
}
// 发布消息到指定主题
bool publish(const BaseConnection::ptr &conn, const std::string &key, const std::string &msg)
{
return commonRequest(conn, key, TopicOptype::TOPIC_PUBLISH, msg);
}
2.3.处理服务器推送过来的响应
如果说我们客户端订阅了一个主题,当有客户端往这个主题里面推送消息,服务端会自动的往订阅了这个主题的客户端推送消息。
那么我们的客户就必须对这个服务端推送过来的消息作出处理。
cpp
// 处理服务端推送过来的消息(发布的消息)
void onPublish(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
// 1. 从消息中取出操作类型进行判断,是否是消息发布请求
auto type = msg->optype();
if (type != TopicOptype::TOPIC_PUBLISH)
{
ELOG("收到了错误类型的主题操作!");
return;
}
// 2. 取出消息主题名称,以及消息内容
std::string topic_key = msg->topicKey();
std::string topic_msg = msg->topicMsg();
// 3. 通过主题名称,查找对应主题的回调处理函数,有在处理,无在报错
auto callback = getSubscribe(topic_key);
if (!callback)
{
ELOG("收到了 %s 主题消息,但是该消息无主题处理回调!", topic_key.c_str());
return;
}
// 调用回调函数处理消息
return callback(topic_key, topic_msg);
}
2.4.完整代码
cpp
#pragma once
#include "requestor.hpp"
#include <unordered_set>
namespace jsonRpc
{
namespace client
{
// 客户端主题管理器类,负责管理客户端的主题相关操作
class TopicManager
{
public:
// 定义订阅回调函数类型,当收到订阅消息时调用
using SubCallback = std::function<void(const std::string &key, const std::string &msg)>;
using ptr = std::shared_ptr<TopicManager>;
// 构造函数,需要传入请求器对象用于发送请求
TopicManager(const Requestor::ptr &requestor) : _requestor(requestor) {}
// 创建主题
bool create(const BaseConnection::ptr &conn, const std::string &key)
{
return commonRequest(conn, key, TopicOptype::TOPIC_CREATE);
}
// 删除主题
bool remove(const BaseConnection::ptr &conn, const std::string &key)
{
return commonRequest(conn, key, TopicOptype::TOPIC_REMOVE);
}
// 订阅主题,需要指定回调函数来处理接收到的消息
bool subscribe(const BaseConnection::ptr &conn, const std::string &key, const SubCallback &cb)
{
// 先将回调函数添加到本地映射表中
addSubscribe(key, cb);//添加主题回调函数到本地映射表
// 发送订阅请求到服务端
bool ret = commonRequest(conn, key, TopicOptype::TOPIC_SUBSCRIBE);
// 如果订阅失败,从本地映射表中删除回调函数
if (ret == false)
{
delSubscribe(key);//从本地映射表中删除主题回调函数
return false;
}
return true;
}
// 取消订阅主题
bool cancel(const BaseConnection::ptr &conn, const std::string &key)
{
// 先从本地映射表中删除回调函数
delSubscribe(key);
// 发送取消订阅请求到服务端
return commonRequest(conn, key, TopicOptype::TOPIC_CANCEL);
}
// 发布消息到指定主题
bool publish(const BaseConnection::ptr &conn, const std::string &key, const std::string &msg)
{
return commonRequest(conn, key, TopicOptype::TOPIC_PUBLISH, msg);
}
// 处理服务端推送过来的消息(发布的消息)
void onPublish(const BaseConnection::ptr &conn, const TopicRequest::ptr &msg)
{
// 1. 从消息中取出操作类型进行判断,是否是消息发布请求
auto type = msg->optype();
if (type != TopicOptype::TOPIC_PUBLISH)
{
ELOG("收到了错误类型的主题操作!");
return;
}
// 2. 取出消息主题名称,以及消息内容
std::string topic_key = msg->topicKey();
std::string topic_msg = msg->topicMsg();
// 3. 通过主题名称,查找对应主题的回调处理函数,有在处理,无在报错
auto callback = getSubscribe(topic_key);
if (!callback)
{
ELOG("收到了 %s 主题消息,但是该消息无主题处理回调!", topic_key.c_str());
return;
}
// 调用回调函数处理消息
return callback(topic_key, topic_msg);
}
private:
// 添加主题回调函数到本地映射表
void addSubscribe(const std::string &key, const SubCallback &cb)
{
std::unique_lock<std::mutex> lock(_mutex);
_topic_callbacks.insert(std::make_pair(key, cb));
}
// 从本地映射表中删除主题回调函数
void delSubscribe(const std::string &key)
{
std::unique_lock<std::mutex> lock(_mutex);
_topic_callbacks.erase(key);
}
// 根据主题名称获取对应的回调函数
const SubCallback getSubscribe(const std::string &key)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _topic_callbacks.find(key);
if (it == _topic_callbacks.end())
{
return SubCallback(); // 返回空回调函数
}
return it->second;
}
// 通用请求函数,处理所有主题相关的请求
bool commonRequest(const BaseConnection::ptr &conn,
const std::string &key,
TopicOptype type,
const std::string &msg = "")
{
// 1. 构造请求对象,并填充数据
auto msg_req = MessageFactory::create<TopicRequest>();
msg_req->setId(UUID::uuid());
msg_req->setMType(MType::REQ_TOPIC);
msg_req->setOptype(type);
msg_req->setTopicKey(key);
// 如果是发布消息请求,还需要设置消息内容
if (type == TopicOptype::TOPIC_PUBLISH)
{
msg_req->setTopicMsg(msg);
}
// 2. 向服务端发送请求,等待响应
BaseMessage::ptr msg_rsp;
bool ret = _requestor->send(conn, msg_req, msg_rsp);//发送同步请求,会阻塞到响应到来,响应会填充进msg_rsp里面
if (ret == false)
{
ELOG("主题操作请求失败!");
return false;
}
// 3. 判断请求处理是否成功
auto topic_rsp_msg = std::dynamic_pointer_cast<TopicResponse>(msg_rsp);//将父类指针转换成子类指针,这是一定可以成功的,
//因为无论请求还是响应,我们都说通过工厂类来生成一个子类,但是却返回一个父类指针,现在我们在这里只不过是还原到最初的状态而已
if (!topic_rsp_msg)
{
ELOG("主题操作响应,向下类型转换失败!");
return false;
}
if (topic_rsp_msg->rcode() != RCode::RCODE_OK)//响应表示这次操作是失败的
{
ELOG("主题操作请求出错:%s", errReason(topic_rsp_msg->rcode()).c_str());
return false;
}
return true;
}
private:
std::mutex _mutex; // 保护_topic_callbacks的互斥锁
std::unordered_map<std::string, SubCallback> _topic_callbacks; // 主题名称到回调函数的映射表
Requestor::ptr _requestor; // 请求器对象,用于发送请求和接收响应
};
}
}
客户端的思路就还是很简单的。
2.5.封装主题客户端
其实这个思路也是很简单
封装了5个主题核心操作
cpp
// 主题客户端类,提供发布-订阅功能
class TopicClient
{
public:
// 构造函数,初始化主题客户端
// @param ip: 服务器的IP地址
// @param port: 服务器的端口号
TopicClient(const std::string &ip, int port) : _requestor(std::make_shared<Requestor>()), // 创建请求器实例
_dispatcher(std::make_shared<Dispatcher>()), // 创建调度器实例
_topic_manager(std::make_shared<TopicManager>(_requestor))
{ // 创建主题管理器实例
// 设置主题响应回调函数,将响应交给请求器处理
auto rsp_cb = std::bind(&Requestor::onResponse, _requestor.get(),
std::placeholders::_1, std::placeholders::_2);
_dispatcher->registerHandler<BaseMessage>(MType::RSP_TOPIC, rsp_cb);
// 设置主题发布消息回调函数,处理发布的消息
auto msg_cb = std::bind(&TopicManager::onPublish, _topic_manager.get(),
std::placeholders::_1, std::placeholders::_2);
_dispatcher->registerHandler<TopicRequest>(MType::REQ_TOPIC, msg_cb);
// 设置消息回调函数,将收到的消息交给调度器处理
auto message_cb = std::bind(&Dispatcher::onMessage, _dispatcher.get(),
std::placeholders::_1, std::placeholders::_2);
_rpc_client = ClientFactory::create(ip, port); // 创建客户端连接
_rpc_client->setMessageCallback(message_cb); // 设置消息回调
_rpc_client->connect(); // 连接服务器
}
// 创建主题
// @param key: 主题名称
// @return: 创建成功返回true,失败返回false
bool create(const std::string &key)
{
return _topic_manager->create(_rpc_client->connection(), key);
}
// 删除主题
// @param key: 主题名称
// @return: 删除成功返回true,失败返回false
bool remove(const std::string &key)
{
return _topic_manager->remove(_rpc_client->connection(), key);
}
// 订阅主题
// @param key: 主题名称
// @param cb: 订阅回调函数,当主题有消息发布时被调用
// @return: 订阅成功返回true,失败返回false
bool subscribe(const std::string &key, const TopicManager::SubCallback &cb)
{
return _topic_manager->subscribe(_rpc_client->connection(), key, cb);
}
// 取消订阅
// @param key: 主题名称
// @return: 取消成功返回true,失败返回false
bool cancel(const std::string &key)
{
return _topic_manager->cancel(_rpc_client->connection(), key);
}
// 发布消息到主题
// @param key: 主题名称
// @param msg: 要发布的消息内容
// @return: 发布成功返回true,失败返回false
bool publish(const std::string &key, const std::string &msg)
{
return _topic_manager->publish(_rpc_client->connection(), key, msg);
}
// 关闭客户端连接
void shutdown()
{
_rpc_client->shutdown();
}
private:
Requestor::ptr _requestor; // 请求器
TopicManager::ptr _topic_manager; // 主题管理器
Dispatcher::ptr _dispatcher; // 调度器
BaseClient::ptr _rpc_client; // 客户端连接
};
没什么好说的,就很简单
四.测试
server.cpp
cpp
#include "../../server/rpc_server.hpp"
int main()
{
auto server = std::make_shared<jsonRpc::server::TopicServer>(7070);
server->start();
return 0;
}
subscribe_client.cpp
cpp
#include "../../client/rpc_client.hpp"
void callback(const std::string &key, const std::string &msg) {
ILOG("%s 主题收到推送过来的消息: %s", key.c_str(), msg.c_str());
}
int main()
{
//1. 实例化客户端对象
auto client = std::make_shared<jsonRpc::client::TopicClient>("127.0.0.1", 7070);
//2. 创建主题
bool ret = client->create("hello");
if (ret == false) {
ELOG("创建主题失败!");
}
//3. 订阅主题
ret = client->subscribe("hello", callback);
//4. 等待->退出
std::this_thread::sleep_for(std::chrono::seconds(10));
client->shutdown();
return 0;
}
publish_client.cpp
cpp
#include "../../client/rpc_client.hpp"
int main()
{
//1. 实例化客户端对象
auto client = std::make_shared<jsonRpc::client::TopicClient>("127.0.0.1", 7070);
//2. 创建主题
bool ret = client->create("hello");
if (ret == false) {
ELOG("创建主题失败!");
}
//3. 向主题发布消息
for (int i = 0; i < 10; i++) {
client->publish("hello", "Hello World-" + std::to_string(i));
}
client->shutdown();
return 0;
}
makefile
bash
CFLAG= -I ../../../build/release-install-cpp11/include/
LFLAG= -L ../../../build/release-install-cpp11/lib -lmuduo_net -lmuduo_base -pthread -ljsoncpp
all : server publish_client subscribe_client
server : server.cpp
g++ -g $(CFLAG) $^ -o $@ $(LFLAG)
publish_client : publish_client.cpp
g++ -g $(CFLAG) $^ -o $@ $(LFLAG)
subscribe_client : subscribe_client.cpp
g++ -g $(CFLAG) $^ -o $@ $(LFLAG)
.PHONY: clean
clean:
rm -f server publish_client subscribe_client
接下来我们就进行测试


接下来我们启动订阅客户端


接下来我们启动发布客户端

我们的订阅客户端就收到了下面这些消息
