RabbitMQ简单实现RPC模式
RPC介绍
-
RPC (Remote Procedure Call)是一种进程间通信方式,可以理解为:一个电脑上的程序远程调用另一个电脑上的函数。
-
RPC 主要用于公司内部的服务调用,性能消耗低,传输效率高,实现复杂。
-
RPC 使用场景:大型的网站,内部子系统较多、接口非常多的情况下适合使用 RPC。
-
但是RPC也有其诟病:不知道使用的函数是本地的还是远程调用的,增加了系统的不可预测性和调试的复杂性;滥用RPC不仅不能简化程序,反而会造成面条版的代码。
-
实现RPC不难,难的是实现RPC框架。
# 补充注意:
- 1.确保调用本地函数和调用远程函数时有明显的区别。
- 2.写好文档注释,明确组件之间的依赖关系。
- 3.提前规划好,当RPC服务端出现死机情况时,客户端的处理方式
实现原理
-
首先,服务端先创建好队列
rpc_queue
,等待客户端发送请求Requesr
;接收到客户端的请求后,处理请求,将请求结果Reply给某一个队列。 -
服务端响应消息时,使用的队列是客户端发请求时通过参数
relpy_to
携带过来的。服务端直接使用这个携带过来的队列amq.gen-Xa2...
-
客户端,朝服务端的
rpc_queue
发出请求Request
后,则会一直监听自己的队列amq.gen-Xa2...
,等待响应Reply
。 -
当有了响应结果后,客户端先检查响应的结果是不是带有标识(correlation_id),如果有标识且这个标识与发出响应时携带的标识一致,则表明这个响应结果是发出去的那个请求
Request
的Replay
;如果标识不匹配,则将响应消息直接丢掉即可。
补充:
- 每个客户端只有一个队列,该客户端的所有响应消息都通过这个队列接收;这样做为了避免每次发请求都创建一个队列时的低效率。这个队列在客户端诞生时自动创建,客户端死亡时自动回收。
- 因为所有响应消息都通过一个队列接受,所以通过绑定一个唯一标识
correlation_id
来分辨响应消息对应的请求。 - 客户端将自己的接受消息的队列通知给服务端,是通过消息属性参数
reply_to
完成;标识id通过消息属性参数correlation_id
完成。 - 当客户端收到的响应消息在匹配id时,如果匹配失败则直接将消息丢掉而不报错。之所以这样设置主要是考虑到一个极端情况:服务端发送响应消息后还没来得及
send an acknowledgment message for the request
就挂掉了;再启动RPC服务端时,服务端会重新处理客户端的这个请求。这就是为什么在客户端这样处理这个重复响应的原因。
代码演示
参照官网的例子,客户端远程调用RPC服务端的斐波那契数列函数,并等待返回的结果。
客户端和服务端的启动顺序,不分先后。
服务端.py
import pika
# 被调用的函数
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(%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) # 手动确认
if __name__ == '__main__':
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
channel.queue_declare(queue='rpc_queue') # 声明接收响应消息的队列
channel.basic_qos(prefetch_count=1)
channel.basic_consume(queue='rpc_queue',
on_message_callback=on_request, # 收到请求后交给on_request处理
auto_ack=False)
print(" [x] Awaiting RPC requests")
channel.start_consuming()
客户端.py
import pika
import uuid
class FibonacciRpcClient(object):
def __init__(self):
self.response = None # 接收响应结果
self.corr_id = None # 标识消息id
self.init_connect_config()
def init_connect_config(self):
connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()
result = self.channel.queue_declare(queue='', exclusive=True) # 创建临时队列
self.callback_queue = result.method.queue # 获取临时队列的名字,发布请求时携带次队列交给rpc服务端
self.channel.basic_consume(
queue=self.callback_queue, # 从自己的临时队列中监听数据
on_message_callback=self.on_response, # 收到响应交给on_response判断处理
auto_ack=True)
def on_response(self, ch, method, props, body): # 匹配标识
if self.corr_id == props.correlation_id:
self.response = body # 匹配成功,将消息保存在self.response
def call(self, n):
self.corr_id = str(uuid.uuid4()) # 设置请求的唯一id
self.channel.basic_publish(
exchange='',
routing_key='rpc_queue', # 向服务端的rpc_queue队列发布请求
properties=pika.BasicProperties(
reply_to=self.callback_queue, # 通过参数reply_to携带接受消息的队列给服务端
correlation_id=self.corr_id), # 通过参数correlation_id携带请求唯一标识
body=str(n))
while self.response is None:
self.connection.process_data_events() # 开始监听直到请求得到响应
return int(self.response)
if __name__ == '__main__':
fibonacci_rpc = FibonacciRpcClient() # 实例化客户端对象,同时声明自己的临时队列
print(" [x] Requesting fib(30)")
response = fibonacci_rpc.call(3) # 向服务端发请求
print(" [.] Got %r" % response)
补充:消息属性
AMQP 0-9-1协议给消息预定义了一系列的14个属性,以下四个属性使用的频率较高。
- delivery_mode:将消息标记为持久的(2)或暂存的(除了2之外的其他任何值);用于消息持久化设置。
- content_type:用来描述编码的mime-type。例如在实际使用中常常使用application/json来描述JOSN编码类型。
- reply_to(回复目标):通常用来告知对方回复的的地址,比如回调队列。
- correlation_id(关联标识):用来将RPC的响应和请求关联起来。