文章目录
前言
RPC,全称Remote Procedure Call, 即远程过程调用。
主要作用是屏蔽网络编程细节,实现调用远程方法就像调用本地方法(同一个进程中的方法)一样的体验。
同时屏蔽底层网络通信的复杂性,让我们更加专注业务逻辑的开发。
一、什么是rpc,rpc开发的挑战是什么
- RPC (Remote Procedure Call)远程过程调用,简单的理解是一个节点请求另一个节点提供的服务。
- 对应rpc的是本地过程调用,函数调用是最常见的本地过程调用。
- 将本地过程调用变成远程过程调用会面临各种问题。
1.本地调用过程
函数调用过程:
- 将1和2压入add函数的栈
- 进入add函数,从栈中取出1和2分别赋值给a和b
- 执行a + b将结果赋值给局部的total并压栈
- 将栈中的值取出来赋值给全局的total
2.远程过程面临的问题
在远程调用时,我们需要执行的函数体是在远程的机器上的,也就是说,add是在另一个进程中执行的。这就带来了几个新的问题
-
Call的id映射:我们怎么告诉远程机器我们要调用add,而不是sub或者Foo呢?在本地调用中,函数体是直接通过函数指针来指定的,我们调用add,编译器就自动帮我们调用它相应的函数指针。但是在远程调用中函数指针是不行的,因为两个进程的地址空间是完全不一样的。所以,在RPC中,所有的函数都必须有自己的一个ID。这个ID在所有进程中都是唯一确定的。客户端在做远程过程调用时,必须附上这个ID。然后我们还需要在客户端和服务端分别维护一个{函数<–>Call ID}的对应表。两者的表不一定需要完全相同,但相同的函数对应的Call ID必须相同。当客户端需要进行远程调用时,它就查一下这个表,找出相应的Call lD,然后把它传给服务端,服务端也通过查表,来确定客户端需要调用的函数,然后执行相应函数的代码。
-
序列化和反序列化:客户端怎么把参数值传给远程的函数呢?在本地调用中,我们只需要把参数压到栈里,然后让函数自己去栈里读就行。但是在远程过程调用时,客户端跟服务端是不同的进程,不能通过内存来传递参数。甚至有时候客户端和服务端使用的都不是同一种语言(比如服务端用C++,客户端用Java或者Python)。这时候就需要客户端把参数先转成一个字节流,传给服务端后,再把字节流转成自己能读取的格式。这个过程叫序列化和反序列化。同理,从服务端返回的值也需要序列化反序列化的过程。
-
网络传输:网络传输。远程调用往往用在网络上,客户端和服务端是通过网络连接的。所有的数据都需要通过网络传输,因此就需要有一个网络传输层。网络传输层需要把Call ID和序列化后的参数字节流传给服务端,然后再把序列化后的调用结果传回客户端。只要能完成这两者的,都可以作为传输层使用。因此,它所使用的协议其实是不限的,能完成传输就行。尽管大部分RPC框架都使用TCP协议,但其实UDP也可以,而gRPC干脆就用了HTTP2。Java的Netty也属于这层的东西。
import json
def add(a, b):
total = a + b
return total
#现在想把add函数放到另一台服务器上去调用
#网络 web框架
#json http请求
#json 就是协议 json是一种数据格式 协议 json.dumps() 序列化 json.loads() 反序列化 成dict list
class Company:
name = "慕课网"
address = "北京市"
class Student:
name = "bobby"
company = Company()
def to_json(self):
json_data = {
"name":self.name,
"company":{
"name":self.company.name,
"address":self.company.address
}
}
return json.dumps(json_data)
def print_info_rpc(student):
#1. 建立连接 requests, socket
#2. 将student变成json字符串 序列化
#3. 发送json字符串
#4. 等待对方发送结果过来 json - 去解析 反序列化 性能比较低 grpc
#5. 继续解析的结果进行业务逻辑
print(f"姓名:{student.name}, 公司:{student.company.name}")
#student的类, python的类, 不行
#服务器采用的是go语言
#这个内存中的对象可以变成一个网络中的对象, 二进制
#json
print_info_rpc(Student())
#http协议来说 有一个问题: 一次性 一旦对方返回了结果 连接断开 http2.0 长连接 grpc
二、使用httpserver实现rpc
1.rpc、http以及restful之间的区别
这之间有关系 但不是一个层级的 没有必要拿着rpc和http之间做比较
(1)rpc和http
- 序列化和反序列化
- 网络传输协议(http还是tcp协议)
所以这里大家应该能看到了http本身属于网络传输协议的一种,rpc要实现目的必须要依赖网络传输协议,所以有同学会问了:网络协议http可以传输,我们直接基于tcp协议直接链接不也可以达到网络传输的效果吗?
是的,确实是这样,所以这里我们可以得出结论了: http协议只是我们实现rpc框架的一种选择而已,你可以选择也可以不选择,所以rpc和http之间不是竞争关系。
接下来看看rpc和restful的关系
(2)rpc和restful
这两个不是互斥的,rpc和restful不是非此即彼,一般我们的服务想要对外提供服务的时候一般采用的是http请求,但是这么多接口按照什么规范放出去了,这就是restful,当然restful只是一个规范而已,你完全可以不遵守。rpc一般是系统内部服务之间调用,其中rpc的协议灵活性会使
2、通过httpserver实现rpc
首先一点需要明确:一定会发起一个网络请求,一定会有个网络连接(tcp/udp)
把远程的函数变成一个http请求
服务端:
import json
from urllib.parse import urlparse, parse_qsl
# HTTPServer, BaseHTTPRequestHandler就相当于服务端的存根
from http.server import HTTPServer, BaseHTTPRequestHandler
host = ('', 8003)
#将url映射到对应的函数
#在flask中叫urlconfig用route装饰器也就完成了call id的映射 你客户端发过来的url我能用函数处理
#反序列化
class AddHandler(BaseHTTPRequestHandler):
def do_GET(self):
parsed_url = urlparse(self.path)
qs = dict(parse_qsl(parsed_url.query)) #这其实就是一种反序列化
a = int(qs.get("a", 0))
b = int(qs.get("b", 0))
self.send_response(200)
self.send_header("content-type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({
"result":a+b
}).encode("utf-8"))
if __name__ == "__main__":
server = HTTPServer(host, AddHandler)
print("启动服务器")
server.serve_forever()
客户端:
import json
import requests
# 自己实现了一个demo级别的rpc封装
class ClientStub:
def __init__(self, url):
self.url = url
def add(self, a, b):
#1. call id
#2.序列化和反序列化
#3.传输协议http 自己写的Stub协议可以屏蔽这些 这里只屏蔽了call id也就是url
rsp = requests.get(f"{self.url}/?a={a}&b={b}")
return json.loads(rsp.text).get("result", 0)
#这不是就是写一个web服务器无非就是自己封装一下client
#不想知道过多的细节只想像本地一样调用
client = ClientStub("http://127.0.0.1:8003")
print(client.add(1, 2))
print(client.add(2, 3))
print(client.add(22, 33))
三、rpc的开发要素分析
1.rpc开发的四大要素:
RPC技术在架构设计上有四部分组成,分别是:客户端、客户端存根、服务端、服务端存根。
- 客户端(Client): 服务调用发起方,也称为服务消费者。
- 客户端存根(Client Stub): 该程序运行在客户端所在的计算机机器上,主要用来存储要调用的服务器的地址,另外,该程序还负责将客户端请求远端服务器程序的数据信息打包成数据包,通过网络发送给服务端Stub程序;其次,还要接收服务端Stub程序发送的调用结果数据包,并解析返回给客户端。
- 服务端(Server): 远端的计算机机器上运行的程序,其中有客户端要调用的方法。
- 服务端存根(Server Stub): 接收客户Stub程序通过网络发送的请求消息数据包,并调用服务端中真正的程序功能方法,完成功能调用;其次,将服务端执行调用的结果进行数据处理打包发送给客户端Stub程序。
了解完了RPC技术的组成结构我们来看一下具体是如何实现客户端到服务端的调用的。实际上,如果我们想要在网络中的任意两台计算机上实现远程调用过程,要解决很多问题,比如:
- 两台物理机器在网络中要建立稳定可靠的通信连接。
- 两台服务器的通信协议的定义问题,即两台服务器上的程序如何识别对方的请求和返回结果。也就是说两台计算机必须都能够识别对方发来的信息,并且能够识别出其中的请求含义和返回含义,然后才能进行处理。这其实就是通信协议所要完成的工作。
在上述图中,说明了RPC每一步的调用过程。具体描述为:
- 客户端想要发起一个远程过程调用,首先通过调用本地客户端Stub程序的方式调用想要使用的功能方法名;
- 客户端Stub程序接收到了客户端的功能调用请求,将客户端请求调用的方法名,携带的参数等信息做序列化操作,并打包成数据包。
- 客户端Stub查找到远程服务器程序的IP地址,调用Socket通信协议,通过网络发送给服务端。
- 服务端Stub程序接收到客户端发送的数据包信息,并通过约定好的协议将数据进行反序列化,得到请求的方法名和请求参数等信息。
- 服务端Stub程序准备相关数据,调用本地Server对应的功能方法进行,并传入相应的参数,进行业务处理。
- 服务端程序根据已有业务逻辑执行调用过程,待业务执行结束,将执行结果返回给服务端Stub程序。
- 服务端Stub程序**将程序调用结果按照约定的协议进行序列化,**并通过网络发送回客户端Stub程序。
- 客户端Stub程序接收到服务端Stub发送的返回数据,对数据进行反序列化操作, 并将调用返回的数据传递给客户端请求发起者。
- 客户端请求发起者得到调用结果,整个RPC调用过程结束。
2.rpc需要使用到的术语
通过上文一系列的文字描述和讲解,我们已经了解了RPC的由来和RPC整个调用过程。我们可以看到RPC是一系列操作的集合,其中涉及到很多对数据的操作,以及网络通信。因此,我们对RPC中涉及到的技术做一个总结和分析:
-
- 动态代理技术: 上文中我们提到的Client Stub和Sever Stub程序,在具体的编码和开发实践过程中,都是使用动态代理技术自动生成的一段程序。
-
- 序列化和反序列化: 在RPC调用的过程中,我们可以看到数据需要在一台机器上传输到另外一台机器上。在互联网上,所有的数据都是以字节的形式进行传输的。而我们在编程的过程中,往往都是使用数据对象,因此想要在网络上将数据对象和相关变量进行传输,就需要对数据对象做序列化和反序列化的操作。
-
- 序列化: 把对象转换为字节序列的过程称为对象的序列化,也就是编码的过程。
-
- 反序列化: 把字节序列恢复为对象的过程称为对象的反序列化,也就是解码的过程。
我们常见的Json
,XML
等相关框架都可以对数据做序列化和反序列化编解码操作。后面我们要学习的Protobuf
协议,这也是一种数据编解码的协议,在RPC框架
中使用的更广泛。
四、基于xml的rpc库
服务端:
from xmlrpc.server import SimpleXMLRPCServer
#python中类的命名方式遵循驼峰命名法
#1. 没有出现url的映射
#2. 没有编码和解码
#序列化和反序列化协议是 xml json
class Calculater:
def add(self, x, y):
return x + y
def multiply(self, x, y):
return x * y
def subtract(self, x, y):
return abs(x-y)
def divide(self, x, y):
return x/y
obj = Calculater()
server = SimpleXMLRPCServer(("localhost", 8088))
# 将实例注册给rpc server
server.register_instance(obj)
print("Listening on port 8088")
server.serve_forever()
客户端:
from xmlrpc import client
#xmlrpc挺好用的 和我们调用django的服务器 django这种web框架来说一定是可以做到xmlrpc的效果 django的目的不是这种
# requests调用 httpie postman http协议
#rpc强调的是本地调用效果
#rpc在内部调用很多
server = client.ServerProxy("http://localhost:8088")
print(server.add1(2, 3))
然后,我们通过 server_proxy 对象就可以远程调用之前的rpc server的函数了。
五、基于json的rpc技术
SimpleXMLRPCServer 是基于 xml-rpc
实现的远程调用,上面我们也提到 除了 xml-rpc
之外,还有 json-rpc
协议。
那 python 如何实现基于 json-rpc
协议呢?
答案是很多,很多web框架其自身都自己实现了json-rpc
,但我们要独立这些框架之外,要寻求一种较为干净的解决方案,我们使用jsonrpclib
官方的github文档:https://github.com/tcalmant/jsonrpclib/
1.安装
pip install jsonrpclib-pelix -i https://pypi.douban.com/simple
它与 Python 标准库的 SimpleXMLRPCServer 很类似(因为它的类名就叫做 SimpleJSONRPCServer ,不明真相的人真以为它们是亲兄弟)。或许可以说,jsonrpclib
就是仿照 SimpleXMLRPCServer 标准库来进行编写的。
它的导入与 SimpleXMLRPCServer 略有不同,因为SimpleJSONRPCServer分布在jsonrpclib
库中。
2.代码
不推荐的服务端:
from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer
def add(a, b):
return a + b
#1. 实例化server
server = SimpleJSONRPCServer(('localhost', 8000))
#2. 将函数注册到server中
server.register_function(add)
#3. 启动server
server.serve_forever()
#多线程
#协程 go中 netty asyncio
#jsonrpclib如果只是完成了这样一个简单的调用那么jsonrpclib和xmlrpcserver几乎没有优势可言
#任何一个web服务如果不具备并发接收和处理的能力的话 那么这个server就没有用
服务端:
from jsonrpclib.SimpleJSONRPCServer import PooledJSONRPCServer
from jsonrpclib.threadpool import ThreadPool
def add(a, b):
import time
time.sleep(1)
return a + b
# Setup the notification and request pools
nofif_pool = ThreadPool(max_threads=10, min_threads=0)
request_pool = ThreadPool(max_threads=50, min_threads=10)
# Don't forget to start them
nofif_pool.start()
request_pool.start()
# Setup the server
server = PooledJSONRPCServer(('localhost', 8000), thread_pool=request_pool)
server.set_notification_pool(nofif_pool)
# Register methods
server.register_function(add)
#1. 超时机制 - 重试
#2. 限流 处于长期可用的状态 - 高可用
#3. 解耦
#4. 负载均衡 微服务 -分布式应用的一种具体的体现
#5. json-rpc是否满足上述的要求
#6. 序列化和反序列化数据压缩是否高效 json这种数据格式已经非常的简单了 1.这个序列化协议能将数据的压缩变得更小 2. 这个序列化和反序列化的速度够快
#json.dumps() json.loads()
#做架构 技术选型的时候 这些都是我们需要考虑到的点
#更加高效和更加全面的技术 zerorpc
#7. 这个rpc框架是否支持多语言 生态很好
try:
server.serve_forever()
finally:
# Stop the thread pools (let threads finish their current task)
request_pool.stop()
nofif_pool.stop()
server.set_notification_pool(None)
客户端:
import jsonrpclib
import threading
def request():
server = jsonrpclib.ServerProxy('http://localhost:8000')
print(server.add(2, 3))
for i in range(10):
thread = threading.Thread(target=request)
thread.start()
import time
time.sleep(30)
六、基于zeromq的rpc框架
zerorpc 是利用 zeroMQ消息队列
+ msgpack 消息序列化(二进制)
来实现类似 grpc
的功能,跨语言远程调用。
主要使用到 zeroMQ
的通信模式是 ROUTER–DEALER
,模拟 grpc
的 请求-响应式 和 应答流式 RPC :
zerorpc 还支持 PUB-SUB
通信模式的远程调用。
zerorpc实际上会依赖msgpack-python
, pyzmq
, future
, greenlet
, gevent
官方github文档:https://github.com/0rpc/zerorpc-python
zerorpc的调用过程:
1.安装
pip install zerorpc -i https://pypi.douban.com/simple
2.一元调用
服务端:
import zerorpc
class HelloRPC(object):
def hello(self, name):
#调用了另一个服务
#流处理
#本地查询了数据, 源源不断的给数据给客户端
return "Hello, %s" % name
#1. 实例化一个server
#2. 绑定我们的业务代码到server中
#3. 启动server
s = zerorpc.Server(HelloRPC())
s.bind("tcp://0.0.0.0:4242")
s.run()
客户端:
import zerorpc
c = zerorpc.Client()
c.connect("tcp://127.0.0.1:4242")
for item in c.streaming_range(10, 20, 2):
print(item)
2.流式响应
服务端:
import zerorpc
class StreamingRPC(object):
@zerorpc.stream #@zerorpc.stream这里的函数修饰是必须的,否则会有异常,如TypeError: can’t serialize
def streaming_range(self, fr, to, step):
return range(fr, to, step)
s = zerorpc.Server(StreamingRPC())
s.bind("tcp://0.0.0.0:4242")
s.run()
客户端:
import zerorpc
c = zerorpc.Client()
c.connect("tcp://127.0.0.1:4242")
for item in c.streaming_range(10, 20, 2):
print(item)
3.传入多个参数
服务端:
import zerorpc
class myRPC(object):
def listinfo(self,message):
return "get info : %s"%message
def getpow(self,n,m):
return n**m
s = zerorpc.Server(myRPC())
s.bind("tcp://0.0.0.0:4242")
s.run()
客户端:
import zerorpc
c = zerorpc.Client()
c.connect("tcp://127.0.0.1:4242")
print(c.listinfo("this is test string"))
print(c.getpow(2,5))
七、rpc需要解决的问题
- ID映射
- 传输协议 tcp/http
- 数据的编码和解码 http/hson/xml/其他
- 如何解决高并发的问题
- 负载均衡的问题
- 集群的问题
选择哪一种rpc解决方案
生态
支持的语言(多语言/单语言)