python 之 rabbitMQ
RabbitMQ
RabbitMQ是一个在AMQP基础上完整的,可复用的企业消息系统。他遵循Mozilla Public License开源协议。
MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法。应用程序通过读写出入队列的消息(针对应用程序的数据)来通信,而无需专用连接来链接它们。消 息传递指的是程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信,直接调用通常是用于诸如远程过程调用的技术。排队指的是应用程序通过 队列来通信。队列的使用除去了接收和发送应用程序同时执行的要求。
RabbitMQ安装
安装配置epel源 # 64位源 $ rpm -ivh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm # 32位源 $ rpm -ivh http://dl.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm 安装erlang(依赖包) $ yum -y install erlang 安装RabbitMQ $ yum -y install rabbitmq-server
默认端口为:5672
启动rabbitmq: service rabbitmq-server start/stop
安装API
pip3 install pika or easy_install pika or 源码 https://pypi.python.org/pypi/pika
使用API操作RabbitMQ
MQ是消费-生产者模型的一个典型的代表,一端往消息队列中不断写入消息,而另一端则可以读取或者订阅队列中的消息。下面以消费-生产者模型为例:
基础概念
消息持久
RabbitMQ 的结构图:
生产者-消费者模型的简单实例
对于RabbitMQ来说,生产和消费不再针对内存里的一个Queue对象,而是某台服务器上的RabbitMQ Server实现的消息队列。
-----------------------------生产者----------------------------------- # /usr/bin/env python # -*- coding:utf8 -*- # auth rain import pika # 创建连接,连接到消息队列服务器 connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost')) # 创建通道 channel = connection.channel() # 创建任务队列 channel.queue_declare(queue='task_queue') # 发布消息 # exchange -- 它使我们能够确切地指定消息应该到哪个队列去。 # 向队列插入数值 routing_key是队列名 body 是要插入的内容 channel.basic_publish( exchange='', routing_key='task_queue', body='test rabbitMQ' ) print("[X] sent 'test rabbitMq'") # 缓冲区已经flush而且消息已经确认发送到了RabbitMQ中,关闭链接 connection.close() # [X] sent 'test rabbitMq'
----------------------------消费者----------------------------------- # /usr/bin/env python # -*- coding:utf8 -*- # auth rain import pika # 创建连接,连接到消息队列服务器 connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost')) # 创建通道 channel = connection.channel() # 如果生产者没有运行创建队列,那么消费者也许就找不到队列了。为了避免这个问题所以消费者也创建这个队列 channel.queue_declare(queue='task_queue') # 接收消息需要使用callback这个函数来接收,他会被pika库来调用 def callback(ch, method, properties, body): print(" [x] Received %r" % body) # 从队列取数据 callback是回调函数 如果拿到数据 那么将执行callback函数 channel.basic_consume(callback, queue='task_queue', no_ack=True) print(' [*] Waiting for messages. To exit press CTRL+C') # 永远循环等待数据处理和callback处理的数据 channel.start_consuming() ''' [*] Waiting for messages. To exit press CTRL+C [x] Received b'test rabbitMQ' [x] Received b'test rabbitMQ' '''
Queue:消息队列载体,每个消息都会被投入到一个或多个队列。
Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来。
Routing Key:路由关键字,exchange根据这个关键字进行消息投递。
publish:消息生产者,就是投递消息的程序。
consumer:消息消费者,就是接受消息的程序。
channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务。
(2)客户端声明一个exchange,并设置相关属性。
(3)客户端声明一个queue,并设置相关属性。
(4)客户端使用routing key,在exchange和queue之间建立好绑定关系。
(5)客户端投递消息到exchange。
import pika import sys import time # 创建连接,连接到消息队列服务器 connection = pika.BlockingConnection(pika.ConnectionParameters( host='10.10.36.101')) # 创建通道 channel = connection.channel() # 创建任务队列 channel.queue_declare(queue='task_queue') # 发布消息 # exchange -- 它使我们能够确切地指定消息应该到哪个队列去。 # 向队列插入数值 routing_key是队列名 body 是要插入的内容 message = ' '.join(sys.argv[1:]) or "Hello World!" # 循环发送数据 for i in range(20): channel.basic_publish( exchange='', routing_key='task_queue', body=message ) time.sleep(0.5) print("[X] sent %s " % message) # 缓冲区已经flush而且消息已经确认发送到了RabbitMQ中,关闭链接 connection.close()
import pika import time # 创建连接,连接到消息队列服务器 connection = pika.BlockingConnection(pika.ConnectionParameters(host='10.10.36.101')) # 创建通道 channel = connection.channel() # 如果生产者没有运行创建队列,那么消费者也许就找不到队列了。为了避免这个问题所以消费者也创建这个队列 channel.queue_declare(queue='task_queue') # 接收消息需要使用callback这个函数来接收,他会被pika库来调用 def callback(ch, method, properties, body): print(" [x] Received %r" % body) # time.sleep(body.count('.')) # print('[x] Done') # 从队列取数据 callback是回调函数 如果拿到数据 那么将执行callback函数 channel.basic_consume(callback, queue='task_queue', no_ack=True) print(' [*] Waiting for messages. To exit press CTRL+C') # 永远循环等待数据处理和callback处理的数据 channel.start_consuming()
1、acknowledgment 消息不丢失
消息确认
当处理一个比较耗时得任务的时候,你也许想知道消费者(consumers)是否运行到一半就挂掉。当前的代码中,当消息被RabbitMQ发送给消费者(consumers)之后,马上就会在内存中移除。这种情况,你只要把一个工作者(worker)停止,正在处理的消息就会丢失。同时,所有发送到这个工作者的还没有处理的消息都会丢失。
我们不想丢失任何任务消息。如果一个工作者(worker)挂掉了,我们希望任务会重新发送给其他的工作者(worker)。
为了防止消息丢失,RabbitMQ提供了消息响应(acknowledgments)。消费者会通过一个ack(响应),告诉RabbitMQ已经收到并处理了某条消息,然后RabbitMQ就会释放并删除这条消息。
如果消费者(consumer)挂掉了,没有发送响应,RabbitMQ就会认为消息没有被完全处理,然后重新发送给其他消费者(consumer)。这样,及时工作者(workers)偶尔的挂掉,也不会丢失消息。
消息是没有超时这个概念的;当工作者与它断开连的时候,RabbitMQ会重新发送消息。这样在处理一个耗时非常长的消息任务的时候就不会出问题了。
消息响应默认是开启的。之前的例子中我们可以使用no_ack=True标识把它关闭。是时候移除这个标识了,当工作者(worker)完成了任务,就发送一个响应。
no-ack = False,如果消费者遇到情况(its channel is closed, connection is closed, or TCP connection is lost)挂掉了,那么,RabbitMQ会重新将该任务添加到队列中。
消息响应默认是开启的。之前的例子中我们可以使用no_ack=True标识把它关闭。是时候移除这个标识了,当工作者(worker)完成了任务,就发送一个响应。
ch.basic_ack(delivery_tag=method.delivery_tag)
一个很容易犯的错误就是忘了basic_ack,后果很严重。消息在你的程序退出之后就会重新发送,如果它不能够释放没响应的消息,RabbitMQ就会占用越来越多的内存。
为了排除这种错误,你可以使用rabbitmqctl命令,输出messages_unacknowledged字段:
sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged
import pika import sys import time # 创建连接,连接到消息队列服务器 connection = pika.BlockingConnection(pika.ConnectionParameters( host='10.10.36.101')) # 创建通道 channel = connection.channel() # 创建任务队列 channel.queue_declare(queue='task_queue') # 发布消息 # exchange -- 它使我们能够确切地指定消息应该到哪个队列去。 # 向队列插入数值 routing_key是队列名 body 是要插入的内容 message = ' '.join(sys.argv[1:]) or "Hello World!" # 循环发送数据 for i in range(20): channel.basic_publish( exchange='', routing_key='task_queue', body=message ) time.sleep(0.5) print("[X] sent %s " % message) # 缓冲区已经flush而且消息已经确认发送到了RabbitMQ中,关闭链接 connection.close()
import pika import time # 创建连接,连接到消息队列服务器 connection = pika.BlockingConnection(pika.ConnectionParameters(host='10.10.36.101')) # 创建通道 channel = connection.channel() # 如果生产者没有运行创建队列,那么消费者也许就找不到队列了。为了避免这个问题所以消费者也创建这个队列 channel.queue_declare(queue='task_queue') # 接收消息需要使用callback这个函数来接收,他会被pika库来调用 def callback(ch, method, properties, body): print(" [x] Received %r" % body) time.sleep(body.decode().count('...')) print(" [x] Done") # 消息不丢失 ch.basic_ack(delivery_tag=method.delivery_tag) # 从队列取数据 callback是回调函数 如果拿到数据 那么将执行callback函数 channel.basic_consume(callback, queue='task_queue', ) print(' [*] Waiting for messages. To exit press CTRL+C') # 永远循环等待数据处理和callback处理的数据 channel.start_consuming()
2、消息持久化
如果你没有特意告诉RabbitMQ,那么在它退出或者崩溃的时候,将会丢失所有队列和消息。为了确保信息不会丢失,有两个事情是需要注意的:我们必须把“队列”和“消息”设为持久化。
首先,为了不让队列消失,需要把队列声明为持久化(durable):
channel.queue_declare(queue='hello', durable=True)
这个queue_declare必须在生产者(producer)和消费者(consumer)对应的代码中修改。
这时候,我们就可以确保在RabbitMq重启之后queue_declare队列不会丢失。另外,我们需要把我们的消息也要设为持久化——将delivery_mode的属性设为2。
# 生产者端 channel.basic_publish(exchange='', routing_key='task_queue', properties=pika.BasicProperties( delivery_mode=2, # 即使服务端挂了, 消息也能持久化 )) # 消费者端 def callback(ch, method, properties, body): print(body) time.sleep(body.count()) ch.basic_ack(delivery_tag=method.delivery_tag)
注意:消息持久化
将消息设为持久化并不能完全保证不会丢失。以上代码只是告诉了RabbitMq要把消息存到硬盘,但从RabbitMq收到消息到保存之间还是有一个很小的间隔时间。因为RabbitMq并不是所有的消息都使用fsync(2)——它有可能只是保存到缓存中,并不一定会写到硬盘中。并不能保证真正的持久化,但已经足够应付我们的简单工作队列。如果你一定要保证持久化,你需要改写你的代码来支持事务(transaction)。
3、公平调度
你应该已经发现,它仍旧没有按照我们期望的那样进行分发。比如有两个工作者(workers),处理奇数消息的比较繁忙,处理偶数消息的比较轻松。然而RabbitMQ并不知道这些,它仍然一如既往的派发消息。
这时因为RabbitMQ只管分发进入队列的消息,不会关心有多少消费者(consumer)没有作出响应。它盲目的把第n-th条消息发给第n-th个消费者
我们可以使用basic.qos方法,并设置prefetch_count=1。这样是告诉RabbitMQ,再同一时刻,不要发送超过1条消息给一个工作者(worker),直到它已经处理了上一条消息并且作出了响应。这样,RabbitMQ就会把消息分发给下一个空闲的工作者(worker)。
channel.basic_qos(prefetch_count=1)
注意:关于队列大小
如果所有的工作者都处理繁忙状态,你的队列就会被填满。你需要留意这个问题,要么添加更多的工作者(workers),要么使用其他策略。
----------------------------------生产者-------------------------------------------- #!/usr/bin/env python import pika import sys connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost')) channel = connection.channel() # 队列持久化 durable=True channel.queue_declare(queue='task_queue', durable=True) message = ' '.join(sys.argv[1:]) or "Hello World!" channel.basic_publish(exchange='', routing_key='task_queue', body=message, properties=pika.BasicProperties( delivery_mode = 2, # 消息持久化 )) print " [x] Sent %r" % (message,) connection.close()
----------------------------------消费者--------------------------------------------
#!/usr/bin/env python import pika import time connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost')) channel = connection.channel() # 队列持久化 durable=True channel.queue_declare(queue='task_queue', durable=True) print ' [*] Waiting for messages. To exit press CTRL+C' def callback(ch, method, properties, body): print " [x] Received %r" % (body,) time.sleep( body.count('.') ) print " [x] Done" ch.basic_ack(delivery_tag = method.delivery_tag) channel.basic_qos(prefetch_count=1) channel.basic_consume(callback, queue='task_queue') channel.start_consuming()
4、发布/订阅
分发一个消息给多个消费者(consumers)。这种模式被称为“发布/订阅”。
为了描述这种模式,我们将会构建一个简单的日志系统。它包括两个程序——第一个程序负责发送日志消息,第二个程序负责获取消息并输出内容。
在我们的这个日志系统中,所有正在运行的接收方程序都会接受消息。我们用其中一个接收者(receiver)把日志写入硬盘中,另外一个接受者(receiver)把日志输出到屏幕上。
最终,日志消息被广播给所有的接受者(receivers)。
交换机(Exchanges)
RabbitMQ消息模型的核心理念是:发布者(producer)不会直接发送任何消息给队列。事实上,发布者(producer)甚至不知道消息是否已经被投递到队列。
发布者(producer)只需要把消息发送给一个交换机(exchange)。交换机非常简单,它一边从发布者方接收消息,一边把消息推送到队列。交换机必须知道如何处理它接收到的消息,是应该推送到指定的队列还是是多个队列,或者是直接忽略消息。这些规则是通过交换机类型(exchange type)来定义的。
交换机类型:
直连交换机(direct),
主题交换机(topic),
头交换机 (headers),
扇型交换机(fanout)。
channel.exchange_declare(exchange='logs', type='fanout')
扇型交换机(fanout)
扇型交换机(fanout)很简单,你可能从名字上就能猜测出来,它把消息发送给它所知道的所有队列。这正是我们的日志系统所需要的。
匿名的交换器
前面的教程中我们对交换机一无所知,但仍然能够发送消息到队列中。因为我们使用了命名为空字符串("")默认的交换机。
回想我们之前是如何发布一则消息:
channel.basic_publish(exchange='', routing_key='hello', body=message)
exchange参数就是交换机的名称。空字符串代表默认或者匿名交换机:消息将会根据指定的routing_key分发到指定的队列。
现在,我们就可以发送消息到一个具名交换机了:
channel.basic_publish(exchange='logs',
routing_key='',
body=message)
临时队列
第一步, 当我们连接上RabbitMQ的时候,我们需要一个全新的、空的队列。我们可以手动创建一个随机的队列名,或者让服务器为我们选择一个随机的队列名(推荐)。我们只需要在调用queue_declare方法的时候,不提供queue参数就可以了:
result = channel.queue_declare()
这时候我们可以通过result.method.queue获得已经生成的随机队列名。它可能是这样子的:amq.gen-U0srCoW8TsaXjNh73pnVAw==。
第二步,当与消费者(consumer)断开连接的时候,这个队列应当被立即删除。exclusive标识符即可达到此目的。
result = channel.queue_declare(exclusive=True)
绑定(Bindings)
我们已经创建了一个扇型交换机(fanout)和一个队列。现在我们需要告诉交换机如何发送消息给我们的队列。交换器和队列之间的联系我们称之为绑定(binding)。
channel.queue_bind(exchange='logs', queue=result.method.queue)
现在,logs交换机将会把消息添加到我们的队列中。
绑定(binding)列表
你可以使用
rabbitmqctl list_bindings
列出所有现存的绑定。
一个发送日志的实例
#!/bin/bin/env python # -*-coding:utf-8 -*- # Author : rain import pika import sys connection = pika.BlockingConnection(pika.ConnectionParameters(host='10.10.36.101')) channel = connection.channel() # 指定exchange,其类型为fanout channel.exchange_declare(exchange='logs', type='fanout') message = ''.join(sys.argv[1:] or 'hello world') # 发送给指定的exchange channel.publish( exchange='logs', routing_key='', body='message' ) print(" [x] Sent %r" % (message,)) connection.close()
#!/bin/bin/env python # -*-coding:utf-8 -*- # Author : rain import pika connection = pika.BlockingConnection(pika.ConnectionParameters(host='10.10.36.101')) channel = connection.channel() # 指定exchange,其类型为fanout channel.exchange_declare(exchange='logs', type='fanout') # 创建临时(queue)队列 result = channel.queue_declare(exclusive=True) queue_name = result.method.queue # 将queue绑定到指定的exchange上 channel.queue_bind(exchange='logs', queue=queue_name) print(' [*] Waiting for logs. To exit press CTRL+C') def callback(ch, method, properties, body): print(" [x] %r" % (body,)) channel.basic_consume(callback, queue=queue_name, no_ack=True ) channel.start_consuming()
直连交换机(Direct exchange)
绑定(binding)是指交换机(exchange)和队列(queue)的关系。可以简单理解为:这个队列(queue)对这个交换机(exchange)的消息感兴趣。
绑定的时候可以带上一个额外的routing_key参数。为了避免与basic_publish的参数混淆,我们把它叫做绑定键(binding key)。以下是如何创建一个带绑定键的绑定。
channel.queue_bind(exchange=exchange_name, queue=queue_name, routing_key='black')
绑定键的意义取决于交换机(exchange)的类型。我们之前使用过的扇型交换机(fanout exchanges)会忽略这个值。
应用场景:
我们的日志系统广播所有的消息给所有的消费者(consumers)。我们打算扩展它,使其基于日志的严重程度进行消息过滤。例如我们也许只是希望将比较严重的错误(error)日志写入磁盘,以免在警告(warning)或者信息(info)日志上浪费磁盘空间。
我们使用的扇型交换机(fanout exchange)没有足够的灵活性 —— 它能做的仅仅是广播。
我们将会使用直连交换机(direct exchange)来代替。路由的算法很简单 —— 交换机将会对绑定键(binding key)和路由键(routing key)进行精确匹配,从而确定消息该分发到哪个队列。
下图能够很好的描述这个场景:
在这个场景中,我们可以看到直连交换机 X和两个队列进行了绑定。第一个队列使用orange作为绑定键,第二个队列有两个绑定,一个使用black作为绑定键,另外一个使用green。
这样以来,当路由键为orange的消息发布到交换机,就会被路由到队列Q1。路由键为black或者green的消息就会路由到Q2。其他的所有消息都将会被丢弃。
多个绑定(Multiple bindings)
多个队列使用相同的绑定键是合法的。这个例子中,我们可以添加一个X和Q1之间的绑定,使用black绑定键。这样一来,直连交换机就和扇型交换机的行为一样,会将消息广播到所有匹配的队列。带有black路由键的消息会同时发送到Q1和Q2。
发送日志
我们将会发送消息到一个直连交换机,把日志级别作为路由键。这样接收日志的脚本就可以根据严重级别来选择它想要处理的日志。我们先看看发送日志。
severity = ['info','warning','error']
我们需要创建一个交换机(exchange): channel.exchange_declare(exchange='direct_logs', type='direct') 然后我们发送一则消息: channel.basic_publish(exchange='direct_logs', routing_key=severity, body=message) 我们先假设“severity”的值是info、warning、error中的一个。
订阅 处理接收消息的方式和之前差不多,只有一个例外,我们将会为我们感兴趣的每个严重级别分别创建一个新的绑定。 result = channel.queue_declare(exclusive=True) queue_name = result.method.queue for severity in severities: channel.queue_bind(exchange='direct_logs', queue=queue_name, routing_key=severity)
1.生产者:
#!/bin/bin/env python # -*-coding:utf-8 -*- # Author : rain import pika import sys connection = pika.BlockingConnection(pika.ConnectionParameters(host='10.10.36.101')) channel = connection.channel() channel.exchange_declare(exchange='direct_logs', type='direct') severity = sys.argv[1] if len(sys.argv) > 1 else 'info' message = ' '.join(sys.argv[2:]) or 'Hello World!' channel.basic_publish(exchange='direct_logs', routing_key=severity, body=message) print(" [x] Sent %r:%r" % (severity, message)) connection.close()
2.消费者:
#!/bin/bin/env python # -*-coding:utf-8 -*- # Author : rain import sys import pika connection = pika.BlockingConnection(pika.ConnectionParameters(host='10.10.36.101')) channel = connection.channel() channel.exchange_declare(exchange='direct_logs', type='direct') result = channel.queue_declare(exclusive=True) queue_name = result.method.queue severities = sys.argv[1:] if not severities: sys.stderr.write("Usage: %s [info] [warning] [error]\n" % sys.argv[0]) sys.exit(1) for severity in severities: channel.queue_bind(exchange='direct_logs', queue=queue_name, routing_key=severity) print(' [*] Waiting for logs. To exit press CTRL+C') def callback(ch, method, properties, body): print(" [x] %r:%r" % (method.routing_key, body)) channel.basic_consume(callback, queue=queue_name, no_ack=True) channel.start_consuming()
主题交换机(topic exchange)
发送到主题交换机(topic exchange)的消息不可以携带随意什么样子的路由键(routing_key),它的路由键必须是一个由.
分隔开的词语列表。这些单词随便是什么都可以,但是最好是跟携带它们的消息有关系的词汇。以下是几个推荐的例子:"stock.usd.nyse", "nyse.vmw", "quick.orange.rabbit"。词语的个数可以随意,但是不要超过255字节。
绑定键也必须拥有同样的格式。主题交换机背后的逻辑跟直连交换机很相似 —— 一个携带着特定路由键的消息会被主题交换机投递给绑定键与之想匹配的队列。但是它的绑定键和路由键有两个特殊应用方式:
*
(星号) 用来表示一个单词.#
(井号) 用来表示任意数量(零个或多个)单词。
这个例子里,我们发送的所有消息都是用来描述小动物的。发送的消息所携带的路由键是由三个单词所组成的,这三个单词被两个 . 分割开。路由键里的第一个单词描述的是动物的手脚的利索程度,第二个单词是动物的颜色,第三个是动物的种类。所以它看起来是这样的: <celerity>.<colour>.<species> 。 我们创建了三个绑定:Q1的绑定键为 *.orange.* ,Q2的绑定键为 *.*.rabbit 和 lazy.# 。 这三个绑定键被可以总结为: •Q1 对所有的桔黄色动物都感兴趣。 •Q2 则是对所有的兔子和所有懒惰的动物感兴趣。 一个携带有 quick.orange.rabbit 的消息将会被分别投递给这两个队列。携带着 lazy.orange.elephant 的消息同样也会给两个队列都投递过去。另一方面携带有 quick.orange.fox 的消息会投递给第一个队列,携带有 lazy.brown.fox 的消息会投递给第二个队列。携带有 lazy.pink.rabbit 的消息只会被投递给第二个队列一次,即使它同时匹配第二个队列的两个绑定。携带着 quick.brown.fox 的消息不会投递给任何一个队列。 如果我们违反约定,发送了一个携带有一个单词或者四个单词( "orange" or "quick.orange.male.rabbit" )的消息时,发送的消息不会投递给任何一个队列,而且会丢失掉。 但是另一方面,即使 "lazy.orange.male.rabbit" 有四个单词,他还是会匹配最后一个绑定,并且被投递到第二个队列中。
主题交换机 主题交换机是很强大的,它可以表现出跟其他交换机类似的行为 当一个队列的绑定键为 "#"(井号) 的时候,这个队列将会无视消息的路由键,接收所有的消息。 当 * (星号) 和 # (井号) 这两个特殊字符都未在绑定键中出现的时候,此时主题交换机就拥有的直连交换机的行为。
#!/bin/bin/env python # -*-coding:utf-8 -*- # Author : rain import sys import pika connection = pika.BlockingConnection(pika.ConnectionParameters(host='10.10.36.101')) channel = connection.channel() channel.exchange_declare(exchange='topic_logs', type='topic') routing_key = sys.argv[1] if len(sys.argv) > 1 else 'anonymous.info' message = ' '.join(sys.argv[2:]) or 'Hello World!' channel.basic_publish(exchange='topic_logs', routing_key=routing_key, body=message) print(" [x] Sent %r:%r" % (routing_key, message)) connection.close()
#!/bin/bin/env python # -*-coding:utf-8 -*- # Author : rain import pika import sys connection = pika.BlockingConnection(pika.ConnectionParameters(host='10.10.36.101')) channel = connection.channel() channel.exchange_declare(exchange='topic_logs', type='topic') result = channel.queue_declare(exclusive=True) queue_name = result.method.queue binding_keys = sys.argv[1:] if not binding_keys: sys.stderr.write("Usage: %s [info] [warning] [error]\n" % sys.argv[0]) sys.exit(1) for binding_key in binding_keys: channel.queue_bind(exchange='topic_logs', queue=queue_name, routing_key=binding_key) print(' [*] Waiting for logs. To exit press CTRL+C') def callback(ch, method, properties, body): print(" [x] %r:%r" % (method.routing_key, body,)) channel.basic_consume(callback, queue=queue_name, no_ack=True) channel.start_consuming()
执行下边命令 接收所有日志: python receive_logs_topic.py "#" 执行下边命令 接收来自”kern“设备的日志: python receive_logs_topic.py "kern.*" 执行下边命令 只接收严重程度为”critical“的日志: python receive_logs_topic.py "*.critical" 执行下边命令 建立多个绑定: python receive_logs_topic.py "kern.*" "*.critical" 执行下边命令 发送路由键为 "kern.critical" 的日志: python emit_log_topic.py "kern.critical" "A critical kernel error" 执行上边命令试试看效果吧。另外,上边代码不会对路由键和绑定键做任何假设,所以你可以在命令中使用超过两个路由键参数。
远程过程调用(RPC Remote Procedure Call)
使用RabbitMQ来构建一个RPC系统:包含一个客户端和一个RPC服务器。
消息属性
AMQP协议给消息预定义了一系列的14个属性。大多数属性很少会用到,除了以下几个:
- delivery_mode(投递模式):将消息标记为持久的(值为2)或暂存的(除了2之外的其他任何值)。第二篇教程里接触过这个属性,记得吧?
- content_type(内容类型):用来描述编码的mime-type。例如在实际使用中常常使用application/json来描述JOSN编码类型。
- reply_to(回复目标):通常用来命名回调队列。
- correlation_id(关联标识):用来将RPC的响应和请求关联起来。
关联标识
我们建议给每一个RPC请求新建一个回调队列。这不是一个高效的做法,幸好这儿有一个更好的办法 —— 我们可以为每个客户端只建立一个独立的回调队列。
这就带来一个新问题,当此队列接收到一个响应的时候它无法辨别出这个响应是属于哪个请求的。correlation_id 就是为了解决这个问题而来的。我们给每个请求设置一个独一无二的值。稍后,当我们从回调队列中接收到一个消息的时候,我们就可以查看这条属性从而将响应和请求匹配起来。如果我们接手到的消息的correlation_id是未知的,那就直接销毁掉它,因为它不属于我们的任何一条请求。
你也许会问,为什么我们接收到未知消息的时候不抛出一个错误,而是要将它忽略掉?这是为了解决服务器端有可能发生的竞争情况。尽管可能性不大,但RPC服务器还是有可能在已将应答发送给我们但还未将确认消息发送给请求的情况下死掉。如果这种情况发生,RPC在重启后会重新处理请求。这就是为什么我们必须在客户端优雅的处理重复响应,同时RPC也需要尽可能保持幂等性。
RPC工作流程:
- 当客户端启动的时候,它创建一个匿名独享的回调队列。
- 在RPC请求中,客户端发送带有两个属性的消息:一个是设置回调队列的 reply_to 属性,另一个是设置唯一值的 correlation_id 属性。
- 将请求发送到一个 rpc_queue 队列中。
- RPC工作者(又名:服务器)等待请求发送到这个队列中来。当请求出现的时候,它执行他的工作并且将带有执行结果的消息发送给reply_to字段指定的队列。
- 客户端等待回调队列里的数据。当有消息出现的时候,它会检查correlation_id属性。如果此属性的值与请求匹配,将它返回给应用。
#!/bin/bin/env python # -*-coding:utf-8 -*- # Author : rain import pika connection = pika.BlockingConnection(pika.ConnectionParameters(host='10.10.36.101')) channel = connection.channel() channel.queue_declare(queue='rpc_queue') def fib(n): if n == 0: return 0 elif n == 1: return 1 else: return fib(n - 1) + fib(n - 2) def on_request(ch, method, props, body): n = int(body) print("[.] fib(%s)" % n) response = fib(n) ch.basic_publish(exchange='', routing_key=props.reply_to, properties=pika.BasicProperties( correlation_id=props.correlation_id,), body=str(response) ) ch.basic_ack(delivery_tag=method.delivery_tag) channel.basic_qos(prefetch_count=1) channel.basic_consume(on_request, queue='rpc_queue') print(" [x] Awaiting RPC requests") channel.start_consuming()
#!/bin/bin/env python # -*-coding:utf-8 -*- # Author : rain import pika import uuid class FibonacciRpcClient: def __init__(self): self.connection = pika.BlockingConnection(pika.ConnectionParameters(host='10.10.36.101')) self.channel = self.connection.channel() result = self.channel.queue_declare(exclusive=True) self.callback_queue = result.method.queue self.channel.basic_consume(self.on_response, no_ack=True, queue=self.callback_queue) def on_response(self, ch, method, props, body): if self.corr_id == props.correlation_id: self.response = body def call(self, n): self.response = None self.corr_id = str(uuid.uuid4()) self.channel.basic_publish(exchange='', routing_key='rpc_queue', properties=pika.BasicProperties( reply_to=self.callback_queue, correlation_id=self.corr_id,), body=str(n)) while self.response is None: self.connection.process_data_events() return int(self.response) fibonacci_rpc = FibonacciRpcClient() print(" [x] Requesting fib(30)") response = fibonacci_rpc.call(30) print(" [.] Got %r" % (response,))
此处呈现的设计并不是实现RPC服务的唯一方式,但是他有一些重要的优势:
- 如果RPC服务器运行的过慢的时候,你可以通过运行另外一个服务器端轻松扩展它。试试在控制台中运行第二个 rpc_server.py 。
- 在客户端,RPC请求只发送或接收一条消息。不需要像 queue_declare 这样的异步调用。所以RPC客户端的单个请求只需要一个网络往返。
我们的代码依旧非常简单,而且没有试图去解决一些复杂(但是重要)的问题,如:
- 当没有服务器运行时,客户端如何作出反映。
- 客户端是否需要实现类似RPC超时的东西。
- 如果服务器发生故障,并且抛出异常,应该被转发到客户端吗?
- 在处理前,防止混入无效的信息(例如检查边界)
rabbitMQ中文文档: http://rabbitmq.mr-ping.com/tutorials_with_python/[1]Hello_World.html