gRPC 是一个由 Google 开发的跨语言 RPC 支持库。本文系 gRPC 的入门向导型文章。

参考资料

这次直接把参考资料放在第一小节,喜欢自己阅读文档起手的朋友可以直接先看文档。

概念简介

什么是 RPC?

RPC,你可以理解为是调用一个远程函数。

例说 gRPC

传输图片

准备工作

开始之前,先把演示用的仓库 clone 下来:

1
git clone https://github.com/jonblearn/simple-gRPC.git

用 VSCode 打开 ./grpc 文件夹,创建虚拟环境、并安装依赖:

1
2
python -m venv env
pip install grpcio grpcio-tools numpy

解读

示例项目假设的需求情景类似于「拍立淘」:client 拍一张照片,通过 gRPC 传给 server,server 透过深度学习模型识别图上的是什么商品,把结果通过 gRPC 还给 client。

在示例项目中,下图黑笔划掉的文件是 gRPC 自动生成的、剩下的才是程序员自己写的:

目录结构

每一个用到 gRPC 的项目都需要一个 proto 文件,用以定义 gRPC 里面承载的远程函数与数据类型。

打开 image_procedure.proto 文件。其中英文是原作者的注释,中文是酱瓜加的注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
syntax = "proto3";		// 表明该文件是 proto3 格式, 抄就是了

// input image, width, height
message B64Image {
string b64image = 1; // 这里的数字 1/2/3 只是代表该对象序列化后,
int32 width = 2; // 各变量存在于序列字符串里面的相对位置,
int32 height = 3; // 未必要从 1 开始, 只要有相对大小顺序即可.
}

// output prediction
message Prediction {
int32 channel = 4;
float mean = 5;
}

// service
service ImageProcedure {
rpc ImageMeanWH(B64Image) returns (Prediction) {}
}

在终端中执行下面这行命令,就可以让 gRPC 从 proto 文件生成对应的 Python 下的数据格式定义文件 *_pb2*.py。这两个自动生成的文件后面在 client/server 中直接调用即可,不需要做任何修改。

1
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. image_procedure.proto

然后看 server.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# ...
# 虽然是一个小示例, 但作者也进行了模块化开发
import image_procedure

# import the generated classes
import image_procedure_pb2
import image_procedure_pb2_grpc

# based on .proto service
class ImageProcedureServicer(image_procedure_pb2_grpc.ImageProcedureServicer):
def ImageMeanWH(self, request, context):
response = image_procedure_pb2.Prediction()
response.channel, response.mean = image_procedure.predict(request.b64image, request.width, request.height)
return response

# create a gRPC server
server = grpc.server(futures.ThreadPoolExecutor(max_workers=12))

# add the defined class to the server
image_procedure_pb2_grpc.add_ImageProcedureServicer_to_server(
ImageProcedureServicer(), server)

# listen on port 5005
print('Starting server. Listening on port 5005.')
server.add_insecure_port('[::]:5005')
server.start()
# ...

最后是 client.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# ...
# import the generated classes
import image_procedure_pb2
import image_procedure_pb2_grpc
# ...

# open a gRPC channel
channel = grpc.insecure_channel('127.0.0.1:5005')

# create a stub (client)
stub = image_procedure_pb2_grpc.ImageProcedureStub(channel)

# encoding image/numpy array
for _ in range(1000):
frame = np.random.randint(0,255, (416,416,3), dtype=np.uint8) # dummy rgb image

# compress
# 之所以把 frame 复制一份成 data, 其实是为了日后能加入压缩功能做准备
data = frame # zlib.compress(frame)
data = base64.b64encode(data)

# create a valid request message
image_req = image_procedure_pb2.B64Image(b64image = data, width = 416, height = 416)

# make the call
response = stub.ImageMeanWH(image_req)

真正对图像进行处理的模块是 image_procedure.py:

1
2
3
4
5
6
# A procedure which decodes base64 image, runs some machine learning model/ operation(s) (in our case we'll just return the mean of the pixel value)
def predict(b64img_compressed, w, h):
b64decoded = base64.b64decode(b64img_compressed)
decompressed = b64decoded # zlib.decompress(b64decoded)
imgarr = np.frombuffer(decompressed, dtype=np.uint8).reshape(w, h, -1)
return imgarr.shape[2], np.mean(imgarr)

总结

用 gRPC 传图片,发图的一方需要对图片进行编码、收图的一方需要先对图片进行解码,具体代码片段如下。

发送前编码

1
2
3
4
# 发送前编码
data = np.zeros((height, width, 3), np.uint8) # RGB
data[:] = frame
data64 = base64.b64encode(data)

然后是一些不重要的代码:

1
2
3
4
# 构造 gRPC 对象
image_req = image_procedure_pb2.B64Image(b64image=data64, width=width, height=height)
# 发起 RPC 调用, 返回值存在 response 中
response = stub.SimSwap(image_req)

接收后解码的通用代码段:

1
2
3
# 接收后解码
decompressed = base64.b64decode(response.b64image)
imgarr = np.frombuffer(decompressed, dtype=np.uint8).reshape(height, width, -1)

下面这句话意在表达 imgarr 可以直接保存成文件:

1
2
# 保存到文件
cv2.imwrite("./grpc_test/{}.jpg".format(i), imgarr)

本节参考

性能测试

SimSwap

测试一是 SimSwap 处理一张图片的时间。

具体参数:

  • pic_a 是 6.jpg,pic_b 是 ds.jpg
  • gRPC 客户端和服务器均开在同一个机器上,即 127.0.0.1
  • 分别处理 10 次,取平均值

结果如下:

  • 本地处理平均耗时:1.1012424230575562 s
  • gRPC 处理平均耗时:1.1019452095031739 s

可见,加入 gRPC 并没有显著提高延迟。