1. 简介
前面的教程中,我们发送消息到队列并从中取出消息。现在是时候介绍RabbitMQ中完整的消息模型了。
让我们简单的概括一下之前的教程:
发布者(producer)是发布消息的应用程序。
队列(queue)用于消息存储的缓冲。
消费者(consumer)是接收消息的应用程序。
RabbitMQ核心概念,发布者是将消息直接发送给交换机,由交换机来决定消息是发送到哪个队列,或者是忽略消息
发布者(producer)只需要把消息发送给一个交换机(exchange)。交换机非常简单,它一边从发布者方接收消息,一边把消息推送到队列。交换机必须知道如何处理它接收到的消息,是应该推送到指定的队列还是是多个队列,或者是直接忽略消息。这些规则是通过交换机类型(exchange type)来定义的,可以参见下图:
而前两节我们的处理模式,都是没有指定交换机,看起来是直接指定到队列的。然而实际是,我们声明的是一个匿名交换器,exchange参数就是交换机的名称。空字符串代表默认或者匿名交换机:消息将会根据指定的routing_key分发到指定的队列。
2. 发布/订阅
上节我们的应用场景是所有worker处理事情的逻辑完全一致,队列中的消息交给任何一个worker处理都可以。而我们本节的应用场景是,实现一套简单的日志系统,两个消费者对消息的处理逻辑不一样,一个消费者负责将消息输出到屏幕,一个消费者负责写入到磁盘。因此,我们的消息不是发送给任意一个消费者,而是两个消费者都需要接收到所有的消息。本节我们就基于交换机来实现这个应用场景。
交换机(Exchanges)
RabbitMQ有四种内置交换机:
直连交换机——direct
主题交换机——topic
头交换机——headers
扇形交换机——fanout也可以根据自身需求自定义交换机,本节不做深入探究。
我们本节使用的是扇形交换机,重点对他做些分析。
扇型交换机从名字上就能猜测出来,它把消息发送给它所知道的所有队列。这正是我们的日志系统所需要的。
交换器列表
可以通过rabbitmqctl list_exchanges查看队列信息
[root@iZ250x18mnzZ tRabbitMQ]# rabbitmqctl list_exchanges Listing exchanges ... amq.rabbitmq.trace topic amq.rabbitmq.log topic amq.match headers amq.headers headers amq.topic topic amq.direct direct amq.fanout fanout direct [root@iZ250x18mnzZ tRabbitMQ]#
例如amq.*都是系统内置的队列,最后一个没有名字的就是我们前两节所使用的空字符的交换机,这就是匿名交换机
匿名交换机
像前两节没有创建exchange也可以使用的情况,其实是系统默认创建了一个匿名交换机
channel.basic_publish(exchange='', routing_key='hello', body=message)其默认创建的交换机,其实是直连交换机,咱到下一节再对直连交换机进行分解。
可以像下面这样创建交换机。
channel.basic_publish(exchange='logs', routing_key='', body=message)
临时队列
前两节我们在创建队列时,都是指定了队列名称(hello,task_queue), 队列名称显然是必须的,因为我们在worker中是需要指定我们处理哪个队列的内容。
但是我们本节的日志系统,显然用原来的方式是不可行的,因为我们是要处理队列中的所有消息,而不是使用队列中的某一部分消息,所以每个worker中的队列都应该是一个独立的、全新的、空队列。
我们可以手动创建一个随机的队列名,或者让服务器为我们选择一个随机的队列名(推荐)。我们只需要在调用queue_declare方法的时候,不提供queue参数就可以了
tmp_queue = channel.queue_declare()队列已经创建出来了,显然我们需要队列的名字在消费时使用
<pre name="code" class="python"># 声明临时队列 , param exclusive 互斥 tmp_queue = channel.queue_declare(exclusive=True) queue_name = tmp_queue.method.queue
当与消费者(consumer)断开连接的时候,这个队列应当被立即删除。exclusive标识符即可达到此目的。也就是上面那段代码中的参数。
绑定(bindings)
当队列与交换机都已经创建成功之后,如何将二者关联起来呢?
那我们就要通过绑定(bingdings)将二者关联起来,像下图一样
绑定列表
我们可以通过rabbitmqctl list_bindings查看列表
[root@iZ250x18mnzZ tRabbitMQ]# rabbitmqctl list_bindings Listing bindings ... exchange task_queue queue task_queue [] [root@iZ250x18mnzZ tRabbitMQ]#
总结
经过上面提到的这么多关键词,我们队整个过程再重新总结一下
生产者
创建连接
创建交换机(扇形交换机,由交换机决定发送给哪个队列)
发送消息到指定交换机
消费者
创建连接
创建交换机(与生产者交换机对应)
创建临时队列(每个消费者都要创建的独立的队列,用于接收所有消息)
创建绑定(将交换机与临时队列绑定)
在指定交换机接收消息并处理
整合代码
emit_log.py
#!/usr/bin/env python# -*- coding: utf-8 -*-# @Date : 2016-02-28 21:28:17# @Author : mx (mx472756841@gmail.com)# @Link : http://www.shujutiyu.com/# @Version : $Id$import osimport pikaimport sys conn = Nonemessage = ' '.join(sys.argv[1:]) or "info: Hello World!"try: # 获取连接 conn = pika.BlockingConnection(pika.ConnectionParameters('localhost')) # 获取通道 channel = conn.channel() # 声明交换机 channel.exchange_declare(exchange='logs', type='fanout') # 在RabbitMQ中发送消息,指定交换机(exchange) ret = channel.basic_publish(exchange='logs', routing_key='', body=message,) print " [x] Sent '{0}'".format(message) print retexcept Exception, e: raise efinally: if conn: conn.close()recv_logs.py
#!/usr/bin/env python# -*- coding: utf-8 -*-# @Date : 2016-02-29 16:30:21# @Author : mx (mx472756841@gmail.com)# @Link : http://www.shujutiyu.com/# @Version : $Id$import osimport pikaimport time conn = Nonedef callback(ch, method, properties, body): """ @ch: channel 通道,是由外部调用时上送 out body 读取队列内容做不同的操作 """ print " [x] Recived %r" % (body, ) print " [x] ch {0}".format(ch) print " [x] method {0}".format(method) print " [x] properties {0}".format(properties) print " [x] Done %r" % (body, )try: # get connection conn = pika.BlockingConnection(pika.ConnectionParameters( 'localhost') ) # get channel channel = conn.channel() # 声明交换机 channel.exchange_declare(exchange='logs', type='fanout') # 声明临时队列 , param exclusive 互斥 tmp_queue = channel.queue_declare(exclusive=True) queue_name = tmp_queue.method.queue # 绑定交换机与队列 channel.queue_bind( exchange='logs', queue=queue_name ) # 在队列中读取信息 channel.basic_consume(callback, queue=queue_name, no_ack=True) print ' [*] Waiting for messages. To exit press CTRL+C' channel.start_consuming()except Exception, e: raise efinally: if conn: conn.close()
注:
1. 如果交换机还没有绑定队列,即消费者还没有运行,而先运行了生产者,那交换机就自动把消息丢弃了
2. 上一节提到了队列和消息可以持久化,有兴趣的不妨试一下交换机的持久化
3. 参考资料
官网资料:http://www.rabbitmq.com/tutorials/tutorial-three-python.html