rabbitmq(中间消息代理)在python中的使用详解


Posted in Python onDecember 14, 2017

在之前的有关线程,进程的博客中,我们介绍了它们各自在同一个程序中的通信方法。但是不同程序,甚至不同编程语言所写的应用软件之间的通信,以前所介绍的线程、进程队列便不再适用了;此种情况便只能使用socket编程了,然而不同程序之间的通信便不再像线程进程之间的那么简单了,要考虑多种情况(比如其中一方断线另一方如何处理;消息群发,多个程序之间的通信等等),如果每遇到一次程序间的通信,便要根据不同情况编写不同的socket,还要维护、完善这个socket这会使得编程人员的工作量大大增加,也使得程序更易崩溃。所以,一般遇到这种情况,便使用消息队列MQ(Message Queue),那么问题来了。

1. 什么是消息队列MQ?

MQ是一种应用程序对应用程序的通信方法。应用程序通过读出(写入)队列的消息(针对应用程序的数据)来通信,而无需使用专用连接来链接它们。消息传递指的是程序之间通过在消息中发送数据进行通信,而不是通过直接调用彼此来通信,排队指的是应用程序通过 队列来通信。队列的使用排除了接收和发送应用程序同时执行的要求。

2. 什么是rabbitmq?如何使用它?

RabbitMQ是流行的开源消息队列系统,用erlang语言开发。RabbitMQ是AMQP(高级消息队列协议)的标准实现。

RabbitMQ也是前面所提到的生产者消费者模型,一端发送消息(生产任务),一端接收消息(处理任务)。

rabbitmq的详细使用(包括各种系统的安装配置)可参见其官方文档:http://www.rabbitmq.com/documentation.html

由于应用程序之间的通信情况异常复杂,rabbitmq支持的编程语言有10多种,所以在此博客中不可能完全演示rabbitmq的所有使用。本片博客将会介绍rabbitmq在python中的基本使用,如果你只想使用rabbitmq完成一些简单的任务,则本篇博客足以满足你的需求;如果你想深入学习了解rabbitmq的工作原理,那么读完本篇博客,你可以更容易的读懂rabbitmq的官方文档;当然这些只限于你在使用python编程。

在python中我们使用pika(第三方模块,使用pip安装即可使用)模块进行rabbitmq的操作,接下来,使用python实现一个rabbitmq最简单的通信。

In the diagram below, "P" is our producer and "C" is our consumer. The box in the middle is a queue - a message buffer that RabbitMQ keeps on behalf of the consumer.

Our overall design will look like:

rabbitmq(中间消息代理)在python中的使用详解

Producer sends messages to the "hello" queue. The consumer receives messages from that queue.

例一(简单的消息收发):

Sending

rabbitmq(中间消息代理)在python中的使用详解

Our first programsend.pywill send a single message to the queue. The first thing we need to do is to establish a connection with RabbitMQ server.

import pika
connection = pika.BlockingConnection(pika.ConnectionParameters("localhost")) # 建立程序与rabbitmq的连接
channel = connection.channel()
channel.queue_declare(queue='hello') # 定义hello队列
channel.basic_publish(exchange='',
      routing_key='hello', # 告诉rabbitmq将消息发送到hello队列中
      body='Hello world!') # 发送消息的内容
print(" [x] Sent 'Hello World!'")
connection.close() # 关闭与rabbitmq的连接

rabbitmq(中间消息代理)在python中的使用详解

Our second programreceive.pywill receive messages from the queue and print them on the screen.

import pika
import time
connection = pika.BlockingConnection(pika.ConnectionParameters("localhost")) # 建立程序与rabbitmq的连接
channel = connection.channel()
# 在接收端定义队列,参数与发送端的相同
channel.queue_declare(queue='hello')
def callback(ch, method, properties, body):
 """
 收到消息调用callback处理消息
 :param ch:
 :param method:
 :param properties:
 :param body:
 :return:
 """
 print(" [x] received %r" % body)
 # time.sleep(30)
 print("Done....")
channel.basic_consume(callback,
      queue='hello', # 告诉rabbitmq此程序从hello队列中接收消息
      no_ack=True)

# channel.basic_consume(callback,
#      queue='hello')
print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming() # 开始接收,未收到消息阻塞

注1:我们可以打开time.sleep()的注释(模仿任务处理所需的时间),将no_ack设为默认值(不传参数),同时运行多个receive.py, 运行send.py发一次消息,第一个开始运行的receive.py接收到消息,开始处理任务,如果中途宕机(任务未处理完);那么第二个开始运行的receive.py就会接收到消息,开始处理任务;如果第二个也宕机了,则第三个继续;如果依次所有运行的receive都宕机(任务未处理完)了,则下次开始运行的第一个receive.py将继续接收消息处理任务,这个机制防止了一些必须完成的任务由于处理任务的程序异常终止导致任务不能完成。如果将no_ack设为True,中途宕机,则后面的接收端不会再接收消息处理任务。

注2:如果发送端不停的发消息,则接收端分别是第一个开始运行的接收,第二个开始运行的接收,第三个开始运行接收,依次接收,这是rabbitmq的消息轮循机制(相当于负载均衡,防止一个接收端接收过多任务卡死,当然这种机制存在弊端,就是如果就收端机器有的配置高有的配置低,就会使配置高的机器得不到充分利用而配置低的机器一直在工作)。这一点可以启动多个receive.py,多次运行send.py验证。

上面的例子我们介绍了消息的接收端(即任务的处理端)宕机,我们该如何处理。接下来,我们将重点放在消息的发送端(即服务端),与接收端不同,如果发送端宕机,则会丢失存储消息的队列,存储的消息(要发送给接收端处理的任务),这些信息一旦丢失会造成巨大的损失,所以下面的重点就是消息的持久化,即发送端异常终止,重启服务后,队列,消息都将自动加载进服务里。其实只要将上面的代码稍微修改就可实现。

例二(消息的持久化):

Sending:

import pika
connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = connection.channel()
channel.queue_declare(queue='task_queue', durable=True)  #使队列持久化
message = "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()

Receiving:

import pika
import time

connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))
channel = connection.channel()

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(2)
 print(" [x] Done")
 # 因为没有设置no_ask=True, 所以需要告诉rabbitmq消息已经处理完毕,rabbitmq将消息移出队列。
 ch.basic_ack(delivery_tag=method.delivery_tag)

#同一时间worker只接收一条消息,等这条消息处理完在接收下一条
channel.basic_qos(prefetch_count=1)
channel.basic_consume(callback,
      queue='task_queue')


channel.start_consuming()

注1:worker.py中的代码如果不设置,则new_task.py意外终止在重启后,worker会同时接收终止前没有处理的所有消息。两个程序中的queue设置的参数要相同,否则程序出错。no_ask=True如果没设置,则worker.py中的ch.basic_ack(delivery_tag=method.delivery_tag)这行代码至关重要,如果不写,则不管接收的消息有没有处理完,此消息将一直存在与队列中。

注2:这句代码---channel.basic_qos(prefetch_count=1),解决了上例中消息轮循机制的代码,即接收端(任务的处理端)每次只接收一个任务(参数为几接收几个任务),处理完成后通过向发送端的汇报(即注1中的代码)来接收下一个任务,如果有任务正在处理中它不再接收新的任务。

前面所介绍的例一,例二都是一条消息,只能被一个接收端收到。那么该如何实现一条消息多个接收端同时收到(即消息群发或着叫广播模式)呢?

其实,在rabbitmq中只有consumer(消费者,即接收端)与queue绑定,对于producer(生产者,即发送端)只是将消息发送到特定的队列。consumer从与自己相关的queue中读取消息而已。所以要实现消息群发,只需要将同一条放到多个消费者队列即可。在rabbitmq中这个工作由exchange来做,它可以设定三种类型,它们分别实现了不同的需求,我们分别来介绍。

例三(exchange的类型为fanout):

当exchange的类型为fanout时,所有绑定这个exchange的队列都会收到发来的消息。

rabbitmq(中间消息代理)在python中的使用详解

import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
# 申明一个exchange,两个参数分别为exchange的名字和类型;当exchang='fanout'时,所有绑定到此exchange的消费者队列都将收到消息
channel.exchange_declare(exchange='logs',
       exchange_type='fanout')
# 消息可以在命令行启动脚本时以参数的形式传入
# message = ' '.join(sys.argv[1:]) or "info: Hello World!"
message = 'Hello World!'
channel.basic_publish(exchange='logs',
      routing_key='',
      body=message)
print(" [x] Sent %r" % message)
connection.close()
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='logs',
       exchange_type='fanout')
# 随机生成一个queue,此queue唯一,且在连接端开后自动销毁
result = channel.queue_declare(exclusive=True)
# 得到随机生成消费者队列的名字
queue_name = result.method.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] received %r" % body)

channel.basic_consume(callback,
      queue=queue_name,
      no_ack=True)

channel.start_consuming()

注1:emit_log.py为消息的发送端,receive_logs.py为消息的接收端。可以同时运行多个receive_logs.py,当emit_log.py发送消息时,可以发现所有正在运行的receive_logs.py都会收到来自发送端的消息。

注2:类似与广播,如果消息发送时,接收端没有运行,那么它将不会收到此条消息,即消息的广播是即时的。

例四(exchange的类型为direct):

当exchange的类型为direct时,发送端和接收端都要指明消息的级别,接收端只能接收到被指明级别的消息。

rabbitmq(中间消息代理)在python中的使用详解

import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='direct_logs',
       exchange_type='direct')
# 命令行启动时,以参数的的形式传入发送消息的级别,未传怎默认设置未info
# severity = sys.argv[1] if len(sys.argv) > 2 else 'info'
# 命令行启动时,以参数的的形式传入发送消息的内容,未传怎默认设置Hello World!
# message = ' '.join(sys.argv[2:]) or 'Hello World!'
# 演示使用,实际运用应用上面的方式设置消息级别
severity = 'info' #作为例子直接将消息的级别设置为info
# severity = 'warning'
message = 'Hello World'

#使用exchang的direct模式时,routing_key的值为消息的级别
channel.basic_publish(exchange='direct_logs',
      routing_key=severity,
      body=message)
print(" [x] Sent %r:%r" % (severity, message))
connection.close()
#!/usr/bin/env python
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='direct_logs',
       exchange_type='direct')
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
# 命令行启动时以参数的形式传入要接收哪个级别的消息,可以传入多个级别
# severities = sys.argv[1:]
# 演示使用,实际运用应该用上面的方式指明消息级别
# 作为演示,直接设置两个接收级别,info 和 warning
severities = ['info', 'warning']
if not severities:
 """如果要接收消息的级别不存在则提示用户输入级别并退出程序"""
 sys.stderr.write("Usage: %s [info] [warning] [error]\n" % sys.argv[0])
 sys.exit(1)
for severity in severities:
 """依次为每个消息级别绑定queue"""
 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()

注1:exchange_type=direct时,rabbitmq按消息级别发送和接收消息,接收端只能接收被指明级别的消息,其他消息,即时是由同一个发送端发送的也无法接收。当在接收端传入多个消息级别时,应逐个绑定消息队列。

注2:exchange_type=direct时,同样是广播模式,也就是如果给多个接收端指定相同的消息级别,它们都可以同时收到这一级别的消息。

例三(exchange的类型为topic):

当exchange的类型为topic时,在发送消息时,应指明消息消息的类型(比如mysql.log、qq.info等),我们可以在接收端指定接收消息类型的关键字(即按关键字接收,在类型为topic时,这个关键字可以是一个表达式)。

rabbitmq(中间消息代理)在python中的使用详解

import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='topic_logs',
       exchange_type='topic')
# 以命令行的方式启动发送端,以参数的形式传入发送消息类型的关键字
routing_key = sys.argv[1] if len(sys.argv[1]) > 2 else 'anonymous.info' 
# routing_key = 'anonymous.info'
# routing_key = 'abc.orange.abc'
# routing_key = 'abc.abc.rabbit'
# routing_key = 'lazy.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()
#!/usr/bin/env python
import pika
import sys
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.exchange_declare(exchange='topic_logs',
       exchange_type='topic')
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
binding_keys = sys.argv[1:]
# binding_keys = '#'  #接收所有的消息
# binding_keys = ['*.info']  #接收所有以".info"结尾的消息
# binding_keys = ['*.orange.*'] #接收所有含有".orange."的消息
# binding_keys = ['*.*.rabbit', 'lazy.*'] #接收所有含有两个扩展名且结尾是".rabbit"和所有以"lazy."开头的消息
if not binding_keys:
 sys.stderr.write("Usage: %s [binding_key]...\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()

注:当exchange的类型为topic时,发送端与接收端的代码都跟类型为direct时很像(基本只是变一个类型,如果接收消息类型的指定不用表达式,它们几乎一样),但是topic的应用场景更广。

注:rabbitmq指定消息的类型的表达式其实很简单:

'#':代表接收所有的消息(一般单独使用),使用它相当于exchang的类型为fanout。

'*':代表任意一个字符(一般与其他单词配合使用)。

不使用'#'或'*',使用它相当于exchang的类型为direct。

前面介绍的都是一端发送,一端接收的消息传递模式,那么rabbitmq该如何实现客户端和服务端都要发送和接收(即RPC)呢?

我们先来简单了解以下RPC,RPC(Remote Procedure Call)采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。

例五(通过rabbitmq实现rpc):

先来看以下在rabbitmq中rpc的消息传递模式:

rabbitmq(中间消息代理)在python中的使用详解

我们以客户端发送一个数字n,服务端计算出斐波那契数列的第n个数的值返回给客户端为例。

import pika
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='rpc_queue')
def fib(n):
 """
 计算斐波那契数列中第n个数的值
 :param n:
 :return:
 """
 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), # 指明处理消息的id 用于客户端确认
      body=str(response))
 ch.basic_ack(delivery_tag = method.delivery_tag) # 未申明no_ack = True, 消息处理完毕需向rabbitmq确认
channel.basic_qos(prefetch_count=1) # 每次只处理一条消息
channel.basic_consume(on_request, queue='rpc_queue')
print(" [x] Awaiting RPC requests")
channel.start_consuming() # 开始接收消息,未收到消息处于阻塞状态

注1:测试时,先运行rpc_server.py,再运行rpc_client.py。

注2:客户端之所以每隔一秒检测一次服务端有没有返回结果,是因为客户端接收时时无阻塞的,在这一端时间内(不一定是1秒,但执行的任务消耗的时间不要太长)客户端可以执行其他任务提高效率。

注3:为什么客户端和服务端不使用一个队列来传递消息? 答:如果使用一个队列,以客户端为例,它一边在检测这个队列中有没有它要接收的消息,一边又往这个队列里发送消息,会形成死循环。

(PS:本文例中出现的所有代码是做了一些简单修改(方便读者理解)后的rabbitmq官方文档中的代码。)

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
python使用smtplib模块通过gmail实现邮件发送的方法
May 08 Python
Python代码解决RenderView窗口not found问题
Aug 28 Python
python+pyqt实现12306图片验证效果
Oct 25 Python
Python中defaultdict与lambda表达式用法实例小结
Apr 09 Python
pyQt5实时刷新界面的示例
Jun 25 Python
浅谈Pytorch中的torch.gather函数的含义
Aug 18 Python
Python input函数使用实例解析
Nov 22 Python
Python爬虫库BeautifulSoup获取对象(标签)名,属性,内容,注释
Jan 25 Python
深入浅析Python 函数注解与匿名函数
Feb 24 Python
python中format函数如何使用
Jun 22 Python
Requests什么的通通爬不了的Python超强反爬虫方案!
May 20 Python
python模拟浏览器 使用selenium进入好友QQ空间并留言
Apr 12 Python
用python的requests第三方模块抓取王者荣耀所有英雄的皮肤实例
Dec 14 #Python
用Python删除本地目录下某一时间点之前创建的所有文件的实例
Dec 14 #Python
python编程通过蒙特卡洛法计算定积分详解
Dec 13 #Python
Python编程产生非均匀随机数的几种方法代码分享
Dec 13 #Python
windows下Virtualenvwrapper安装教程
Dec 13 #Python
python实现机械分词之逆向最大匹配算法代码示例
Dec 13 #Python
Python语言描述KNN算法与Kd树
Dec 13 #Python
You might like
解析wamp5下虚拟机配置文档
2013/06/27 PHP
php防止sql注入示例分析和几种常见攻击正则表达式
2014/01/12 PHP
Thinkphp+smarty+uploadify实现无刷新上传
2015/07/30 PHP
php文档工具PHP Documentor安装与使用方法
2016/01/25 PHP
php 伪造HTTP_REFERER页面URL来源的三种方法
2016/09/22 PHP
php使用parse_str实现查询字符串解析到变量中的方法
2017/02/17 PHP
PHP基于cookie实现统计在线人数功能示例
2019/01/16 PHP
javascript字典探测用户名工具
2006/10/05 Javascript
JS event使用方法详解
2008/04/28 Javascript
再次分享18个非常棒的jQuery表格插件
2011/04/10 Javascript
jQuery选择器的工作原理和优化分析
2011/07/25 Javascript
基于jquery的可多选的下拉列表框
2012/07/20 Javascript
JavaScript实现基于十进制的四舍五入实例
2015/07/17 Javascript
javascript实现超炫的向上滑行菜单实例
2015/08/03 Javascript
理解javascript中的MVC模式
2016/01/28 Javascript
js仿淘宝和百度文库的评分功能
2016/05/15 Javascript
jQuery使用getJSON方法获取json数据完整示例
2016/09/13 Javascript
微信小程序 开发之全局配置
2017/05/05 Javascript
ajax +NodeJS 实现图片上传实例
2017/06/06 NodeJs
vue.js中ref和$refs的使用及示例讲解
2019/08/14 Javascript
layui table 多行删除(id获取)的方法
2019/09/12 Javascript
微信小程序 SOTER 生物认证DEMO 指纹识别功能
2019/12/13 Javascript
微信小程序实现弹框效果
2020/05/26 Javascript
python不带重复的全排列代码
2013/08/13 Python
Python实现中一次读取多个值的方法
2018/04/22 Python
Python线程之定位与销毁的实现
2019/02/17 Python
python反编译学习之字节码详解
2019/05/19 Python
实例讲解使用SVG制作loading加载动画的方法
2016/04/05 HTML / CSS
大韩航空官方网站:Korean Air
2017/10/25 全球购物
SHEIN台湾:购买最新流行女装服饰
2019/05/18 全球购物
英格兰足协官方商店:England Store
2019/07/12 全球购物
《狼》教学反思
2014/03/02 职场文书
2015清明节祭奠英烈寄语大全
2015/03/04 职场文书
学习经验交流会策划书
2015/11/02 职场文书
javaScript Array api梳理
2021/03/31 Javascript
MySql新手入门的基本操作汇总
2021/05/13 MySQL