开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情
今天接着来学习下RabbitMQ中的发布和订阅,上一篇文章中介绍了如何创建一个工作队列,每个任务只能发送一个消费者,那么接下来就要实现一个任务多个消费者都可以接收到,这就是今天要介绍的订阅/发布模式
发布和订阅
通过搭建一个简单的日志系统来说明这个模式,这个日志系统由两个程序组成:一个发送日志,另一个接收消息并且输出日志信息,相当于已经发布的日志消息将被广播到所有消费者手里。
Exchanges-交换器
之前的文章中讲到,生产者是发送消息的程序,队列是消息存储的缓冲区,消费者是接收消息的程序 RabbitMQ消息传递模型中核心思想是不将消息直接发送到队列上,生产者不知道消息是否传递到队列上
真实情况下,生产者只能将消息发送到Exchanges交换器,交换器的作用一是接收来自生产者的消息,另一面方面将消息送入队列中,不同类型的交换器定义有不同的处理消息的规则,例如是直接添加到特定的队列,还是添加其他多个队列,或者直接丢弃等。
交换器类型:
- direct
- topic
- headers
- fanout
下面以fanout类型交换器为例子,它的作用是将接收到的所有消息广播到所有已知的队列中
err = ch.ExchangeDeclare(
"logs", // name
"fanout", // type
true, // durable
false, // auto-deleted
false, // internal
false, // no-wait
nil, // arguments
)
回想下之前是怎么发布消息的,使用了一个默认的交换器,该交换器没有名称,以route_key参数指定的名称路由到队列
err = ch.Publish(
"", // exchange
q.Name, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(body),
})
现在可以改为发布到我们自己的定义的交换器
err = ch.ExchangeDeclare(
"logs", // 使用命名的交换器
"fanout", // 交换器类型
true, // durable
false, // auto-deleted
false, // internal
false, // no-wait
nil, // arguments
)
DoError(err, "Failed to declare an exchange")
content := bodyFrom(os.Args)
err = ch.Publish(
"logs", // exchange
"", // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(content),
})
临时队列
之前使用的具有特定名称的队列,传递一个空字符串作为队列名称时,会随机生成的名称创建一个非持久队列,一旦消费者断开连接,队列就会自动删除
q, err := ch.QueueDeclare(
"", // 空字符串作为队列名称
false, // 非持久队列
false, // delete when unused
true, // 独占队列(当前声明队列的连接关闭后即被删除)
false, // no-wait
nil, // arguments
)
绑定
我们已经创建了一个fanout类型的交换器和一个队列。现在我们需要告诉交换器将消息发送到我们的队列,交换器和队列之间的联系叫做绑定
err = ch.QueueBind(
q.Name, // queue name
"", // routing key
"logs", // exchange
false,
nil,
)
生产者的代码最重要的是将消息发布到logs交换器,而不是空的消息交换器。
发送时,我们需要提供一个routingKey,但是对于fanout型交换器,它的值可以被忽略(传空字符串)
func main() {
conn, err := amqp.Dial("amqp://admin:admin@localhost:5672/")
DoError(err, "Failed connect to RabbitMQ!")
defer conn.Close()
ch, err := conn.Channel()
DoError(err, "Failed to open a channel")
defer ch.Close()
//这里注释
//queue, err := ch.QueueDeclare(
// "YYQQ",
// true,
// false,
// false,
// false,
// nil,
//)
//DoError(err, "Failed to declare a queue")
err = ch.ExchangeDeclare(
"logs",
"fanout",
true,
false,
false,
false,
nil)
DoError(err, "Failed to declare a exchange")
content := getParam(os.Args) //从命令行中返回数据
err = ch.Publish(
"logs",
"",
false,
false,
amqp.Publishing{
DeliveryMode: amqp.Persistent,
ContentType: "text/plain",
Body: []byte(content),
})
DoError(err, "Failed to publish a message")
}
注意: 禁止发布到未声明的交换器,如果没有队列绑定交换器则消息丢失,这个做法是安全的,因为没有消费者接收就说明没有人需要订阅这个消息,直接丢弃即可。
func main() {
conn, err := amqp.Dial("amqp://admin:admin@localhost:5672/")
DoError(err, "Failed to connect to RabbitMQ")
defer conn.Close()
ch, err := conn.Channel()
DoError(err, "Failed to open a channel")
defer ch.Close()
err = ch.ExchangeDeclare(
"logs",
"fanout",
true,
false,
false,
false,
nil)
DoError(err, "Failed to declare a exchange")
queue, err := ch.QueueDeclare(
"", // name
false, // durable
false, // delete when unused
true, // exclusive
false, // no-wait
nil, // arguments
)
DoError(err, "Failed to declare a queue")
fmt.Println("fmt.name",queue.Name)
//绑定交换器
err = ch.QueueBind(
queue.Name,
"",
"logs",
false,
nil)
DoError(err, "Failed to bind a queue")
content, err := ch.Consume(
queue.Name, // queue
"", // consumer
true, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
DoError(err, "Failed to register a consumer")
var wait chan struct{}
go func() {
for msg := range content {
log.Printf("Received a message %s", msg.Body)
}
}()
<-wait
}
如果将日志保存到文件,就可以运行下面代码
go run send.go : 发送日志
go run receive.go > logs.log
如果想直接在控制台上查看日志,可以终端上输入
go run receive.go
使用rabbitmqctl list_bindings命令,可以查看绑定关系和队列,开启两个receive.go程序会有两个logs绑定信息,如下图所示
消费者一
消费者二
总结
今天浅谈了Go与RabbitMQ(三),还有很多相关的知识后面会继续深入,对于刚入门go语言的我来说,还有许多地方需要学习,有错误的地方欢迎大家指出,共同进步!!