上篇回顾:万物互联之~深刻篇html
Code:https://github.com/lotapp/BaseCode/tree/master/python/6.net/6.rpc/python
其余专栏最新篇:协程增强之~兼容答疑篇 | 聊聊数据库~SQL环境篇git
RPC
(Remote Procedure Call
):分布式系统常见的一种通讯方法(远程过程调用),通俗讲:能够一台计算机的程序调用另外一台计算机的子程序(能够把它当作以前咱们说的进程间通讯,只不过这一次的进程不在同一台PC上了)github
PS:RPC
的设计思想是力图使远程调用中的通信细节对于使用者透明,调用双方无需关心网络通信的具体实现shell
引用一张网上的图:
数据库
和HTTP
有点类似,你能够这样理解:json
HTTP/1.0
是短连接,而RPC
是长链接进行通讯
HTTP/1.1
支持了长链接(Connection:keep-alive
),基本上和RPC
差很少了
keep-alive
通常都限制有最长时间,或者最多处理的请求数,而RPC
是基于长链接的,基本上没有这个限制HTTP/2.0
创建了gRPC
,它们之间的基本上也就差很少了
HTTP-普通话
和RPC-方言
的区别了RPC
和HTTP
调用不用通过中间件,而是端到端的直接数据交互
Socket
实现的(RPC
、HTTP
都是Socket
的读写操做)简单归纳一下RPC
的优缺点就是:服务器
PS:HTTP更可能是Client
与Server
的通信;RPC
更可能是内部服务器间的通信网络
上面说这么多,可能尚未来个案例实在,咱们看个案例:架构
本地调用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
官方文档:
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
环境的时候有详细说:http://www.javashuo.com/article/p-uxkudhsn-dw.html
# 添加 --permanent永久生效(没有此参数重启后失效) firewall-cmd --zone=public --add-port=80/tcp --permanent
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)
看了上面的引入案例,是否是感受RPC
不过如此?NoNoNo,要是真这么简单也就谈不上RPC架构
了,上面两个是最简单的RPC服务了,能够这么说:生产环境基本上用不到,只能当案例练习罢了,对Python来讲,最经常使用的RPC就两个gRPC
and Thrift
PS:国产最出名的是Dubbo
and Tars
,Net最经常使用的是gRPC
、Thrift
、Surging
要本身实现一个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
)下面咱们就根据上面的流程来手写一个简单的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
上篇回顾:万物互联之~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() # 无参函数
Python比较6的同志对上节课的Code确定嗤之以鼻,上次自定义协议是同的通用方法,这节课咱们先来简化下代码:
再贴一下上节课的时序图:
官方文档: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)
在原有基础上只须要修改下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我没有改变
输出图示:
RPC其实更多的是二进制的序列化方式,这边简单介绍下
官方文档:https://docs.python.org/3/library/pickle.html
用法和Json
相似,PS:序列化:pickle.dumps(obj)
,反序列化:pickle.loads(buffer)
和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高级的内容(会涉及到注册中心
),我们后面说架构的时候继续,网络这边就说到这