rabbitmq消息队列——发布订阅

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

rabbitmq消息队列——发布订阅
三、”发布订阅”
上⼀节的练习中我们创建了⼀个⼯作队列。

队列中的每条消息都会被发送⾄⼀个⼯作进程。

这节,我们将做些完全不同的事情——我们将发送单个消息发送⾄多个消费者。

这种模式就是⼴为⼈知的“发布订阅”模式。

为了说明这种模式,我们将构建⼀个简单的⽇志系统。

包括2个应⽤程序,⼀个传送⽇志消息另⼀个接收并打印这些消息。

我们的⽇志系统中每⼀个运作的接收端程序都会收到这些消息。

这种⽅式下,我们就可以运⾏⼀个接收端发送⽇志消息⾄硬盘,同时可以运⾏另⼀个接收端将⽇志打印到屏幕上。

理论上讲,已发布的⽇志消息将会被⼴播到所有的接收者。

交换器(Exchange)
之前的⼏节练习中我们发送接收消息都是在队列中进⾏,是时候介绍下RabbitMQ完整的消息传递模式了。

先来迅速的回顾下我们之前章节:
⼀个⽣产者就是⼀个⽤来发送消息的应⽤程序
⼀个队列好⽐存储消息的缓存buffer
⼀个消费者就是⼀个⽤户应⽤程序⽤来接收消息
RabbitMQ消息传递模型的核⼼思想是⽣产者从来不会直接发送消息⾄队列。

事实上,⽣产者经常都不知道消息会被分发⾄哪个队列。

相反的是,⽣产者仅仅发送消息⾄交换器。

交换器是⾮常简单的东西:⼀边从⽣产者那边接收消息⼀边发送这些消息⾄队列。

交换器必须准确的知道这些被接收的消息该如何处理。

它应该被添加到某个特定队列?或者添加到多个队列?甚⾄直接放弃。

具体的传输规则就是通过交换器类型来定义的。

交换器类型有四种:direct、topic、headers、fanout。

这节我们主要关注最后⼀种——fanout。

让我们来创建⼀个fanout类型的交换器,命名为logs:
err = ch.ExchangeDeclare(
"logs", // name
"fanout", // type
true, // durable
false, // auto-deleted
false, // internal
false, // no-wait
nil, // arguments
)
正如你从名字中猜测的⼀样,它仅仅⼴播所有消息到所有已知的接收队列。

实际上这正是我们需要的⽇志系统。

备注:之前的⼏节练习中我们并不知道交换器,但我们依然能够将消息发送⾄队列中,之所以可以实现是因为我们使⽤了默认的交换器,使⽤空字符串表⽰。

回顾下之前我们发送消息是这样⼦的:
err = ch.Publish(
"", // exchange
, // routing key
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(body),
})
这⾥我们可以使⽤默认也可以⾃⼰命名交换器:如果路由键存在的话,消息会被路由到加上路由键参数的地址,注意fanout类型会直接忽略路由键的存在。

以下是修改后的代码:
err = ch.ExchangeDeclare(
"logs", // name 定义⼀个名为logs的交换器
"fanout", // type 交换器类型为fanout即⼴播类型
true, // durable 持久化
false, // auto-deleted ⽆队列绑定时是否⾃动删除
false, // internal
false, // no-wait
nil, // arguments
)
failOnError(err, "Failed to declare an exchange")
body := bodyFrom(os.Args)
err = ch.Publish(
"logs", // exchange 指定消息发送的交换器名称
"", // routing key 因为fanout类型会⾃动忽略路由键,所以这⾥的路由键参数任意,⼀般不填
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
Body: []byte(body),
})
临时队列
你可能记得之前我们声明队列的时候都会指定⼀个队列名称(记得hello和task_queue?)。

队列的命名对我们来说⾄关重要——我们需要将⼯作进程指向同⼀个队列。

当你需要在消费者和⽣产者之间共享队列的话声明队列就显得很重要。

但这对我们的⽇志系统来说⽆关重要。

我们需要监听的是所有的⽇志消息,⽽不是他们中的某⼀类。

我们只关注当前流中的消息⽽不关注旧的那些。

解决这个我们需要做两件事。

⾸先,每当链接RabbitMQ的时候我们需要创建⼀个新的、空的队列。

为做到这点,我们必须创建⼀个名称随机的队列,甚⾄更好的实现⽅式是——让服务端给我们⾃动⽣成⼀个随机的队列。

其次,⼀旦消费者链接断开,该队列便会⾃动删除。

在amqp客户端中,当我们给⼀个队列名称设定为空字符串时,我们就创建了⼀个⾮持久化的⽣成队列:
q, err := ch.QueueDeclare(
"", // name 满⾜第⼀点:服务端⾃动产⽣随机队列
false, // durable
false, // delete when usused
true, // exclusive 满⾜第⼆点:连接断开⽴即删除
false, // no-wait
nil, // arguments
)
当该⽅法返回的时候,声明好的队列便包含⼀个由RabbitMQ⽣成的随机队列名称。

举例来说,队列名称形如:amq.gen-JzTY20BRgKO-HjmUJj0wLg这种的。

当消费者的链接宣布关闭后,队列便像exclusive参数设置的那样,⾃动删除。

绑定
我们已经创建了⼀个fanout类型的交换器和⼀个队列,现在我们需要告诉交换器将消息发送⾄我们的队列。

这种交换器和队列中的关联关系就叫做绑定。

err = ch.QueueBind(
, // queue name 绑定的队列名称
"", // routing key 绑定的路由键
"logs", // exchange 绑定的交换器名称
false,
nil
)
从现在起,logs交换器便能发送消息⾄我们的队列。

糅合在⼀起
⽣产者的程序,也就是发送消息端,跟之前⼏节的发送代码差不多。

最重要的是我们现在要发送消息到logs交换器⽽⾮默认的交换器。

发送的时候我们可以设置⼀个路由键,但是对于fanout类型的交换器来说它将被忽略。

下⾯就是发送⽇志⽅的代码:
// rabbitmq_3_emit_log.go project main.go
package main
import (
"log"
"os"
"strings"
"/streadway/amqp"
)
func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s:%s", err, msg)
panic(fmt.Sprintf("%s:%s", err, msg))
}
}
func bodyForm(args []string) string {
var s string
if len(args) < 2 || os.Args[1] == "" {
s = "Hello World! This is a test!"
} else {
s = strings.Join(args[1:], "")
}
return s
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "failed to dial rabbitmq server")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "failed to declare the channel")
defer ch.Close()
//声明⼀个交换器,交换器名称logs,类型fanout
err = ch.ExchangeDeclare("logs", "fanout", true, false, false, false, nil)
failOnError(err, "failed to declare the exchange")
body := bodyForm(os.Args)
//发送消息到交换器
err = ch.Publish("logs", "", false, false, amqp.Publishing{
Body: []byte(body),
ContentType: "text/plain",
})
failOnError(err, "failed to publish the message")
}
备注:这⾥发送⽅并不需要声明队列之类的,不像之前的代码需要声明,这⾥的发送⽅唯⼀关联的是交换器,所以只需声明交换器并发送消息⾄交换器即可。

正如你想的那样,链接建⽴后我们声明交换器,这⼀步是必须的因为发送消息到⼀个不存在的交换器是完全禁⽌的。

如果该交换器上⾯没有队列绑定的话那么发送⾄该交换器的消息将全部丢失,但这对我们来时ok;如果没有消费者我们会安全地丢弃这些消息。

下⾯是⽇志接收⽅的代码:
// rabbitmq_3_receive_logs.go project main.go
package main
import (
"fmt"
"log"
"os"
"strings"
"/streadway/amqp"
)
func failOnError(err error, msg string) {
if err != nil {
log.Fatalf("%s:%s", err, msg)
panic(fmt.Sprintf("%s:%s", err, msg))
}
}
func bodyForm(args []string) string {
var s string
if len(args) < 2 || os.Args[1] == "" {
s = "Hello World! This is a test!"
} else {
s = strings.Join(args[1:], "")
}
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
failOnError(err, "failed to dial rabbitmq server")
defer conn.Close()
ch, err := conn.Channel()
failOnError(err, "failed to declare the channel")
defer ch.Close()
//声明⼀个交换器,交换器名称logs,类型fanout
err = ch.ExchangeDeclare("logs", "fanout", true, false, false, false, nil)
failOnError(err, "failed to declare the exchange")
//声明⼀个队列
q, err := ch.QueueDeclare("", false, false, true, false, nil)
failOnError(err, "failed to declare the queue")
//设置绑定(第⼆个参数为路由键,这⾥为空)
err = ch.QueueBind(, "", "logs", false, nil)
failOnError(err, "failed to bind the queue")
//注册⼀个消费者
msgs, err := ch.Consume(, "", true, false, false, false, nil)
failOnError(err, "Failed to register a consumer")
forever := make(<-chan bool)
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.go > logs_from_rabbit.log
如果你仅仅想在屏幕上查看⽇志,开启⼀个新的控制台执⾏如下命令:
go run receive_logs.go
当然了,你最后还要发出⽇志才⾏:
go run emit_log.go
使⽤rabbitmqctl list_bindings命令可以直接查看所有的绑定,如运⾏2个receive_logs.go程序你就会看到如下输出:rabbitmqctl list_bindings
Listing bindings ...
logs exchange amq.gen-JzTY20BRgKO-HjmUJj0wLg queue []
logs exchange amq.gen-vso0PVvyiRIL2WoV3i48Yg queue []
...done.
实际效果:
分别开启两个控制台,均监听相同队列,同时收到消息并打印了,说明两个随机的队列均收到了logs交换器发来的消息,发送⽅略。

相关文档
最新文档