其他专栏最新篇: 协程加强之~ 兼容答疑篇 https://mp.weixin.qq.com/s/Alv0Ds9LDvWbWSNgOgG43A | 聊聊数据库~ SQL 环境篇
上篇回顾: 万物互联之~ 深入篇 https://mp.weixin.qq.com/s/KbiAz3Z3yyyKRHws1WW6vg
3.RPC 引入
Code:
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.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 服务进一步简化与演变, 手写一个简单的 REST 接口
来源: https://www.cnblogs.com/dunitian/p/10279946.html