RabbitMQ消息队列

一:整体面貌

讲解基础概念的前面,我们先来整体构造一个结构图,这样会方便们更好地去理解RabbitMQ的基本原理。
在这里插入图片描述

  • 通过上面这张应用相结合的结构图既能够清晰的看清楚整体的send MessageReceive Message的一个大致的流程。当然上面有很多名词都相比还没有介绍到,不要着急接下来我们就开始对其进行详细的讲解。
  • Queue(队列)RabbitMQ的作用是存储消息,队列的特性是先进先出。上图可以清晰地看到Client A和Client B是生产者,生产者生产消息最终被送到RabbitMQ的内部对象Queue中去,而消费者则是从Queue队列中取出数据。可以简化成表示为:
    在这里插入图片描述
  • 生产者Send Message A被传送到Queue中,消费者发现消息队列Queue中有订阅的消息,就会将这条消息A读取出来进行一些列的业务操作。
  • 这里只是一个消费正好对应一个队列Queue,也可以多个消费者订阅同一个队列Queue
  • 如果多个消费者订阅同一个Queue队列,这里就会将Queue里面的消息平分给其他的消费者
  • 但是会存在一个问题,如果每个消息的处理时间不同,就会导致某些消费者一直在忙碌中,而有的消费者处理完了消息后一直处于空闲状态
  • 因为前面已经提及到了Queue会平分这些消息给相应的消费者。这里我们就可以使用prefetchCount来限制每次发送给消费者消息的个数。详情见下图所示:
    在这里插入图片描述
  • 这里的prefetchCount=1是指每次从Queue中发送一条消息来。等消费者处理完这条消息后Queue会再发送一条消息给消费者。
  • 即:每个消费者队列最多只有一个任务,如果没处理掉,则该消费者不再从队列中拿取新的任务。
  • channel.basic_qos(prefetch_count=1)

二:Centos7安装配置RabbitMQ

1. 切换root用户

su root

2. 安装配置epel源

rpm -ivh http://dl.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm

3. 安装erlang

yum -y install erlang

4. 安装RabbitMQ

yum -y install rabbitmq-server

5. 启动/关闭

service rabbitmq-server start/stop

6. 查看用户列表

rabbitmqctl list_users

在这里插入图片描述
7. 创建用户

rabbitmqctl add_user 用户名 密码

# 设置超级管理员标签
rabbitmqctl set_user_tags 用户名 administrator

# 设置权限
rabbitmqctl set_permissions -p / 用户名 ".*" ".*" ".*"

在这里插入图片描述
8. 删除用户

rabbitmqctl delete_user 用户名

在这里插入图片描述
9. 修改用户

rabbitmqctl change_password 用户名 新密码

在这里插入图片描述
10. 开启防火墙5672 / 15672端口

firewall-cmd --zone=public --add-port=5672/tcp --permanent
firewall-cmd --zone=public --add-port=15672/tcp --permanent
firewall-cmd --reload

11. 插件管理

RabbitMQ的有些插件没有集成在初始的安装中,它们需要额外安装,这些文件的后缀为.ez,安装时需要将.ez文件拷贝到安装的插件目录。以下是不同系统中默认安装的插件目录路径:

插件目录
Linux/usr/lib/rabbitmq/lib/rabbitmq_server-version/plugins
WindowsC:\Program Files\RabbitMQ\rabbitmq_server-version\plugins(安装rabbitmq的目录)
Homebrew/usr/local/Cellar/rabbitmq/version/plugins
Generic Unixrabbitmq_server-version/plugins (安装rabbitmq的目录)

在这里插入图片描述
12. 开启管理界面配置

  1. 查看rabbitmq所在目录
whereis rabbitmq

rabbitmq: /usr/lib/rabbitmq /etc/rabbitmq
  1. 进入该路径下的bin目录(一般存放的是二进制可执行文件)
cd /usr/lib/rabbitmq/bin
  1. 执行插件安装命令
./rabbitmq-plugins enable rabbitmq_management
  1. 重启服务
service rabbitmq-server start
  1. 打开浏览器访问以下路径
http://127.0.0.1:15672
  1. 外部访问
    在这里插入图片描述
http://192.168.78.128:15672/

三:使用python操作RabbitMQ(一对一的消息收发)

(1)普通的消息收发

  • 当生产者将消息放入队列后如果有消费者接收了该消息,就直接将其从队列中删除了,假如此时接到该消息的处理者在没有处理完该消息的情况下挂掉了,那么该条消息就以失败告终,者显然不是我们想要看到的结果。
# sender.py
import pika

# 方式一: 用户名密码形式连接
# 连接rabbitmq(相当于socket链接)
# 使用密码连接
credentials = pika.PlainCredentials('admin', 'password@123')
connection = pika.BlockingConnection(pika.ConnectionParameters(host=
									'localhost', credentials=credentials))
# 直接连接
# connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))

# 方式二: url形式连接
# connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@123@192.168.78.128:5672'))

# 建立rabbitmq协议通道
channel = connection.channel()

# 声明一个队列
channel.queue_declare(queue='q1')

# 发送消息
channel.basic_publish(exchange='', routing_key='q1', body='hello world')

# 关闭队列
connection.close()

1. basic_publish:方法详解

def basic_publish(self, exchange, routing_key, 
				  body, properties=None, 
				  mandatory=False, immediate=False):
  • exchange:交换器的名称,指明消息需要发送到哪个交换器。如果设置为空字符串,则消息会被发生到 RabbitMQ 默认的交换器中,后面会讲到。
  • routing_key:路由键,交换器根据路由键将消息存储到相应的队列之中
  • properties:消息的基本属性集,其包含 14 个属性成员,分别有 contentType、deliveryMode、proiotity等。
  • body:消息体 payload,真正需要发送的消息。
  • mandatory:当参数设置为 true 时,交换器无法根据自身的类型和路由键找到一个符合条件的队列,那么 RabbitMQ 会调用 Basic.Return 命令,将消息返回给生产者。当参数设置为 false 时,出现上述情况,则消息直接丢失。
  • immediate:当参数设置为 true 时,如果交换器在将消息路由到队列时发现队列上并不存在任何消费者,那么这条消息将不会存入队列中。当与路由键匹配的所有队列都没有消费者时,该消息会通过 Basic.Return 返回至生产者。
# reciver.py

import pika

# 建立连接
connection = pika.BlockingConnection(pika.ConnectionParameters("localhost"))

# 创建channel
channel = connection.channel()

# 如果生产者先运行并创建了队列这里就可以不用声明,但是有可能消费者先运行
# 如果消费者先运行,下面的basic_consume就会因为没有队列报错。
channel.queue_declare(queue="q1")

# 定义回调函数用于取出队列中的数据
def callback(ch, method, properties, body):
	"""
    :param ch: channel通道
    :param method: 类似于http响应头,携带各种信息
    :param properties: 属性
    :param body: 消息主体内容,bytes格式
    """
    print(" [x] Received %r" % body)

# 基于1.01版本的 pika
channel.basic_consume(queue="q1",
                      on_message_callback=callback,
                      auto_ack=True  # 不用确认消息)

# 如果是0.12版本的 pika
# channel.basic_consume(consumer_callback=callback,
#                       queue="q1",
#                       no_ack=True  # 不用确认消息)
                      
print(" [*] Waiting for messages. To exit press Ctrl + C ")
# 监听数据
channel.start_consuming()

1. basic_consume:方法详解

def basic_consume(consumer_callback, queue='', 
				  no_ack=False, exclusive=False, 
				  consumer_tag=None, arguments=None):

# def basic_consume(queue='', on_message_callback=callback,
# 				  auto_ack=False, exclusive=False, 
# 				  consumer_tag=None, arguments=None):
  • consumer_callback: 设置消费者的回调函数。用来处理 RabbitMQ 推送过来的消息,比如:DefaultConsumer,使用时需要客户端重写其中的方法。
  • queue:队列的名称。
  • no_ack:设置是否自动确认。建议设置成 false,即不自动确认。
  • exclusive:设置是否排他。
  • consumer_tag:消费者标签,用来区分多个消费者。
  • arguments:设置消费者的其他参数。

(2)安全的消息收发:主要变化在消费者

  • 当生产者将消息放入队列后,如果有消费者接收了该消息,系统将给该消息打上标签,此时其他消费者将不会接收该消息,如果拿到消息的消费者将其处理完成(返回标签,系统好知道是哪条消息处理完成了),此时系统将会把该消息从队列中删除,假如消费者在拿到消息后的处理过程中挂掉(即socket连接中断),此时系统将去掉该消息的标签,使其可以被其他消费者接收并处理,这样一来,消息将不会因为某个消费者挂掉而丢失,确保了消息的安全性。
import pika

# 连接rabbitmq(相当于socket链接)
connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@123@192.168.78.128'))

# 建立rabbitmq协议通道
channel = connection.channel()

# 声明一个队列
channel.queue_declare(queue='q1')

# 发送消息
channel.basic_publish(exchange='',
                      routing_key='q1',
                      body='hello world')

# 关闭队列
connection.close()
  • 生产者:无变化
import pika

# 建立连接
connection = pika.BlockingConnection(pika.ConnectionParameters("192.168.78.128"))

# 创建channel
channel = connection.channel()

# 如果生产者先运行并创建了队列这里就可以不用声明,但是有可能消费者先运行
# 如果消费者先运行,下面的basic_consume就会因为没有队列报错。
channel.queue_declare(queue="q1")


# 定义回调函数用于取出队列中的数据
def callback(ch, method, properties, body):
    """
    :param ch: channel通道
    :param method: 类似于http响应头,携带各种信息
    :param properties: 属性
    :param body: 消息主体内容,bytes格式
    """
    print(" [x] Received %r" % body)
    ch.basic_ack(delivery_tag=method.delivery_tag)  # 发送确认消息

channel.basic_consume(queue="q1",
                      on_message_callback=callback,
                      auto_ack=False)  # 处理完成必须手动发送确认消息

print(" [*] Waiting for messages. To exit press Ctrl + C ")
# 监听数据
channel.start_consuming()
  • 消费者两处变化
  • auto_ack=False -----> 默认行为,所以可以不用写
  • ch.basic_ack(delivery_tag=method.delivery_tag)

(3) 队列持久化: 主要变化在生产者

  • 上述实现了在rabbitmq服务不挂掉的情况下,确保了消息不会丢失,但在生产环境中我们无法确保rabbitmq服务能够一直运行不会挂掉,所以我们需要实现队列持久化消息持久化
import pika

# 连接rabbitmq(相当于socket链接)
connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@123@192.168.78.128'))

# 建立rabbitmq协议通道
channel = connection.channel()

# 声明一个队列
# durable 声明的是一个持久化的队列
channel.queue_declare(queue='q2', durable=True)

# 发送消息
channel.basic_publish(exchange='',
                      routing_key='q2',
                      body='hello world')

# 关闭队列
connection.close()
  • durable=True
  • 当我们实现了队列持久化后,当我们的rabbitmq服务挂掉后,我们的队列依然存在,当我们服务恢复时,队列依旧可用,但是队列里面的消息则会随着服务挂掉而一去不复返,这显然不是我们想看到的,所以我们必须同时实现消息持久化。

(4)消息持久化:主要变化在生产者

  • 要实现消息持久化,必须确定已经开启了队列持久化,因为消息是存在于队列中,如果队列都挂了,那么消息何来持久化。
import pika

# 连接rabbitmq(相当于socket链接)
connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@123@192.168.78.128'))

# 直接连接
# connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))

# 建立rabbitmq协议通道
channel = connection.channel()

# 声明一个队列
# durable 声明的是一个持久化的队列
channel.queue_declare(queue='q2', durable=True)

# 发送消息
channel.basic_publish(exchange='',
                      routing_key='q2',
                      body='hello world',
                      properties=pika.BasicProperties(
                            delivery_mode=2  # 消息持久化
                      )
                      )

# 关闭队列
connection.close()
  • properties=pika.BasicProperties( delivery_mode=2) # 消息持久化

(5)说明

  1. 上述示例都是一对一的消息收发,即一条消息只能被一个消费者消费
  2. 队列持久化和消息持久化时消费者可以不用声明队列,如果声明队列,则必须保证队列也是持久化的,不然会由于歧义报错。
import pika

# 建立连接
connection = pika.BlockingConnection(pika.ConnectionParameters("192.168.78.128"))

# 创建channel
channel = connection.channel()

# 因为队列实现了持久化,所以消费者端可以不用声明,如果声明了,则必须指定为持久化队列,否则会报错
# channel.queue_declare(queue="q2")  报错
# channel.queue_declare(queue="q2", durable=True)

# 定义回调函数用于取出队列中的数据
def callback(ch, method, properties, body):
    """
    :param ch: channel通道
    :param method: 类似于http响应头,携带各种信息
    :param properties: 属性
    :param body: 消息主体内容,bytes格式
    """
    print('receive msg... start processing....', body)
    import time
    time.sleep(20)
    print(" [x] Received %r" % body)
    print('method.delivery_tag: ', method.delivery_tag)
    ch.basic_ack(delivery_tag=method.delivery_tag)


# 基于1.01版本的 pika
channel.basic_consume(queue="q2",
                      on_message_callback=callback,
                      auto_ack=False) 

print(" [*] Waiting for messages. To exit press Ctrl + C ")
# 监听数据
channel.start_consuming()

四:Exchange(一对多的消息收发)

           我们有没有疑问,开篇那个应用结构图里面,消费者Client A和消费者Client B是如何知道我发送的消息是给Queue1还是给Queue2,有没有过这个问题,那么我们就来解开这个面纱,看看到底是个什么构造。首先明确一点就是生产者产生的消息并不是直接发送给消息队列Queue的,而是要经过Exchange(交换器),由Exchange再将消息路由到一个或多个Queue,当然这里还会对不符合路由规则的消息进行丢弃掉,这里指的是后续要谈到的Exchange Type。那么Exchange是怎样将消息准确的推送到对应的Queue的呢?那么这里的功劳最大的当属Binding,RabbitMQ是通过Binding将Exchange和Queue链接在一起,这样Exchange就知道如何将消息准确的推送到Queue中去。简单示意图如下所示

在这里插入图片描述

广播:订阅发布模式

  • 提示:广播并不是所有人监听同一个队列,如果所有人都监听一个队列,那么根据先到先得规则,消息只能被第一个接收者拿到,其他人是听不到的。所以广播是每个收听者一个单独的队列,但是我们怎么知道哪些用户(即哪些队列)收听了我们,此时就需要用到消息转发器(Exchange),首先广播队列将消息发送给Exchange,发送的同时会产生一个Routing Key,与Exchange关联的队列会携带一个Binding Key,当Routing Key 和 Binding Key对应上的时候,消息就会发送到对应的Queue中去。
  • Exchange有四种类型,不同的类型有着不同的策略。也就是表明不同的类型将决定绑定的Queue不同,换言之就是说生产者发送了一个消息,Routing Key的规则是A,那么生产者会将Routing Key=A的消息推送到Exchange中,这时候Exchange中会有自己的规则,对应的规则去筛选生产者发来的消息,如果能够对应上Exchange的内部规则就将消息推送到对应的Queue中去。那么接下来就来详细讲解下Exchange里面类型。

Exchange Type

(1) fanout:fanout类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中(广播
在这里插入图片描述

  • 上图所示,生产者(P)生产消息1将消息1推送到Exchange,由于Exchange Type=fanout这时候会遵循fanout的规则将消息推送到所有与它绑定Queue,也就是图上的两个Queue最后两个消费者消费。
# fanout_sender.py

import pika

connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@123@192.168.78.128'))
channel = connection.channel()

# 声明消息转发器
channel.exchange_declare(exchange='logs',  # 以日志级别定义转发器(即上述提到的 Routing Key)
                         exchange_type='fanout'  # 转发器类型
                         )

# 推送消息
channel.basic_publish(exchange='logs',
                      routing_key='',  # 消息转发到的队列,因为此时是广播,所以不需要声明队列,系统会将消息转发的转发器exchange上
                      body='hello world',  # 消息内容
                      )

# 关闭连接
connection.close()
# fanout_receiver.py

import pika

connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@123@192.168.78.128'))
channel = connection.channel()

channel.exchange_declare(exchange='logs',  # 以日志级别定义转发器
                         exchange_type='fanout'  # 转发器类型
                         )

# 声明队列 exclusive:排他(即声明不重复的队列),同时还会在此队列断开后自动将其删掉
result = channel.queue_declare(queue='', exclusive=True)
# 获取队列名称
queue_name = result.method.queue

# 将自己的队列绑定到要接收消息的消息转发器上(即上述提到的 Binding Key)
channel.queue_bind(exchange='logs', queue=queue_name)


# 消息处理函数
def callback(ch, method, properties, body):
	# 此处收到的消息是bytes格式
    print('receive message: %s' % body)


# 消费者
channel.basic_consume(on_message_callback=callback, queue=queue_name)

# 启动消费
channel.start_consuming()

(2)direct:direct类型的Exchange路由规则也很简单,它会把消息路由到那些binding key与routing key完全匹配的Queue中(组播
在这里插入图片描述

  • 当生产者(P)发送消息时Rotuing key=booking时,这时候将消息传送给Exchange,Exchange获取到生产者发送过来消息后,会根据自身的规则进行与匹配相应的Queue,这时发现Queue1和Queue2都符合,就会将消息传送给这两个队列,如果我们以Rotuing key=create和Rotuing key=confirm发送消息时,这时消息只会被推送到Queue2队列中,其他Routing Key的消息将会被丢弃。
# direct_sender.py

import sys

import pika


connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@123@192.168.78.128'))
channel = connection.channel()

# 声明消息转发器
channel.exchange_declare(exchange='booking',
                         exchange_type='direct')

# 定义发送消息级别,只有绑定了该转发器对应级别的队列才能收到相应的消息
severity = sys.argv[1] if len(sys.argv) > 1 else 'create'

message = 'message: hello world from {} level'.format(severity)

# 推送消息
channel.basic_publish(exchange='booking',
                      routing_key=severity,  # 组播,转发消息给绑定了该转发器的这四个级别的队列
                      body=message,  # 消息内容
                      )

print('send {} level message: {}'.format(severity, message))

# 关闭连接
connection.close()
# direct_receiver.py

import sys

import pika


connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@123@192.168.78.128'))
channel = connection.channel()

channel.exchange_declare(exchange='booking',
                         exchange_type='direct')

# 声明队列 exclusive:排他(即声明不重复的队列),同时还会在此队列断开后自动将其删掉
result = channel.queue_declare(queue='', exclusive=True)
# 获取队列名称
queue_name = result.method.queue

# 定义该队列要绑定的队列级别
severities = sys.argv[1:]

for severity in severities:
    # 循环将自己的队列绑定到要接收消息的消息转发器上对应级别上
    # routing_key:booking转发器级别列表子集
    channel.queue_bind(exchange='booking', queue=queue_name, routing_key=severity)


# 消息处理函数
def callback(ch, method, properties, body):
    print('receive message: %s' % body)


# 消费者
channel.basic_consume(on_message_callback=callback, queue=queue_name)

# 启动消费
channel.start_consuming()

在这里插入图片描述
(3) topic:前面提到的direct规则是严格意义上的匹配,换言之Routing Key必须与Binding Key相匹配的时候才将消息传送给Queue,那么topic这个规则就是模糊匹配,可以通过通配符满足一部分规则就可以传送。(特征播

  • routing key为一个句点号. 分隔的字符串(我们将被句点号. 分隔开的每一段独立的字符串称为一个单词),如stock.usd.nysenyse.vmwquick.orange.rabbit
  • binding key与routing key一样也是句点号. 分隔的字符串
  • binding key中可以存在两种特殊字符*#,用于做模糊匹配,其中*用于匹配一个单词,#用于匹配多个单词(可以是零个)
    在这里插入图片描述
  • 当生产者发送消息Routing Key=F.C.E的时候,这时候只满足Queue1,所以会被路由到Queue中,如果Routing Key=A.C.E这时候会被同是路由到Queue1和Queue2中,如果Routing Key=A.F.B时,这里只会发送一条消息到Queue2中。
# topic_sender.py

import sys

import pika


connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@123@192.168.78.128'))
channel = connection.channel()

# 声明消息转发器
channel.exchange_declare(exchange='topic_test',
                         exchange_type='topic')

# 定义发送消息级别,只有绑定了该转发器对应级别的队列才能收到相应的消息
severity = sys.argv[1] if len(sys.argv) > 1 else 'create'

message = 'message: hello world from {} level'.format(severity)

# 推送消息
channel.basic_publish(exchange='topic_test',
                      routing_key=severity,  # 组播,转发消息给绑定了该转发器的这四个级别的队列
                      body=message,  # 消息内容
                      )

print('send {} level message: {}'.format(severity, message))

# 关闭连接
connection.close()
# topic_receiver.py

import sys

import pika


connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@123@192.168.78.128'))
channel = connection.channel()

channel.exchange_declare(exchange='topic_test',
                         exchange_type='topic')

# 声明队列 exclusive:排他(即声明不重复的队列),同时还会在此队列断开后自动将其删掉
result = channel.queue_declare(queue='', exclusive=True)
# 获取队列名称
queue_name = result.method.queue

# 定义该队列要绑定的队列级别
severities = sys.argv[1:]

if not severities:
    sys.stderr.write('Usage: {} [binding_key]... \n'.format(sys.argv[0]))
    sys.exit(1)

for severity in severities:
    # 循环将自己的队列绑定到要接收消息的消息转发器上对应级别上
    # routing_key:booking转发器级别列表子集
    channel.queue_bind(exchange='topic_test', queue=queue_name, routing_key=severity)


# 消息处理函数
def callback(ch, method, properties, body):
    print('receive message: %s' % body)


# 消费者
channel.basic_consume(on_message_callback=callback, queue=queue_name)

# 启动消费
channel.start_consuming()

在这里插入图片描述
(4)headers:headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。(消息头播

          在绑定Queue与Exchange时指定一组键值对;当消息发送到Exchange时,RabbitMQ会取到该消息的headers(也是一个键值对的形式),对比其中的键值对是否完全匹配Queue与Exchange绑定时指定的键值对;如果完全匹配则消息会路由到该Queue,否则不会路由到该Queue。
该类型的Exchange没有用到过(不过也应该很有用武之地),所以不做介绍。

五:RPC远程调用

  • 上述所有示例都是单向消息队列,即消息只能单向收发,如果生产者想要获取消费者对消息处理的结果,此时单向消息队列就不能满足需求了,需要用到RPC远程过程调用。
  • 问题一:生产者(即:客户端)发起一个任务,将该任务放入消息队列,等待消费者(即:服务端)来处理该任务,同时需要获取消费者处理的结果,假使消费者任务处理成功,那么怎样返回该结果,如果通过队列返回,那么要返回到哪个队列?
  • 解决方案:生产者发送任务时,就指定好任务处理结果返回的队列(将该队列信息放在消息头中),让拿到该任务的消费者将处理结果以消息头中指定的队列返回。
  • 问题二:由于一个队列可能存在多个任务,且每个任务耗时不同,那么就会导致发出去的任务和接收到的处理结果不一定是一一对应的,即发出的任务和处理的结果可能对应不上,那么需要如何处理?
  • 解决方案:生产者发出任务时不仅需要指定任务结果返回队列,还需要指定一个唯一标识来标识该任务,当消费者处理完任务时,将该任务加上任务本身唯一标识,以生产者指定的队列返回。
# rpc_client.py

import pika
import uuid


class FibonacciRpcClient():
    def __init__(self):
        self.connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@123@192.168.78.128'))
        self.channel = self.connection.channel()

        # 生成唯一的队列
        result = self.channel.queue_declare(exclusive=True, queue='')
        self.callback_queue = result.method.queue

        # 准备接收消息
        self.channel.basic_consume(on_message_callback=self.on_response,
                                   auto_ack=True, queue=self.callback_queue)

    # 收到消息处理函数
    def on_response(self, ch, method, props, body):
        """
        接收任务处理结果
        :param ch: 
        :param method: 
        :param props: 
        :param body: 
        :return: 
        """
        # 根据任务唯一标识符,判断是不是当前任务返回的结果
        if self.corr_id == props.correlation_id:
            # 修改任务返回结果
            self.response = body

    def call(self, n):
        """
        发出任务
        :param n: 
        :return: 
        """
        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('request fib(30)')
response = fibonacci_rpc.call(30)  # 发出任务
print('get response result: {}'.format(response))
# rpc_server.py

import pika


connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@123@192.168.78.128'))
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({})'.format(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_message_callback=on_request, queue='rpc_queue')

print('Awaiting RPC requests')

# 启动消费者
channel.start_consuming()
  • 当服务端有多个任务处理函数时,可以根据app_id进行引导
# rpc_client.py

import pika

connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@123@192.168.78.128'))

channel = connection.channel()

channel.queue_declare(queue='rpc_queues')


def python():
    return 'python'


def java():
    return 'java'


def on_request(ch, method, props, body):
    fun_name = props.app_id

    if fun_name == "p":
        response = python()
    elif fun_name == "j":
        response = java()

    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(queue='rpc_queues', on_message_callback=on_request)

print(" [x] Awaiting RPC requests")

channel.start_consuming()
  • 此时body可以作为形参,接收参数传递,body格式必须为字符串
# rpc_server.py

import pika

import uuid


class RpcClient(object):

    def __init__(self):

        self.connection = pika.BlockingConnection(pika.URLParameters('amqp://admin:password@123@192.168.78.128'))

        self.channel = self.connection.channel()

        result = self.channel.queue_declare(exclusive=True, queue='')
        self.callback_queue = result.method.queue

        self.channel.basic_consume(on_message_callback=self.on_response, auto_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, name):
        self.response = None
        self.corr_id = str(uuid.uuid4())
        self.channel.basic_publish(exchange='',
                                   routing_key='rpc_queues',
                                   properties=pika.BasicProperties(
                                       reply_to=self.callback_queue,
                                       correlation_id=self.corr_id,
                                       app_id=str(name),
                                   ),
                                   body='')

        while self.response is None:
            self.connection.process_data_events()

        return self.response.decode()


rpc = RpcClient()

print(" [x] Requesting")

response = rpc.call("j")

print(" [.] Got %r" % response)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值