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用于url解码和中文解析的小脚本(python url decoder)
Aug 11 Python
Python ORM框架SQLAlchemy学习笔记之数据添加和事务回滚介绍
Jun 10 Python
使用python开发vim插件及心得分享
Nov 04 Python
Python中取整的几种方法小结
Jan 06 Python
dataframe 按条件替换某一列中的值方法
Jan 29 Python
Python对HTML转义字符进行反转义的实现方法
Apr 28 Python
python的常见矩阵运算(小结)
Aug 07 Python
python matplotlib画盒图、子图解决坐标轴标签重叠的问题
Jan 19 Python
TFRecord格式存储数据与队列读取实例
Jan 21 Python
在python tkinter界面中添加按钮的实例
Mar 04 Python
新手学习Python2和Python3中print不同的用法
Jun 09 Python
JAVA SpringMVC实现自定义拦截器
Mar 16 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
一个更简单的无限级分类菜单代码
2007/01/16 PHP
PHP实现使用DOM将XML数据存入数组的方法示例
2017/09/27 PHP
详解PHP实现支付宝小程序用户授权的工具类
2018/12/25 PHP
JavaScript中的Screen屏幕对象
2008/01/16 Javascript
jQuery中:checkbox选择器用法实例
2015/01/03 Javascript
jquery删除指定子元素代码实例
2015/01/13 Javascript
JavaScript中常见获取元素的方法汇总
2015/03/04 Javascript
JavaScript中的Math.sin()方法使用详解
2015/06/15 Javascript
Sublime Text 3常用插件及安装方法
2015/12/16 Javascript
jQuery短信验证倒计时功能实现方法详解
2016/05/25 Javascript
AngularJS教程之环境设置
2016/08/16 Javascript
js正则表达式验证密码强度【推荐】
2017/03/03 Javascript
一些你可能不熟悉的JS知识点总结
2019/03/15 Javascript
js实现数字从零慢慢增加到指定数字示例
2019/11/07 Javascript
Vue 图片压缩并上传至服务器功能
2020/01/15 Javascript
Postman无法正常返回结果问题解决
2020/08/28 Javascript
axios封装与传参示例详解
2020/10/18 Javascript
[15:35]教你分分钟做大人:天怒法师
2014/10/30 DOTA
[42:32]VP vs RNG 2019国际邀请赛淘汰赛 败者组 BO3 第一场 8.21.mp4
2020/07/19 DOTA
Python实现将数据库一键导出为Excel表格的实例
2016/12/30 Python
使用requests库制作Python爬虫
2018/03/25 Python
python通过微信发送邮件实现电脑关机
2018/06/20 Python
Python3列表内置方法大全及示例代码小结
2019/05/10 Python
利用 Flask 动态展示 Pyecharts 图表数据方法小结
2019/09/04 Python
python中sys模块是做什么用的
2020/08/16 Python
利用python查看数组中的所有元素是否相同
2021/01/08 Python
Django url 路由匹配过程详解
2021/01/22 Python
python绘制汉诺塔
2021/03/01 Python
详解canvas.toDataURL()报错的解决方案全都在这了
2020/03/31 HTML / CSS
MYSQL相比于其他数据库有哪些特点
2013/07/19 面试题
家长对老师的评语
2014/04/18 职场文书
装饰公司活动策划方案
2014/08/23 职场文书
党的群众路线教育实践活动心得体会(乡镇)
2014/11/03 职场文书
大学新生入学感想
2015/08/07 职场文书
Web应用开发TypeScript使用详解
2022/05/25 Javascript
详解Go语言中配置文件使用与日志配置
2022/06/01 Golang