仿RabbitMQ实现消息队列———整体框架

目录

一、项目简介

需求分析

[AMQP 特点:](#AMQP 特点:)

[AMQP 模型:](#AMQP 模型:)

交换机类型

持久化

网络通信

二、服务端模块

1、交换机数据管理

2、队列数据管理

3、绑定数据管理

4、消息数据管理

5、虚拟机数据管理

6、路由匹配管理

7、消费者管理

8、信道管理

9、连接管理

10、服务器模块

三、客户端模块

1、消费者管理

2、信道管理

3、连接管理

4、异步线程池模块


一、项目简介

在实际的后端开发中, 尤其是分布式系统⾥, 跨主机之间使⽤⽣产者消费者模型, 也是⾮常普遍的需求。因此, 我们通常会把阻塞队列封装成⼀个独⽴的服务器程序, 并且赋予其更丰富的功能。 这样的服务程 序我们就称为 消息队列 (Message Queue, MQ)。
其中 RabbitMQ 是⼀个⾮常知名、功能强⼤且⼴泛使⽤的消息队列。本项目就是仿照RabbitMQ模拟实现一个简单的消息队列。

需求分析

• ⽣产者 (Producer)
• 消费者 (Consumer)
• 中间⼈ (Broker)
• 发布 (Publish)
• 订阅 (Subscribe)
我们需要实现的内容包括:

1.broker服务器:消息队列代理服务器
2.消息发布客户端:向服务器发布消息(生产者)
3.消息订阅客户端:从服务器订阅消息(消费者)
我们的消息队列是基于对AMQP协议的理解进行的整合,那么什么是AMQP协议呢:
AMQP (Advanced Message Queuing Protocol) 是一种网络协议,主要用于消息中间件之间的异步通信。AMQP 提供了一种标准的方式来发送和接收消息,使得不同厂商的消息中间件可以互相操作。

AMQP 特点:

  • 开放标准:AMQP 是一个开放标准,这意味着它的规范是公开的,并且任何人都可以实现它。
  • 二进制协议:AMQP 使用二进制编码,这使得它比基于文本的协议更高效。
  • 可靠性:AMQP 设计为确保消息的可靠传输,包括确认机制、事务支持等。
  • 灵活性:AMQP 支持多种消息路由模式,如点对点 (point-to-point) 和发布/订阅 (publish/subscribe)。
  • 互操作性:AMQP 允许不同厂商的消息中间件互相通信,不受客户端或中间件使用的编程语言的影响。

AMQP 模型:

  • Broker (消息代理):这是消息中间件的核心组件,负责接收、存储和转发消息。
  • Exchange (交换器):Exchange 接收来自生产者的消息,并根据配置规则将消息发送到一个或多个队列。
  • Queue (队列):队列是用来存储消息的数据结构,直到消费者取走这些消息。
  • Binding (绑定) :绑定定义了 Exchange 和 Queue 之间的关系,确定消息如何从 Exchange 到达 Queue。所谓的 Exchange 和 Queue 可以理解成 "多对多" 关系, 和数据库中的 "多对多" ⼀样. 意思是: ⼀个 Exchange 可以绑定多个 Queue (可以向多个 Queue 中转发消息) ⼀个 Queue 也可以被多个 Exchange 绑定 (⼀个 Queue 中的消息可以来⾃于多个 Exchange)
  • Producer (生产者):生产者是向消息中间件发送消息的应用程序。
  • Consumer (消费者):消费者是从消息中间件接收消息的应用程序。
    总结下来就是这样一张图

对于Broker来说,要实现一下核心API来实现消息队列的基本功能

1.创建交换机

2.销毁交换机

3.创建队列

4.销毁队列

5.创建绑定

6.解除绑定

7.发布消息

8.订阅消息

9.取消订阅

10.确认消息

生产者消费者则通过网络发送请求来调用这些API,实现生产者消费者模型

交换机类型

本项目实现三种交换机类型,也是最常见的:
• Direct: ⽣产者发送消息时, 直接指定被该交换机绑定的队列名
• Fanout: ⽣产者发送的消息会被复制到该交换机的所有队列中
• Topic: 绑定队列到交换机上时, 指定⼀个字符串为 bindingKey。发送消息指定⼀个字符串 routingKey。当 routingKey 和 bindingKey 满⾜⼀定的匹配条件的时候, 则把消息投递到指定队列

持久化

交换机,队列,绑定,消息都是需要持久化的,我们需要根据持久化来保证在程序或主机重启时数据不会丢失。在项目中我们使用了Sqlite数据库来进行本地的轻量级存储

网络通信

⽣产者和消费者都是客⼾端程序, Broker 则是作为服务器,通过⽹络进⾏通信。
我们在broker的基础上,再加上建立连接和打开信道的操作,这样 可以更好地复用TCP连接,达到长连接的效果,避免频繁的创建关闭TCP连接。

二、服务端模块

1、交换机数据管理

交换机数据管理就是描述了交换机应该有哪些数据

我们可以设置交换机的类型以及消息的基本属性,基本结构如下:

cpp 复制代码
syntax ='proto3';
package bitmq;
enum ExchangeType
{
    UNKNOWTYPE=0;
    DIRECT=1;
    FANOUT=2;
    TOPIC=3;
};

enum DeliverMode
{
    UNKNOWMODE=0;
    UNDURABLE=1;
    DURABLE=2;
};

message BasicProperties
{
    string id=1;
    DeliverMode delivery_mode=2;
    string routing_key=3;
};
message message
{
    message Payload
    {
        BasicProperties properties=1;
        string body=2;
        string valid=3;
    };
    Payload payload=1;
    uint32 offset=2;
    uint32 length=3;
};

1、交换机的名称:也是交换机的唯一标识

2、交换机的类型:决定了消息的转发方式(三种 )

每个队列与交换机绑定信息中有binding_key,每条消息中有routing_key

1.直接交换:binding_key与routing_key相同时,将消息放入队列

2.广播交换:交换机绑定的所有队列都放入消息

3.主题交换:根据具体的匹配算法,将符合匹配条件的binding_key和routing_key对应的队列放入消息。

3、持久化标志:决定当前交换机的数据是否需要持久化存储

4、自动删除标志:如果关联该交换机的客户端都退出了,是否需要自动删除交换机。

5、交换机的其他参数,在本项目中未具体使用。

对交换机的管理:

1、创建交换机:如果已存在就直接成功,不存在就创建(后续的一些结构也是如此操作)

cpp 复制代码
//声明交换机
        bool declareExchange(const std::string &name,ExchangeType type,bool durable,bool auto_delete,
        const google::protobuf::Map<std::string,std::string> &args)
        // bool declareExchange(const std::string &name,ExchangeType type,bool durable,bool auto_delete,
        // const std::unordered_map<std::string,std::string> &args)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            auto it=_exchanges.find(name);
            if(it!=_exchanges.end())
            {
                //如果交换机已经存在,那么直接返回,不需要新增
                return true;
            }
            auto exp=std::make_shared<Exchange>(name,type,durable,auto_delete,args);
            if(durable==true)
            {
                //若为持久化,则添加到数据库中
                bool ret=_mapper.insert(exp);
                if(!ret) return ret;
            }
            //添加到交换机表中
            _exchanges.insert(std::make_pair(name,exp));
            return true;
        }

2、删除交换机:每个交换机都会绑定一个或多个队列,所以在删除前也要删除掉所有相关的绑定信息,若交换机为持久化,也要在数据库中删除,后续结构也是类似操作。

cpp 复制代码
//删除交换机
        void deleteExchange(const std::string &name)
        {
            std::unique_lock<std::mutex> lock(_mutex);
            auto it=_exchanges.find(name);
            if(it==_exchanges.end())
            {
                return;
            }
            if(it->second->durable==true) _mapper.remove(name);
            _exchanges.erase(name);
        }

3、获取指定名称交换机

4、获取当前所有交换机数量

2、队列数据管理

队列数据管理需要管理的数据

1、队列名称

2、持久化存储标志:决定是否将队列持久化存储起来,重启后队列是否依旧存在

3、是否独占标志:如果独占,那么只有当前客户端可以订阅该队列消息

4、自动删除标志:当订阅了当前队列的客户端退出后,是否删除队列。本项目中未实现

5、其他参数

队列管理类(与交换机管理类似):

1、创建队列

2、删除队列

3、获取指定队列信息

4、获取队列数量

5、获取所有队列的名称:因为系统重启会重新加载数据,消息是以队列为单元存储在文件中的,所以加载消息需要知道队列的名称,因为在存储消息时,存储文件以队列名称进行取名

3、绑定数据管理

绑定数据管理:描述队列与交换机的绑定信息

管理的数据

1、交换机的名称

2、队列的名称

3、对应的binding_key:绑定密钥,在交换机主题交换和直接交换时要用到。

由数字,字符,_,#, . , * 组成,比如news.sport.# 就可以与news.sport.football匹配成功

管理的操作:

1、添加绑定信息

2、解除绑定信息

3、获取交换机所有的相关绑定信息:

1、在删除交换机的时候要删除相关的绑定信息

2、交换机也要通过这些信息来发布到指定的队列

4、获取队列所有的绑定信息:

1、删除队列的时候,也要删除相关的绑定信息

5、获取绑定信息的数量

4、消息数据管理

消息的基本属性

cpp 复制代码
message BasicProperties
{
    string id=1;
    DeliverMode delivery_mode=2;
    string routing_key=3;
};
message message
{
    message Payload
    {
        BasicProperties properties=1;
        string body=2;
        string valid=3;
    };
    Payload payload=1;
    uint32 offset=2;
    uint32 length=3;
};

消息属性:

1、消息ID:唯一标识

2、持久化标志(同队列与交换机)

3、 routing_key:决定了要发布的队列,交换机根据交换类型与binding来匹配

4、消息主体

以下是服务端在管理消息时添加的信息:

存储偏移量:消息以队列为单元存储在文件中,这个偏移量是相对于文件起始位置的偏移量

消息长度:从偏移量位置取出指定长度的消息。解决粘包问题

是否有效标志:标识当前消息是否被删除,因为如果删除消息就进行一次文件读写比较耗费资源,那么我们的策略是(删除一条消息不会将后面的数据拷贝到前边,而是重置了标志位valid,每次删除消息后判断:如果有效消息的数量占消息总数比例不到50%,且数据量超过2000,则进行垃圾回收,重新整理文件系统,当系统重启,也只需要加载有效消息。

消息的管理

以队列为单元进行管理,因为对于消息的所有操作是以队列为单元的

管理数据:

1、消息链表:保存所有待推送的消息

2、待确认hash:消息推送给客户后,会等待客户端确认,收到确认后,才会删除消息。

3、持久化hash:假设消息都需要持久化,操作过程中会垃圾回收,但是垃圾回收会改变存储位置,内存中消息的存储位置也需要改变,要用新位置去更新持久化数据

垃圾回收:将有效消息读出来,然后重新截断文件,将消息写入文件中

4、持久化的有效消息总量

5、持久化的总的消息数量:决定了什么时候进行消息回收

管理操作

1、向队列新增消息

2、获取队首消息:获取消息后,将消息从待推送链表中删除,加入到待确认消息中

3、对消息进行确认:从待确认消息中移除,并进行持久话数据的删除

4、恢复队列的历史消息:主要在构造函数中进行

5、垃圾回收(队列持久化子模块完成):持久化文件中有效消息比例小于50%,总消息数超过2000进行垃圾回收

6、删除队列相关消息文件:当一个队列被删除了,他的消息也没有存在的意义了。

队列消息管理

1、初始化队列消息结构

2、移除队列消息结构:在队列被删除时调用

3、向队列中新增消息

4、对队列消息进行确认

5、恢复队列历史消息

5、虚拟机数据管理

虚拟机数据管理

对于交换机+队列+绑定信息+消息数据管理的整合

要管理的数据

1、交换机的管理句柄

2、队列数据的管理句柄

3、绑定信息的数据管理句柄

4、消息数据管理句柄

要管理的操作:

1、声明/删除交换机:在删除交换机的时候要删除相关的绑定信息

2、声明/删除队列:在删除队列的时候要删除相关的绑定信息以及数据

3、队列的绑定/解除绑定:绑定的时候,交换机和队列必须存在

4、获取指定队列的消息

5、对指定队列的指定消息进行确认

6、获取交换机相关的所有绑定信息:一条消息要发布给指定交换机的时候,交换机获取所有的绑定信息,来确认消息要发送到哪个队列

6、路由匹配管理

决定了一个消息能否发布到指定的队列中

在交换机与队列的绑定信息中有一个banding_key,这是队列发布的匹配规则

在每条要发布的消息中,都有一个routing_key,这是消息的发布规则

根据交换机的类型进行相关的匹配操作

路由匹配模块本质上来说没有管理的数据,只有向外提供的路由匹配操作:

1、判断routing_key是否符合规定:

格式判定:只能由数字,字母,_, . 构成

2、判断binding_key是否符合规定:

格式判断:只能由数字,字母,_ , # , * 构成

3、判断routing_key与binding_key能否匹配成功的接口

7、消费者管理

客户端有两种:发布消息,订阅消息

只有订阅了消息的客户端才是一个消费者

消费者数据存在的意义:当指定的客户端有了消息后,需要将消息推送给这个消费者客户端

推送的时候就要找到这个客户端的相关信息---------连接

消费者信息

1、消费者标识

2、订阅队列名称:当前队列有消息就会推送给这个客户端,当客户端收到消息,需要对指定队列的消息进行确认

3、自动确认标志:自动确认--推送消息后,直接删除消息不需要额外确认,手动确认------推送消息后需要等到确认回复再去删除消息

4、消费处理回调函数指针:队列有一条消息后,通过哪个函数进行处理(向指定客户端推送消息)

消费者管理

以队列为单元进行管理

每个消费者订阅的都是指定队列的消息,消费者对这个消息进行确认也是以队列进行确认

当队列中有消息了,必然是获取订阅了这个队列的消费者进行推送

队列消费者管理结构

数据信息:消费者链表------保存当前队列的所有消费者信息(RR轮转每次取出下一个消费者 进行推送------一条消息只需要被一个客户端进行处理即可

管理操作:

1、新增消费者

2、RR轮转获取一个消费者

3、删除消费者

4、队列消费者数量

5、队列的消费者列表是否为空

消费者管理操作(以队列为单元进行管理)

1、初始化队列消费者结构

2、删除队列消费者结构

3、向指定队列中添加消费者

4、删除指定队列的指定消费者

5、获取指定队列的消费者

8、信道管理

信道是网络通信的一个概念,叫做通信信道

网络通信的时候,必然是通过网络连接来完成的,为了充分利用资源,细化出了信道的概念,对于用户来说,一个通信信道就是进行网络通信的载体,而一个真正的通信连接,可以创建出多个信道

每一个信道对于用户来说是独立的,但是本质的底层使用的是一个通信连接。

因此,信道是用户眼中的一个通道,所以所有的网络通信服务都由信道提供

1、声明/删除交换机

2、声明/删除队列

3、绑定信息的绑定/删除

4、消息的发布/订阅队列消息/取消订阅/队列消息的Ack

信道要管理的数据

1、信道ID

2、信道关联的消费者句柄:当信道关闭时,所有关联的消费者订阅都要取消,相当于删除所有的消费者。

3、信道关联的虚拟机句柄

4、工作线程池句柄:信道进行消息发布到指定队列后,要从指定队列的消费者链表中获取一个消费者,对这条消息进行消费,也就是将这条消息推送给客户端的操作要交给线程池来进行。并非每个信道都有一个线程池,而是整个服务器有一个线程池,大家所有的信道都是同一个线程池进行异步操作而已。

信道的管理

1、打开一个信道

2、关闭一个信道

3、获取指定信道句柄

9、连接管理

概念:网络通信连接

在网络通信模块,我们使用muduo库来实现底层通信,muduo库中本身就有Connection连接的概念和对象类。但是在我们的连接中还有一个上层的信道概念,这个概念在muduo库中是没有的。

因此我们需要在用户层面上对muduo库中的Connection连接进行二次封装,形成我们所需的连接管理

管理数据

1、muduo库的通信连接

2、当前连接关联的信道管理句柄

连接提供的操作

1、创建信道

2、关闭信道

连接管理的操作

1、新增连接

2、关闭连接

3、获取指定的连接信息

10、服务器模块

Broker服务器模块是一个功能的整合,本质上这个模块并不提供实质的功能性操作

这个模块最重要的是资源的整合,是一个资源的载体

1、一个服务器有一个工作线程池,其他所有的信道操作都是这一个线程池的。

2、一个服务器有一个虚拟机,其他所有的交换机,队列,绑定,消息的操作都是针对这个虚拟机进行的

3、一个服务器有一个消费者管理

4、通信相关的连接管理,协议处理模块句柄,也是一整个服务器有一套

三、客户端模块

1、消费者管理

消费者信息

1、消费者标识

2、订阅的队列名称

3、自动确认标志

4、消息的回调处理函数

当消费者订阅了一个队列的消息,这个队列有了消息后,就会将消息推送给这个客户端,这时候收到了消息就会通过回调函数处理,处理完毕后根据确认标志判断是否进行消息确认。

消费者管理:增 删 查 操作

2、信道管理

所有提供的操作与服务端基本对应,因为客户端需要给用户提供什么服务,服务器就要给客户端提供什么服务。

管理信息

1、信道ID

2、消费者管理句柄:每个信道都有自己相关的消费者

3、线程池句柄:对推送的消息进行回调处理,处理过程通过工作线程来进行

4、信道关联的连接

信道提供的服务

1、声明/删除交换机

2、声明/删除队列

3、绑定信息的绑定/删除

4、消息的发布/订阅队列消息/取消订阅/队列消息的Ack

5、创建/关闭信道

信道的管理:信道的 增 删 查 操作

3、连接管理

客户端连接的管理,本质是对客户端TcpClient的二次封装和管理

对于用户,不需要有客户端的概念,连接对于用户来说就是客户端,通过连接创建信道来完成所需的服务,客户端这边的连接对用户来说就是一个资源的载体

管理操作:

1、连接服务器

2、创建信道

3、关闭信道

4、关闭连接

管理的资源:

异步线程池 连接关联的信道管理句柄

4、异步线程池模块

TcpClient需要一个EventLoopThread模块进行IO事件监控

收到推送的消息,需要对推送过来的消息进行处理,因此需要一个线程池来帮助我们完成消息处理的过程。

将异步工作线程模块单独出来,是因为多个连接用一个EventLoopThread进行IO监控就够了,所有推送的消息处理也只需要有一个线程池就够了。

以上就是本项目的整体框架,完整代码已上传仓库,有兴趣的话可以查看:

https://gitee.com/Faiz--555/MQ

相关推荐
添砖java_8571 天前
基于RabbitMQ实现的轻量队列测试报告
分布式·rabbitmq
Zhu7581 天前
使用腾讯CNB构建Hadoop定制容器镜像
大数据·hadoop·分布式
TTBIGDATA1 天前
【Ambari Plus】13.Spark 安装
大数据·hadoop·分布式·spark·ambari·sqoop·ambari plus
sunxunyong1 天前
Hadoop租户创建
大数据·hadoop·分布式
得物技术1 天前
得物 OceanBase 落地实践
数据库·分布式·架构
GuangdongTong1 天前
基于RabbitMQ实现延迟任务
后端·rabbitmq
heimeiyingwang1 天前
【架构实战】分布式ID生成:从自增ID到雪花算法
分布式·架构
微三云 - 廖会灵 (私域系统开发)2 天前
电商系统微服务拆分实战:从单体到Spring Cloud的分布式事务踩坑与补偿方案
分布式·spring cloud·微服务
ACP广源盛139246256732 天前
破局 PCIe 4.0 交换瓶颈@ACP#IX8024 / ASM58024参数及应用对比
大数据·人工智能·分布式·嵌入式硬件
淡然的煎蛋2 天前
开始使用RabbitMQ
分布式·rabbitmq·ruby