Topics

112 阅读4分钟

原文链接

在前面的教程中,使用 fanout类型的交换机实现了广播消息, direct 交换机对消息进行筛选。

虽然 direct 交换机已经可以帮我们实现消息分类和筛选,但还是不够灵活。

假设我们日志系统希望可以增加一个维度来区别消息,仅仅使用日志的错误严重性不太够,还需要增加一个日志来源加以区分。

来自 corn系统的消息,我们只关心错误日志,而来自 kern 系统的话,我们则需要所有的日志信息。

RabbitMQ 提供了 topic 交换机来实现这种效果。

Topic exchange

topic 交换机的 routing key的值有一定的要求 - 使用点.来分割的一组单词。一般使用和消息内容相关的一组单词。比如: stock.usd.nysenyse.vmw, quick.orange.rabbit ,长度只要不超过 255 个字节都可以。

binding keyrouting key 的格式相同。topicdirect 类型的交换机有一点相似 - 消息的routing key 和队列绑定的binding key相匹配时,会将其加入到队列之中。

有 2 种特殊的格式需要注意:

  • * 通配符,可以表示所有的单词
  • # 可以表示一个或者多个单词

python-five

假设我们现在需要发送所有关于动物的消息,这些消息将会根据动物的描述信息来进行区分。行动速度.颜色.物种 的格式进行区分。

上图中有三个分发消息的规则,颜色 为橙色的动物需要分发到 Q1 队列中,而物种为 兔子和 动作速度缓慢的动物都分发到 Q2 队列中。

如果此时有一个 routing keyquick.orange.rabbit,同时满足binding key ,所以 Q1Q2 都会收到该消息。同理 lazy.orange.elephant 也会同时分发到两个队列中,而 quick.orange.fox 只满足了 Q1binding key ,只有 Q1才会收到该消息。lazy.brown.fox 只会分发到 Q2 队列中。lazy.pink.rabbit - 虽然同时满足 Q2的两个 binding key,但只会向 Q2 队列添加一次。而 binding keyquick.brown.fox 的消息,则会被丢弃,因为无法找到满足条件的 binding key

当消息的 routing key 不符合 binding key 的规则时会发生什么? 单个单词orange 或者四个单词 quick.orange.new.rabbit,不符合 binding key 中的任何一个规则,那么它就会被丢弃掉。

那么lazy.orange.new.rabbit这个又会发生什么?它会被分发到 Q2 队列中,因为 Q2binding key 规则中有一个#,表示 lazy 后面接多少都是满足的,只要是lazy.开头即可。

Topic 类型交换机

topic 类型的交换机十分灵活,可以模拟其他类型的交换机

  • binding key 设置成 #,那么就会接收所有的消息,就像是 fanout类型的交换机一样
  • binding key 中不要设置 * 或者 #,那么它又会像 direct 类型的交换机一样了

Putting it all together

将原来的日志系统使用topic交换机改造一下,规定binding key格式: <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() {
	// 1.建立连接
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "Failed to connect RabbitMQ")
	defer conn.Close()

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

	// 3. 声明交换机
	err = ch.ExchangeDeclare(
		"logs_topic",
		"topic",
		true,
		false,
		false,
		false,
		nil)
	failOnError(err, "Failed to declare an exchange")

	// 4.创建上下文
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// 5.构造消息体
	body := bodyFrom(os.Args)

	// 6.发送消息
	err = ch.PublishWithContext(ctx,
		"logs_topic",
		severityFrom(os.Args),
		false,
		false,
		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() {
	// 1.建立连接
	conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
	failOnError(err, "Failed to connect RabbitMQ")
	defer conn.Close()

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

	// 3.声明交换机
	err = ch.ExchangeDeclare(
		"logs_topic",
		"topic",
		true,
		false,
		false,
		false,
		nil)
	failOnError(err, "Failed to declare an exchange")

	// 4.声明队列
	q, err := ch.QueueDeclare("",
		false,
		false,
		true,
		false,
		nil)
	failOnError(err, "Failed to declare a quque")

	// 5.获取参数
	if len(os.Args) < 2 {
		log.Printf("Usage: %s [binding_key]...", os.Args[0])
		os.Exit(0)
	}

	// 6.绑定队列
	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,
			s,
			"logs_topic",
			false,
			nil)
		failOnError(err, "Failed to bind a queue")
	}

	// 7.创建消费者
	msgs, err := ch.Consume(
		q.Name,
		"",
		true,
		false,
		false,
		false,
		nil)
	failOnError(err, "Failed to register a consumer")

	// 8.监听消息,避免退出
	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
}

完成代码: emit_log_topic.goreceive_logs_topic.go

启动接收程序

// 接收所有消息
go run receive_logs_topic.go "#"

// 接收来自 `kern` 的消息
go run receive_logs_topic.go "kern.*"

// 接收类型为 `critical` 的消息
go run receive_logs_topic.go "*.critical"

// 绑定多个key
go run receive_logs_topic.go "kern.*" "*.critical"

启动发送消息程序

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

发送消息后,上述所有的接收端都会接收到消息