C++ - 仿 RabbitMQ 实现消息队列--客户端模块实现

目录

订阅者模块

信道管理模块

异步工作线程实现

连接管理模块


在 RabbitMQ 中,提供服务的是信道,因此在客户端的实现中,弱化了 Client 客户端的概念,也就是说在 RabbitMQ 中并不会向用户展示网络通信的概念出来,而是以一种提供服务的形式来体现。

其实现思想类似于普通的功能接口封装,一个接口实现一个功能,接口内部完成向客户端请求的过程,但是对外并不需要体现出客户端与服务端通信的概念,用户需要什么服务就调用什么接口就行。

基于以上的思想,客户端的实现共分为四大模块:

  • 订阅者模块
    • 一个并不直接对用户展示的模块,其在客户端体现的作用就是对于角色的描述,表示这是一个消费者。
  • 信道模块
    • 一个直接面向用户的模块,内部包含多个向外提供的服务接口,用户需要什么服务,调用对应接口即可。
    • 其包含交换机声明/删除,队列声明/删除,绑定/解绑,消息发布/确认,订阅/解除订阅等服务。
  • 连接模块
    • 这是唯一能体现出网络通信概念的一个模块了,它向用户提供的功能就是用于打开/关闭信道。
  • 异步线程模块
    • 虽然客户端部分,并不对外体现网络通信的概念,但是本质上内部还是包含有网络通信的,因此既然有网络通信,那么就必须包含有一个网络通信 IO 事件监控线程模块,用于进行客户端连接的 IO 事件监控,以便于在事件出发后进行 IO操作。
    • 其次,在客户端部分存在一个情况就是,当一个信道作为消费者而存在的时候,服务端会向信道推送消息,而用户这边需要对收到的消息进行不同的业务处理,而这个消息的处理需要一个异步的工作线程池来完成。
    • 因此异步线程模块包含两个部分:▪ 客户端连接的 IO 事件监控线程▪ 推送过来的消息异步处理线程。

基于以上模块,实现一个客户端的流程也就比较简单了

  1. 实例化异步线程对象
  2. 实例化连接对象
  3. 通过连接对象,创建信道
  4. 根据信道获取自己所需服务
  5. 关闭信道
  6. 关闭连接

订阅者模块

与服务端,并无太大差别,客户端这边虽然订阅者的存在感微弱了很多,但是还是有的,当进行队列消息订阅的时候,会伴随着一个订阅者对象的创建,而这个订阅者对象有以下几个作用:

  • 描述当前信道订阅了哪个队列的消息。
  • 描述了收到消息后该如何对这条消息进行处理。
  • 描述收到消息后是否需要进行确认回复。

订阅者信息:

  • 订阅者标识
  • 订阅队列名
  • 是否自动确认标志
  • 回调处理函数(收到消息后该如何处理的回调函数对象)
cpp 复制代码
#pragma once
#include "../mqcommon/helper.hpp"
#include "../mqcommon/log.hpp"
#include "../mqcommon/msg.pb.h"
#include <unordered_map>
#include <memory>
#include <functional>

namespace jiuqi
{
    // 消费者表示, 属性, 消息
    using ConsumerCallback = std::function<void(const std::string&, const BasicProperties*, const std::string&)>;
    struct Consumer
    {
        using ptr = std::shared_ptr<Consumer>;
        std::string tag;           // 消费者标识
        std::string qname;         // 绑定的队列名称
        bool auto_ack;             // 是否自动应答
        ConsumerCallback callback; // 回调函数

        Consumer() { DEBUG("new Consumer %p", this); }
        Consumer(const std::string &ctag, const std::string &queue_name, bool ack, const ConsumerCallback &cb)
            : tag(ctag), qname(queue_name), auto_ack(ack), callback(cb) { DEBUG("new Consumer %p", this); }
        ~Consumer() { DEBUG("del Consumer %p", this); } 
    };
}

信道管理模块

同样的,客户端也有信道,其功能与服务端几乎一致,或者说不管是客户端的channel 还是服务端的 channel 都是为了用户提供具体服务而存在的,只不过服务端是为客户端的对应请求提供服务,而客户端的接口服务是为了用户具体需要服务,也可以理解是用户通过客户端 channel 的接口调用来向服务端发送对应请求,获取请求的服务。

  • 信道信息:
    • 信道 ID
    • 信道关联的网络通信连接对象
    • protobuf 协议处理对象
    • 信道关联的消费者
    • 请求对应的响应信息队列(这里队列使用<请求 ID,响应>hash 表,以便于查找指定的响应)
    • 互斥锁&条件变量(大部分的请求都是阻塞操作,发送请求后需要等到响应才能继续,但是 muduo 库的通信是异步的,因此需要我们自己在收到响应后,通过判断是否是等待的指定响应来进行同步)
    1. 信道操作:
    • 提供创建信道操作
    • 提供删除信道操作
    • 提供声明交换机操作(强断言-有则 OK,没有则创建)
    • 提供删除交换机
    • 提供创建队列操作(强断言-有则 OK,没有则创建)
    • 提供删除队列操作
    • 提供交换机-队列绑定操作
    • 提供交换机-队列解除绑定操作
    • 提供添加订阅操作
    • 提供取消订阅操作
    • 提供发布消息操作
    • 提供确认消息操作
  • 信道管理:
    • 创建信道
    • 查询信道
    • 删除信道
cpp 复制代码
#pragma once
#include "muduo/net/TcpConnection.h"
#include "muduo/proto/codec.h"
#include "muduo/proto/dispatcher.h"
#include "../mqcommon/helper.hpp"
#include "../mqcommon/log.hpp"
#include "../mqcommon/msg.pb.h"
#include "../mqcommon/proto.pb.h"
#include "../mqcommon/threadpool.hpp"
#include "consumer.hpp"
#include <unordered_map>
#include <condition_variable>
#include <memory>

namespace jiuqi
{
    using ProtobufCodecPtr = std::shared_ptr<ProtobufCodec>;
    using basicConsumeResponsePtr = std::shared_ptr<basicConsumeResponse>;
    using basicCommonResponsePtr = std::shared_ptr<basicCommonResponse>;
    class Channel
    {
    public:
        using ptr = std::shared_ptr<Channel>;
        Channel(const muduo::net::TcpConnectionPtr &conn, const ProtobufCodecPtr &codec)
            : _cid(UUIDHelper::uuid()), _conn(conn), _codec(codec), _consumer(nullptr) {}
        ~Channel()
        {
            basicCancel();
        }

        std::string cid() { return _cid; }

        bool openChannel()
        {
            openChannelRequest req;
            std::string rid = UUIDHelper::uuid();
            req.set_rid(rid);
            req.set_cid(_cid);
            _codec->send(_conn, req);
            basicCommonResponsePtr resp = waitResponse(rid);
            return resp->ok();
        }

        void closeChannel()
        {
            closeChannelRequest req;
            std::string rid = UUIDHelper::uuid();
            req.set_rid(rid);
            req.set_cid(_cid);
            _codec->send(_conn, req);
            basicCommonResponsePtr resp = waitResponse(rid);
        }

        bool declareExchange(const std::string &ename,
                             ExchangeType etype,
                             bool edurable,
                             bool eauto_delete,
                             google::protobuf::Map<std::string, std::string> &eargs)
        {
            // 构造请求对象
            declareExchangeRequest req;
            std::string rid = UUIDHelper::uuid();
            req.set_rid(rid);
            req.set_cid(_cid);
            req.set_ename(ename);
            req.set_etype(etype);
            req.set_durable(edurable);
            req.set_auto_delete(eauto_delete);
            req.mutable_args()->swap(eargs);
            // 向服务器发送请求
            _codec->send(_conn, req);
            // 等待相应
            basicCommonResponsePtr resp = waitResponse(rid);
            // 返回
            return resp->ok();
        }
        void deleteExchange(const std::string &ename)
        {
            deleteExchangeRequest req;
            std::string rid = UUIDHelper::uuid();
            req.set_rid(rid);
            req.set_cid(_cid);
            req.set_ename(ename);
            _codec->send(_conn, req);
            waitResponse(rid);
        }

        bool declareQueue(const std::string &qname,
                          bool qdurable,
                          bool qexclusive,
                          bool qauto_delete,
                          google::protobuf::Map<std::string, std::string> qargs)
        {
            declareQueueRequest req;
            std::string rid = UUIDHelper::uuid();
            req.set_rid(rid);
            req.set_cid(_cid);
            req.set_qname(qname);
            req.set_durable(qdurable);
            req.set_auto_delete(qauto_delete);
            req.set_exclusive(qexclusive);
            req.mutable_args()->swap(qargs);
            _codec->send(_conn, req);
            basicCommonResponsePtr resp = waitResponse(rid);
            return resp->ok();
        }
        void deleteQueue(const std::string &qname)
        {
            deleteQueueRequest req;
            std::string rid = UUIDHelper::uuid();
            req.set_rid(rid);
            req.set_cid(_cid);
            req.set_qname(qname);
            _codec->send(_conn, req);
            waitResponse(rid);
        }

        bool queueBind(const std::string &ename,
                       const std::string &qname,
                       const std::string &key)
        {
            queueBindRequest req;
            std::string rid = UUIDHelper::uuid();
            req.set_rid(rid);
            req.set_cid(_cid);
            req.set_qname(qname);
            req.set_ename(ename);
            req.set_bindingkey(key);
            _codec->send(_conn, req);
            basicCommonResponsePtr resp = waitResponse(rid);
            return resp->ok();
        }
        void queueUnbind(const std::string &ename, const std::string &qname)
        {
            queueUnbindRequest req;
            std::string rid = UUIDHelper::uuid();
            req.set_rid(rid);
            req.set_cid(_cid);
            req.set_ename(ename);
            req.set_qname(qname);
            _codec->send(_conn, req);
            waitResponse(rid);
        }

        void basicPublish(const std::string &ename,
                          const BasicProperties *bp,
                          const std::string &body)
        {
            basicPublishRequest req;
            std::string rid = UUIDHelper::uuid();
            req.set_rid(rid);
            req.set_cid(_cid);
            req.set_ename(ename);
            req.set_body(body);
            if (bp != nullptr)
            {
                req.mutable_properties()->set_id(bp->id());
                req.mutable_properties()->set_deliver_mode(bp->deliver_mode());
                req.mutable_properties()->set_routing_key(bp->routing_key());
            }
            _codec->send(_conn, req);
            waitResponse(rid);
        }

        void basicAck(const std::string &msgid)
        {
            if (_consumer == nullptr)
            {
                DEBUG("消息确认时没有找到消费者");
                return;
            }
            basicAckRequest req;
            std::string rid = UUIDHelper::uuid();
            req.set_rid(rid);
            req.set_cid(_cid);
            req.set_qname(_consumer->qname);
            req.set_mid(msgid);
            _codec->send(_conn, req);
            waitResponse(rid);
        }

        void basicCancel()
        {
            if (_consumer == nullptr)
            {
                DEBUG("取消订阅时没有找到消费者");
                return;
            }
            basicCancelRequest req;
            std::string rid = UUIDHelper::uuid();
            req.set_rid(rid);
            req.set_cid(_cid);
            req.set_qname(_consumer->qname);
            req.set_ctag(_consumer->tag);
            _codec->send(_conn, req);
            waitResponse(rid);
            _consumer = nullptr;
        }

        bool basicConsumer(const std::string &consumer_tag,
                           const std::string &qname,
                           bool auto_ack,
                           const ConsumerCallback &cb)
        {
            if (_consumer != nullptr)
            {
                DEBUG("当前信道已订阅其他信道");
                return false;
            }
            basicConsumeRequest req;
            std::string rid = UUIDHelper::uuid();
            req.set_rid(rid);
            req.set_cid(_cid);
            req.set_ctag(consumer_tag);
            req.set_qname(qname);
            req.set_auto_ack(auto_ack);
            _codec->send(_conn, req);
            basicCommonResponsePtr resp = waitResponse(rid);
            if (resp->ok() == false)
            {
                DEBUG("添加订阅失败");
                return false;
            }
            _consumer = std::make_shared<Consumer>(consumer_tag, qname, auto_ack, cb);
            return true;
        }

    public:
        // 连接收到基础响应后, 向hash中添加
        void putBasicResponse(const basicCommonResponsePtr &resp)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            _basic_resp.insert(std::make_pair(resp->rid(), resp));
            _cv.notify_all();
        }
        // 连接收到消息推送后, 需要通过信道找到对应的消费者对象, 通过回调函数处理数据
        void consume(const basicConsumeResponsePtr &resp)
        {
            if (_consumer == nullptr)
            {
                DEBUG("消息处理时, 未找到订阅者信息");
                return;
            }
            if (resp->ctag() != _consumer->tag)
            {
                DEBUG("收到的推送消息中的消费者标识与当前信道的消费者标识不一致: %s %s", resp->ctag().c_str(), _consumer->tag.c_str());
                return;
            }
            _consumer->callback(resp->ctag(), resp->mutable_properties(), resp->body());
        }

    private:
        basicCommonResponsePtr waitResponse(const std::string &rid)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            _cv.wait(lock, [&rid, this]()
                     { return _basic_resp.find(rid) != _basic_resp.end(); });
            basicCommonResponsePtr basic_resp = _basic_resp[rid];
            _basic_resp.erase(rid);
            return basic_resp;
        }

    private:
        std::string _cid;
        muduo::net::TcpConnectionPtr _conn;
        ProtobufCodecPtr _codec;
        Consumer::ptr _consumer;
        std::mutex _mutex;
        std::condition_variable _cv;
        std::unordered_map<std::string, basicCommonResponsePtr> _basic_resp;
    };

    class ChannelManager
    {
    public:
        using ptr = std::shared_ptr<ChannelManager>;
        ChannelManager() {}

        Channel::ptr create(const muduo::net::TcpConnectionPtr &conn,
                            const ProtobufCodecPtr &codec)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            auto channel = std::make_shared<Channel>(conn, codec);
            _channels.insert(std::make_pair(channel->cid(), channel));
            return channel;
        }

        void remove(const std::string &cid)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            _channels.erase(cid);
        }

        Channel::ptr get(const std::string &cid)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            auto it = _channels.find(cid);
            if (it == _channels.end())
                return nullptr;
            return it->second;
        }

    private:
        std::mutex _mutex;
        std::unordered_map<std::string, Channel::ptr> _channels;
    };
}

异步工作线程实现

客户端这边存在两个异步工作线程:

  1. 一个是 muduo 库中客户端连接的异步循环线程 EventLoopThread,
  2. 一个是当收到消息后进行异步处理的工作线程池。

这两项都不是以连接为单元进行创建的,而是创建后,可以用以多个连接中,因此单独进行封装。

cpp 复制代码
#pragma once
#include "muduo/net/EventLoopThread.h"
#include "../mqcommon/helper.hpp"
#include "../mqcommon/log.hpp"
#include "../mqcommon/msg.pb.h"
#include "../mqcommon/proto.pb.h"
#include "../mqcommon/threadpool.hpp"

namespace jiuqi
{
    class AsyncWorker
    {
    public:
        using ptr = std::shared_ptr<AsyncWorker>;
        muduo::net::EventLoopThread loopthread;
        ThreadPool pool;
    };
}

连接管理模块

在客户端这边,RabbitMQ 弱化了客户端的概念,因为用户所需的服务都是通过信道来提供的,因此操作思想转换为先创建连接,通过连接创建信道,通过信道提供服务这一流程。

这个模块同样是针对 muduo 库客户端连接的二次封装,向用户提供创建 channel 信道的接口,创建信道后,可以通过信道来获取指定服务。

cpp 复制代码
#pragma once

#include "muduo/proto/dispatcher.h"
#include "muduo/proto/codec.h"

#include "muduo/base/Logging.h"
#include "muduo/base/Mutex.h"
#include "muduo/net/TcpClient.h"
#include "muduo/net/EventLoopThread.h"
#include "muduo/base/CountDownLatch.h"

#include "channel.hpp"
#include "worker.hpp"

namespace jiuqi
{
    class Connection
    {
    public:
        using ptr = std::shared_ptr<Connection>;
        using MessagePtr = std::shared_ptr<google::protobuf::Message>;

        Connection(const std::string ip, int port, AsyncWorker::ptr &worker)
            : _latch(1),
              _client(worker->loopthread.startLoop(), muduo::net::InetAddress(ip, port), "connection"),
              _dispatcher(std::bind(&Connection::onUnknownMessage, this,
                                    std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)),
              _codec(std::make_shared<ProtobufCodec>(std::bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher,
                                                               std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))),
              _worker(worker), _channel_manager(std::make_shared<ChannelManager>())
        {
            // 注册请求处理函数
            _dispatcher.registerMessageCallback<basicCommonResponse>(std::bind(&Connection::basicResponse, this,
                                                                               std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
            _dispatcher.registerMessageCallback<basicConsumeResponse>(std::bind(&Connection::consumeResponse, this,
                                                                                std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));

            _client.setMessageCallback(std::bind(&ProtobufCodec::onMessage, _codec.get(),
                                                 std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
            _client.setConnectionCallback(std::bind(&Connection::onConnection, this, std::placeholders::_1));
            _client.connect();
            _latch.wait(); // 阻塞等待
        }

        Channel::ptr openChannel()
        {
            Channel::ptr channel = _channel_manager->create(_conn, _codec);
            bool ret = channel->openChannel();
            if (ret == false)
            {
                DEBUG("打开信道失败");
                return nullptr;
            }
            return channel;
        }

        void closeChannel(const Channel::ptr &channel)
        {
            channel->closeChannel();
            _channel_manager->remove(channel->cid());
        }

    private:
        void basicResponse(const muduo::net::TcpConnectionPtr &conn, const basicCommonResponsePtr &message, muduo::Timestamp)
        {
            Channel::ptr channel = _channel_manager->get(message->cid());
            if (channel == nullptr)
            {
                DEBUG("未找到信道");
                return;
            }
            channel->putBasicResponse(message);
        }

        void consumeResponse(const muduo::net::TcpConnectionPtr &conn, const basicConsumeResponsePtr &message, muduo::Timestamp)
        {
            Channel::ptr channel = _channel_manager->get(message->cid());
            if (channel == nullptr)
            {
                DEBUG("未找到信道");
                return;
            }
            _worker->pool.push([channel, message]()
                               { channel->consume(message); });
        }

        void onUnknownMessage(const muduo::net::TcpConnectionPtr &,
                              const MessagePtr &message,
                              muduo::Timestamp)
        {
            LOG_INFO << "onUnknownMessage: " << message->GetTypeName();
        }

        void onConnection(const muduo::net::TcpConnectionPtr &conn)
        {
            if (conn->connected())
            {
                _conn = conn;
                std::cout << "连接建立成功, 准备countDown" << std::endl;
                _latch.countDown();
                std::cout << "countDown完成" << std::endl;
            }
            else
            {
                std::cout << "连接关闭" << std::endl;
            }
        }

    private:
        muduo::CountDownLatch _latch; // 实现同步的
        AsyncWorker::ptr _worker;
        muduo::net::TcpClient _client;
        ProtobufDispatcher _dispatcher;     // 请求分发器
        ProtobufCodecPtr _codec;            // 协议处理器
        muduo::net::TcpConnectionPtr _conn; // 客户端对应连接
        ChannelManager::ptr _channel_manager;
    };
}
相关推荐
Pitayafruit3 小时前
【📕分布式锁通关指南 12】源码剖析redisson如何利用Redis数据结构实现Semaphore和CountDownLatch
redis·分布式·后端
Code季风4 小时前
分布式系统中的幂等性设计:从理论到实现的全面指南
redis·分布式·微服务
dessler4 小时前
RabbitMQ-日常运维命令
linux·运维·rabbitmq
都叫我大帅哥6 小时前
RabbitMQ惰性队列:拯救内存的“树懒”战士 🦥
java·rabbitmq
右手嘚温暖12 小时前
分布式事务Seata、LCN的原理深度剖析
spring boot·分布式·后端·spring·spring cloud·中间件·架构
vision_wei_16 小时前
Redis中间件(二):Redis协议与异步方式
网络·数据库·c++·redis·分布式·缓存·中间件
爱学习的小熊猫_17 小时前
在Linux上部署RabbitMQ、Redis、ElasticSearch
linux·redis·elasticsearch·中间件·rabbitmq
风与尘18 小时前
RabbitMQ延时队列的两种实现方式
spring boot·分布式·中间件·rabbitmq
Keya20 小时前
鸿蒙开发样式复用:@Styles、@Extend与AttributeModifier深度对比
前端·分布式·harmonyos