在上一个教程中,我们建立了一个简单的日志系统,将消息广播给多个消费者。
在本教程中,我们将给它添加一个功能 - 允许订阅部分消息,而不是所有的消息全部都要。比如,所有的消息会在控制台中打印,但其中的错误信息我们需要将其保存到磁盘中。
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类型的交换机 x, 绑定了两个队列,Q1的 binding key 为 orange, Q2的 binding key 为black 和 green。
后续发送routing key 为 orange 的消息就会被分发到 Q1, black 和 green 会被分发到 Q2 中,而其他没有 routing key的消息都会被丢弃。
Multiple bindings

direct 交换机使用同一个 binding key 绑定多个不同的队列是完全合法的。Q1 和 Q2 都使用 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, warning 和 error。
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

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
}