RabbitMQ消息队列产品

前言:最近看了相关文章基于erlang的攻击面以及自己审计onlyoffice的nodejs代码的时候也有发现RabbitMQ的使用,这里的话就不得不补下相关的知识点了,之前只是知道是一个消息队列的中间件,这里的话有时间所以学习下

参考文章:https://www.rabbitmq.com/connections.html
参考文章:https://amqp-node.github.io/amqplib/channel_api.html
参考文章:https://help.aliyun.com/document_detail/141604.htm

什么是RabbitMQ消息队列

RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现,RabbitMQ是消息队列中间件,消息队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。

RabbitMQ是一款基于AMQP协议的消息中间件,它能够在应用之间提供可靠的消息传输。在易用性,扩展性,高可用性上表现优秀。使用消息中间件利于应用之间的解耦,生产者(客户端)无需知道消费者(服务端)的存在。而且两端可以使用不同的语言编写,大大提供了灵活性。

常见的消息队列中间件有如下几个:

  • Kafka:分布式消息系统,高吞吐量(大数据使用)

  • ActiveMQ:基于JMS

  • RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好

  • RocketMQ:基于JMS,阿里巴巴产品,目前交由Apache基金会

RabbitMQ环境搭建

地址:https://www.rabbitmq.com/download.html

配置插件:rabbitmq-plugins enable rabbitmq_management

启动rabbitmq:rabbitmq-server.bat,接着访问 127.0.0.1:15672 guest/guest 即可访问到如下界面

RabbitMQ和AMQP的关系

AMQP(Advanced Message Queue Protocol)定义了一种消息系统协议规范。这个规范描述了在一个分布式的系统中各个子系统如何通过消息交互。

AMQP将分布式系统中各个子系统隔离开来,子系统之间不再有依赖。子系统仅依赖于消息。子系统不关心消息的发送者,也不关心消息的接受者。

AMQP中有一些概念,用于定义与应用层的交互。这些概念包括:message、queue、exchange、channel, connection, broker、vhost

AMQP的结构

message

即消息,简单来说就是用户层定义的通过应用层来要发送的数据

queue

队列,用于存储消息,一个rabbitmq中不止是只有一个队列,可以是有多个队列的,每个队列都拥有一个属于自身队列的名字

connection和channel

connection:提供了应用程序与broker的网络连接的对象

channel:提供了connection跟rabbitmq通信的通道对象

exchange

exchange:定义了消息要如何进行发送,因为上面提到了rabbitmq中不一定只有一个队列,那么如果有多个队列的话,那么生产者的消息应该如何分配到对应的队列呢呢,这个时候就可以通过exchange来进行定义选择消息分配的行为。

exchange有四种路由模式,分别是direct,topic,headers,fanout

direct

direct模式(默认路由模式)下发送消息时也可以不指定exchange,这个时候消息的投递将依赖于routing_key,routing_key在这种场景下就对应着目标queue的名字

fanout

fanout路由模式会忽略Routing Key和Binding Key的匹配规则将消息路由到所有绑定的Queue,那么exchange将收到的所有消息广播到它所知道的全部队列中。

  • 路由规则:fanout Exchange忽略Routing Key和Binding Key的匹配规则将消息路由到所有绑定的Queue。

  • 使用场景:fanout Exchange适用于广播消息的场景。例如,分发系统使用Fanout Exchange来广播各种状态和配置更新。

  • 路由示例:fanout Exchange忽略Routing Key和Binding Key的匹配规则将消息路由到所有绑定的Queue的示例如下

topic

Topic Exchange根据Binding Key和Routing Key通配符匹配的规则路由消息。

这里的话其实可以加深理解Binding Key和Routing Key的关系,Routing Key其实是给消息打上对应的标签,而这边的Binding Key则是匹配Routing Key是否符合来进行绑定队列的操作

  • 路由规则:Topic Exchange根据Binding Key和Routing Key通配符匹配的规则路由消息。Topic Exchange支持的通配符包括星号()和井号(#)。星号()代表一个英文单词(例如cn)。井号(#)代表零个、一个或多个英文单词,英文单词间通过英文句号(.)分隔,例如cn.zj.hz。

  • 使用场景:Topic Exchange适用于通过通配符区分消息的场景。Topic Exchange常用于多播路由。例如,使用Topic Exchange分发有关于特定地理位置的数据。

  • 路由示例:Topic Exchange根据Binding Key和Routing Key通配符匹配的规则路由消息的示例如下:

headers

这个具体没太了解过,可以自己去学习下

JMS Queue/Topic Exchange

这个具体没太了解过,可以自己去学习下

exchange和queue之间的Binding和Binding Key和Routing Key的关系

Binding(绑定):Binding是用于将交换机和队列关联起来的操作。通过Binding,你可以告诉交换机将消息发送到特定的队列。绑定需要指定交换机、队列和绑定键(Binding Key)三个要素。

Binding Key(绑定键):Binding Key是在创建Binding时指定的一个属性,用于指定将消息路由到哪些队列的规则。当消息发送到交换机时,交换机会根据消息的Routing Key(路由键)和Binding Key进行匹配,从而确定将消息发送到哪些队列。

简单的理解可以是Binding操作包含了交换机、队列和绑定键(Binding Key),而其中绑定键(Binding Key)和Routing Key(路由键)共同决定了exchange交换机将消息转发到哪个Queue中

broker

可简单理解为实现AMQP的服务,例如RabbitMQ服务

nodejs实现tutorials

参考文章:https://www.rabbitmq.com/getstarted.html

代码实现:https://github.com/chibd2000/rabbitmq_demo

amqp_config.js

module.exports.socketOptions
module.exports.mq_url = 'amqp://127.0.0.1'

单队列/生产/消费者行为

producer.js

var amqp_config = require('./amqp_config')
var amqp_lib = require('amqplib/callback_api');
const log = console.log
// 连接amqp的地址,创建一个connection对象
amqp_lib.connect(amqp_config.mq_url,(error, connection)=>{
log("create connection ", connection)
// create channel
connection.createChannel((error, channel) => {
log("create channel ", channel)
// 定义队列
let queue = 'test_queue'
let msg = "hello world"
// 明确指定要通信的队列
channel.assertQueue(queue, {durable: true})
// 发送到指定队列任务,并且属性是消息持久化
channel.sendToQueue(q, Buffer.from(msg), {persistent: true})
log("send message to queue...")
})
})

上面这种写法默认就是""(默认交换),这里还可以提供另外一种写法,区别就是通过publish可以来制定exchange,而这边的exchange为"",那么就是默认交换

amqp_lib.connect(amqp_config.mq_url,(error, connection)=>{
// create channel
connection.createChannel((error, channel) => {
// 定义队列
let queue = 'test_queue'
let msg = "hello world"
// 明确指定要通信的队列
channel.assertQueue(queue, {durable: true})
setInterval(() => {
// 发送到指定队列任务,并且属性是消息持久化
channel.publish('', queue, Buffer.from(msg), {persistent: true});
log("[+] send message to queue -> ", msg)
}, 500);
})
})

consumer.js

var amqp_config = require('./amqp_config')
var amqp_lib = require('amqplib/callback_api');
const log = console.log;
log(" [*] Waiting for messages in %s. To exit press CTRL+C", queue);
// 连接amqp的地址,创建一个connection对象
amqp_lib.connect(amqp_config.mq_url,(error, connection)=>{
log("create connection ", connection)
// create channel
connection.createChannel((error, channel) => {
log("create channel ", channel)
// 定义队列
let queue = 'test_queue'
// 明确指定要通信的队列
channel.assertQueue(queue, {durable: true})
// 消费者从指定的队列中取出message
channel.consume(queue, (msg) => {
log("receive message from queue ", msg)
}, {noAck: true})
})
})

一对多个消费者行为

代码跟上面的没有多大区别,这里主要就是说明队列是可以一对多个消费者的,如下行为所示

producer.js

var amqp_config = require('./amqp_config')
var amqp_lib = require('amqplib/callback_api');
const log = console.log
// 连接amqp的地址,创建一个connection对象
amqp_lib.connect(amqp_config.mq_url,(error, connection)=>{
// create channel
connection.createChannel((error, channel) => {
// 定义队列
let queue = 'test_queue'
let msg = "hello world"
// 明确指定要通信的队列
channel.assertQueue(queue, {durable: true})
setInterval(() => {
// 发送到指定队列任务,并且属性是消息持久化
channel.sendToQueue(queue, Buffer.from(msg), {persistent: true})
log("[+] send message to queue -> ", msg)
}, 500);
})
})

consumer.js

var amqp_config = require('./amqp_config')
var amqp_lib = require('amqplib/callback_api');
const log = console.log;
// 连接amqp的地址,创建一个connection对象
amqp_lib.connect(amqp_config.mq_url,(error, connection)=>{
// create channel
connection.createChannel((error, channel) => {
// 定义队列
let queue = 'test_queue'
// 明确指定要通信的队列
channel.assertQueue(queue, {durable: true})
// 设置公平遣派
channel.prefetch(1)
// 消费者从指定的队列中取出message
channel.consume(queue, (msg) => {
setTimeout(() => {
channel.ack(msg);
log("[+] receive message from queue -> ", msg.content.toString())
}, 50);
}, {noAck: false})
})
})

shell1 -> node ./producer.js
shell2 -> node ./consumer.js
shell3 -> node ./consumer.js

指定exchange为fanout进行通信

注意的点就是当指定exchange的时候,那么我们就需要,这里的话实现的就是如下所示

producer.js

因为这边exchange指定为fanout的了,所以对于publish中的第二个参数routingKey就没有必要进行设置了,因为这个fanout类型就是对所有已知队列进行广播操作

var amqp_config = require('./amqp_config')
var amqp_lib = require('amqplib/callback_api');
const log = console.log
// 连接amqp的地址,创建一个connection对象
amqp_lib.connect(amqp_config.mq_url,(error, connection)=>{
// create channel
connection.createChannel((error, channel) => {
let msg = "hello world"
let exchange = "logs"
// 生产一个非持久的未命名的队列
channel.assertQueue('', {exclusive: true});
// 指定路由类型fanout,将收到的所有消息广播到它知道的所有队列
channel.assertExchange(exchange, 'fanout', {durable: false});
// 执行publish
setInterval(() => {
channel.publish(exchange, '', Buffer.from(msg)); // 路由为fanout的时候默认忽略routing key
log("[+] send message to queue -> ", msg)
}, 2000);
})
})

consumer.js

var amqp_config = require('./amqp_config');
var amqp_lib = require('amqplib/callback_api');
const log = console.log;
// 连接amqp的地址,创建一个connection对象
amqp_lib.connect(amqp_config.mq_url,(error, connection)=>{
// create channel
connection.createChannel((error, channel) => {
// 设置路由名称
let exchange = 'logs';
// 选择路由
channel.assertExchange(exchange, 'fanout', {durable: false});
// 创建队列
channel.assertQueue('', {exclusive: true}, function(error, q){
// 设置公平遣派
channel.prefetch(1)
// 需要告诉交换器将消息发送到我们的队列
channel.bindQueue(q.queue, exchange, '')
// 消费者从指定的队列中取出message
channel.consume(q.queue, (msg) => {
setTimeout(() => {
if(msg.content){
log("[+] receive message from ", q.queue.toString(), " -> ", msg.content.toString())
}
}, 1000);
}, {noAck: true})
})
})
})

仅订阅消息的子集(实质上就是消费者如何过滤生产者的消息)

消费者想要实现仅订阅消息的子集,这句话如何理解,这里举个例子,比如生产者产生的对象有三个,分别是error warning info,那么此时消费者需要处理这些对象,但是这边需要有一个需求就是让其中的一个消费者只能处理生产者中的error对象,如果这个能实现的话那么就是仅订阅消息的子集

如下图所示,在此设置中,我们可以看到交换器X绑定了两个队列,其中第一个队列绑定了绑定键orange,第二个队列有两个绑定,一个绑定键为black ,另一个绑定为green。

在这样的设置中,发布到带有路由键 orange的交换器的消息将被路由到队列Q1,路由键为黑色或绿色的消息将转到Q2,所有其他消息将被丢弃

实现上述的需求的代码如下,我们可以指定当前exchange为direct来进行传输,然后消费者的实现主要通过channel.bindQueue的第三个参数pattern来指定到对应的匹配的队列中

producer.js

var amqp = require('amqplib/callback_api');
const { checkServerIdentity } = require('tls');
amqp.connect('amqp://localhost', function(error0, connection) {
if (error0) {
throw error0;
}
connection.createChannel(function(error1, channel) {
if (error1) {
throw error1;
}
// 路由 -> direct_logs
var exchange = 'direct_logs';
var args = process.argv.slice(2);
var msg = 'Hello World!';
// var severity = (args.length > 0) ? args[0] : 'info';
channel.assertExchange(exchange, 'direct', {durable: false});
var severity;
setInterval(() => {
severity = args[parseInt(Math.random()*3)];
channel.publish(exchange, severity, Buffer.from(msg));
console.log(" [x] Sent %s: '%s'", severity, msg);
}, 500);
});
});

consumer.js

var amqp = require('amqplib/callback_api');
var args = process.argv.slice(2);
if (args.length == 0) {
console.log("Usage: receive_logs_direct.js [info] [warning] [error]");
process.exit(1);
}
amqp.connect('amqp://localhost', function(error0, connection) {
if (error0) {
throw error0;
}
connection.createChannel(function(error1, channel) {
if (error1) {
throw error1;
}
var exchange = 'direct_logs';
channel.assertExchange(exchange, 'direct', {durable: false});
channel.assertQueue('', {
exclusive: true
}, function(error2, q) {
if (error2) {
throw error2;
}
console.log(' [*] Waiting for logs. To exit press CTRL+C');
args.forEach(function(severity) {
channel.bindQueue(q.queue, exchange, severity);
});
channel.consume(q.queue, function(msg) {
console.log(" [x] %s: '%s'", msg.fields.routingKey, msg.content.toString());
}, {
noAck: true
});
});
});
});

知识点:这边有个知识点就是如下情况的时候,上面这种情况是否可以通过direct的exchange来进行实现呢?同样是可以的,这种情况的话直接交换将表现为扇出的效果,直接将当前消息广播给所有匹配的队列

topic(对消费者如何接受对应的消息队列的任务的规则进行了细分,可以通过*和#来进行匹配要处理的消息)

producer.js

var amqp = require('amqplib/callback_api');
const { checkServerIdentity } = require('tls');
amqp.connect('amqp://localhost', function(error0, connection) {
if (error0) {
throw error0;
}
connection.createChannel(function(error1, channel) {
if (error1) {
throw error1;
}
var exchange = 'my_topic';
var msg = 'Hello World!';
channel.assertExchange(exchange, 'topic', {durable: false});
send_array = ['quick.orange.rabbit', 'quick.orange.fox', 'lazy.pink.rabbit', 'quick.brown.fox']
send_array.forEach(element => {
setTimeout(() => {
channel.publish(exchange, element, Buffer.from(msg));
console.log(" [x] Sent %s: '%s'", element, msg);
}, 1000);
});
});
});

consumer.js

var amqp = require('amqplib/callback_api');
amqp.connect('amqp://localhost', function(error0, connection) {
if (error0) {
throw error0;
}
connection.createChannel(function(error1, channel) {
if (error1) {
throw error1;
}
var exchange = 'my_topic';
channel.assertExchange(exchange, 'topic', {durable: false});
channel.assertQueue('', {exclusive: true}, function(error2, q) {
if (error2) {
throw error2;
}
console.log(' [*] Waiting for logs. To exit press CTRL+C');
['*.orange.*','*.*.rabbit','lazy.#'].forEach(function(key) {
channel.bindQueue(q.queue, exchange, key);
});
channel.consume(q.queue, function(msg) {
console.log(" [x] %s: '%s'", msg.fields.routingKey, msg.content.toString());
}, {noAck: true});
});
});
});

posted @   zpchcbd  阅读(130)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
点击右上角即可分享
微信分享提示