发布订阅模式
前面提到的工作队列,前提是假设每条消息都只有一个消费者,如果每条消息有多个消费者的话,就得使用发布订阅模式了
为了说明这种模式,这次将建立一个简单的日志系统,它将由两个程序组成--第一个程序将发送日志信息,第二个程序将接收并打印这些信息。
在我们的日志系统中,每个运行中的接收程序副本都会得到消息。这样,我们将能够运行一个接收器,并将日志写入磁盘;与此同时,我们将能够运行另一个接收器,并在屏幕上看到这些日志。
从本质上来讲,发布的日志信息将被广播给所有接收者。
交换机
在谈交换机之前,先回顾一下以下几个概念:
生产者(P):发送消息的用户应用程序
队列(Queue):接收消息的消息缓冲区
消费者(C):一个接收消息的用户应用程序
RabbitMQ的消息传递的核心思想是:生产者从不直接向队列发送任何消息。实际上,很多时候生产者甚至不知道一条消息是否会被传递到任何队列。
相反的,生产者只能将消息发送给交换机。交换机是一个非常简单的东西。交换机一边从生产者方接受数据,一边将消息推送到队列中。交换机必须明确的知道需要对接收到的信息如何进行处理。它应该被附加到许多队列中吗?它应该被丢弃吗?关于这些的规则是由交换机类型定义的。
这里有集中交换机类型可供选择:direct
,topic
,headers
,fanout
我们先看最后一个类型--fanout
,下面代码创建了一个fanout
类型的交换机,并且命名为logs
channel.ExchangeDeclare("logs", ExchangeType.Fanout);
fanout
翻译过来是扇形交换机,它非常简单。它所做的就是把它收到的所有消息广播到它知道的所有队列中,是所有类型的交换机中速度最快的,这个特性正是我们刚才提到的日志记录所需要的。
要在服务器上列出所有交换机,可以执行以下命令
sudo rabbitmqctl list_exchanges #返回的结果中,会有一些amq.*交换机和默认的(未命名的)交换机
默认交换机
以下代码没有定义交换机名字,但是也可以传递消息,这是因为我们使用的是一个默认的交换机,我们用空字符串来标识它
var message = GetMessage(args);
var body = Encoding.UTF8.GetBytes(message);
channel.BasicPublish(exchange: "", //这个参数就时我们使用的默认交换机,空字符串表示默认或无名交换
routingKey: "hello",
basicProperties: null,
body: body);
现在知道了交换机的概念后,就可以修改代码为:
var message = GetMessage(args);
var body = Encoding.UTF8.GetBytes(message);
channel.BasicPublish(exchange: "logs", //定义交换机名称
routingKey: "",
basicProperties: null,
body: body);
当我们想在生产者和消费者之间通过队列传递消息的时候,给队列取名字是很重要的,只有相同的名字才能让消费者和生产者指向同一个队列。但是对于我们现在想要实现的logger
来说,情况并非如此,我们想要监听所有和日志有关的消息,而不仅仅是其中的一个子集。我们也只对当前流动的消息感兴趣,而不是旧消息。为了解决这个问题我们需要做两件事情:
- 首先,每当我们连接到RabbitMQ时,我们需要一个新的空队列。为了达到这一点,我们可以用一个随机的名字创建一个队列,或者更好的方法是让服务器选择一个随机的队列名字给我们
- 其次,一旦我们断开连接,队列应该被自动删除
在.net 中,如果我们没有给队列声明QueueDeclare()
提供任何参数的时候,我们就会创建一个非持久的,独占的,自动删除的队列,其名称是自动生成的
var queueName = channel.QueueDeclare().QueueName;
我们已经创建了一个fanout
交换机和一个队列。现在我们需要告诉交换机将消息发送到我们的队列。交换机和队列之间的这种关系被称为绑定。
channel.QueueBind(queue: queueName,
exchange: "logs",
routingKey: "");
-
新建两个控制台项目
EmitLog
和ReceiveLogs
(win10环境,vs2022,.net core 6.0) -
EmitLog.cs
代码如下
using RabbitMQ.Client;
using System.Text;
var factory = new ConnectionFactory { HostName = "localhost" };
using (var connection = factory.CreateConnection())
using (var channel = connection.CreateModel())
{
//声明交换机信息
channel.ExchangeDeclare("logs", ExchangeType.Fanout);
for (int i = 0; i < 100; i++)
{
string message = $"[{i}]:Hello World!";
var body = Encoding.UTF8.GetBytes(message);
//往交换机投递消息
channel.BasicPublish(exchange: "logs",
routingKey: "",
basicProperties: null,
body: body);
Console.WriteLine($"{DateTime.Now:mm:ss:fff} 发送消息: [{message}]");
Thread.Sleep(500);
}
Console.ReadLine();
}
-
ReceiveLogs.cs
代码如下
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
var factory = new ConnectionFactory { HostName = "localhost" };
using (var connection = factory.CreateConnection())
using (var channel = connection.CreateModel())
{
channel.ExchangeDeclare(exchange: "logs",
type: ExchangeType.Fanout);
//自动生成一个队列,名称格式为amq.******
var queueName = channel.QueueDeclare().QueueName;
//使用上一步得到的队列名字,绑定到交换机,后续交换机收到消息的时候,会投递到该队列
channel.QueueBind(queue: queueName,
exchange: "logs",
routingKey: "");
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (sender, data) =>
{
var body = data.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine($"{DateTime.Now:mm:ss:fff} 收到消息: [{message}]");
};
channel.BasicConsume(queue: queueName,
autoAck: true,
consumer: consumer);
Console.ReadLine();
}
上述代码编译后,运行Emitlog,然后再使用命令行运行两个ReceiveLogs,得到结果如下图
fanout
类型的交换机,会在收到消息的时候,把消息投递给所有与他绑定的队列