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中执行shell命令的几个方法小结
Sep 18 Python
python获取文件版本信息、公司名和产品名的方法
Oct 05 Python
opencv python 基于KNN的手写体识别的实例
Aug 03 Python
Python嵌套式数据结构实例浅析
Mar 05 Python
详解python和matlab的优势与区别
Jun 28 Python
python编写猜数字小游戏
Oct 06 Python
解决django FileFIELD的编码问题
Mar 30 Python
利用python生成照片墙的示例代码
Apr 09 Python
基于python实现复制文件并重命名
Sep 16 Python
使用PyCharm官方中文语言包汉化PyCharm
Nov 18 Python
Python机器学习算法之决策树算法的实现与优缺点
May 13 Python
使用pandas生成/读取csv文件的方法实例
Jul 09 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
基于mysql的论坛(6)
2006/10/09 PHP
php超快高效率统计大文件行数
2015/07/05 PHP
PHP生成条形码大揭秘
2015/09/24 PHP
PHP编程获取音频文件时长的方法【基于getid3类】
2017/04/20 PHP
云网广告中的代码,提示出错,大家找找
2006/11/21 Javascript
利用jQuery的$.event.fix函数统一浏览器event事件处理
2009/12/21 Javascript
javascript 主动派发事件总结
2011/08/09 Javascript
jQuery extend 的简单实例
2013/09/18 Javascript
js超时调用setTimeout和间歇调用setInterval实例分析
2015/01/28 Javascript
js 获取范围内的随机数实例代码
2016/08/02 Javascript
JavaScript基础——使用Canvas绘图
2016/11/02 Javascript
浅谈$_FILES数组为空的原因
2017/02/16 Javascript
jQuery实现base64前台加密解密功能详解
2017/08/29 jQuery
微信小程序录音与播放录音功能
2017/12/25 Javascript
vue webpack打包后图片路径错误的完美解决方法
2018/12/07 Javascript
详解关于微信setData回调函数中的坑
2019/02/18 Javascript
javascript中的闭包概念与用法实践分析
2019/07/26 Javascript
jquery validate 实现动态增加/删除验证规则操作示例
2019/10/28 jQuery
[34:39]Secret vs VG 2018国际邀请赛淘汰赛BO3 第二场 8.23
2018/08/24 DOTA
Python模块学习 filecmp 文件比较
2012/08/27 Python
Django自定义插件实现网站登录验证码功能
2017/04/19 Python
Python解惑之整数比较详解
2017/04/24 Python
Python嵌套列表转一维的方法(压平嵌套列表)
2018/07/03 Python
python list格式数据excel导出方法
2018/10/31 Python
Python求平面内点到直线距离的实现
2020/01/19 Python
在keras中对单一输入图像进行预测并返回预测结果操作
2020/07/09 Python
Java中实现多态的机制
2015/08/09 面试题
主管职责范文
2013/11/09 职场文书
12岁生日感言
2014/01/21 职场文书
《吃水不忘挖井人》教学反思
2014/04/15 职场文书
医院安全生产月活动总结
2014/07/05 职场文书
不服从上级领导安排的检讨书
2014/09/14 职场文书
高中生逃课检讨书
2014/10/10 职场文书
担保贷款承诺书
2015/04/30 职场文书
创业计划书之暑假培训班
2019/11/09 职场文书
Rust中的Struct使用示例详解
2022/08/14 Javascript