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),如果有标识且这个标识与发出响应时携带的标识一致,则表明这个响应结果是发出去的那个请求RequestReplay;如果标识不匹配,则将响应消息直接丢掉即可。

补充

  • 每个客户端只有一个队列,该客户端的所有响应消息都通过这个队列接收;这样做为了避免每次发请求都创建一个队列时的低效率。这个队列在客户端诞生时自动创建,客户端死亡时自动回收。
  • 因为所有响应消息都通过一个队列接受,所以通过绑定一个唯一标识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的响应和请求关联起来。
posted @ 2020-05-16 18:33  the3times  阅读(481)  评论(0)    收藏  举报