跳至主内容

RabbitMQ 教程 - Topic

Topic

(使用 Go RabbitMQ 客户端)

信息

先决条件

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

哪里寻求帮助

如果您在学习本教程时遇到困难,可以通过 GitHub DiscussionsRabbitMQ 社区 Discord 联系我们。

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

虽然使用 direct 交换机改进了我们的系统,但它仍然存在局限性——它无法基于多个标准进行路由。

在我们的日志系统中,我们可能不仅想根据严重性订阅日志,还想根据发出日志的来源订阅日志。您可能从 syslog unix 工具中了解过这个概念,该工具根据严重性(info/warn/crit...)和设施(auth/cron/kern...)来路由日志。

这将为我们提供极大的灵活性——我们可能只想收听来自“cron”的关键错误,但也想收听来自“kern”的所有日志。

为了在我们的日志系统中实现这一点,我们需要了解一个更复杂的 topic 交换机。

Topic 交换机

发送到 topic 交换机的消息不能具有任意的 routing_key——它必须是由点分隔的单词列表。单词可以是任何内容,但通常它们指定与消息相关的一些特征。一些有效的路由键示例:stock.usd.nysenyse.vmwquick.orange.rabbit。路由键中的单词数量不限,最多 255 字节。

绑定键也必须采用相同的形式。topic 交换机背后的逻辑与 direct 交换机类似——具有特定路由键的消息将发送到所有与匹配绑定键绑定的队列。但是,绑定键有两个重要的特殊情况:

  • * (星号) 可以替代正好一个单词。
  • # (哈希) 可以替代零个或多个单词。

用一个例子来解释最简单

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

我们创建了三个绑定:Q1 绑定了绑定键 *.orange.*,Q2 绑定了 *.*.rabbitlazy.#

这些绑定可以总结为:

  • Q1 对所有橙色的动物感兴趣。
  • Q2 想听到关于兔子的一切,以及关于懒惰动物的一切。

路由键设置为 quick.orange.rabbit 的消息将发送到两个队列。消息 lazy.orange.elephant 也将发送到这两个队列。另一方面,quick.orange.fox 只会发送到第一个队列,而 lazy.brown.fox 只会发送到第二个队列。lazy.pink.rabbit 将只发送到第二个队列一次,即使它匹配两个绑定。quick.brown.fox 不匹配任何绑定,因此将被丢弃。

如果我们破坏了约定并发送一个单词或四个单词的消息,例如 orangequick.orange.new.rabbit,会发生什么?好吧,这些消息将不匹配任何绑定,并且将被丢失。

另一方面,lazy.orange.new.rabbit 尽管有四个单词,但将匹配最后一个绑定,并将被发送到第二个队列。

Topic 交换机

Topic 交换机功能强大,并且可以像其他交换机一样工作。

当一个队列用 # (哈希) 绑定键绑定时——它将接收所有消息,而不管路由键是什么——就像 fanout 交换机一样。

当绑定中不使用特殊字符 * (星号) 和 # (哈希) 时,topic 交换机将像 direct 交换机一样工作。

总而言之

我们将在日志系统中使用 topic 交换机。我们最初的假设是日志的路由键将有两个单词:<facility>.<severity>

代码与上一篇教程中的几乎相同。

emit_log_topic.go 的代码

package main

import (
"context"
"log"
"os"
"strings"
"time"

amqp "github.com/rabbitmq/amqp091-go"
)

func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}

func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close()

ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close()

err = ch.ExchangeDeclare(
"logs_topic", // name
"topic", // type
true, // durable
false, // auto-deleted
false, // internal
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare an exchange")

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

body := bodyFrom(os.Args)
err = ch.PublishWithContext(ctx,
"logs_topic", // exchange
severityFrom(os.Args), // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(body),
})
failOnError(err, "Failed to publish a message")

log.Printf(" [x] Sent %s", body)
}

func bodyFrom(args []string) string {
var s string
if (len(args) < 3) || os.Args[2] == "" {
s = "hello"
} else {
s = strings.Join(args[2:], " ")
}
return s
}

func severityFrom(args []string) string {
var s string
if (len(args) < 2) || os.Args[1] == "" {
s = "anonymous.info"
} else {
s = os.Args[1]
}
return s
}

receive_logs_topic.go 的代码

package main

import (
"log"
"os"

amqp "github.com/rabbitmq/amqp091-go"
)

func failOnError(err error, msg string) {
if err != nil {
log.Panicf("%s: %s", msg, err)
}
}

func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "Failed to connect to RabbitMQ")
defer conn.Close()

ch, err := conn.Channel()
failOnError(err, "Failed to open a channel")
defer ch.Close()

err = ch.ExchangeDeclare(
"logs_topic", // name
"topic", // type
true, // durable
false, // auto-deleted
false, // internal
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare an exchange")

q, err := ch.QueueDeclare(
"", // name
false, // durable
false, // delete when unused
true, // exclusive
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare a queue")

if len(os.Args) < 2 {
log.Printf("Usage: %s [binding_key]...", os.Args[0])
os.Exit(0)
}
for _, s := range os.Args[1:] {
log.Printf("Binding queue %s to exchange %s with routing key %s",
q.Name, "logs_topic", s)
err = ch.QueueBind(
q.Name, // queue name
s, // routing key
"logs_topic", // exchange
false,
nil)
failOnError(err, "Failed to bind a queue")
}

msgs, err := ch.Consume(
q.Name, // queue
"", // consumer
true, // auto ack
false, // exclusive
false, // no local
false, // no wait
nil, // args
)
failOnError(err, "Failed to register a consumer")

var forever chan struct{}

go func() {
for d := range msgs {
log.Printf(" [x] %s", d.Body)
}
}()

log.Printf(" [*] Waiting for logs. To exit press CTRL+C")
<-forever
}

接收所有日志

go run receive_logs_topic.go "#"

接收来自 kern 设施的所有日志

go run receive_logs_topic.go "kern.*"

或者如果你只想听到关于 critical 日志的信息

go run receive_logs_topic.go "*.critical"

您可以创建多个绑定

go run receive_logs_topic.go "kern.*" "*.critical"

要发出具有路由键 kern.critical 的日志,请键入:

go run emit_log_topic.go "kern.critical" "A critical kernel error"

尽情玩转这些程序吧。请注意,代码不会对路由键或绑定键做任何假设,您可能需要尝试使用两个以上的路由键参数。

(emit_log_topic.goreceive_logs_topic.go 的完整源代码)

接下来,在教程 6 中了解如何进行远程过程调用的往返消息传递。

© . This site is unofficial and not affiliated with VMware.