RabbitMQ是一个消息代理,作用就是发送消息和接收消息。MQ是Message Queue的缩写,RabbitMQ服务类似于MYSQL,APACHE服务,只是提供的功能不一样而已,Rabbit MQ是用于提供发送消息的服务,可用在不同应用程序间进行通信。
RabbitMQ是用erlang语言写的,所以要先安装erlang依赖。
Ubuntu1604上安装:
sudo apt-get install rabbitmq-server
命令执行完后,RabbitMQ消息代理就已经在后台运行,并准备好传输消息。
有三种方式可对交换机和队列进行操作管理:
一是RabbitMQ管理Web界面
二是服务器上的命令行界面
三是Python源代码内
几个常用命令如下:
1.查看状态:
sudo rabbitmqctl status
2.停止rabbitMQ server:
sudo rabbitmqctl stop
3.运行rabbitMQ server:
sudo rabbitmq-server
4.启动web管理插件:
sudo rabbitmq-plugins enable rabbitmq_management
禁止插件:
sudo rabbitmq-plugins disable rabbitmq_management
配置文件修改:
sudo vim /etc/rabbitmq/rabbitmq-env.conf
用户权限管理命令:
1.添加用户:
sudo rabbitmqctl add_user {username} {password}
2.查看用户列表:
sudo rabbitmqctl list_users
此时用户huf-test权限为空,还需要给它赋权
3.为用户分配角色,如管理员administrator:
sudo rabbitmqctl set_user_tags {username} {tag ...}
4.查看消息队列:
5.查看交换机:
6.查看绑定情况:
7.其他命令:
删除用户:
sudo rabbitmqctl delete_user {username}
修改密码:
sudo rabbitmqctl change_password {username} {newpassword}
删除密码:
sudo rabbitmqctl clear_password {username}
用户认证:
sudo rabbitmqctl authenticate_user {username} {password}
然后通过以下地址去登陆消息界面管理rabbitmq:
http://192.168.1.115:15672/#/
192.168.1.115是IP地址。其中guest这个默认用户只能通过http://localhost:15672来登陆访问。
登陆后,但用户还没有对队列和交换机资源访问的权限:
rabbitmq客户端连接服务端内部大概流程:
rabbitmq客户端连接到一个服务端时,在它的操作指令中指定了一个虚拟主机,服务端首先会检查是否有访问该虚拟主机的权限,若没有权限则会拒绝连接。
而对于交换机和队列等资源,是位于某个虚拟主机内的,不同虚拟主机内即使名称相同也代表不同的资源,当特定操作在资源上执行时,第二级访问控制开始生效。
rabbitmq在某个资源上有配置,写和读操作:
1.配置操作创建,销毁资源或更改资源的行为
2.写操作将消息注入进资源中
3.读操作从资源中获取消息
要执行特定操作用户必须授予合适的权限。
而由上图可直观看出因权限问题使得用户huf-test无法对交换机和队列做操作,因此这里通过命令行给其赋权,也可直接在Web内给其赋权:
给用户在对应的vhost上分配相应的权限
sudo rabbitmqctl set_permissions [-p vhost] {user} {conf} {write} {read}
比如:
rabbitmqctl set_permissions -p /myvhost huf "^huf-.*" ".*" ".*"
给用户huf在myvhost分配权限,包括:
将配置权限赋给huf-开头的全部资源以及将读写权限赋给所有的资源。
所以这里:
sudo rabbitmqctl set_permissions -p / huf-test ".*" ".*" ".*"
再次回到web界面创建队列资源,成功,交换机类似。
其他于vhost相关命令如下:
获取vhost列表:
sudo rabbitmqctl list_vhosts [vhostinfoitem ...]
添加vhost列表:
sudo rabbitmqctl add_vhost {vhost}
删除vhost列表:
sudo rabbitmqctl delete_vhost {vhost}
清除权限:
sudo rabbitmqctl clear_permissions [-p vhost] {username}
显示vhost权限分配列表:
sudo rabbitmqctl list_permissions [-p vhost]
显示user权限列表:
sudo rabbitmqctl list_user_permissions {username}
以上均在ubuntu16.04内操作。
下面在Windows10上使用该消息代理的一些实例:
所需环境:
1.Python2.7(也可以Python3.x)
2.pip 10.0.1(它是Python的软件管理包,这里是要用于安装pika模块)
以下URL下载脚本安装pip:
https://bootstrap.pypa.io/get-pip.py
下载完后将get-pip.py放在F:\Python27\Scripts目录下运行:
python.exe get-pip.py
将路径F:\Python27\Scripts添加到环境变量PATH中,为系统提供pip命令。
安装pika
pip install pika
3.PowerShell
以下实例为远程连接到RabbitMQ环境,非localhost,若需要localhost,将connection替换为如下即可:
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
实例一:
下面的实例主要内容就是从send.py发出"Hello World!"消息到RabbitMQ,receive从RabbitMQ接收到send.py发送的消息:
发送消息实例: send.py
#!/usr/bin/env python
#coding:utf-8
import pika
#第一步,连接RabbitMq服务器
rabbit_username='huf-test'
rabbit_password='123'
credentials = pika.PlainCredentials(rabbit_username, rabbit_password)
connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.1.115',credentials=credentials))
#channel是进行消息读写的通道
channel = connection.channel()
#第二步,创建一个名为hello的队列,然后把消息发送到这个队列
channel.queue_declare(queue='hello')
#第三步,现在可以发送消息,但是RabbitMQ不能把消息直接发送到队列,要发送到交换器,这个稍后介绍,这里使用默认交换器(exchange),它使用一个空字符串标
#识,routing_key参数必须指定为队列名称,这里为hello
channel.basic_publish(exchange='',
routing_key='hello',
body='hello world')
print "send.py:send message 'hello world',wait for receive.py deal with this message"
#退出程序前,通过关闭连接保证消息已经投递到RabbitMq
connection.close()
接收消息实例:receive.py
#!/usr/bin/env python
#coding:utf-8
import pika
#第一步,同样连接RabbitMq服务器
rabbit_username='huf-test'
rabbit_password='123'
credentials = pika.PlainCredentials(rabbit_username, rabbit_password)
connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.1.115',credentials=credentials))
channel = connection.channel()
#为确保队列存在,再次执行queue_declare创建一个队列,我们可以多次运行该命令,但是只有一个队列会创建
#因为不能保证send.py先执行还是receive.py先执行,所以重复声明队列来确保其存在
channel.queue_declare(queue='hello')
#第三步,定义一个回调函数,当获得消息时,Pika库调用这个回调函数来处理消息,该回调函数将消息内容打印到屏幕
def callback(ch, method, properties, body):
print "receive.py: Received message %r" % (body,)
#第四步,告诉rabbbitMq回调函数将从queue队列接收消息
channel.basic_consume(callback,
queue='hello',
no_ack=True)
#第五步,输入一个无限循环来等待消息数据并运行回调函数
print ' [*] Waiting for messages. To exit press CTRL+C'
channel.start_consuming()
在Window10上开两个终端分别运行send.py和receive.py:
可在服务器上用查看队列命令,有一个名为hello的队列被添加到RabbitMQ服务内:
实例二:
工作队列
消息也可理解为任务,消息发送者可理解为任务分配者,消息接收者可理解为工作者,当工作者接收到一个任务,还没完成的时候,任务分配者又发来一个任务,此时就忙不过来了,因此就需要多个工作者来共同处理这些任务,这些工作者就被称为工作队列。结构图如下:
任务分配者: new_task.py
#!/usr/bin/env python
import pika
import sys
rabbit_username='huf-test'
rabbit_password='123'
credentials = pika.PlainCredentials(rabbit_username, rabbit_password)
connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.1.115',credentials=credentials))
channel = connection.channel()
channel.queue_declare(queue='task_queue', durable=True) #new queue "task_queue" created
message = ' '.join(sys.argv[1:]) or "Hello World!" #message resolved from commandline, if no message then "Hello World!" sent
channel.basic_publish(exchange='', #default exchange
routing_key='task_queue',
body=message,
properties=pika.BasicProperties(
delivery_mode = 2, # make message persistent
))
print " [x] Sent %r" % (message,)
connection.close()
工作者:worker.py
#!/usr/bin/env python
import pika
import time
rabbit_username='huf-test'
rabbit_password='123'
credentials = pika.PlainCredentials(rabbit_username, rabbit_password)
connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.1.115',credentials=credentials))
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( 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()
运行一个new_task.py模拟一个任务分配者,运行三个worker.py模拟三个工作者。
这里有几个概念:
1.消息确认:就是当工作者完成任务后,会反馈给RabbitMQ。这边停顿和new_task.py发送过来的"."相同数量的秒数,方便使用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)
以下代码运行后:
ch.basic_ack(delivery_tag = method.delivery_tag
即使其中一个工作者CTRL+C退出后,正在执行的任务也不会丢失,RabbitMQ会将任务重新分配给其他工作者。
2.消息持久化:虽然有消息反馈机制,但若RabbitMQ自身挂掉的话,那么任务还是会丢失。所以需要将任务持久化存储起来,声明持久化存储如下:
channel.queue_declare(queue='task_queue', durable=True)
在发送任务时,需要以delivery_mode=2来设置任务为持久化存储:
channel.basic_publish(exchange='', #default exchange
routing_key='task_queue',
body=message,
properties=pika.BasicProperties(
delivery_mode = 2, # make message persistent
))
3.公平调度:虽然每个工作者都是一次分配到任务,但每个任务不一定一样。可能有的任务比较重,执行时间比较久,有的任务比较轻,执行时间比较短。此时需要公平调度,可将basic_qos设置为prefetch_count=1来实现,可使得RabbitMQ不会在同一时间给工作者分配多个任务,也就是说只有工作者完成任务之后,才会再次接收到任务。
在工作者内设置:
channel.basic_qos(prefetch_count=1)
实例三:
交换机
实例二的工作队列每次消息都只会发送给其中一个接收端,若需要将消息广播出去,让每个接收端都能收到,则需要使用交换机。
交换机工作原理:消息发送端先将消息发送给交换机,交换机再将消息发送到绑定的消息队列,而后每个接收端都能从各自的消息队列内接收到消息。
消息发送端:send-exchanges-msg.py
#!/usr/bin/env python
#coding=utf8
import pika
import sys
rabbit_username='huf-test'
rabbit_password='123'
credentials = pika.PlainCredentials(rabbit_username, rabbit_password)
#message = ' '.join(sys.argv[1:]) or "Hello World!"
connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.1.115',credentials=credentials))
channel = connection.channel()
#define exchanges
channel.exchange_declare(exchange='messages', exchange_type='fanout')
#message sent to exchanges
channel.basic_publish(exchange='messages', routing_key='', body='Hello World!')
#channel.queue_declare(queue='hello')
#channel.basic_publish(exchange='',
# routing_key='hello',
# body=message)
#channel.basic_publish(exchange='', routing_key='hello', body='Hello World!')
#print " [x] Sent 'Hello World!'"
#print " [x] Sent %r" % (message,)
print " [x] Sent 'Hello World!'"
connection.close()
消息接收端:receive-exchange-msg.py
#!/usr/bin/env python
#coding=utf8
import pika
rabbit_username='huf-test'
rabbit_password='123'
credentials = pika.PlainCredentials(rabbit_username, rabbit_password)
connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.1.115',credentials=credentials))
channel = connection.channel()
#define exchanges
channel.exchange_declare(exchange='messages', exchange_type='fanout')
#generate queues randomly,then bind to exchanges
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
channel.queue_bind(exchange='messages', queue=queue_name)
def callback(ch, method, properties, body):
print " [x] Received %r" % (body,)
channel.basic_consume(callback, queue=queue_name, no_ack=True)
print ' [*] Waiting for messages. To exit press CTRL+C'
channel.start_consuming()
这里接收端做了两件事:
1.定义交换机
channel.exchange_declare(exchange='messages', exchange_type='fanout')
交换机有四种类型:direct,topic,headers和fanout。要四种规则是因为每种规则匹配时CPU开销是不同的,因此根据不同需求选择合适的交换机。
比如,一个topic类型的交换机会将消息的routingkey与类似"dog.*"的模式进行匹配,一个direct类型的交换机会将routingkey与"dogs"进行匹配,匹配末端通配符比直接比较消耗更多的CPU,所以若用不到topic类型交换机带来的灵活性,就通过direct类型交换机获得更高的处理效率。
a.direct交换机:转发消息到routingkey指定队列,完全匹配,单播。
routingkey与队列名完全匹配,若一个队列绑定到交换机要求routingkey为"dog",则只转发routingkey标记为dog的消息,不会转发dog.puppy,也不会转发dog.guard等。
b.topic交换机,按规则转发消息,最灵活,组播
topic类型交换机通过模式匹配分配消息的routingkey属性,将routingkey和某个模式进行匹配,此时队列需要绑定到一个模式上。它将routingkey和bindingkey的字符串切分成单词,这些单词之间用点隔开,它同样也会识别两个通配符:符号"#"和符号"*"。#匹配0个或多个单词,*匹配不多不少一个单词。
例如:bindingkey: *.stock.# 匹配routingkey: usd.stock和eur.stock.db,但是不匹配stock.nana。
例如:"audit.#"能够匹配"audit.irs.corporate",但"audit.*"只能匹配到"audit.irs"
c.fanout交换机:转发消息到所有绑定队列,最快,广播
fanout交换机不处理routingkey,简单地将队列绑定到交换机上,每个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上,很像子网广播,每台子网内的主机都获得一份复制的消息,fanout交换机转发消息是最快的。
d.headers交换机(使用比较少):
headers交换机主要通过发送request message中的header进行匹配,忽略routingkey的一种路由方式,其中匹配规则又分为all和any,这两种规则必须在接收端用键值x-match来定义。而对direct,topic和fanout的routingkey都需要以字符串形式,但是headers则无此要求,因为键值对的值可以为任何类型。
all表示必须所有的键值对匹配
any表示只需要一个键值对匹配,默认规则。
发送者在发送时定义一些键值对,接收者在绑定时传入一些键值对,两者匹配的话,则对应的队列就可收到消息。
注意:
a.若没有队列绑定在交换机上,则发送到该交换机上的消息会丢失
b.一个交换机可绑定多个队列,一个队列可被多个交换机绑定
c.还有一些其他类型的交换机类型,如failover,system等,但现在当前rabbitmq版本中还未实现
d.由于交换机是命名实体,声明一个已经存在的交换机,但是试图赋予不同类型是会导致错误的,客户端需要删除这个已经存在的交换机,然后重新声明并赋予新的类型。
e.交换机的属性:
①持久性:若启用,交换机将会在server重启前都有效
②自动删除:若启用,则交换机将会在其绑定的队列都被删掉之后删除自身
③惰性:若没有声明交换机,则在执行到使用的时候会导致异常,并不会主动声明
f.队列的属性:
①持久性:若启用,队列将在server服务重启前都有效
②自动删除:若启用,则队列将会在所有的消费者停止使用后自动删除自身
③惰性:若没有声明队列,则在执行到使用的时候会导致异常,并不会主动声明
④排他性:若启用,队列只能被声明它的消费者使用
2.随机生成临时队列,并绑定到交换机上
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
channel.queue_bind(exchange='messages', queue=queue_name)
其中queue_declare参数exclusive=True表示当接收端退出时,销毁临时产生的队列,这样就不会占用资源。
运行receive-exchange-msg.py程序
a.使用rabbitmqctl list_exchanges命令查看交换机信息:
b.使用命令rabbitmqctl list_queues产看产生的随机消息队列:
这里发送端也做了两件事:
1.定义交换机
2.不是将消息发送到hello队列,而是发送到交换机
从代码中可知,basic_publish方法参数exchange被设定为相应交换机,因为是要广播出去,发送到所有队列,因此routing_key就不需要设定了。
channel.basic_publish(exchange='messages', routing_key='', body='Hello World!')
若exchange设为空,则表示使用匿名交换机,在rabbitmqctl list_exchanges可看到有amq.*字样的交换机,就是系统默认的交换机。routing_key在使用匿名交换机时才需要指定,表示发送到哪个队列的意思。
运行一个send-exchanges-msg.py和两个receive-exchange-msg.py,可同时收到消息。
实例四:
routing-key
实例三是关于交换机的使用,已经能实现给所有接收端发送消息,但若需要自由定制,有的消息发送给其中一些接收端,有的消息发送给另外一些接收端,则需要使用routing-key。
routing-key的工作原理:每个接收端的消息队列在绑定交换机时,可设定相应的routing-key,发送端通过交换机发送信息时,可指明routing-key,交换机会根据routing-key把消息发送到相应的消息队列,这样接收端就能接收到消息了。
本实例主要功能是将info,warning,error三种级别的信息发送到不同的接收端。
发送端:send-exchanges-msg-direct.py
#!/usr/bin/env python
#coding=utf8
import pika
import sys
rabbit_username='huf-test'
rabbit_password='123'
credentials = pika.PlainCredentials(rabbit_username, rabbit_password)
#message = ' '.join(sys.argv[1:]) or "Hello World!"
connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.1.115',credentials=credentials))
channel = connection.channel()
#define exchanges
channel.exchange_declare(exchange='messagesd', exchange_type='direct')
routings = ['info', 'warning', 'error']
#message sent to exchanges
for routing in routings:
message = '%s message.' % routing
channel.basic_publish(exchange='messagesd', routing_key=routing, body=message)
print message
connection.close()
接收端:receive-exchanges-msg-direct.py
#!/usr/bin/env python
#coding=utf8
import pika
import sys
rabbit_username='huf-test'
rabbit_password='123'
credentials = pika.PlainCredentials(rabbit_username, rabbit_password)
connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.1.115',credentials=credentials))
channel = connection.channel()
#define exchanges,set type as direct
channel.exchange_declare(exchange='messagesd', exchange_type='direct')
#get router key args from commandline,otherwise, set it as info
routings = sys.argv[1:]
if not routings:
routings = ['info']
#generate queues randomly,then bind to exchanges
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
for routing in routings:
channel.queue_bind(exchange='messagesd', queue=queue_name, routing_key=routing)
def callback(ch, method, properties, body):
print " [x] Received %r" % (body,)
channel.basic_consume(callback, queue=queue_name, no_ack=True)
print ' [*] Waiting for messages. To exit press CTRL+C'
channel.start_consuming()
这里发送端代码做了两件事:
1.将交换机类型设置为direct,上一个实例为fanout,表示广播的意思,会将消息发送到所有接收端,这里设置direct表示要根据设定的routing-key来发送消息。
#define exchanges
channel.exchange_declare(exchange='messagesd', exchange_type='direct')
2.发送消息时设置发送的routing-key
#define exchanges
channel.exchange_declare(exchange='messagesd', exchange_type='direct')
routings = ['info', 'warning', 'error']
这里接收端代码做了三件事:
1.将交换机类型设置为direct
#define exchanges
channel.exchange_declare(exchange='messagesd', exchange_type='direct')
2.增加命令行获取参数功能,参数为routing-key
#get router key args from commandline,otherwise, set it as info
routings = sys.argv[1:]
if not routings:
routings = ['info']
3.将队列绑定到交换机上时,设置routing-key
for routing in routings:
channel.queue_bind(exchange='messagesd', queue=queue_name, routing_key=routing)
这里运行一个send-exchanges-msg-direct.py和两个receive-exchanges-msg-direct.py,其中receive-exchanges-msg-direct.py分别带参数info和warning,可看到带info的终端直接收到info,而带warning只接收到warning。
在以上程序运行期间,在服务器端输入绑定查询命令:
sudo rabbitmqctl list_bindings
实例五
routing-key模糊匹配
上个实例说明routing-key的功能,通过设置routing-key可将消息发送到相应的队列,这里的routing-key是要完全匹配的,比如info消息只能发到routing-key为info的消息队列。
routing-key模糊匹配就是可使用正则表达式,与常用的正则表达式不同,这里的#表示所有,全部的意思,*只匹配一个词。
本例的主要内容是,假如:
1.有一个知心好友,不管开心,伤心,工作上的还是生活上的事情都可以和她说
2.还有一些好友可分享开兴的事情
3.还有一些好友,可把工作上的事和她说
发送者:send-exchanges-msg-topic.py
#!/usr/bin/env python
import pika
import sys
rabbit_username='huf-test'
rabbit_password='123'
credentials = pika.PlainCredentials(rabbit_username, rabbit_password)
#message = ' '.join(sys.argv[1:]) or "Hello World!"
connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.1.115',credentials=credentials))
channel = connection.channel()
#define exchanges
channel.exchange_declare(exchange='messagestopic', exchange_type='topic')
#define routing-key
routings = ['happy.work', 'happy.life', 'sad.work', 'sad.life']
#message sent to exchanges,and routing-key set
for routing in routings:
message = '%s message.' % routing
channel.basic_publish(exchange='messagestopic', routing_key=routing, body=message)
print message
connection.close()
接收者:receive-exchanges-msg-topic.py
#!/usr/bin/env python
import pika
import sys
rabbit_username='huf-test'
rabbit_password='123'
credentials = pika.PlainCredentials(rabbit_username, rabbit_password)
connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.1.115',credentials=credentials))
channel = connection.channel()
#define exchanges,set type as direct
channel.exchange_declare(exchange='messagestopic', exchange_type='topic')
#get router key args from commandline,otherwise, set it as info
routings = sys.argv[1:]
if not routings:
print >> sys.stderr, "Usage: %s [routing_key]..." % (sys.argv[0],)
exit()
#generate queues randomly,then bind to exchanges
result = channel.queue_declare(exclusive=True)
queue_name = result.method.queue
for routing in routings:
channel.queue_bind(exchange='messagestopic', queue=queue_name, routing_key=routing)
def callback(ch, method, properties, body):
print " [x] Received %r" % (body,)
channel.basic_consume(callback, queue=queue_name, no_ack=True)
print ' [*] Waiting for messages. To exit press CTRL+C'
channel.start_consuming()
这里运行一个send-exchanges-msg-topic.py和三个receive-exchanges-msg-topic.py,其中三个receive-exchanges-msg-topic.py分别带参数"#", "happy.*"和"*.work",分别对应所有的事,开兴的事,和工作的事。
可看到结果如下所示:
实例六
远程结果返回
前面的实例都是发送端发送消息出去后无返回结果,若只是单纯发送消息这样是没有问题的,但在实际应用中,常会需要接收端将收到消息进行处理后,返回结果给发送端。
处理方法描述:
发送端在发送消息前,产生一个接收消息的临时队列,该队列用于接收返回的结果,实际在这里,接收端,发送端的概念比较模糊了,因为发送端也同样要接收消息,接收端同样也要发送消息,因此这里使用另外实例来演示这一过程。
实例功能:假设有一个控制中心和一个计算节点,控制中心会将一个自然数N发送给计算节点,计算节点将N值加1后,返回给控制中心,这里以center.py模拟控制中心,以compute.py模拟计算节点。
控制中心:center.py
#!/usr/bin/env python
import pika
class Center(object):
def __init__(self):
#connect to rabbitmq
rabbit_username='huf-test'
rabbit_password='123'
credentials = pika.PlainCredentials(rabbit_username, rabbit_password)
self.connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.1.115',credentials=credentials))
self.channel = self.connection.channel()
#define queue for responsed msg
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)
#define method for receiving msg
def on_response(self, ch, method, props, body):
self.response = body
def request(self, n):
self.response = None
#send request to client, return queue
self.channel.basic_publish(exchange='',
routing_key='compute_queue',
properties=pika.BasicProperties(
reply_to = self.callback_queue,
),
body=str(n))
#receive response data
while self.response is None:
self.connection.process_data_events()
return int(self.response)
center = Center()
print " [x] Requesting increase(31)"
response = center.request(31)
print " [.] Got %r" % (response,)
计算节点:compute.py
#!/usr/bin/env python
import pika
#connect to rabbitmq
rabbit_username='huf-test'
rabbit_password='123'
credentials = pika.PlainCredentials(rabbit_username, rabbit_password)
connection = pika.BlockingConnection(pika.ConnectionParameters( host='192.168.1.115',credentials=credentials))
channel = connection.channel()
#define queue
channel.queue_declare(queue='compute_queue')
print ' [*] Waiting for n'
#increase 1 step by step
def increase(n):
return n + 1
#define method for receiving msg
def request(ch, method, properties, body):
print " [.] increase(%s)" % (body,)
response = increase(int(body))
#send result back controle center
ch.basic_publish(exchange='',
routing_key=properties.reply_to,
body=str(response))
ch.basic_ack(delivery_tag = method.delivery_tag)
channel.basic_qos(prefetch_count=1)
channel.basic_consume(request, queue='compute_queue')
channel.start_consuming()
这里计算节点代码比较简单,原来的接收方法都是直接将消息打印出来,这里进行了加1计算,再将结果发送回控制中心
这里控制中心代码定义了接收返回数据的队列和处理方法,并在发送请求的时候将该队列赋值给reply_to,在计算节点代码中是通过这个参数来获取返回队列的。
这里运行一个compute.py和一个center.py,如下所示:
实例七
相互关联编号correlation id-RPC服务
RabbitMQ中实现RPC机制是:
1.客户端发送消息请求时,在消息的属性设置两个值reply_to和correlation_id
reply_to和correlation_id,这两个属性是AMQP中消息的属性,reply_to是服务端处理完成后将结果发送给客户端指定的队列;而correlation_id是将RPC的请求和响应关联起来,此次RPC请求的标识号,服务处理消息完成后将此属性返回,客户端根据这个id理解哪条请求被成功执行或执行失败。
AMQP协议给消息预定义了一系列的14个属性,大多数属性很少会用到,除以下几个:
delivery_mode(投递模式):将消息标记为持久的(值为2)或暂存的(除2之外的所有值)
content_type(内容类型):用来描述编码的mime-type,例如在实际使用中常使用application/json来描述JSON编码类型
reply_to(回复目标):通常用来命名回调队列
correlation_id(关联标识):用来将RPC的相应和请求关联起来
2.服务器端接收到消息并处理
3.服务端处理完消息后,将生成的应答消息到reply_to指定的队列中,同时带上correlation_id属性
4.客户端之前已经订阅reply_to指定的queue,从中接收到服务器的应答消息后,根据其中的属性correlation_id分析哪条请求被执行了,根据执行结果进行后续业务处理。
实例功能:
这里演示如何使用一个RPC服务,使用一个call接口用于发送一个RPC请求同时一直阻塞到有一个应答接收为止。
发送端生产者:send_rpc_producer.py
#coding:utf-8
import pika
import uuid
class SSHRpcClient(object):
def __init__(self):
credentials = pika.PlainCredentials('huf-test', '123')
self.connection = pika.BlockingConnection(pika.ConnectionParameters(
host='192.168.1.115',credentials=credentials))
self.channel = self.connection.channel()
result = self.channel.queue_declare(exclusive=True) #客户端的结果必须要返回到这个queue
self.callback_queue = result.method.queue
self.channel.basic_consume(self.on_response,queue=self.callback_queue) #声明从这个queue里收结果
def on_response(self, ch, method, props, body):
if self.corr_id == props.correlation_id: #任务标识符
self.response = body
print(body)
# 返回的结果,放在callback_queue中
def call(self, n):
self.response = None
self.corr_id = str(uuid.uuid4()) #唯一标识符
self.channel.basic_publish(exchange='',
routing_key='rpc_queue3', #声明一个Q
properties=pika.BasicProperties(
reply_to=self.callback_queue,
correlation_id=self.corr_id,
),
body=str(n))
print("start waiting for cmd result ")
count = 0
while self.response is None: #如果命令没返回结果
print("loop ",count)
count +=1
self.connection.process_data_events() #以不阻塞的形式去检测有没有新事件
#如果没事件,那就什么也不做, 如果有事件,就触发on_response事件
return self.response
ssh_rpc = SSHRpcClient()
print(" [x] sending cmd")
response = ssh_rpc.call("ipconfig")
print(" [.] Got result ")
print(response.decode("gbk"))
接收端消费者:recv_rpc_consumer.py
import pika
import time
import subprocess
credentials = pika.PlainCredentials('huf-test', '123')
connection = pika.BlockingConnection(pika.ConnectionParameters(
host='192.168.1.115', credentials=credentials))
channel = connection.channel()
channel.queue_declare(queue='rpc_queue3')
def SSHRPCServer(cmd):
print("recv cmd:",cmd)
cmd_obj = subprocess.Popen(cmd.decode(),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
result = cmd_obj.stdout.read() or cmd_obj.stderr.read()
return result
def on_request(ch, method, props, body):
print(" [.] fib(%s)" % body)
response = SSHRPCServer(body)
ch.basic_publish(exchange='',
routing_key=props.reply_to,
properties=pika.BasicProperties(correlation_id= \
props.correlation_id),
body=response)
channel.basic_consume(on_request, queue='rpc_queue3')
print(" [x] Awaiting RPC requests")
channel.start_consuming()
先运行一个recv_rpc_consumer.py,再运行一个send_rpc_producer.py,可看到如下结果: