基于Rabbit实现的RPC
最近在学习项目中的通用技术,其中一个是在项目中会经常使用的基于RabbitMQ实现的RPC。这里一共有三个点要学习,分别是:RPC是什么?RabbitMQ是什么?如何使用RabbitMQ实现RPC。奔着这三个目标,查阅了资料。做笔记记录。
RPC
rpc的全称叫:远程过程调用,可以通俗的理解为通过网络调用另一台电脑上的函数的业务处理思想。首先,我们先看看本地的函数调用流程是怎样。
本地调用:
def fun(a,b): sum = a + b return sum if __name__ = __main__ print "i use a function to sum " sum_main = fun(2,3) print sum_main
本地调用当执行到sum=fun(2,3)时,程序会在内存中查找函数指针fun,然后带着参数进入fun()函数中运算,最后返回给sum_main。如果是远程调用,则是从一个电脑A上调用另一个电脑B上的函数。
RPC思想的好处是:
1、更符合编程思想。想要实现什么功能直接调用相应的函数,这是编程最直接的思想。
2、减少代码重复率。A想实现的功能如果B中已经实现了,那么A就直接调用B的函数,避免自己再重复实现。
RPC调用:
rpc多使用http传输请求,格式有xml,json等,这里是xml。如下是使用python中自带的RPC调用框架来实现的一个最简单的RPC调用。
client.py
from xmlrpclib import ServerProxy #导入xmlrpclib的包 s = ServerProxy("http://172.171.5.205:8080") #定义xmlrpc客户端 print s.fun_add(2,3) #调用服务器端的函数
server.py
from SimpleXMLRPCServer import SimpleXMLRPCServer def fun_add(a,b): totle = a + b return totle if __name__ == '__main__': s = SimpleXMLRPCServer(('0.0.0.0', 8080)) #开启xmlrpcserver s.register_function(fun_add) #注册函数fun_add print "server is online..." s.serve_forever() #开启循环等待
先启动服务器端
后启动客户端
这样就完成了一次RPC调用。RPC的调用流程如下图所示。调用流程是:
- client调用以本地调用方式调用服务;
- client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
- client stub找到服务地址,并将消息发送到服务端;
- server stub收到消息后进行解码;
- server stub根据解码结果调用本地的服务;
- 本地服务执行并将结果返回给server stub;
- server stub将返回结果打包成消息并发送至消费方;
- client stub接收到消息,并进行解码;
- 服务消费方得到最终结果。
RabbitMQ
RabbitMQ是实现了AMQP(Advanced Message Queuing Protocol,高级消息队列协议)的软件。主要功能是
- 解耦服务。使用rabbitmq可以将自个服务解耦,实现模块化
- 扩展性高。系统中增加一项功能不需要 从头开始,自需要增加模块即可
- 解决高并发瓶颈。消息队列具有缓存消息功能,能够有效解决高并发请求。
以下摘录自知乎:
对于初学者,举一个饭店的例子来解释这三个分别是什么吧。不是百分百恰当,但是应该足以解释这三者的区别。 RPC:假设你是一个饭店里的服务员,顾客向你点菜,但是你不会做菜,所以你采集了顾客要点什么之后告诉后厨去做顾客点的菜, 这叫RPC(remote procedure call),因为厨房的厨师相对于服务员而言是另外一个人(在计算机的世界里就是remote的机器上的一个进程)。 厨师做好了的菜就是RPC的返回值。 任务队列和消息队列:本质都是队列,所以就只举一个任务队列的例子。假设这个饭店在高峰期顾客很多,而厨师只有很少的几个, 所以服务员们不得不把单子按下单顺序放在厨房的桌子上,供厨师们一个一个做,这一堆单子就是任务队列(当然,取决于问题的语境, 可能要把放订单的桌子也算在里面一起构成所谓的任务队列平台),厨师们每做完一个菜,就从桌子上的订单里再取出一个单子继续做菜。
简单消息队列:
最简单的消息队列,生产者-消费者模式。一端产生消息,发送到队列,另一端消费者收取消息。
consume_simple.py
1 #coding:UTF-8
2
3 import pika
4 import time
5
6 # 建立实例
7 connection = pika.BlockingConnection(pika.ConnectionParameters(
8 'localhost'))
9 # 声明管道
10 channel = connection.channel()
11
14 channel.queue_declare(queue='hello')
15
16 def callback(ch, method, properties, body):
17
18 print "ch",ch
19 print "method",method
20 print "properties",properties
21 print "body",body
25 print(" [x] Received %r" % body)
27 # 消费消息
28 channel.basic_consume(
29 callback, # 如果收到消息,就调用callback函数来处理消息
30 queue='hello', # 你要从那个队列里收消息
33 )
34
35 print(' [*] Waiting for messages. To exit press CTRL+C')
36 channel.start_consuming() # 开始消费消息
productor_simple.py
1 #coding:UTF-8
2 import pika
3
4 # 建立一个实例
5 connection = pika.BlockingConnection(
6 pika.ConnectionParameters('localhost')
7 )
8 # 声明一个管道,在管道里发消息
9 channel = connection.channel()
10 # 在管道里声明queue
11 channel.queue_declare(queue='hello')
13 channel.basic_publish(exchange='',
14 routing_key='hello', # queue名字
15 body='Hello World!') # 消息内容
16 print(" [x] Sent 'Hello World!'")
17 connection.close() # 队列关闭
先运行消费者
在运行生产者
观察消费者获取的消息
RabbitMQ实现RPC
RPC的要求是等待获得返回值,而RabbitMQ常出现的场景是异步等待。这就要求RabbitMQ可以立即返回结果。使用了两种技术:
一、为调用指明id,要求id和结果一起返回,使用id来判断是哪一个函数的调用返回;
二、指明返回的队列名,返回结果时指明返回队列的名字确保会正确返回到调用者。
工作流程:
- 客户端创建message时指定reply_to队列名、correlation_id标记调用者。
- 通过队列,服务端收到消息。调用函数处理,然后返回。
- 返回的队列是reply_to指定的队列,并携带correlation_id。
- 返回消息到达客户端,客户端根据correlation_id判断是哪一个函数的调用返回。
#coding:UTF-8
import pika import uuid import time class FibonacciRpcClient(object): def __init__(self): self.connection = pika.BlockingConnection(pika.ConnectionParameters( host='localhost')) self.channel = self.connection.channel() result = self.channel.queue_declare(exclusive=True) self.callback_queue = result.method.queue self.channel.basic_consume(self.on_response, # 只要一收到消息就调用on_response no_ack=True, queue=self.callback_queue) # 收这个queue的消息 def on_response(self, ch, method, props, body): # 必须四个参数 # 如果收到的ID和本机生成的相同,则返回的结果就是我想要的指令返回的结果 if self.corr_id == props.correlation_id: self.response = body def call(self, n): self.response = None # 初始self.response为None self.corr_id = str(uuid.uuid4()) # 随机唯一字符串 self.channel.basic_publish( exchange='', routing_key='rpc_queue', # 发消息到rpc_queue properties=pika.BasicProperties( # 消息持久化 reply_to = self.callback_queue, # 让服务端命令结果返回到callback_queue correlation_id = self.corr_id, # 把随机uuid同时发给服务器 ), body=str(n) ) while self.response is None: # 当没有数据,就一直循环 # 启动后,on_response函数接到消息,self.response 值就不为空了 self.connection.process_data_events() # 非阻塞版的start_consuming() # print("no msg……") # time.sleep(0.5) # 收到消息就调用on_response return int(self.response) if __name__ == '__main__': fibonacci_rpc = FibonacciRpcClient() print(" [x] Requesting fib(7)") response = fibonacci_rpc.call(7) print(" [.] Got %r" % response)
#coding:UTF-8
import pika import time 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, # 客户端要求返回想用的queue # 返回客户端发过来的correction_id 为了让客户端验证消息一致性 properties=pika.BasicProperties(correlation_id = props.correlation_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') # 声明一个rpc_queue , channel.basic_qos(prefetch_count=1) # 在rpc_queue里收消息,收到消息就调用on_request channel.basic_consume(on_request, queue='rpc_queue') print(" [x] Awaiting RPC requests") channel.start_consuming()