信道管理模块

1. 信道管理

在 AMQP 模型中,除了通信连接 Connection 概念外,还有一个 Channel 的概念,Channel 是针对 Connection 连接的一个更细粒度的通信信道,多个 Channel 可以使用同一个通信连接 Connection 进行通信,但是同一个 Connection 的 Channel 之间相互独立。

信道本身不直接进行网络通信,而是通过其所属的Connection进行网络通信。 更具体地说:

  • 当我们通过Channel发送一个AMQP方法(比如声明队列、发布消息等)时,实际上是将该方法封装成一个AMQP帧,然后通过Connection的TCP连接发送出去。
  • 同样,当服务器通过Channel向客户端发送数据(比如消息推送)时,也是将数据封装成AMQP帧,然后通过同一个TCP连接发送,并在帧头中指定信道ID。

而信道模块就是再次将之前的模块进行整合提供服务的模块

  1. 管理信息:
  • 信道 ID:信道的唯一标识
  • 信道关联的消费者:用于消费者信道在关闭的时候取消订阅,删除订阅者信息
  • 信道关联的连接:用于向客户端发送数据(响应,推送的消息)
  • protobuf 协议处理句柄:网络通信前的协议处理
  • 消费者管理句柄:信道关闭/取消订阅的时候,通过句柄删除订阅者信息
  • 虚拟机句柄:交换机/队列/绑定/消息数据管理
  • 工作线程池句柄(一条消息被发布到队列后,需要将消息推送给订阅了对应队列的消费者,过程由线程池完成)
  1. 管理操作:
  • 提供声明&删除交换机操作(删除交换机的同时删除交换机关联的绑定信息)
  • 提供声明&删除队列操作(删除队列的同时,删除队列关联的绑定信息,消息,消费者信息)
  • 提供绑定&解绑队列操作
  • 提供订阅&取消订阅队列消息操作
  • 提供发布&确认消息操作
  1. 信道管理
  • 信道的增删查。

在AMQP中,信道(Channel)是逻辑上的通信通道,所有的AMQP协议操作(如声明队列、发布消息等)都是在信道上进行的。为了使得信道管理模块能够处理各种操作,需要定义相应的请求和响应参数


2. 网络通信协议设计

2.1 设计应用层协议

使用二进制的方式设计应用层协议。 因为 MQMessage 的消息体是使用 Protobuf 进行序列化的,本身是按照二进制存储的,所以不太适合用 json 等文本格式来定义协议。

下面我们设计一下应用层协议:请求/响应报文设计

  • len:4 个字节, 表示整个报文的长度
  • nameLen: 4 个字节, 表示 typeName 数组的长度
  • typeName:是个字节数组, 占 nameLen 个字节, 表示请求/响应报文的类型名,作用是分发不同消息到对应的远端接口调用中
  • protobufData:是个字节数组, 占 len - nameLen - 8 个字节, 表示请求/响应参数数据通过 protobuf 序列化之后的二进制
  • checkSum:4 个字节, 表示整个消息的校验和, 作用是为了校验请求/响应报文的完整性

2.2 定义请求/响应参数

因为这里的参数需要进行网络传输以及序列化, 所以我们需要将参数定义在 pb 文件中,

  1. channel 创建与关闭参数
protobuf 复制代码
syntax = "proto3";

package rabbitmq;

import "msg.proto";

//信道的打开与关闭
message openChannelRequest{
    string rid = 1; // 唯一请求/响应 id, 用来将请求和响应对上
    string cid = 2; // 标识唯一 channel
};
message closeChannelRequest{
    string rid = 1;
    string cid = 2;
};
  1. exchange 声明与删除参数
protobuf 复制代码
//交换机的声明与删除
message declareExchangeRequest{
    string rid = 1;
    string cid = 2;
    string exchange_name = 3;
    ExchangeType exchange_type = 4;
    bool durable = 5;
    bool auto_delete = 6;
    map<string, string> args = 7;
};
message deleteExchangeRequest{
    string rid = 1;
    string cid = 2;
    string exchange_name = 3;
};

一个创建交换机的请求, 如下图示:

按照 len - nameLen - 8 的长度读取出 protobufData 就可以将读到的二进制数据反序列化成 ExchangeDeclareArguments 对象进行后续处理。后续的请求报文和这里都是类似的。

  1. 其他参数
protobuf 复制代码
//队列的声明与删除
message declareQueueRequest{
    string rid = 1;
    string cid = 2;
    string queue_name = 3;
    bool exclusive = 4;
    bool durable = 5;
    bool auto_delete = 6;
    map<string, string> args = 7;
};
message deleteQueueRequest{
    string rid = 1;
    string cid = 2;
    string queue_name = 3;
};
//队列的绑定与解除绑定
message queueBindRequest{
    string rid = 1;
    string cid = 2;
    string exchange_name = 3;
    string queue_name = 4;
    string binding_key = 5;
};
message queueUnBindRequest{
    string rid = 1;
    string cid = 2;
    string exchange_name = 3;
    string queue_name = 4;
};
//消息的发布
message basicPublishRequest {
    string rid = 1;
    string cid = 2;
    string exchange_name = 3;
    string body = 4;
    BasicProperties properties = 5;
};
//消息的确认
message basicAckRequest {
    string rid = 1;
    string cid = 2;
    string queue_name = 3;
    string message_id = 4;
};
//队列的订阅
message basicConsumeRequest {
    string rid = 1;
    string cid = 2;
    string consumer_tag  =3;
    string queue_name = 4;
    bool auto_ack = 5;
};
//订阅的取消
message basicCancelRequest {
    string rid = 1;
    string cid = 2;
    string consumer_tag = 3;
    string queue_name = 4;
};
//消息的推送
message basicConsumeResponse {
    string cid = 1;
    string consumer_tag = 2;
    string body = 3;
    BasicProperties properties = 4;
};
//通用响应
message basicCommonResponse {
    string rid = 1;
    string cid = 2;
    bool ok = 3;
}

对该.proto文件进行编译生成相关文件


3. 封装信道管理类

还记得我们在介绍muduo库时,写了几个示例来熟悉muduo库的使用

在当时我们就针对不同的请求定义了智能指针类型,在回调函数中作为参数来使用,这里我们也这样来做

cpp 复制代码
#ifndef __M_CHANNEL_H__
#define __M_CHANNEL_H__
#include "muduo/net/TcpConnection.h"
#include "muduo/proto/codec.h"
#include "muduo/proto/dispatcher.h"
#include "../mqcommon/logger.hpp"
#include "../mqcommon/helper.hpp"
#include "../mqcommon/msg.pb.h"
#include "../mqcommon/proto.pb.h"
#include "../mqcommon/threadpool.hpp"
#include "consumer.hpp"
#include "host.hpp"
#include "route.hpp"


namespace rabbitmq
{
    using ProtobufCodecPtr = std::shared_ptr<ProtobufCodec>;
    using openChannelRequestPtr = std::shared_ptr<openChannelRequest>;
    using closeChannelRequestPtr = std::shared_ptr<closeChannelRequest>;
    using declareExchangeRequestPtr = std::shared_ptr<declareExchangeRequest>;
    using deleteExchangeRequestPtr = std::shared_ptr<deleteExchangeRequest>;
    using declareQueueRequestPtr = std::shared_ptr<declareQueueRequest>;
    using deleteQueueRequestPtr = std::shared_ptr<deleteQueueRequest>;
    using queueBindRequestPtr = std::shared_ptr<queueBindRequest>;
    using queueUnBindRequestPtr = std::shared_ptr<queueUnBindRequest>;
    using basicPublishRequestPtr = std::shared_ptr<basicPublishRequest>;
    using basicAckRequestPtr = std::shared_ptr<basicAckRequest>;
    using basicConsumeRequestPtr = std::shared_ptr<basicConsumeRequest>;
    using basicCancelRequestPtr = std::shared_ptr<basicCancelRequest>;
    class Channel
    {
    public:
        using ptr = std::shared_ptr<Channel>;
        Channel(const std::string &id, 
                const VirtualHost::ptr &host, 
                const ConsumerManager::ptr &cmp, 
                const ProtobufCodecPtr &codec, 
                const muduo::net::TcpConnectionPtr &conn,
                const threadpool::ptr &pool)
                :_cid(id),_conn(conn),_codec(codec),_cmp(cmp),_host(host),_pool(pool)
        {
            DLOG("new Channel: %p", this);
        }
        ~Channel() 
        {
            if (_consumer.get() != nullptr) 
            {
                _cmp->remove(_consumer->_tag, _consumer->_qname);
            }
            DLOG("del Channel: %p", this);
        }
        //交换机的声明与删除
        void declareExchange(const declareExchangeRequestPtr &req);
        void deleteExchange(const deleteExchangeRequestPtr &req);
        //队列的声明与删除
        void declareQueue(const declareQueueRequestPtr &req);
        void deleteQueue(const deleteQueueRequestPtr &req);
        //队列的绑定与解除绑定
        void queueBind(const queueBindRequestPtr &req);
        void queueUnBind(const queueUnBindRequestPtr &req);
        //消息的发布
        void basicPublish(const basicPublishRequestPtr &req);
        //消息的确认
        void basicAck(const basicAckRequestPtr &req);
        //订阅队列消息
        void basicConsume(const basicConsumeRequestPtr &req);
        //取消订阅
        void basicCancel(const basicCancelRequestPtr &req);
    private:
        std::string _cid;
        Consumer::ptr _consumer;
        muduo::net::TcpConnectionPtr _conn;
        ProtobufCodecPtr _codec;
        ConsumerManager::ptr _cmp;
        VirtualHost::ptr _host;
        threadpool::ptr _pool;
    };
}

#endif

信道管理类的框架已经完成,下面我们就来将这些接口一一完成

完整代码如下:

cpp 复制代码
#ifndef __M_CHANNEL_H__
#define __M_CHANNEL_H__
#include "muduo/net/TcpConnection.h"
#include "muduo/proto/codec.h"
#include "muduo/proto/dispatcher.h"
#include "../mqcommon/logger.hpp"
#include "../mqcommon/helper.hpp"
#include "../mqcommon/msg.pb.h"
#include "../mqcommon/proto.pb.h"
#include "../mqcommon/threadpool.hpp"
#include "consumer.hpp"
#include "host.hpp"
#include "route.hpp"


namespace rabbitmq
{
    using ProtobufCodecPtr = std::shared_ptr<ProtobufCodec>;
    using openChannelRequestPtr = std::shared_ptr<openChannelRequest>;
    using closeChannelRequestPtr = std::shared_ptr<closeChannelRequest>;
    using declareExchangeRequestPtr = std::shared_ptr<declareExchangeRequest>;
    using deleteExchangeRequestPtr = std::shared_ptr<deleteExchangeRequest>;
    using declareQueueRequestPtr = std::shared_ptr<declareQueueRequest>;
    using deleteQueueRequestPtr = std::shared_ptr<deleteQueueRequest>;
    using queueBindRequestPtr = std::shared_ptr<queueBindRequest>;
    using queueUnBindRequestPtr = std::shared_ptr<queueUnBindRequest>;
    using basicPublishRequestPtr = std::shared_ptr<basicPublishRequest>;
    using basicAckRequestPtr = std::shared_ptr<basicAckRequest>;
    using basicConsumeRequestPtr = std::shared_ptr<basicConsumeRequest>;
    using basicCancelRequestPtr = std::shared_ptr<basicCancelRequest>;
    class Channel
    {
    public:
        using ptr = std::shared_ptr<Channel>;
        Channel(const std::string &id, 
                const VirtualHost::ptr &host, 
                const ConsumerManager::ptr &cmp, 
                const ProtobufCodecPtr &codec, 
                const muduo::net::TcpConnectionPtr &conn,
                const threadpool::ptr &pool)
                :_cid(id),_conn(conn),_codec(codec),_cmp(cmp),_host(host),_pool(pool)
        {
            DLOG("new Channel: %p", this);
        }
        ~Channel() 
        {
            if (_consumer.get() != nullptr) 
            {
                _cmp->remove(_consumer->_tag, _consumer->_qname);
            }
            DLOG("del Channel: %p", this);
        }
        //交换机的声明与删除
        void declareExchange(const declareExchangeRequestPtr &req)
        {
            bool ret = _host->declareExchange(req->exchange_name(), 
                req->exchange_type(), req->durable(), 
                req->auto_delete(), req->args());
            basicResponse(ret, req->rid(), req->cid());
        }
        void deleteExchange(const deleteExchangeRequestPtr &req)
        {
            _host->deleteExchange(req->exchange_name());
            basicResponse(true, req->rid(), req->cid());
        }
        //队列的声明与删除
        void declareQueue(const declareQueueRequestPtr &req)
        {
            bool ret = _host->declareQueue(req->queue_name(),
                req->durable(), req->exclusive(),
                req->auto_delete(), req->args());
            if (ret == false) 
            {
                basicResponse(false, req->rid(), req->cid());
                return;
            }
            _cmp->initQueueConsumer(req->queue_name());//初始化队列的消费者管理句柄
            basicResponse(true, req->rid(), req->cid());
        }
        void deleteQueue(const deleteQueueRequestPtr &req)
        {
            _cmp->destroyQueueConsumer(req->queue_name());
            _host->deleteQueue(req->queue_name());
            basicResponse(true, req->rid(), req->cid());
        }
        //队列的绑定与解除绑定
        void queueBind(const queueBindRequestPtr &req)
        {
            bool ret = _host->bind(req->exchange_name(), 
                req->queue_name(), req->binding_key());
            basicResponse(ret, req->rid(), req->cid());
        }
        void queueUnBind(const queueUnBindRequestPtr &req)
        {
            _host->unBind(req->exchange_name(), req->queue_name());
            basicResponse(true, req->rid(), req->cid());
        }
        //消息的发布
        void basicPublish(const basicPublishRequestPtr &req)
        {
            //1. 获取交换机对象
            auto ep = _host->selectExchange(req->exchange_name());
            if(ep.get() == nullptr)
            {
                basicResponse(false, req->rid(), req->cid());
                return;
            }
            //2. 进行交换路由,判断消息可以发布到交换机绑定的哪个队列中
            MsgQueueBindingMap mqbmap = _host->exchangeBindings(req->exchange_name());
            BasicProperties* properties = nullptr;
            std::string routing_key;
            if(req->has_properties())
            {
                properties = req->mutable_properties();
                routing_key = properties->routing_key();
            }

            for(auto& binding : mqbmap)
            {
                if(Router::route(ep->_type, routing_key, binding.second->_binding_key))
                {
                    //3. 将消息添加到队列中(添加消息的管理)
                    _host->basicPublish(binding.first, properties, req->body());
                    //4. 向线程池中添加一个消息消费任务(向指定队列的订阅者去推送消息--线程池完成)
                    auto task = std::bind(&Channel::consume, this, binding.first);
                    _pool->push(task);
                }
            }
            basicResponse(true, req->rid(), req->cid());
        }
        //消息的确认
        void basicAck(const basicAckRequestPtr &req)
        {
            _host->basicAck(req->queue_name(), req->message_id());
            basicResponse(true, req->rid(), req->cid());
        }
        //订阅队列消息
        void basicConsume(const basicConsumeRequestPtr &req)
        {
            //1. 判断队列是否存在
            bool ret = _host->existsQueue(req->queue_name());
            if(!ret)
            {
                basicResponse(false, req->rid(), req->cid());
                return;
            }
            //2. 创建队列的消费者
            auto cb = std::bind(&Channel::callback, this, std::placeholders::_1, 
                std::placeholders::_2, std::placeholders::_3);
            //创建了消费者之后,当前的channel角色就是个消费者
            _consumer = _cmp->create(req->consumer_tag(), req->queue_name(), req->auto_ack(), cb);
            basicResponse(true, req->rid(), req->cid());
        }
        //取消订阅
        void basicCancel(const basicCancelRequestPtr &req)
        {
            // 取消订阅意味着不再需要消费者帮客户端从队列中消费消息
            // 直接将消费者删除
            _cmp->remove(req->consumer_tag(), req->queue_name());
            basicResponse(true, req->rid(), req->cid()); 
        }
    private:
        void callback(const std::string tag, const BasicProperties *bp, const std::string &body)
        {
            //针对参数组织出推送消息请求,将消息推送给channel对应的客户端
            basicConsumeResponse resp;
            resp.set_cid(_cid);
            resp.set_consumer_tag(tag);
            resp.set_body(body);
            if(bp)
            {
                resp.mutable_properties()->set_id(bp->id());
                resp.mutable_properties()->set_delivery_mode(bp->delivery_mode());
                resp.mutable_properties()->set_routing_key(bp->routing_key());
            }
            _codec->send(_conn, resp);
        }
        void consume(const std::string &qname) 
        {
            //指定队列消费消息
            //1. 从队列中取出一条消息
            MessagePtr msg = _host->basicConsume(qname);
            if(msg.get() == nullptr)
            {
                DLOG("执行消费任务失败,%s 队列没有消息!", qname.c_str());
                return;
            }
            //2. 从队列订阅者中取出一个订阅者
            Consumer::ptr consumer = _cmp->choose(qname);
            if(consumer.get() == nullptr)
            {
                DLOG("执行消费任务失败,%s 队列没有消费者!", qname.c_str());
                return;
            }
            //3. 调用订阅者对应的消息处理函数,实现消息的推送
            consumer->_callback(consumer->_tag, msg->mutable_payload()->mutable_properties(), msg->payload().body());
            //4. 判断如果订阅者是自动确认---不需要等待确认,直接删除消息,否则需要外部收到消息确认后再删除
            if(consumer->_auto_ack)
            {
                _host->basicAck(qname, msg->payload().properties().id());
            }
        }
        void basicResponse(bool ok, const std::string &rid, const std::string &cid) 
        {
            basicCommonResponse resp;
            resp.set_rid(rid);
            resp.set_cid(cid);
            resp.set_ok(ok);
            _codec->send(_conn, resp);
        }
    private:
        std::string _cid;
        Consumer::ptr _consumer;
        muduo::net::TcpConnectionPtr _conn;
        ProtobufCodecPtr _codec;
        ConsumerManager::ptr _cmp;
        VirtualHost::ptr _host;
        threadpool::ptr _pool;
    };
}

#endif

这个Channel类是RabbitMQ服务器的核心业务处理器,它:

  • 作为AMQP协议的信道抽象
  • 处理客户端与服务器之间的各种AMQP操作
  • 管理消息的发布、消费和确认流程

核心成员变量解析

  1. VirtualHost (_host)
  • 管理交换机、队列的存储和查找
  • 实现消息的路由和持久化
  1. ConsumerManager (_cmp)
  • 管理消费者的创建、删除和选择
  • 实现消费者负载均衡
  1. ProtobufCodec (_codec)
  • 协议的编码和解码
  • 负责与客户端的网络通信
  1. threadpool (_pool)
  • 提供异步任务处理能力
  • 提高系统的并发性能
  1. TcpConnection (_conn)
  • 与客户端的连接通道
  • 实现网络层通信

消息流完整路径

发布流程:

plain 复制代码
客户端请求 → ProtobufCodec解码 → Channel::basicPublish 
→ VirtualHost路由 → 消息入队 → 线程池消费任务
→ Channel::consume → 选择消费者 → 回调推送

消费流程:

plain 复制代码
客户端订阅 → basicConsume → 创建消费者
→ 消息到达 → consume任务执行 → 回调函数
→ 编码响应 → 网络发送 → 客户端接收

4. 封装信道管理对外的类

cpp 复制代码
    class ChannelManager 
    {
    public:
        using ptr = std::shared_ptr<ChannelManager>;
        ChannelManager(){}
        bool openChannel(const std::string &id, 
            const VirtualHost::ptr &host, 
            const ConsumerManager::ptr &cmp, 
            const ProtobufCodecPtr &codec, 
            const muduo::net::TcpConnectionPtr &conn,
            const threadpool::ptr &pool) 
        {
            std::unique_lock<std::mutex> lock(_mutex);
            auto it = _channels.find(id);
            if (it != _channels.end()) 
            {
                DLOG("信道:%s 已经存在!", id.c_str());
                return false;
            }
            auto channel = std::make_shared<Channel>(id, host, cmp, codec, conn, pool);
            _channels.insert(std::make_pair(id, channel));
            return true;
        }
        void closeChannel(const std::string &id)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            _channels.erase(id);
        }
        Channel::ptr getChannel(const std::string &id) 
        {
            std::unique_lock<std::mutex> lock(_mutex);
            auto it = _channels.find(id);
            if (it == _channels.end()) 
            {
                return Channel::ptr();
            }
            return it->second;
        }
    private:
        std::mutex _mutex;
        std::unordered_map<std::string, Channel::ptr> _channels;
    };
相关推荐
袁袁袁袁满1 小时前
Linux网络连接之ss命令详细使用指南(从入门到运维实战)
linux·运维·服务器·网络·ssh·网络连接·ss命令
航哥的女人2 小时前
基于TCP的socket编程
网络·网络协议·tcp/ip
掌心天涯2 小时前
大疆御Pro加装4G模块
网络·智能路由器
志栋智能2 小时前
AI驱动的自动化运维机器人:从“数字劳动力”到“智能协作者”的进化
大数据·运维·网络·人工智能·机器人·自动化
guizhoumen3 小时前
建站从零开始之域名、服务器和CMS网站程序的选择
运维·服务器·网络
国科安芯3 小时前
多相交错并联系统的时钟同步精度与输入纹波抵消效应研究
网络·单片机·嵌入式硬件·fpga开发·性能优化
三万棵雪松3 小时前
【Linux网络编程试验方案】
linux·服务器·网络·嵌入式linux
枷锁—sha3 小时前
【CTFshow-pwn系列】03_栈溢出【pwn 047】详解:Ret2Libc 之 已知关键地址
网络·安全·网络安全
『往事』&白驹过隙;3 小时前
系统编程的内存零拷贝(Zero-Copy)技术
linux·c语言·网络·c++·物联网·iot