【消息队列项目】消费者管理模块实现

目录

一.设计

[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();
}
相关推荐
lzhdim2 小时前
C#应用程序取得当前目录和退出
开发语言·数据库·microsoft·c#
努力的小郑2 小时前
MyBatis 两个隐蔽深坑实录:Arrays.asList() 与数字 0 的“离奇失踪”
java·面试·mybatis
故渊ZY2 小时前
SpringMVC核心原理与实战全解析
java·spring
秋邱2 小时前
Java基础语法核心:程序结构、注释规范、变量常量与数据类型
java·开发语言·spring cloud·tomcat·hibernate
故渊ZY2 小时前
SpringBoot与Redis实战:企业级缓存进阶指南
java·spring boot
廋到被风吹走2 小时前
【Spring】核心类研究价值排行榜
java·后端·spring
wanghowie2 小时前
01.05 Java基础篇|I/O、NIO 与序列化实战
java·开发语言·nio
孔明兴汉2 小时前
springboot4 项目从零搭建
java·java-ee·springboot
电商API&Tina2 小时前
跨境电商速卖通(AliExpress)数据采集与 API 接口接入全方案
大数据·开发语言·前端·数据库·人工智能·python