(五)「消息队列」之 RabbitMQ 主题(使用 .NET 客户端)

0、引言

先决条件

本教程假设 RabbitMQ 已安装并且正在 本地主机标准端口5672)上运行。如果您使用了不同的主机、端口或凭证,则要求调整连接设置。

获取帮助

如果您在阅读本教程时遇到问题,可以通过邮件列表或者 RabbitMQ 社区 Slack 与 RabbitMQ 官方取得联系。

上一篇教程中我们改进了日志系统。我们没有使用只能进行虚拟广播的 fanout 交换机,而是使用了 direct 直连交换机,从而获得了选择性接收日志的可能性。

尽管使用了 direct 直连交换机改进了我们的系统,但它仍然有局限性 ------ 它不能基于多个标准进行路由。

在我们的日志系统中,我们可能不仅希望订阅根据严重程度区分的日志,还希望订阅根据来源区分的日志。您可能从 syslog unix 工具中了解过这个概念,它根据严重程度(info/warn/crit...)和设备(auth/cron/kern...)路由日志。

这可以给我们很大的灵活性 ------ 我们也许希望仅了解来自于 'corn' 的关键错误,但同样也希望了解来自于 'kern' 的所有日志。

为了在我们的日志系统中实现这一点,我们需要学习一种更复杂的 topic 主题交换机。

原文链接:https://www.rabbitmq.com/tutorials/tutorial-five-dotnet.html

1、主题交换机

发送给 topic 主题交换机的消息不能有任意的路由键 ------ 它只能是一个由点分隔的单词列表。单词可以是任何东西,但通常它们指定与消息相关的一些特征。下面是一些有效的路由键示例:"stock.usd.nyse"、"nyse.vmw"、"quick.orange.rabbit"。路由键中可以有任意多的单词,最多不超过 255 字节。

绑定键也必须是同样的形式。topic 主题交换机背后的逻辑与 direct 直连交换机类似 ------ 使用特定 routing key 路由键发送的消息将被传递给使用匹配 binding key 绑定键绑定的所有队列。但是,绑定键有两种重要的特殊情况:

  • *(星号)只能匹配一个单词。
  • #(井号)可以匹配零或者多个单词。

用一个例子来解释这一点最简单:

在这个例子当中,我们将会发送一些均用来描述动物的消息。我们将会使用包含三个单词(两个点)的 routing key 路由键发送这些消息。路由键中的第一个单词用于描述速度,第二个描述颜色以及第三个描述物种:"<speed>.<colour>.<species>"。

我们创建三个绑定:Q1 使用 "*.orange.*" 绑定键绑定;Q2 使用 "*.*.rabbit" 和 "lazy.#" 绑定。

这些绑定可以被总结为:

  • Q1 对所有的橙色动物感兴趣。
  • Q2 想知道关于兔子以及懒惰动物的一切信息。
  1. 带有 "quick.orange.rabbit" 路由键的消息将被同时传递给两个队列。"lazy.orange.elephant" 消息同样也会同时前往两个队列。
  2. 另一方面,"quick.orange.fox" 只会前往 Q1,而 "lazy.brown.fox" 只会前往 Q2
  3. "lazy.pink.rabbit" 只会被传递给 Q2 一次,即便它与两个绑定匹配。
  4. "quick.brown.fox" 不匹配任何绑定,因此将被丢弃。

如果我们违反约定,发送带有一个或者四个单词的消息(比如 "orange" 或者 "quick.orange.new.rabbit"),会发生什么呢?好吧,这些消息将不会匹配任何绑定并且丢失。

另一方面,即便 "lazy.orange.new.rabbit" 有四个单词,但仍然匹配最后一个绑定,所以将会被传递给 Q2 队列。

主题交换机

主题交换机十分强大并且能够模拟其他交换机。

当一个队列使用 "#"(井号)绑定键绑定时 ------ 它将会忽略路由键接收所有消息 ------ 就像 fanout 扇出交换机那样。

当绑定中不使用 "*"(星号)与 "#"(井号)特殊字符时,topic 主题交换机将会表现得像是 direct 直连交换机。

2、将所有的东西放到一起

我们将在日志系统中使用 topic 主题交换机。我们将从一个工作假设开始:即日志的路由键将有两个单词:"<facility>.<severity>"。

代码几乎与上一篇教程中一致。

EmitLogTopic.cs 的代码:

csharp 复制代码
using System.Text;
using RabbitMQ.Client;

var factory = new ConnectionFactory { HostName = "localhost" };

using var connection = factory.CreateConnection();
using var channel = connection.CreateModel();

channel.ExchangeDeclare(exchange: "topic_logs", type: ExchangeType.Topic);

var routingKey = (args.Length > 0) ? args[0] : "anonymous.info";
var message = (args.Length > 1)
              ? string.Join(" ", args.Skip(1).ToArray())
              : "Hello World!";
var body = Encoding.UTF8.GetBytes(message);
channel.BasicPublish(exchange: "topic_logs",
                     routingKey: routingKey,
                     basicProperties: null,
                     body: body);
Console.WriteLine($" [x] Sent '{routingKey}':'{message}'");

ReceiveLogsTopic.cs 的代码:

csharp 复制代码
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;

var factory = new ConnectionFactory { HostName = "localhost" };

using var connection = factory.CreateConnection();
using var channel = connection.CreateModel();

channel.ExchangeDeclare(exchange: "topic_logs", type: ExchangeType.Topic);
// declare a server-named queue
var queueName = channel.QueueDeclare().QueueName;

if (args.Length < 1)
{
    Console.Error.WriteLine("Usage: {0} [binding_key...]",
                            Environment.GetCommandLineArgs()[0]);
    Console.WriteLine(" Press [enter] to exit.");
    Console.ReadLine();
    Environment.ExitCode = 1;
    return;
}

foreach (var bindingKey in args)
{
    channel.QueueBind(queue: queueName,
                      exchange: "topic_logs",
                      routingKey: bindingKey);
}

Console.WriteLine(" [*] Waiting for messages. To exit press CTRL+C");

var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
    var body = ea.Body.ToArray();
    var message = Encoding.UTF8.GetString(body);
    var routingKey = ea.RoutingKey;
    Console.WriteLine($" [x] Received '{routingKey}':'{message}'");
};
channel.BasicConsume(queue: queueName,
                     autoAck: true,
                     consumer: consumer);

Console.WriteLine(" Press [enter] to exit.");
Console.ReadLine();

运行如下示例:

接收所有日志:

shell 复制代码
cd ReceiveLogsTopic
dotnet run "#"

接收来自 "kern" 的所有日志:

shell 复制代码
cd ReceiveLogsTopic
dotnet run "kern.*"

或者您只想知道 "critical" 日志:

shell 复制代码
cd ReceiveLogsTopic
dotnet run "*.critical"

您可以创建多个绑定:

shell 复制代码
cd ReceiveLogsTopic
dotnet run "kern.*" "*.critical"

并发出带有 "kern.critical" 类型路由键的日志:

shell 复制代码
cd EmitLogTopic
dotnet run "kern.critical" "A critical kernel error"

运行效果:

😀 玩儿的开心!注意,代码没有对路由键或者绑定键做任何假设,您可能希望试试使用两个以上的路由键参数。

EmitLogTopic.csReceiveLogsTopic.cs 的完整源码)

接下来,在教程六中了解如何将往返消息作为远程过程调用。

5、生产[非]适用性免责声明

请记住,本教程和其他教程都是教程。他们一次展示一个新概念,可能会有意地过度简化一些东西,而忽略其他东西。例如,为了简洁起见,连接管理、错误处理、连接恢复、并发性和指标收集等主题在很大程度上被省略了。这种简化的代码不应该被认为可以用于生产。

在发布您的应用之前,请先查看其他文档。我们特别推荐以下指南:发布者确认和消费者确认生产清单监控

相关推荐
Java Fans1 小时前
C# 中串口读取问题及解决方案
开发语言·c#
盛派网络小助手1 小时前
微信 SDK 更新 Sample,NCF 文档和模板更新,更多更新日志,欢迎解锁
开发语言·人工智能·后端·架构·c#
一只小灿灿1 小时前
VB.NET在 Excel 二次开发中的全面应用
.net·excel
码农君莫笑1 小时前
信管通低代码信息管理系统应用平台
linux·数据库·windows·低代码·c#·.net·visual studio
鲤籽鲲2 小时前
C# Random 随机数 全面解析
android·java·c#
zquwei3 小时前
SpringCloudGateway+Nacos注册与转发Netty+WebSocket
java·网络·分布式·后端·websocket·网络协议·spring
fkdw5 小时前
C# Newtonsoft.Json 反序列化派生类数据丢失问题
c#·json
道一云黑板报6 小时前
Flink集群批作业实践:七析BI批作业执行
大数据·分布式·数据分析·flink·kubernetes
浅尝辄止;8 小时前
C# 异步编程
c#