GRPC 介绍
grpc 是 google 给出的 rpc 调用方式,它基于 google 的 protobuf 定义方式,提供了一整套数据定义和 rpc 传输的方式。现阶段的 grpc 还不完善,有些功能还不具备。
RPC 介绍
在介绍 grpc 之前有必要首先介绍一下 rpc。RPC 的英文全名是 Remote Procedure Call(远程过程调用),它实现了远程函数或方法的本地调用。由于不在一个内存空间,不能直接调用,因此需要通过网络来表达调用的语义和传达调用的数据。其基本流程如下图所示。
客户端需要调用某个远程函数,首先需要在 client stub 进行函数语义和数据的网络表达,之后将转义好的数据通过 sockets 经过网络传到服务器端,之后同样经过 sever stub 的解析调用远程服务的函数。之后通过同样的链路将函数的结果返回给客户端。
Protobuf 介绍
个人之前有一个 protobuf 的详细介绍可以参考:https://blog.csdn.net/a40850273/article/details/90482546。
Protobuf 是 Google 给出的一种通用的数据表示方式,通过 proto 文件定义的数据格式,可以一键式的生成 C++,Python,Java 等各种语言实现。下面给出一个学生类的范例。
syntax = "proto3"; // 指定基于 protobuf 3 协议
package general_match; // 指定对应的包名
message Student {
int32 id = 1; // 定义 int32 类型的成员变量,变量名为 id
double time = 2; // 定义 double 类型的成员变量,变量名为 time
string name = 3; // 定义 string 类型的成员变量,变量名为 name
repeated double score = 4; // 定义一个 double 的数组,变量名为 score
}
将编写的 protobuf 文件编译成指定语言的头文件或者源文件可以使用 protoc 工具。网上下载之后就可以直接使用以下命令编译 protobuf 文件。
protoc --python_out=./ student.proto
其中例子中编译为 python 文件。如果需要编译成其他语言可以参考下图。
生成的 ***_pb2.py 文件中主要是 protobuf 中定义的数据类型,可以在变成时直接引用。
GRPC 介绍
grpc 在基础 protobuf 文件中添加指定 rpc 调用方式的说明。基础形式如下。
syntax = "proto3";
package example;
message BytesData { // 定义了一个二进制的数据格式
int32 size = 1;
bytes data = 2;
}
service Test { // 定义了一个名为 Test 的服务
rpc test(BytesData) returns (BytesData) {} // 提供了一个名为 test 远程调用函数
}
上面给出了一种通用的实现方法,对于复杂的输入和输出参数可以采用例子中的序列化和反序列化的方式来定义,对于没有实现默认序列化和反序列化的数据结构或自定义数据结构,需要自己定义。当然也可以参考 protobuf 中定义数据类型的例子,具体定义类中的每个变量。
添加了 rpc 定义的 protobuf 文件不能再使用上述的 protoc 编译方式,protoc 只能编译其中的通用部分,即只输出 文件名_pb2.py 文件,并不会输出用于远程调用的 文件名_pb2_grpc.py (这里以 python 为例)。其具体编译方式如下。
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. example.proto
编译之前需要先安装 grpc_tools 的 python 库(这里以 python 为例)。其中 -I 指定输入文件位置,--python_out 指定通用 protobuf 文件输出位置,--grpc_python_out 指定 grpc 文件输出位置。
其中 ****_pb2_grpc.py 文件中,主要包含两个类和一个方法:
- class TestStub:Test 为 protobuf 中定义的服务名,其用于客户端实例化可调用远程函数的基类
- class TestServicer:作为服务器端编写自定义服务类的基类
- function add_TestServicer_to_server:将服务器端自定义的基类注册为指定服务
GRPC 客户端,服务端编写(Python 版)
接下来就是基于生成的 example_pb2.py 和 example_pb2_grpc.py 文件编写服务器端和客户端代码。
服务器端代码范例。
#!/usr/bin/env python
# encoding: utf8
from __future__ import print_function
import grpc
import example_pb2_grpc
import example_pb2
import utils_pb2
class MyServicer(example_pb2_grpc.TestServicer): # 继承自动生成的 grpc 文件中的 Servicer 类
def test(self, request, context): # 实现指定的 test 调用函数,输入参数固定
img_str = request.data[:request.size] # request 传入为 protobuf 文件中定义的输入数据
img = cv2.imdecode(np.fromstring(img_str, dtype=np.uint8), cv2.IMREAD_COLOR) # 根据实际情况,反序列化数据,这里以图片为例
out = img_process(img) # 调用本地的处理函数,生成处理结果
return out
def serve(): # 启动服务端程序
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
example_pb2_grpc.add_TestServicer_to_server(MyServicer(), server) # 调用默认生成的 add_TestServicer_to_server 函数
server.add_insecure_port('[::]:40051') # 指定一个端口号,这里为 40051
server.start()
server.wait_for_termination()
if __name__ == '__main__':
serve()
例子中的 MyServicer 是自己指定的远端服务类名。TestServicer 为对应 protobuf 文件中指定的 service Test 服务,自动添加 Servicer 后缀生成的满足 rpc 调用的服务基类名。服务启动调用的 add_TestServicer_to_server 函数,也是自动组合 TestServicer 生成的。
客户端代码范例。
#!/usr/bin/env python
# encoding: utf8
from __future__ import print_function
import example_pb2_grpc
import example_pb2
import grpc
class MyClient(object):
def __init__(self, server_ip="127.0.0.1:40051"):
self.channel = grpc.insecure_channel(server_ip) # 指定远端服务的 ip 和端口号
self.stub = example_pb2_grpc.TestStub(self.channel) # 调用自动生成的 TestStub 基类
def test(self, img):
img_str = cv2.imencode(".png", img)[1].tostring()
out = self.stub.test(example_pb2.BytesData(size=len(img_str), data=img_str)) # 通过 TestStub 基类对象调用远程服务
return out
def main():
client = MyClient("127.0.0.1:40051")
out = client.test("test.jpg")
if __name__ == '__main__':
main()
例子中的 MyClient 是自己指定的本地服务类名。TestStub 为对应 protobuf 文件中指定的 service Test 服务,自动添加 Stub 后缀生成的满足 rpc 调用的客服端基类名,之后就可以通过该基类生成的对象调用 protobuf 文件中定义的远程调用函数。
其他问题处理
个人在 GRPC 调用中遇到的一些问题,具体解决方案汇总如下。
1、远程调用函数定义多个输入、输出参数报错
具体原因是 grpc 现阶段只支持单一输入和输出参数。可以通过在 protobuf 中自定义数据类型,将多个参数组合成为单一参数,作为远程调用输入。
2、调用过程中,提示 grpc: received message larger than max (8653851 vs. 4194304)
具体原因是 grpc 默认传输参数大小为 4M (4 * 1024 * 1024 = 4194304),此时如果输入输出参数过大就会报错。具体可以通过如下方式自己指定参数内存大小。
options = [('grpc.max_send_message_length', 100 * 1024 * 1024),
('grpc.max_receive_message_length', 100 * 1024 * 1024)]
self.channel = grpc.insecure_channel(server_ip, options=options)