目录
[1.1. 消费者信息结构体](#1.1. 消费者信息结构体)
[1.2. 队列消费者管理结构](#1.2. 队列消费者管理结构)
[1.3. 消费者统一管理器](#1.3. 消费者统一管理器)
一.设计
客户端这边每当发起一个订阅请求,意味着服务器这边就多了一个订阅者(处理消息的客户端描述),而这个消费者或者说订阅者它是和队列直接关联的,因为订阅请求中会描述当前用户想要订阅哪一个队列的消息。
而一个信道关闭的时候,或者队列被删除的时候,那么这个信道或队列关联的消费者 也就没有存在的意义了,因此也需要将相关的消费者信息给删除掉。

一个通信通道只有一个消费者
当我们的队列里面有消息了,就会获取订阅它的消费者,然后就能获取到对应通信通道,然后依次溯源到指定用户。
消费者的管理以信道为单元还是以队列为单元呢?
- 1.以信道为单元:一个信道关闭的时候所有关联的消费者都要删除
- 2.以队列为单元:一个队列收到了一条消息,需要找到订阅了队列消息的消费者进行消息推送
我们这里很明显,消费的情况多,所以我们使用以队列为单元。
为了支撑消息队列的订阅与消费机制,需要建立一个完善的消费者信息管理系统。该系统负责维护所有消费者的状态、其与队列的订阅关系,并实现消息的公平分发。
整个设计分为三个层次:消费者个体信息 、以队列为单位的消费者组管理 以及全局统一管理。
1.1. 消费者信息结构体
此结构体定义了单个消费者的完整元数据,是管理的最小单元。
-
消费者标识:唯一标识一个消费者实例,通常由客户端连接和信道信息组合生成。
-
订阅的队列名称:指明此消费者希望从哪个队列接收消息。
-
消息处理回调函数 :这是服务端向消费者推送消息的核心入口。**当有消息需要被该消费者处理时,系统将调用此回调函数。**其内部实现应完成以下逻辑:根据消费者标识找到其对应的网络连接,并将消息内容封装后发送给客户端。
- 函数签名:
void(const std::string&, const mymq::BasicProperties&, const std::string&)
- 函数签名:
-
自动应答标志 :用于控制消息确认模式。若为
true,则消息一旦被传递给消费者回调函数,即被视为消费成功,并从服务器中移除;若为false,则服务器会等待客户端发送明确的确认指令后,才移除消息。
我们很快就能写出下面这个框架
cpp
// 定义消费者回调函数类型
// 当有消息需要被消费者处理时,服务端将调用此回调函数
// 参数1:消费者标识
// 参数2:消息属性
// 参数3:消息体
using ConsumerCallback = std::function<void(const std::string,
const mymq::BasicProperties *bp,
const std::string)>;
// 消费者信息结构体
// 定义单个消费者的完整元数据,是管理的最小单元
struct Consumer
{
using ptr = std::shared_ptr<Consumer>;
std::string tag; // 消费者标识,用于唯一识别一个消费者
std::string qname; // 消费者订阅的队列名称
bool auto_ack; // 自动确认标志
ConsumerCallback callback; // 消息处理回调函数
Consumer()
{
DLOG("新的消费者: %p", this);
}
Consumer(const std::string &ctag, const std::string &queue_name,
bool ack_flag, const ConsumerCallback &cb) : tag(ctag), qname(queue_name), auto_ack(ack_flag),
callback(std::move(cb))
{
DLOG("新的消费者: %p", this);
}
~Consumer()
{
DLOG("删除消费者: %p", this);
}
};
1.2. 队列消费者管理结构
此结构以单个队列为核心 ,管理所有订阅了该队列的消费者,实现了一个"消费者组"。其核心目标是实现消息在组内的负载均衡分发。
-
内部成员:
-
消费者列表 :使用
std::vector存储订阅本队列的所有消费者信息结构体。 -
轮转序号:一个整型索引,用于实现轮询调度策略。当多条消息到达时,系统依次选择不同的消费者进行处理,以达到公平分配的目的。一个队列可能有多个消费者,但是一条消息只需要被一个消费者进行消费,因此采用RR轮转思想来选择到底是哪个。
-
互斥锁:保护消费者列表和轮转序号的并发访问,确保在多线程环境下添加、删除、选择消费者的操作是线程安全的。
-
队列名称:记录本管理单元所服务的队列名称。
-
-
关键操作:
-
新增消费者:当客户端通过信道成功订阅本队列时调用,将新的消费者信息加入列表。
-
删除消费者:在客户端取消订阅、其所在信道关闭或连接断开时调用,从列表中移除对应消费者。
-
获取消费者:当有消息需要投递到本队列时,调用此函数。它将根据当前的轮转序号,从列表中选取一个消费者(轮询方式),并更新序号,然后返回该消费者信息。
-
判断队列消费者是否为空:检查当前是否有活跃的消费者订阅此队列。
-
判断指定消费者是否存在:根据消费者标识,查询其是否在当前的订阅列表中。
-
清理队列所有消费者:清空当前列表,通常在队列被删除前调用。
-
cpp
// 队列消费者管理结构
// 以单个队列为核心,管理所有订阅了该队列的消费者,实现"消费者组"
// 采用轮询调度策略实现消息在组内的负载均衡分发
class QueueConsumer {
public:
using ptr = std::shared_ptr<QueueConsumer>;
// 构造函数:初始化队列名称和轮转序号
QueueConsumer(const std::string &qname);
// 队列新增消费者
// 当客户端通过信道成功订阅本队列时调用此函数
Consumer::ptr create(const std::string &ctag,
const std::string &queue_name,
bool ack_flag, const ConsumerCallback &cb);
// 队列移除消费者
// 在客户端取消订阅、信道关闭或连接断开时调用
void remove(const std::string &ctag);
// 获取消费者:轮询调度(Round-Robin)策略
// 当有消息需要投递到本队列时调用此函数
Consumer::ptr choose();
// 判断队列是否为空(是否有活跃消费者)
bool empty();
// 判断指定消费者是否存在
bool exists(const std::string &ctag);
// 清理所有消费者
// 通常在队列被删除前调用
void clear();
private:
std::string _qname; // 队列名称
std::mutex _mutex; // 互斥锁,保证线程安全
uint64_t _rr_seq; // 轮转序号,用于轮询调度
std::vector<Consumer::ptr> _consumers; // 消费者列表
};
1.3. 消费者统一管理器
这是一个全局管理类,负责协调所有队列的消费者管理工作。它维护一个映射关系(例如 std::unordered_map),将队列名称 映射到对应的队列消费者管理结构对象上。
-
管理方法:
-
初始化/删除队列的消费者信息结构 :在系统创建新队列或删除已有队列时,相应地创建或销毁对应的
队列消费者管理结构对象。 -
向指定队列新增消费者 :封装底层操作。客户端订阅时,通过此接口找到目标队列的管理对象,并调用其
新增消费者方法。 -
从指定队列移除消费者 :客户端取消订阅时,通过此接口找到对应队列管理对象,并调用其
删除消费者方法。 -
移除指定队列的所有消费者:在队列被删除时,先调用此接口清理所有消费者,再销毁队列管理单元。
-
从指定队列获取一个消费者 :这是消息投递流程的关键一环。服务端根据消息的目标队列名,通过此接口找到对应的管理对象,并调用其
获取消费者方法,拿到一个具体的消费者进行消息推送。 -
判断队列中消费者是否为空:查询某队列当前是否有订阅者。
-
判断队列中指定消费者是否存在:用于验证某个消费者订阅状态。
-
清理所有消费者:在系统关闭或重置时,清空所有队列的消费者信息。
-
cpp
// 消费者统一管理器
// 全局管理类,负责协调所有队列的消费者管理工作
// 维护队列名称到队列消费者管理结构的映射
class ConsumerManager {
public:
using ptr = std::shared_ptr<ConsumerManager>;
ConsumerManager();
// 初始化队列消费者管理结构
// 在系统创建新队列时调用
void initQueueConsumer(const std::string &qname);
// 销毁队列消费者管理结构
// 在队列被删除时调用
void destroyQueueConsumer(const std::string &qname);
// 向指定队列新增消费者
// 客户端订阅队列时调用此接口
Consumer::ptr create(const std::string &ctag,
const std::string &queue_name,
bool ack_flag,
const ConsumerCallback &cb);
// 从指定队列移除消费者
// 客户端取消订阅时调用此接口
void remove(const std::string &ctag, const std::string &queue_name);
// 从指定队列获取一个消费者(轮询选择)
// 消息投递流程的关键一环,服务端根据消息的目标队列名调用此接口
Consumer::ptr choose(const std::string &queue_name);
// 判断队列是否为空(是否有消费者)
bool empty(const std::string &queue_name);
// 判断队列中指定消费者是否存在
bool exists(const std::string &ctag, const std::string &queue_name);
// 清理所有消费者
// 在系统关闭或重置时调用
void clear();
private:
std::mutex _mutex; // 互斥锁,保护全局映射表
std::unordered_map<std::string, QueueConsumer::ptr> _qconsumers; // 队列到消费者管理结构的映射
};
二.代码编写
我们的代码都是存放到下面的consumer.hpp里面的

cpp
#ifndef __M_CONSUMER_H__
#define __M_CONSUMER_H__
#include "../mqcommon/logger.hpp"
#include "../mqcommon/helper.hpp"
#include "../mqcommon/msg.pb.h"
#include <iostream>
#include <unordered_map>
#include <mutex>
#include <memory>
#include <vector>
#include <functional>
namespace mymq
{
// 定义消费者回调函数类型
// 当有消息需要被消费者处理时,服务端将调用此回调函数
// 参数1:消费者标识
// 参数2:消息属性
// 参数3:消息体
using ConsumerCallback = std::function<void(const std::string,
const mymq::BasicProperties *bp,
const std::string)>;
// 消费者信息结构体
// 定义单个消费者的完整元数据,是管理的最小单元
struct Consumer
{
using ptr = std::shared_ptr<Consumer>;
std::string tag; // 消费者标识,用于唯一识别一个消费者
std::string qname; // 消费者订阅的队列名称
bool auto_ack; // 自动确认标志
ConsumerCallback callback; // 消息处理回调函数
Consumer()
{
DLOG("新的消费者: %p", this);
}
Consumer(const std::string &ctag, const std::string &queue_name,
bool ack_flag, const ConsumerCallback &cb) : tag(ctag), qname(queue_name), auto_ack(ack_flag),
callback(std::move(cb))
{
DLOG("新的消费者: %p", this);
}
~Consumer()
{
DLOG("删除消费者: %p", this);
}
};
// 队列消费者管理结构
// 以队列为单元来进行管理,管理所有订阅了该队列的消费者,实现"消费者组"
// 采用轮询调度策略实现消息在组内的负载均衡分发
class QueueConsumer
{
public:
using ptr = std::shared_ptr<QueueConsumer>;
// 构造函数:初始化队列名称和轮转序号
QueueConsumer(const std::string &qname) : _qname(qname), _rr_seq(0) {}
// 队列新增消费者
// 当客户端通过信道成功订阅本队列时调用此函数
// 传进来的参数就是Consumer构造函数所需要的
Consumer::ptr create(const std::string &ctag, // 消费者标识,用于唯一识别一个消费者
const std::string &queue_name, // 消费者订阅的队列名称
bool ack_flag, // 自动确认标志
const ConsumerCallback &cb) // 消息处理回调函数
{
// 1. 加锁,保证线程安全
std::unique_lock<std::mutex> lock(_mutex);
// 2. 判断消费者是否已经存在
for (auto &consumer : _consumers) //_consumers是成员变量------消费者列表
{
if (consumer->tag == ctag) // 如果消费者标识相同
{
return Consumer::ptr(); // 已存在,返回空指针
}
}
// 3. 没有重复则创建消费者对象,也就是构建一个Consumer对象
auto consumer = std::make_shared<Consumer>(ctag, queue_name, ack_flag, cb);
// 4. 将构建的Consumer对象添加到管理列表并返回对象
_consumers.push_back(consumer);
return consumer; // 返回新添加的消费者指针
}
// 队列移除消费者
// 在客户端取消订阅、信道关闭或连接断开时调用
void remove(const std::string &ctag)
{
// 1. 加锁,保证线程安全
std::unique_lock<std::mutex> lock(_mutex);
// 2. 遍历查找目标消费者并删除
for (auto it = _consumers.begin(); it != _consumers.end(); ++it) //_consumers是成员变量------消费者列表
{
if ((*it)->tag == ctag) // 根据消费者唯一标识来寻找消费者
{
_consumers.erase(it); // 将消费者从消费者列表里面删除掉
return;
}
}
return;
}
// 获取消费者:轮询调度(Round-Robin)策略
// 当有消息需要投递到本队列时调用此函数
Consumer::ptr choose()
{
// 1. 加锁,保证线程安全
std::unique_lock<std::mutex> lock(_mutex);
// 检查是否有消费者
if (_consumers.size() == 0) //_consumers是成员变量------消费者列表
{
return Consumer::ptr(); // 没有消费者,返回空指针
}
// 2. 计算当前轮转到的下标
int idx = _rr_seq % _consumers.size(); // 就是取模而已
_rr_seq++; // 更新轮转序号
// 3. 返回对应消费者对象
return _consumers[idx];
}
// 判断队列是否为空(是否有活跃消费者)
bool empty()
{
std::unique_lock<std::mutex> lock(_mutex);
return _consumers.size() == 0;
}
// 判断指定消费者是否存在
bool exists(const std::string &ctag)
{
std::unique_lock<std::mutex> lock(_mutex);
// 遍历查找目标消费者
for (auto it = _consumers.begin(); it != _consumers.end(); ++it)
{
if ((*it)->tag == ctag)
{
return true; // 找到目标消费者
}
}
return false; // 未找到目标消费者
}
// 清理所有消费者
// 通常在队列被删除前调用
void clear()
{
std::unique_lock<std::mutex> lock(_mutex);
_consumers.clear(); //_consumers是成员变量------消费者列表
_rr_seq = 0; // 重置轮转序号
}
private:
std::string _qname; // 队列名称
std::mutex _mutex; // 互斥锁,保证线程安全
uint64_t _rr_seq; // 轮转序号,用于轮询调度
std::vector<Consumer::ptr> _consumers; // 消费者列表
};
// 消费者统一管理器
// 全局管理类,负责协调所有队列的消费者管理工作
// 维护队列名称到队列消费者管理结构的映射
class ConsumerManager
{
public:
using ptr = std::shared_ptr<ConsumerManager>;
ConsumerManager() {}
// 初始化队列消费者管理结构
// 在系统创建新队列时调用
void initQueueConsumer(const std::string &qname)
{
// 1. 加锁,保证线程安全
std::unique_lock<std::mutex> lock(_mutex);
// 2. 检查是否已存在该队列的管理结构
auto it = _qconsumers.find(qname);
if (it != _qconsumers.end())
{
return; // 已存在,直接返回
}
// 3. 创建新的队列消费者管理结构并添加到映射表
auto qconsumers = std::make_shared<QueueConsumer>(qname);
_qconsumers.insert(std::make_pair(qname, qconsumers));
}
// 销毁队列消费者管理结构
// 在队列被删除时调用
void destroyQueueConsumer(const std::string &qname)
{
std::unique_lock<std::mutex> lock(_mutex);
_qconsumers.erase(qname);
}
// 向指定队列新增消费者
// 客户端订阅队列时调用此接口
Consumer::ptr create(const std::string &ctag,
const std::string &queue_name,
bool ack_flag,
const ConsumerCallback &cb)
{
// 获取目标队列的消费者管理对象
QueueConsumer::ptr qcp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _qconsumers.find(queue_name);
if (it == _qconsumers.end())
{
DLOG("没有找到队列 %s 的消费者管理句柄!",
queue_name.c_str());
return Consumer::ptr(); // 队列不存在,返回空指针
}
qcp = it->second; // 找到了对应队列的消费者管理句柄
}
// 调用队列管理对象的创建方法
return qcp->create(ctag, queue_name, ack_flag, cb); // 向指定队列新增消费者
}
// 从指定队列移除消费者
// 客户端取消订阅时调用此接口
void remove(const std::string &ctag, const std::string &queue_name)
{
QueueConsumer::ptr qcp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _qconsumers.find(queue_name);
if (it == _qconsumers.end())
{
DLOG("没有找到队列 %s 的消费者管理句柄!",
queue_name.c_str());
return; // 队列不存在,直接返回
}
qcp = it->second; // 找到了对应队列的消费者管理句柄
}
return qcp->remove(ctag); // 从指定队列移除消费者
}
// 从指定队列获取一个消费者(轮询选择)
// 消息投递流程的关键一环,服务端根据消息的目标队列名调用此接口
Consumer::ptr choose(const std::string &queue_name)
{
QueueConsumer::ptr qcp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _qconsumers.find(queue_name);
if (it == _qconsumers.end())
{
DLOG("没有找到队列 %s 的消费者管理句柄!",
queue_name.c_str());
return Consumer::ptr(); // 队列不存在,返回空指针
}
qcp = it->second; // 找到了对应队列的消费者管理句柄
}
return qcp->choose();
}
// 判断队列是否为空(是否有消费者)
bool empty(const std::string &queue_name)
{
QueueConsumer::ptr qcp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _qconsumers.find(queue_name);
if (it == _qconsumers.end())
{
DLOG("没有找到队列 %s 的消费者管理句柄!",
queue_name.c_str());
return false; // 队列不存在,视为非空
}
qcp = it->second; // 找到了对应队列的消费者管理句柄
}
return qcp->empty();
}
// 判断队列中指定消费者是否存在
bool exists(const std::string &ctag, const std::string &queue_name)
{
QueueConsumer::ptr qcp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _qconsumers.find(queue_name);
if (it == _qconsumers.end())
{
DLOG("没有找到队列 %s 的消费者管理句柄!",
queue_name.c_str());
return false; // 队列不存在,消费者肯定不存在
}
qcp = it->second; // 找到了对应队列的消费者管理句柄
}
return qcp->exists(ctag);
}
// 清理所有消费者
// 在系统关闭或重置时调用
void clear()
{
std::unique_lock<std::mutex> lock(_mutex);
_qconsumers.clear();
}
private:
std::mutex _mutex; // 互斥锁,保护全局映射表
std::unordered_map<std::string, QueueConsumer::ptr> _qconsumers; // 队列到消费者管理结构的映射
};
}
#endif
还是很简单的
三.代码测试

新增,移除消费者测试
cpp
#include "../../mqserver/consumer.hpp"
#include "../../mqcommon/logger.hpp"
#include "../../mqcommon/helper.hpp"
#include<gtest/gtest.h>
void cb(const std::string&tag, const mymq::BasicProperties*bp, const std::string&body)
{
std::cout<<tag<<"消费了消息:"<<body<<std::endl;
}
mymq::QueueConsumer::ptr qc;
class QueueConsumerTest : public ::testing::Test {
protected:
void SetUp() override {
qc = std::make_shared<mymq::QueueConsumer>("test_queue");
}
void TearDown() override {
qc->clear();
}
};
//创建消费者测试
TEST_F(QueueConsumerTest, CreateConsumer) {
// 创建标识为"consumer1"的消费者,订阅"test_queue"队列,开启自动应答,使用cb回调函数
auto consumer = qc->create("consumer1", "test_queue", true, cb);
ASSERT_NE(consumer, nullptr);//返回的消费者指针不为空
EXPECT_EQ(consumer->tag, "consumer1");
EXPECT_EQ(consumer->qname, "test_queue");
EXPECT_TRUE(consumer->auto_ack);
EXPECT_TRUE(consumer->callback);
// 测试重复创建相同标识的消费者
// 尝试再次创建标识为"consumer1"的消费者,应返回空指针表示创建失败
auto duplicate = qc->create("consumer1", "test_queue", false, cb);
EXPECT_EQ(duplicate, nullptr);
}
// 测试用例2:移除消费者功能测试
TEST_F(QueueConsumerTest, RemoveConsumer) {
// 步骤1:创建两个测试消费者
qc->create("consumer1", "test_queue", true, cb);
qc->create("consumer2", "test_queue", false, cb);
// 断言1:验证队列不为空(有两个消费者)
EXPECT_FALSE(qc->empty());
// 断言2:验证consumer1确实存在于队列中
EXPECT_TRUE(qc->exists("consumer1"));
// 步骤2:移除一个存在的消费者(consumer1)
qc->remove("consumer1");
// 断言3:验证consumer1已成功移除
EXPECT_FALSE(qc->exists("consumer1"));
// 断言4:验证consumer2仍然存在
EXPECT_TRUE(qc->exists("consumer2"));
// 步骤3:尝试移除一个不存在的消费者
qc->remove("nonexistent");
// 断言5:验证移除不存在的消费者不会影响现有消费者,consumer2应该还在
EXPECT_TRUE(qc->exists("consumer2"));
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
我们编译运行
cpp
g++ test.cpp ../../mqcommon/msg.pb.cc -lgtest -lprotobuf -lsqlite3 -lpthread

没有问题
消费测试
cpp
#include "../../mqserver/consumer.hpp"
#include "../../mqcommon/logger.hpp"
#include "../../mqcommon/helper.hpp"
#include<gtest/gtest.h>
void cb(const std::string&tag, const mymq::BasicProperties*bp, const std::string&body)
{
std::cout<<tag<<"消费了消息:"<<body<<std::endl;
}
mymq::QueueConsumer::ptr qc;
class QueueConsumerTest : public ::testing::Test {
protected:
void SetUp() override {
qc = std::make_shared<mymq::QueueConsumer>("test_queue");
}
void TearDown() override {
qc->clear();
}
};
// 测试用例:消费者轮询选择功能测试
// 验证队列消费者管理中的轮询(Round-Robin)调度算法
TEST_F(QueueConsumerTest, ChooseConsumer) {
// 步骤1:准备测试数据
// 创建三个不同标识的消费者,用于测试轮询算法
std::vector<std::string> tags = {"consumer1", "consumer2", "consumer3"};
// 遍历标签列表,为每个标签创建消费者
for (const auto& tag : tags) {
qc->create(tag, "test_queue", true, cb);
}
// 步骤2:测试第一轮轮询选择
// 第一次选择应该返回第一个消费者(consumer1)
auto c1 = qc->choose();//返回的是一个消费者对象
// 断言1:验证选择结果不为空
ASSERT_NE(c1, nullptr);
// 断言2:验证选择的是consumer1
EXPECT_EQ(c1->tag, "consumer1");
// 第二次选择应该返回第二个消费者(consumer2)
auto c2 = qc->choose();
ASSERT_NE(c2, nullptr);
EXPECT_EQ(c2->tag, "consumer2");
// 第三次选择应该返回第三个消费者(consumer3)
auto c3 = qc->choose();
ASSERT_NE(c3, nullptr);
EXPECT_EQ(c3->tag, "consumer3");
// 步骤3:测试第二轮轮询(验证循环回到起点)
// 第四次选择应该回到第一个消费者(consumer1)
auto c4 = qc->choose();
ASSERT_NE(c4, nullptr);
EXPECT_EQ(c4->tag, "consumer1");
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
