发布订阅模式

前面提到的工作队列,前提是假设每条消息都只有一个消费者,如果每条消息有多个消费者的话,就得使用发布订阅模式了

为了说明这种模式,这次将建立一个简单的日志系统,它将由两个程序组成--第一个程序将发送日志信息,第二个程序将接收并打印这些信息。

在我们的日志系统中,每个运行中的接收程序副本都会得到消息。这样,我们将能够运行一个接收器,并将日志写入磁盘;与此同时,我们将能够运行另一个接收器,并在屏幕上看到这些日志。

从本质上来讲,发布的日志信息将被广播给所有接收者。

交换机

在谈交换机之前,先回顾一下以下几个概念:

生产者(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: "");
  • 新建两个控制台项目EmitLogReceiveLogs(win10环境,vs2022,.net core 6.0)
    RabbitMQ-发布订阅模式&Fanout Exchange_c#

  • 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,得到结果如下图

RabbitMQ-发布订阅模式&Fanout Exchange_应用程序_02

fanout类型的交换机,会在收到消息的时候,把消息投递给所有与他绑定的队列