ASP .NET Core 集成 RabbitMQ

简介

AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。
AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。

RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。

RabbitMQ提供了可靠的消息机制、跟踪机制和灵活的消息路由,支持消息集群和分布式部署。适用于排队算法、秒杀活动、消息分发、异步处理、数据同步、处理耗时任务、CQRS等应用场景。

RabbitMQ工作机制

首先要知道RabbitMQ的三种角色:生产者、消费者、消息服务器

  • 生产者:消息的创建者,负责创建和推送消息到消息服务器
  • 消费者:消息的接收方,接受消息并处理消息
  • 消息服务器:其实RabbitMQ本身,不会产生和消费消息,相当于一个中转站,将生产者的消息路由给消费者

RabbitMQ角色

  • Server:又称broker,接受客户端的链接,实现AMQP实体服务
  • Connection:连接,应用程序与broker的网络连接
  • ConnectionFactory:连接管理,应用程序或消费方与RabbitMQ建立连接的管理器
  • Channel:网络信道,几乎所有的操作都在channel中进行,Channel是进行消息读写的通道。客户端可以建立多个channel,每个channel代表一个会话任务。
  • Exchange:交换机,用于接收分配消息到队列中
  • Queue:保存消息
  • Routingkey:一个路由规则,消息会携带routingKey,决定消息最终的队列;虚拟机可用它来确定如何路由一个特定消息。(如负载均衡)
  • BindingKey:Queue通过bindingKey与交换机绑定;Exchange和Queue之间的虚拟链接,binding中可以包换routing key
  • Message:消息,服务器与应用程序之间传送的数据,由Properties和Body组成.Properties可以对消息进行修饰,必须消息的优先级、延迟等高级特性;Body则是消息体内容。
  • virtualhost: 虚拟地址,用于进行逻辑隔离,最上层的消息路由。一个virtual host里面可以有若干个Exchange和Queue,同一个Virtual Host 里面不能有相同名称的Exchange 或 Queue。

RabbitMQ整体架构

部署RabbitMQ

docker镜像地址

  • rabbitmq:management: 带web管理界面

  • rabbitmq:不带web管理界面

这里拉取rabbitmq:management镜像

docker pull rabbitmq:management

Run

docker run -d --hostname=rabbitmqhost --name rabbitmq -p 5672:5672 -p 15672:15672 -v /dockerdata/rabbitmq/data:/var/lib/rabbitmq  -e RABBITMQ_DEFAULT_VHOST=v_rabbitmqhost  -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin  rabbitmq:management

映射5672客户端通信端口,和15672web管理端口。

参数 含义
hostname 指定rabbitmq的Host名称,保持持久化
-p 5672 客户端通信端口
-p 15672 webUI管理端口
-v /var/lib/rabbitmq 持久化,和hostname最好一起使用
RABBITMQ_DEFAULT_VHOST 容器Host名称
RABBITMQ_DEFAULT_USER/RABBITMQ_DEFAULT_PASS 设置账号密码,不配置的话 默认guest/guest

简化版

docker run -d --hostname=rabbitmqhost --name rabbitmq -p 5672:5672 -p 15672:15672 -v /dockerdata/rabbitmq/data:/var/lib/rabbitmq rabbitmq:management

进入RabbitMQ

docker exec -it rabbitmq bash
命令 含义
rabbitmq-plugins enable rabbitmq_management 启用Web控制台,此镜像默认启用;如果使用rabbitmq镜像此命令开启
rabbitmqctl status 查看RabbitMQ状态
rabbitmqctl add_user username password 添加用户
rabbitmqctl set_user_tags username administrator 赋予用户管理员权限
rabbitmqctl list_users 查看用户列表
rabbitmqctl delete_user username 删除用户
rabbitmqctl oldPassword Username newPassword 修改用户密码

访问rabbitmq:15672,使用 guest/guest登录

Web 管理界面

overview概览

Admin用户管理

添加用户

上面的Tags选项,其实是指定用户的角色,可选的有以下几个:

角色 权限
超级管理员(administrator) 可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。
监控者(monitoring) 可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
策略制定者(policymaker) 可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)
普通管理者(management) 仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。
其他 无法登陆管理控制台,通常就是普通的生产者和消费者

虚拟主机

创建虚拟主机。为了让各个用户可以互不干扰的工作,RabbitMQ添加了虚拟主机(Virtual Hosts)的概念。其实就是一个独立的访问路径,不同用户使用不同路径,各自有自己的队列、交换机,互相不会影响。

队列模式

参考文档:

https://www.rabbitmq.com/getstarted.html

RabbitMQ几种模式进行简要分类,可以分成如下三类(RPC暂不考虑)

  • 简单队列模式,单发单收,一对一模式

  • Worker模式,单发多收(一个生产者多个消费者),一对多模式

  • 发布订阅模式,包括发布订阅、路由、通配符模式,这三种只是交换机类型不同

模式 含义 流程图
Hello World 最简单的一对一队列模式
Work Queues 多个消费者都会收到消息(Worker模式),一个生产者多个消费者。可以理解为负载均衡场景。
Publish/Subscribe 生产者一次向多个消费者发送消息。发布订阅模式。(Fanout Exchange)
Routing 根据路由规则接收消息(Direct Exchange)
Topics 基于模式(主题)接收消息(Topics Exchange)
RPC 请求/回复模式示例
Publisher Confirms 与发布者的可靠消息确认

.NET Core 使用RabbitMQ

Neget安装RabbitMQ.Client

一对一简单队列

生产者

using RabbitMQ.Client;
using System.Text;

Console.WriteLine("Hello, World! 生产者");

var factory = new ConnectionFactory()       // 创建连接工厂对象
{
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};
var connection = factory.CreateConnection();    // 创建连接对象
var channel = connection.CreateModel();         // 创建连接会话对象

string queueName = "queue1";

// 声明一个队列
channel.QueueDeclare(
    queue: queueName,   // 队列名称
    durable: false,     // 是否持久化,true持久化,队列会保存磁盘,服务器重启时可以保证不丢失相关信息
    exclusive: false,   // 是否排他,如果一个队列声明为排他队列,该队列仅对时候次声明它的连接可见,并在连接断开时自动删除
    autoDelete: false,  // 是否自动删除,自动删除的前提是:至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除
    arguments: null     // 设置队列的其他参数
);

string str = string.Empty;

do {
    Console.WriteLine("发送内容:");
    str = Console.ReadLine()!;

    // 消息内容
    byte[] body = Encoding.UTF8.GetBytes(str);

    // 发送消息
    channel.BasicPublish("", queueName, null, body);

    // Console.WriteLine("成功发送消息:" + str);
} while (str.Trim().ToLower() != "exit");

channel.Close();
connection.Close();

code describe

  • 可以看到 RabbitMQ 使用了 IConnectionFactory, IConnection和IModel 来创建链接和通信管道, IConnection 实例对象只负责与 Rabbit 的连接,而发送接收这些实际操作全部由会话通道进行。
  • 而后使用 QueneDeclare 方法进行创建消息队列,创建完成后可以在 RabbitMQ 的管理工具中看到此队列,QueneDelare 方法需要一个消息队列名称的必须参数.后面那些参数则代表缓存,参数等信息。
  • 最后使用 BasicPublish 来发送消息,在一对一中 routingKey 必须和 queueName 一致。

消费者

using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;

Console.WriteLine("Hello, World! 消费者1");

var factory = new ConnectionFactory()       // 创建连接工厂对象
{
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};

IConnection connection = factory.CreateConnection();    // 创建连接对象
IModel channel = connection.CreateModel();         // 创建连接会话对象

string queueName = "queue1";
//声明一个队列
channel.QueueDeclare(
  queue: queueName,//消息队列名称
  durable: false,//是否持久化,true持久化,队列会保存磁盘,服务器重启时可以保证不丢失相关信息。
  exclusive: false,//是否排他,true排他的,如果一个队列声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除.
  autoDelete: false,//是否自动删除。true是自动删除。自动删除的前提是:致少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除.
  arguments: null ////设置队列的一些其它参数
);

// 创建消费者对象
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) => {

    byte[] message = ea.Body.ToArray();
    Console.WriteLine("接收到的消息为:" + Encoding.UTF8.GetString(message));
};

// 消费者开启监听
channel.BasicConsume(queueName, true, consumer);

Console.ReadKey();
channel.Dispose();
connection.Close();

code describe

  • 在接收者中是定义一个EventingBasicConsumer对象的消费者(接收者),这个消费者与会话对象关联,
  • 然后定义接收事件,输出从消息队列中接收的数据,
  • 最后使用会话对象的BasicConsume方法来启动消费者监听.消费者的定义也是如此简单.
  • 不过注意一点,可以看到在接收者代码中也有声明队列的方法,其实这句代码可以去掉,但是如果去掉的话接收者在程序启动时监听队列,而此时这个队列还未存在,所以会出异常,所以往往会在消费者中也添加一个声明队列方法

测试

此时,简单消息队列传输就算写好了,我们可以运行代码就行测试

Worker模式(负载均衡)

Worker模式其实是一对多的模式,但是这个一对多并不是像发布订阅那种,而是信息以顺序的传输给每个接收者,我们可以使用上个例子来运行worker模式甚至,只需要运行多个接收者即可

默认情况下,RabbitMQ会顺序的将message发给下一个消费者。每个消费者会得到平均数量的message。这种方式称之为round-robin(轮询).
但是很多情况下并不希望消息平均分配,而是要消费快的多消费,消费少的少消费。还有很多情况下一旦其中一个宕机,那么另外接收者的无法接收原本这个接收者所要接收的数据。

下面针对上面的两个问题进行处理
首先我们先来看一下所说的宕机丢失数据一说,我们在上个例子Receive接收事件中添加线程等待

consumer.Received += (model, ea) => {
    Thread.Sleep(3000);
    byte[] message = ea.Body.ToArray();
    Console.WriteLine("接收到的消息为:" + Encoding.UTF8.GetString(message));
};

然后再次启动两个接收者进行测试

可以看到发送者发送了1-9的数字,第二个接收者在接收数据途中宕机,第一个接收者也并没有去接收第二个接收者宕机后的数据,有的时候我们会有当接收者宕机后,其余数据交给其它接收者进行消费,那么该怎么进行处理呢,解决这个问题得方法就是改变其消息确认模式

消息确认(RabbitMQ消费失败的处理)

Rabbit中存在两种消息确认模式

  • 自动模式 - 只要消息从队列获取,无论消费者获取到消息后是否成功消费,都认为是消息成功消费.
  • 手动模式 - 消费从队列中获取消息后,服务器会将该消息处于不可用状态,等待消费者反馈。如果消费者在消费过程中出现异常,断开连接切没有发送应答,那么RabbitMQ会将这个消息重新投递。

修改两个消费者代码,并在其中一个中延迟确认。

consumer.Received += (model, ea) => {
    Thread.Sleep(3000);
    byte[] message = ea.Body.ToArray();
    Console.WriteLine("接收到的消息为:" + Encoding.UTF8.GetString(message));
    
    channel.BasicAck(ea.DeliveryTag, true); // 开启返回消息确认
};

channel.BasicConsume(queue: queueName, autoAck: false, consumer); // 将autoAck设置false 关闭自动确认.


如果在延迟中消费者断开连接,那么RabbitMQ会重新投递未确认的消息

能者多劳模式

能者多劳是给消费速度快的消费更多的消息.少的责消费少的消息.能者多劳是建立在手动确认基础上实现。
在延迟确认的消费中添加BasicQos
消费者代码中添加

//声明一个队列
//...

//RabbitMQ每次只能向消费者发送一条消息,消费者未确认之前,不再向他再发送消息
channel.BasicQos(0, 1, false);


// 创建消费者对象
//...

Exchange模式

前面说过发布,路由,通配符这三种模式其实可以算为一种模式,区别仅仅是交互机类型不同.在这里出现了一个交换机的东西,发送者将消息发送发送到交换机,接收者创建各自的消息队列绑定到交换机,

通过上面三幅图可以看出这三种模式本质就是一种订阅模式,路由,通配符模式只是订阅模式的变种模式。使其可以选择发送订阅者中的接收者。
注意:交换机本身并不存储数据,数据存储在消息队列中,所以如果向没有绑定消息队列的交换机中发送信息,那么信息将会丢失

发布订阅模式(Fanout)

生产者代码
using RabbitMQ.Client;
using System.Text;

Console.WriteLine("Hello, World! 生产者");

var factory = new ConnectionFactory()       // 创建连接工厂对象
{
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};
var connection = factory.CreateConnection();    // 创建连接对象
var channel = connection.CreateModel();         // 创建连接会话对象

#region 定义交换机
string exchangeName = "exchange1";

channel.ExchangeDeclare(exchange: exchangeName, type: ExchangeType.Fanout); // 把交换机设置为 fanout 发布订阅模式
#endregion

string str;
do {
    Console.WriteLine("发送内容:");
    str = Console.ReadLine()!;

    byte[] body = Encoding.UTF8.GetBytes(str); // 消息内容

    channel.BasicPublish(exchangeName, "", null, body); // 发送消息
} while (str.Trim().ToLower() != "exit");

channel.Close();
connection.Close();

code describe

  • 代码与上面没有什么差异,只是由上面的消息队列声明变成了交换机声明(交换机类型为fanout),也就说发送者发送消息从原来的直接发送消息队列变成了发送到交换机
消费者代码
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;

Console.WriteLine("Hello, World! 消费者1");

var factory = new ConnectionFactory()       // 创建连接工厂对象
{
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};

IConnection connection = factory.CreateConnection();    // 创建连接对象
IModel channel = connection.CreateModel();         // 创建连接会话对象

#region 声明交换机
string exchangeName = "exchange1";
channel.ExchangeDeclare(exchangeName, ExchangeType.Fanout);
#endregion

#region 声明队列
string queueName = exchangeName + "_" + new Random().Next(1, 1000);
Console.WriteLine("队列名称:" + queueName);

channel.QueueDeclare(
  queue: queueName,//消息队列名称
  durable: false,//是否持久化,true持久化,队列会保存磁盘,服务器重启时可以保证不丢失相关信息。
  exclusive: false,//是否排他,true排他的,如果一个队列声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除.
  autoDelete: false,//是否自动删除。true是自动删除。自动删除的前提是:致少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除.
  arguments: null ////设置队列的一些其它参数
);
#endregion


channel.QueueBind(queueName, exchangeName, ""); // 将队列与交换机绑定

channel.BasicQos(0, 1, false);  // 告诉Rabbit每次只能向消费者发送一条信息,再消费者未确认之前,不再向他发送信息

// 创建消费者对象
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) => {

    byte[] message = ea.Body.ToArray();
    Console.WriteLine("接收到的消息为:" + Encoding.UTF8.GetString(message));

    channel.BasicAck(ea.DeliveryTag, true); // 开启返回消息确认
};

channel.BasicConsume(queue: queueName, autoAck: false, consumer); // 将autoAck设置false 关闭自动确认.

Console.ReadKey();
channel.Dispose();
connection.Close();

code describe

  • 可以看到消费者代码与上面有些差异
  • 首先是声明交换机(同上面一样,为了防止异常)
  • 然后声明消息队列并对交换机进行绑定,在这里使用了随机数,目的是声明不重复的消息队列,如果是同一个消息队列,则就变成worker模式,也就是说对于发布订阅模式有多少接收者就有多少个消息队列,而这些消息队列共同从一个交换机中获取数据

然后同时开两个接收者,结果就如下

发布订阅模式代码汇总(Fanout)

所有发送到Fanout Exchange的消息都会被转发到与该Exchange 绑定(Binding)的所有Queue上。

Fanout Exchange 不需要处理RouteKey 。只需要简单的将队列绑定到exchange 上。这样发送到exchange的消息都会被转发到与该交换机绑定的所有队列上。类似子网广播,每台子网内的主机都获得了一份复制的消息。

所以,Fanout Exchange 转发消息是最快的。

为了演示效果,定义了两个队列,分别为hello1,hello2,每个队列都拥有一个消费者。

示例代码
static void Main(string[] args)
{
	string exchangeName = "TestFanoutChange";
	string queueName1 = "hello1";
	string queueName2 = "hello2";
	string routeKey = "";

	//创建连接工厂
	ConnectionFactory factory = new ConnectionFactory
	{
		UserName = "admin",//用户名
		Password = "admin",//密码
		HostName = "192.168.157.130"//rabbitmq ip
	};

	//创建连接
	var connection = factory.CreateConnection();
	//创建通道
	var channel = connection.CreateModel();

	//定义一个Direct类型交换机
	channel.ExchangeDeclare(exchangeName, ExchangeType.Fanout, false, false, null);

	//定义队列1
	channel.QueueDeclare(queueName1, false, false, false, null);
	//定义队列2
	channel.QueueDeclare(queueName2, false, false, false, null);

	//将队列绑定到交换机
	channel.QueueBind(queueName1, exchangeName, routeKey, null);
	channel.QueueBind(queueName2, exchangeName, routeKey, null);

	//生成两个队列的消费者
	ConsumerGenerator(queueName1);
	ConsumerGenerator(queueName2);


	Console.WriteLine($"\nRabbitMQ连接成功,\n\n请输入消息,输入exit退出!");

	string input;
	do
	{
		input = Console.ReadLine();

		var sendBytes = Encoding.UTF8.GetBytes(input);
		//发布消息
		channel.BasicPublish(exchangeName, routeKey, null, sendBytes);

	} while (input.Trim().ToLower() != "exit");
	channel.Close();
	connection.Close();
}

/// <summary>
/// 根据队列名称生成消费者
/// </summary>
/// <param name="queueName"></param>
static void ConsumerGenerator(string queueName)
{
	//创建连接工厂
	ConnectionFactory factory = new ConnectionFactory
	{
		UserName = "admin",//用户名
		Password = "admin",//密码
		HostName = "192.168.157.130"//rabbitmq ip
	};

	//创建连接
	var connection = factory.CreateConnection();
	//创建通道
	var channel = connection.CreateModel();

	//事件基本消费者
	EventingBasicConsumer consumer = new EventingBasicConsumer(channel);

	//接收到消息事件
	consumer.Received += (ch, ea) =>
	{
		var message = Encoding.UTF8.GetString(ea.Body);

		Console.WriteLine($"Queue:{queueName}收到消息: {message}");
		//确认该消息已被消费
		channel.BasicAck(ea.DeliveryTag, false);
	};
	//启动消费者 设置为手动应答消息
	channel.BasicConsume(queueName, false, consumer);
	Console.WriteLine($"Queue:{queueName},消费者已启动");
}

运行:

路由模式(Direct)

路由模式下,在发布消息时指定不同的routeKey,交换机会根据不同的routeKey分发消息到不同的队列中。

申明一个routeKey值为key1,并在发布消息的时候告诉了RabbitMQ,消息传递时routeKey必须匹配,才会被队列接收否则消息会被抛弃。

生产者代码
Console.WriteLine("Hello, World! 生产者");

Console.WriteLine($"输入 routingKey:");
string routingKey = Console.ReadLine()!;

// 创建连接工厂对象
var factory = new ConnectionFactory() {
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};
var connection = factory.CreateConnection();    // 创建连接对象
var channel = connection.CreateModel();         // 创建连接会话对象

#region 定义交换机
string exchangeName = "exchange2";

channel.ExchangeDeclare(exchange: exchangeName, type: ExchangeType.Direct);
#endregion

string str;
do {
    Console.WriteLine("发送内容:");
    str = Console.ReadLine()!;

    byte[] body = Encoding.UTF8.GetBytes(str); // 消息内容

    channel.BasicPublish(exchangeName, routingKey, null, body); // 发送消息
} while (str.Trim().ToLower() != "exit");

channel.Close();
connection.Close();
消费者代码
Console.WriteLine("Hello, World! 消费者1");

Console.WriteLine($"输入接受key名称:");
string routeKey = Console.ReadLine()!;

var factory = new ConnectionFactory()       // 创建连接工厂对象
{
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};

IConnection connection = factory.CreateConnection();    // 创建连接对象
IModel channel = connection.CreateModel();         // 创建连接会话对象

#region 声明交换机
string exchangeName = "exchange2";
channel.ExchangeDeclare(exchangeName, ExchangeType.Direct);
#endregion

#region 声明队列
string queueName = exchangeName + "_" + new Random().Next(1, 1000);
Console.WriteLine("队列名称:" + queueName);

channel.QueueDeclare(
  queue: queueName,//消息队列名称
  durable: false,//是否持久化,true持久化,队列会保存磁盘,服务器重启时可以保证不丢失相关信息。
  exclusive: false,//是否排他,true排他的,如果一个队列声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除.
  autoDelete: false,//是否自动删除。true是自动删除。自动删除的前提是:致少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除.
  arguments: null ////设置队列的一些其它参数
);
#endregion

channel.QueueBind(queueName, exchangeName, routeKey); // 将队列与交换机绑定
channel.QueueBind(queueName, exchangeName, "key2"); 
channel.QueueBind(queueName, exchangeName, "key3"); // 可以通过绑定多个,来匹配多个路由 

// channel.BasicQos(0, 1, false);  // 告诉Rabbit每次只能向消费者发送一条信息,再消费者未确认之前,不再向他发送信息

// 创建消费者对象
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) => {

    byte[] message = ea.Body.ToArray();
    Console.WriteLine("接收到的消息为:" + Encoding.UTF8.GetString(message));

    channel.BasicAck(ea.DeliveryTag, true); // 开启返回消息确认
};

channel.BasicConsume(queue: queueName, autoAck: false, consumer); // 将autoAck设置false 关闭自动确认.

Console.ReadKey();
channel.Dispose();
connection.Close();

code describe

  • 一个接收者消息队列可以声明多个路由与交换机进行绑定

通配符模式(Topic)

通配符模式与路由模式一致,只不过通配符模式中的路由可以声明为模糊查询,RabbitMQ拥有两个通配符

  • #:匹配0-n个字符语句

  • *:匹配一个字符语句

注意:RabbitMQ中通配符并不像正则中的单个字符,而是一个以“.”分割的字符串,如 ”topic1.*“匹配的规则以topic1开始并且"."后只有一段语句的路由 例:“topic1.aaa”,“topic1.bb”而“#”可以匹配到 “topic1.aaa.bb”,“topic1.bb.cc”

生产者代码
Console.WriteLine("Hello, World! 生产者");

Console.WriteLine($"输入 routingKey:");
string routingKey = Console.ReadLine()!;


// 创建连接工厂对象
var factory = new ConnectionFactory() {
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};
var connection = factory.CreateConnection();    // 创建连接对象
var channel = connection.CreateModel();         // 创建连接会话对象

#region 定义交换机
string exchangeName = "exchange3";

channel.ExchangeDeclare(exchange: exchangeName, type: ExchangeType.Topic);
#endregion

string str;
do {
    Console.WriteLine("发送内容:");
    str = Console.ReadLine()!;

    byte[] body = Encoding.UTF8.GetBytes(str); // 消息内容

    channel.BasicPublish(exchangeName, routingKey, null, body); // 发送消息
} while (str.Trim().ToLower() != "exit");

channel.Close();
connection.Close();
消费者代码
Console.WriteLine("Hello, World! 消费者1");

Console.WriteLine($"输入接受key名称:");  // key.* 或者 key.#
string routeKey = Console.ReadLine()!;

var factory = new ConnectionFactory()       // 创建连接工厂对象
{
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};

IConnection connection = factory.CreateConnection();    // 创建连接对象
IModel channel = connection.CreateModel();         // 创建连接会话对象

#region 声明交换机
string exchangeName = "exchange3";
channel.ExchangeDeclare(exchangeName, ExchangeType.Topic);
#endregion

#region 声明队列
string queueName = exchangeName + "_" + new Random().Next(1, 1000);
Console.WriteLine("队列名称:" + queueName);

channel.QueueDeclare(
  queue: queueName,//消息队列名称
  durable: false,//是否持久化,true持久化,队列会保存磁盘,服务器重启时可以保证不丢失相关信息。
  exclusive: false,//是否排他,true排他的,如果一个队列声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除.
  autoDelete: false,//是否自动删除。true是自动删除。自动删除的前提是:致少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除.
  arguments: null ////设置队列的一些其它参数
);
#endregion

channel.QueueBind(queueName, exchangeName, routeKey); // 将队列与交换机绑定

// 创建消费者对象
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) => {

    byte[] message = ea.Body.ToArray();
    Console.WriteLine("接收到的消息为:" + Encoding.UTF8.GetString(message));
    channel.BasicAck(ea.DeliveryTag, true); // 开启返回消息确认
};

channel.BasicConsume(queue: queueName, autoAck: false, consumer); // 将autoAck设置false 关闭自动确认.

Console.ReadKey();
channel.Dispose();
connection.Close();

只有在通配符匹配通过的情况下才会接收消息

部分内容来自:

https://www.cnblogs.com/stulzq/p/7551819.html
https://www.cnblogs.com/shenghuotaiai/p/16170319.html

集群搭建文档:

https://www.cnblogs.com/weskynet/p/16890741.html

其他文档

--netcore下RabbitMQ队列、死信队列、延时队列及小应用
https://www.cnblogs.com/morec/p/17020566.html
posted @ 2022-11-10 11:02  雨水的命运  阅读(730)  评论(0编辑  收藏  举报