万物互联之~RPC专栏
3.RPC引入
上篇回顾:万物互联之~深入篇
Code:https://github.com/lotapp/BaseCode/tree/master/python/6.net/6.rpc/
其他专栏最新篇:协程加强之~兼容答疑篇 | 聊聊数据库~SQL环境篇
3.1.概念
RPC
(Remote Procedure Call
):分布式系统常见的一种通信方法(远程过程调用),通俗讲:可以一台计算机的程序调用另一台计算机的子程序(可以把它看成之前我们说的进程间通信,只不过这一次的进程不在同一台PC上了)
PS:RPC
的设计思想是力图使远程调用中的通讯细节对于使用者透明,调用双方无需关心网络通讯的具体实现
引用一张网上的图:
和HTTP
有点相似,你可以这样理解:
- 老版本的
HTTP/1.0
是短链接,而RPC
是长连接进行通信- HTTP协议(header、body),RPC可以采取HTTP协议,也可以自定义二进制格式
- 后来
HTTP/1.1
支持了长连接(Connection:keep-alive
),基本上和RPC
差不多了- 但
keep-alive
一般都限制有最长时间,或者最多处理的请求数,而RPC
是基于长连接的,基本上没有这个限制
- 但
- 后来谷歌直接基于
HTTP/2.0
建立了gRPC
,它们之间的基本上也就差不多了- 如果硬是要区分就是:
HTTP-普通话
和RPC-方言
的区别了 - RPC高效而小众,HTTP效率没RPC高,但更通用
- 如果硬是要区分就是:
- PS:
RPC
和HTTP
调用不用经过中间件,而是端到端的直接数据交互- 网络交互可以理解为基于
Socket
实现的(RPC
、HTTP
都是Socket
的读写操作)
- 网络交互可以理解为基于
简单概括一下RPC
的优缺点就是:
- 优点:
- 效率更高(可以自定义二进制格式)
- 发起RPC调用的一方,在编写代码时可忽略RPC的具体实现(跟编写本地函数调用一般)
- 缺点:
- 通用性不如HTTP(方言普及程度肯定不如普通话),如果传输协议不是HTTP协议格式,调用双方就需要专门实现通信库
PS:HTTP更多是Client
与Server
的通讯;RPC
更多是内部服务器间的通讯
3.2.引入
上面说这么多,可能还没有来个案例实在,我们看个案例:
本地调用sum()
:
def sum(a, b):
"""return a+b"""
return a + b
def main():
result = sum(1, 2)
print(f"1+2={result}")
if __name__ == "__main__":
main()
输出:(这个大家都知道)
1+2=3
1.xmlrpc案例
官方文档:
https://docs.python.org/3/library/xmlrpc.client.html
https://docs.python.org/3/library/xmlrpc.server.html
都说RPC
用起来就像本地调用一样,那么用起来啥样呢?看个案例:
服务端:(CentOS7:192.168.36.123:50051
)
from xmlrpc.server import SimpleXMLRPCServer
def sum(a, b):
"""return a+b"""
return a + b
# PS:50051是gRPC默认端口
server = SimpleXMLRPCServer(('', 50051))
# 把函数注册到RPC服务器中
server.register_function(sum)
print("Server启动ing,Port:50051")
server.serve_forever()
客户端:(Win10:192.168.36.144
)
from xmlrpc.client import ServerProxy
stub = ServerProxy("http://192.168.36.123:50051")
result = stub.sum(1, 2)
print(f"1+2={result}")
输出:(Client
用起来是不是和本地差不多?就是通过代理访问了下RPCServer
而已)
1+2=3
PS:CentOS
服务器不是你绑定个端口就一定能访问的,如果不能记让防火墙开放对应的端口
这个之前在说MariaDB
环境的时候有详细说:https://www.cnblogs.com/dotnetcrazy/p/9887708.html#_map4
# 添加 --permanent永久生效(没有此参数重启后失效)
firewall-cmd --zone=public --add-port=80/tcp --permanent
2.ZeroRPC案例:
zeroRPC用起来和这个差不多,也简单举个例子吧:
把服务的某个方法注册到RPCServer
中,供外部服务调用
import zerorpc
class Test(object):
def say_hi(self, name):
return f"Hi,My Name is{name}"
# 注册一个Test的实例
server = zerorpc.Server(Test())
server.bind("tcp://0.0.0.0:50051")
server.run()
调用服务端代码:
import zerorpc
client = zerorpc.Client("tcp://192.168.36.123:50051")
result = client.say_hi("RPC")
print(result)
3.3.简单版自定义RPC
看了上面的引入案例,是不是感觉RPC
不过如此?NoNoNo,要是真这么简单也就谈不上RPC架构
了,上面两个是最简单的RPC服务了,可以这么说:生产环境基本上用不到,只能当案例练习罢了,对Python来说,最常用的RPC就两个gRPC
and Thrift
PS:国产最出名的是Dubbo
and Tars
,Net最常用的是gRPC
、Thrift
、Surging
1.RPC服务的流程
要自己实现一个RPC Server
那么就得了解整个流程了:
Client
(调用者)以本地调用的方式发起调用- 通过
RPC
服务进行远程过程调用(RPC的目标就是要把这些步骤都封装起来,让使用者感觉不到这个过程)- 客户端的
RPC Proxy
组件收到调用后,负责将被调用的方法名、参数
等打包编码成自定义的协议 - 客户端的
RPC Proxy
组件在打包完成后通过网络把数据包发送给RPC Server
- 服务端的
RPC Proxy
组件把通过网络接收到的数据包按照相应格式进行拆包解码
,获取方法名和参数 - 服务端的
RPC Proxy
组件根据方法名和参数进行本地调用 RPC Server
(被调用者)本地执行后将结果返回给服务端的RPC Proxy
- 服务端的
RPC Proxy
组件将返回值打包编码成自定义的协议数据包,并通过网络发送给客户端的RPC Proxy
组件 - 客户端的
RPC Proxy
组件收到数据包后,进行拆包解码,把数据返回给Client
- 客户端的
Client
(调用者)得到本次RPC
调用的返回结果
用一张时序图来描述下整个过程:
PS:RPC Proxy
有时候也叫Stub
(存根):(Client Stub,Server Stub)
为屏蔽客户调用远程主机上的对象,必须提供某种方式来模拟本地对象,这种本地对象称为存根(stub),存根负责接收本地方法调用,并将它们委派给各自的具体实现对象
PRC服务实现的过程中其实就两核心点:
- 消息协议:客户端调用的参数和服务端的返回值这些在网络上传输的数据以何种方式打包编码和拆包解码
- 经典代表:
Protocol Buffers
- 经典代表:
- 传输控制:在网络中数据的收发传输控制具体如何实现(
TCP/UDP/HTTP
)
2.手写RPC
下面我们就根据上面的流程来手写一个简单的RPC:
1.Client调用:
# client.py
from client_stub import ClientStub
def main():
stub = ClientStub(("192.168.36.144", 50051))
result = stub.get("sum", (1, 2))
print(f"1+2={result}")
result = stub.get("sum", (1.1, 2))
print(f"1.1+2={result}")
time_str = stub.get("get_time")
print(time_str)
if __name__ == "__main__":
main()
输出:
1+2=3
1.1+2=3.1
Wed Jan 16 22
2.Client Stub,客户端存根:(主要有打包
、解包
、和RPC服务器通信
的方法)
# client_stub.py
import socket
class ClientStub(object):
def __init__(self, address):
"""address ==> (ip,port)"""
self.socket = socket.socket()
self.socket.connect(address)
def convert(self, obj):
"""根据类型转换成对应的类型编号"""
if isinstance(obj, int):
return 1
if isinstance(obj, float):
return 2
if isinstance(obj, str):
return 3
def pack(self, func, args):
"""打包:把方法和参数拼接成自定义的协议
格式:func:函数名@params:类型-参数,类型2-参数2...
"""
result = f"func:{func}"
if args:
params = ""
# params:类型-参数,类型2-参数2...
for item in args:
params += f"{self.convert(item)}-{item},"
# 去除最后一个,
result += f"@params:{params[:-1]}"
# print(result) # log 输出
return result.encode("utf-8")
def unpack(self, data):
"""解包:获取返回结果"""
msg = data.decode("utf-8")
# 格式应该是"data:xxxx"
params = msg.split(":")
if len(params) > 1:
return params[1]
return None
def get(self, func, args=None):
"""1.客户端的RPC Proxy组件收到调用后,负责将被调用的方法名、参数等打包编码成自定义的协议"""
data = self.pack(func, args)
# 2.客户端的RPC Proxy组件在打包完成后通过网络把数据包发送给RPC Server
self.socket.send(data)
# 等待服务端返回结果
data = self.socket.recv(2048)
if data:
return self.unpack(data)
return None
简要说明下:(我根据流程在Code里面标注了,看起来应该很轻松)
之前有说到核心其实就是消息协议
and传输控制
,我客户端存根
的消息协议是自定义的格式(后面会说简化方案):func:函数名@params:类型-参数,类型2-参数2...
,传输我是基于TCP进行了简单的封装
3.Server端:(实现很简单)
# server.py
import socket
from server_stub import ServerStub
class RPCServer(object):
def __init__(self, address, mycode):
self.mycode = mycode
# 服务端存根(RPC Proxy)
self.server_stub = ServerStub(mycode)
# TCP Socket
self.socket = socket.socket()
# 端口复用
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定端口
self.socket.bind(address)
def run(self):
self.socket.listen()
while True:
# 等待客户端连接
client_socket, client_addr = self.socket.accept()
print(f"来自{client_addr}的请求:\n")
# 交给服务端存根(Server Proxy)处理
self.server_stub.handle(client_socket, client_addr)
if __name__ == "__main__":
from server_code import MyCode
server = RPCServer(('', 50051), MyCode())
print("Server启动ing,Port:50051")
server.run()
为了简洁,服务端代码我单独放在了server_code.py
中:
# 5.RPC Server(被调用者)本地执行后将结果返回给服务端的RPC Proxy
class MyCode(object):
def sum(self, a, b):
return a + b
def get_time(self):
import time
return time.ctime()
4.然后再看看重头戏Server Stub
:
# server_stub.py
import socket
class ServerStub(object):
def __init__(self, mycode):
self.mycode = mycode
def convert(self, num, obj):
"""根据类型编号转换类型"""
if num == "1":
obj = int(obj)
if num == "2":
obj = float(obj)
if num == "3":
obj = str(obj)
return obj
def unpack(self, data):
"""3.服务端的RPC Proxy组件把通过网络接收到的数据包按照相应格式进行拆包解码,获取方法名和参数"""
msg = data.decode("utf-8")
# 格式应该是"格式:func:函数名@params:类型编号-参数,类型编号2-参数2..."
array = msg.split("@")
func = array[0].split(":")[1]
if len(array) > 1:
args = list()
for item in array[1].split(":")[1].split(","):
temps = item.split("-")
# 类型转换
args.append(self.convert(temps[0], temps[1]))
return (func, tuple(args)) # (func,args)
return (func, )
def pack(self, result):
"""打包:把方法和参数拼接成自定义的协议"""
# 格式:"data:返回值"
return f"data:{result}".encode("utf-8")
def exec(self, func, args=None):
"""4.服务端的RPC Proxy组件根据方法名和参数进行本地调用"""
# 如果没有这个方法则返回None
func = getattr(self.mycode, func, None)
if args:
return func(*args) # 解包
else:
return func() # 无参函数
def handle(self, client_socket, client_addr):
while True:
# 获取客户端发送的数据包
data = client_socket.recv(2048)
if data:
try:
data = self.unpack(data) # 解包
if len(data) == 1:
data = self.exec(data[0]) # 执行无参函数
elif len(data) > 1:
data = self.exec(data[0], data[1]) # 执行带参函数
else:
data = "RPC Server Error Code:500"
except Exception as ex:
data = "RPC Server Function Error"
print(ex)
# 6.服务端的RPC Proxy组件将返回值打包编码成自定义的协议数据包,并通过网络发送给客户端的RPC Proxy组件
data = self.pack(data) # 把函数执行结果按指定协议打包
# 把处理过的数据发送给客户端
client_socket.send(data)
else:
print(f"客户端:{client_addr}已断开\n")
break
再简要说明一下:里面方法其实主要就是解包
、执行函数
、返回值打包
输出图示:
再贴一下上面的时序图:
课外拓展:
HTTP1.0、HTTP1.1 和 HTTP2.0 的区别
https://www.cnblogs.com/heluan/p/8620312.html
简述分布式RPC框架
https://blog.csdn.net/jamebing/article/details/79610994
分布式基础—RPC
http://www.dataguru.cn/article-14244-1.html
4.RPC简化与提炼
上篇回顾:万物互联之~RPC专栏 https://www.cnblogs.com/dunitian/p/10279946.html
上节课解答
之前有网友问,很多开源的RPC中都是使用路由表,这个怎么实现?
其实路由表实现起来也简单,代码基本上不变化,就修改一下server_stub.py
的__init__
和exe
两个方法就可以了:
class ServerStub(object):
def __init__(self, mycode):
self.func_dict = dict()
# 初始化一个方法名和方法的字典({func_name:func})
for item in mycode.__dir__():
if not item.startswith("_"):
self.func_dict[item] = getattr(mycode, item)
def exec(self, func, args=None):
"""4.服务端的RPC Proxy组件根据方法名和参数进行本地调用"""
# 如果没有这个方法则返回None
# func = getattr(self.mycode, func, None)
func = self.func_dict[func]
if args:
return func(*args) # 解包
else:
return func() # 无参函数
4.1.Json序列化
Python比较6的同志对上节课的Code肯定嗤之以鼻,上次自定义协议是同的通用方法,这节课我们先来简化下代码:
再贴一下上节课的时序图:
1.Json知识点
官方文档:https://docs.python.org/3/library/json.html
# 把字典对象转换为Json字符串
json_str = json.dumps({"func": func, "args": args})
# 把Json字符串重新变成字典对象
data = json.loads(data)
func, args = data["func"], data["args"]
需要注意的就是类型转换了(eg:python tuple
==> json array
)
Python | JSON |
---|---|
dict | object |
list, tuple | array |
str | string |
int, float | number |
True | true |
False | false |
None | null |
PS:序列化:json.dumps(obj)
,反序列化:json.loads(json_str)
2.消息协议采用Json格式
在原有基础上只需要修改下Stub
的pack
和unpack
方法即可
Client_Stub(类型转换都省掉了)
import json
import socket
class ClientStub(object):
def pack(self, func, args):
"""打包:把方法和参数拼接成自定义的协议
格式:{"func": "sum", "args": [1, 2]}
"""
json_str = json.dumps({"func": func, "args": args})
# print(json_str) # log 输出
return json_str.encode("utf-8")
def unpack(self, data):
"""解包:获取返回结果"""
data = data.decode("utf-8")
# 格式应该是"{data:xxxx}"
data = json.loads(data)
# 获取不到就返回None
return data.get("data", None)
# 其他Code我没有改变
Server Stub()
import json
import socket
class ServerStub(object):
def unpack(self, data):
"""3.服务端的RPC Proxy组件把通过网络接收到的数据包按照相应格式进行拆包解码,获取方法名和参数"""
data = data.decode("utf-8")
# 格式应该是"格式:{"func": "sum", "args": [1, 2]}"
data = json.loads(data)
func, args = data["func"], data["args"]
if args:
return (func, tuple(args)) # (func,args)
return (func, )
def pack(self, result):
"""打包:把方法和参数拼接成自定义的协议"""
# 格式:"data:返回值"
json_str = json.dumps({"data": result})
return json_str.encode("utf-8")
# 其他Code我没有改变
输出图示:
4.2.Buffer序列化
RPC其实更多的是二进制的序列化方式,这边简单介绍下
1.pickle知识点
官方文档:https://docs.python.org/3/library/pickle.html
用法和Json
类似,PS:序列化:pickle.dumps(obj)
,反序列化:pickle.loads(buffer)
2.简单案例
和Json案例类似,也只是改了pack
和unpack
,我这边就贴一下完整代码(防止被吐槽)
1.Client
# 和上一节一样
from client_stub import ClientStub
def main():
stub = ClientStub(("192.168.36.144", 50051))
result = stub.get("sum", (1, 2))
print(f"1+2={result}")
result = stub.get("sum", (1.1, 2))
print(f"1.1+2={result}")
time_str = stub.get("get_time")
print(time_str)
if __name__ == "__main__":
main()
2.ClientStub
import socket
import pickle
class ClientStub(object):
def __init__(self, address):
"""address ==> (ip,port)"""
self.socket = socket.socket()
self.socket.connect(address)
def pack(self, func, args):
"""打包:把方法和参数拼接成自定义的协议"""
return pickle.dumps((func, args))
def unpack(self, data):
"""解包:获取返回结果"""
return pickle.loads(data)
def get(self, func, args=None):
"""1.客户端的RPC Proxy组件收到调用后,负责将被调用的方法名、参数等打包编码成自定义的协议"""
data = self.pack(func, args)
# 2.客户端的RPC Proxy组件在打包完成后通过网络把数据包发送给RPC Server
self.socket.send(data)
# 等待服务端返回结果
data = self.socket.recv(2048)
if data:
return self.unpack(data)
return None
3.Server
# 和上一节一样
import socket
from server_stub import ServerStub
class RPCServer(object):
def __init__(self, address, mycode):
self.mycode = mycode
# 服务端存根(RPC Proxy)
self.server_stub = ServerStub(mycode)
# TCP Socket
self.socket = socket.socket()
# 端口复用
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 绑定端口
self.socket.bind(address)
def run(self):
self.socket.listen()
while True:
# 等待客户端连接
client_socket, client_addr = self.socket.accept()
print(f"来自{client_addr}的请求:\n")
try:
# 交给服务端存根(Server Proxy)处理
self.server_stub.handle(client_socket, client_addr)
except Exception as ex:
print(ex)
if __name__ == "__main__":
from server_code import MyCode
server = RPCServer(('', 50051), MyCode())
print("Server启动ing,Port:50051")
server.run()
4.ServerCode
# 和上一节一样
# 5.RPC Server(被调用者)本地执行后将结果返回给服务端的RPC Proxy
class MyCode(object):
def sum(self, a, b):
return a + b
def get_time(self):
import time
return time.ctime()
5.ServerStub
import socket
import pickle
class ServerStub(object):
def __init__(self, mycode):
self.mycode = mycode
def unpack(self, data):
"""3.服务端的RPC Proxy组件把通过网络接收到的数据包按照相应格式进行拆包解码,获取方法名和参数"""
func, args = pickle.loads(data)
if args:
return (func, args) # (func,args)
return (func, )
def pack(self, result):
"""打包:把方法和参数拼接成自定义的协议"""
return pickle.dumps(result)
def exec(self, func, args=None):
"""4.服务端的RPC Proxy组件根据方法名和参数进行本地调用"""
# 如果没有这个方法则返回None
func = getattr(self.mycode, func)
if args:
return func(*args) # 解包
else:
return func() # 无参函数
def handle(self, client_socket, client_addr):
while True:
# 获取客户端发送的数据包
data = client_socket.recv(2048)
if data:
try:
data = self.unpack(data) # 解包
if len(data) == 1:
data = self.exec(data[0]) # 执行无参函数
elif len(data) > 1:
data = self.exec(data[0], data[1]) # 执行带参函数
else:
data = "RPC Server Error Code:500"
except Exception as ex:
data = "RPC Server Function Error"
print(ex)
# 6.服务端的RPC Proxy组件将返回值打包编码成自定义的协议数据包,并通过网络发送给客户端的RPC Proxy组件
data = self.pack(data) # 把函数执行结果按指定协议打包
# 把处理过的数据发送给客户端
client_socket.send(data)
else:
print(f"客户端:{client_addr}已断开\n")
break
输出图示:
然后关于RPC高级的内容(会涉及到注册中心
),咱们后面说架构的时候继续,网络这边就说到这