Routing

167 阅读4分钟

原文链接

上一个教程中,我们建立了一个简单的日志系统,将消息广播给多个消费者。

在本教程中,我们将给它添加一个功能 - 允许订阅部分消息,而不是所有的消息全部都要。比如,所有的消息会在控制台中打印,但其中的错误信息我们需要将其保存到磁盘中。

Bindings

在上一节中,我们已经成功将交换机和队列绑定了

err = ch.QueueBind(
  q.Name, // queue name
  "",     // routing key
  "logs", // exchange
  false,
  nil)

简单来说就是,这个队列只关心这个交换机中的消息。

在进行队列绑定时,还有一个 routing key 的参数。为了和 Channel.Publish 区别,本教程中会称其为 binding key

err = ch.QueueBind(
  q.Name,    // queue name
  "black",   // routing key
  "logs",    // exchange
  false,
  nil)

binding key 并不是对于所有的交换机类型都生效,fanout类型的交换机就会忽略它的值

Direct exchange

区别于上一个教程,我们想要根据消息的严重性对其进行筛选: 将错误日志写入磁盘,而普通或者警告消息不要写入到磁盘中,以节省磁盘消耗。

fanout类型的交换机,并不能满足这样的需求 - 它只是单纯的将所有的消息发送给所有的消费者,不能进行筛选。

为了实现这样的效果,需要使用 direct 类型的交换机,它的实现逻辑很简单 - 只有当banding key 和消息的routing key相匹配时,消息才会加入到绑定的队列中。

direct-exchange

图例中,direct类型的交换机 x, 绑定了两个队列,Q1的 binding key 为 orangeQ2的 binding key 为blackgreen

后续发送routing keyorange 的消息就会被分发到 Q1blackgreen 会被分发到 Q2 中,而其他没有 routing key的消息都会被丢弃。

Multiple bindings

direct-exchange-multiple

direct 交换机使用同一个 binding key 绑定多个不同的队列是完全合法的。Q1Q2 都使用 black 绑定到交换机上,那么就会表现得像fanout类型的交换机一样,所有的队列都将收到消息, 因为它们的 binding key 是一样的

Emitting logs

这次使用 direct类型交换机来代替 fanout类型的交换机,将日志的严重程度作为 routing key ,据此会发送到不同的队列中。

声明交换机

err = ch.ExchangeDeclare(
  "logs_direct", // name
  "direct",      // type
  true,          // durable
  false,         // auto-deleted
  false,         // internal
  false,         // no-wait
  nil,           // arguments
)

发送消息

err = ch.ExchangeDeclare(
  "logs_direct", // name
  "direct",      // 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_direct",         // exchange
  severityFrom(os.Args), // routing key
  false, // mandatory
  false, // immediate
  amqp.Publishing{
    ContentType: "text/plain",
    Body:        []byte(body),
})

日志的严重程度定义为: info, warningerror

Subscribing

将接收到的严重程度作为 routing key 作为绑定队列的参数

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 [info] [warning] [error]", 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_direct", s)
  err = ch.QueueBind(
    q.Name,        // queue name
    s,             // routing key
    "logs_direct", // exchange
    false,
    nil)
  failOnError(err, "Failed to bind a queue")
}

Putting it all together

python-four

package main

import (
	"context"
	amqp "github.com/rabbitmq/amqp091-go"
	"log"
	"os"
	"strings"
	"time"
)

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. 声明 direct 交换机
	err = ch.ExchangeDeclare(
		"logs_direct",
		"direct",
		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_direct",
		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 = "info"
	} else {
		s = os.Args[1]
	}
	return s
}
package main

import (
	amqp "github.com/rabbitmq/amqp091-go"
	"log"
	"os"
)

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

func main() {
	// 1. 建立连接
	conn, err := amqp.Dial("amqp://admin:admin@192.168.2.14: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. 声明 direct 交换机
	err = ch.ExchangeDeclare(
		"logs_direct",
		"direct",
		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 queue")

	// 5. 判断参数合法性
	if len(os.Args) < 2 {
		log.Printf("Usage: %s [info] [warning] [error]", os.Args[0])
		os.Exit(0)
	}

	// 6. 绑定队列
	for _, s := range os.Args[1:] {
		log.Printf("Binding quque %s to the exchange %s with routing key %s",
			q.Name, "logs_direct", s)

		err = ch.QueueBind(
			q.Name,
			"s",
			"logs_direct",
			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")

	var forever chan struct{}

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

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