1 Protobuf
1.1 Protobuf概述
Protocol Buffers(亦称Protobuf)是一种由 Google 开发的数据交换机制(协议),它被用于对结构化数据的序列化、反序列化和传输。Protobuf具有如下性质:
- Protobuf能够使得结构化数据在使用不同平台、不同语言的通信双方之间进行传输。
- 与XML和JSON类似,Protobuf也是一种基于语义格式的协议设计方法。但由于Protobuf使用二进制格式进行存储,而XML和JSON使用文本格式进行存储,对于相同的数据,Protobuf的序列化和反序列化速度要明显快于XML和JSON,并且Protobuf需要传输的数据量也通常比XML和JSON小3-10倍。
- Protobuf 使用.proto文件来定义结构化数据的格式,这种文件具有较好的可读性和可维护性。通过对这种文件的修改,可以实现通信协议的升级、拓展和兼容。
1.2 Protobuf格式
Protobuf使用.proto文件来定义数据模型和数据结构,本节将介绍proto 3的书写格式。
1.2.1 简单示例
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
repeated string email=3;
}
代码第一个非空非注释行用于声明代码使用的语言是proto 3,若不进行此声明,则Protobuf编译器会假定使用的语言是proto 2。
message 是用来定义消息结构的关键字,Person 是消息的名称,随后的代码段可以近似理解成定义了一个Person的类。大括号内定义了该消息的各个字段的名称及其类型,每个字段都等于一个数字,该数字表示消息中每个字段的唯一编号(从1到536870911,其中19000到19999作为Protobuf的预留字段而不允许用户使用),用于在序列化的二进制数据中标识不同的字段。用户可以在保证唯一性的前提下在可选范围内任意指定各个字段的编号,但是过长的编号会导致序列化后占用更多的字节。
在proto 3中,字段具有singular和repeated两种修饰符。singular表示该字段在消息中的数量不超过一个(即要么存在一个,要么缺省),当字段前没有修饰符时,默认该字段被singular修饰,如示例中的name字段和age字段。repeated字段表示该字段可以重复任意次数,类似于数组或列表,示例中一条Person数据可能包含多个email。
1.2.2 消息结构
.proto文件中的message用于定义的复杂的数据结构,除此之外proto 3常用的消息结构还包括枚举(enum)和服务(service)分别用于定义常量和RPC服务接口。
枚举用于定义一组命名的常量值。每个枚举值都有一个相关联的整数值,它们在消息中用于标识某个字段的可能取值范围。在同一个枚举类型中,枚举值不能重复,并且第一个枚举值必须为0(作为枚举的默认值)。枚举示例如下:
enum Job{
OTHER = 0;
PROGRAMMER = 1;
PROFESSOR = 2;
STUDENT = 3;
}
当使用Protobuf进行RPC(详见后文介绍)时,可以使用service来定义该过程,例如:
service MyService {
rpc GetData (RequestType) returns (ResponseType);
}
message RequestType {
// 定义请求消息的字段
}
message ResponseType {
// 定义响应消息的字段
}
message之所以可以用来定义复杂的数据结构,是因为message中可以嵌套其他message或enum,例如:
message Person {
string name = 1;
int32 age = 2;
repeated string email = 3;
enum Job{
OTHER = 0;
PROGRAMMER = 1;
PROFESSOR = 2;
STUDENT = 3;
}
Job job = 4;
}
message UserGroup{
repeated Person person = 1;
}
1.2.3 proto 3与proto 2的一些区别
- proto 3支持更多编程语言。
- proto 3必须首先声明syntax = “proto3”。
- proto 3的字段修饰符为singular和repeated,而proto 2的字段修饰符为optional、required和repeated。
- proto 3的repeated修饰的字段默认packed = true(使用更紧凑的编码方式),而proto 2则默认packed = false。
- proto 3中系统会自动指定字段的默认值,而不允许用户自定义默认值,proto 2则允许用户定义字段默认值。
1.3 使用Protobuf
.proto文件定义了传输数据的结构,但该文件并不能直接拿来使用。Protobuf之所以能够实现跨平台、跨语言的数据通信,是因为.proto文件可以通过编译器protoc编译成各种语言的代码文件(例如.java文件和.py文件等等)。因此定义好.proto文件后需要将该文件编译成通信双方语言的代码文件,之后方可供通信双方调用。
1.3.1 安装protoc
从Protobuf源码网站(Releases · protocolbuffers/protobuf · GitHub)的Assets中下载与操作系统相适应的压缩包。Windows操作系统需要将解压后文件夹中的bin文件夹的路径加入到系统环境变量中,之后在命令行中输入protoc --version即可验证protoc安装是否成功。
1.3.2 编译
在protoc安装完成后即可将.proto文件编译成目标语言,例如将person.proto文件编译成python语言的文件,需要在命令行中输入:
protoc --python_out=/path_to_target_file/ /path_to_proto_file/person.proto
其中“--python_out”表示编译成python语言,同理若需要编译成java语言则需要替换成“--java_out”。等号后面第一个路径表示编译生成文件的位置,第二个路径表示.proto文件的位置。最终person.proto文件会被编译为person_pb2.py文件。
1.3.3 调用
person.proto的内容如下:
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
repeated string email = 3;
enum Job{
OTHER = 0;
PROGRAMMER = 1;
PROFESSOR = 2;
STUDENT = 3;
}
Job job = 4;
}
message UserGroup{
repeated Person person = 1;
}
以上代码编译生成了person_pb2.py,对该python文件的调用示例如下:
import person_pb2
userGroup = person_pb2.UserGroup()
person1 = userGroup.person.add() # repeated message字段的添加方法
person1.name = 'AAA'
person1.age = 30
person1.email.append('AAA@ustc.edu.cn') # repeated基本数据类型字段的添加方法1
person1.email.append('AAA@mail.ustc.edu.cn')
person1.job=person_pb2.Person.PROFESSOR # 枚举赋值
person2 = userGroup.person.add()
person2.name = 'BBB'
person2.age = 23
person2.email.extend(['BBB@ustc.edu.cn','BBB@mail.ustc.edu.cn']) # repeated基本数据类型字段的添加方法2
person2.job=person_pb2.Person.STUDENT
send_data=userGroup.SerializeToString() # 序列化
# ...省略序列化数据的传递过程...
receive_data=person_pb2.UserGroup()
receive_data.ParseFromString(send_data) # 反序列化
print(receive_data)
2 gRPC
2.1 RPC与gRPC概述
RPC全称为远程过程调用(Remote Procedure Call),它是一种计算机通信协议,是一种让一个计算机程序请求另一个程序执行某项任务的方式,即使它们位于不同的计算机上。该协议能够实现分布式系统之间的交互和通讯,大大简化分布式系统的开发,提高系统的可维护性和可扩展性。
gRPC是Google开源的高性能、通用的RPC框架,它基于Protobuf进行数据传输。由前文所述Protobuf的性质,gRPC具有如下优点:
- gRPC使用二进制格式进行数据传输,具有序列化速度快、传输数据少的优势,同时还可以实现双向流、头部压缩和多路复用等特性。
- gRPC支持多种语言,可以方便地构建跨语言的分布式系统。
- gRPC可以通过编译器将服务定义文件自动转化成目标语言的代码。
- gRPC支持多种负载均衡算法和服务发现机制,以及TLS加密和认证等安全机制。
2.2 gRPC示例
本节演示了一个带有基本用户身份验证检查的 gRPC 服务器的实现,server与client均使用python编写。通信的内容为client向server发送用户名和密码,server检查用户名和密码是否正确,之后将检查结果返回给client,整个过程通过gRPC来完成。
2.2.1 服务定义文件
服务定义文件即为.proto类型文件,代码如下:
syntax = "proto3";
service Login {
rpc LoginCheck (LoginRequest) returns (LoginResponse);
}
message LoginRequest {
string uid = 1;
string password = 2;
}
message LoginResponse {
enum Status{
NULL=0;
SUCCESS=1;
FAILED_PASSWORD_ERROR=2;
FAILED_UID_NOT_EXIST=3;
}
Status status=1;
}
该文件中定义了一种RPC服务Login,它要求通信双方其中一方发来LoginRequest格式的数据,之后另一方以LoginResponse格式的数据进行相应。该文件命名为login.proto。
2.2.2 编译
首先需要安装编译工具所需要的库:
pip install grpcio
pip install grpcio-tools
pip install protobuf
相比之第1章中的编译,gRPC的编译要更为复杂,在命令行中输入:
python -m grpc_tools.protoc -I ./ --python_out=./ --grpc_python_out=. ./login.proto
其中“-I”之后的路径表示.proto文件所在的文件夹,“--python_out=”后的路径表示生成的login_pb2.py所在的文件夹,“--grpc_python_out=”后的路径表示login_pb2_grpc.py所在的文件夹,最后的路径表示.proto文件。按照上述命令,则在当前文件夹中编译生成login_pb2.py和login_pb2_grpc.py两个文件。
2.2.3 client
client代码如下:
import grpc
from login_pb2 import LoginRequest, LoginResponse
from login_pb2_grpc import LoginStub
def login(uid, password):
with grpc.insecure_channel("localhost:54321") as channel:
try:
stub = LoginStub(channel)
request = LoginRequest(uid=uid, password=password)
response = stub.LoginCheck(request)
print(response)
except grpc.RpcError as e:
print(f"gRPC 请求失败: {e}")
except Exception as e:
print(f"发生意外错误: {e}")
if __name__ == '__main__':
uid = input("uid:")
password = input("password:")
login(uid, password)
其中LoginStub是gRPC自动生成的客户端存根(client stub)类,它用于在客户端发起 gRPC 调用。在gRPC中,存根是客户端用于调用远程服务的代理对象。存根隐藏了底层网络通信的复杂性,使得客户端能够像调用本地方法一样调用远程服务的方法。
2.2.4 server
server代码如下:
import time
from concurrent import futures
import grpc
from login_pb2 import LoginRequest, LoginResponse
from login_pb2_grpc import add_LoginServicer_to_server, LoginServicer
class check(LoginServicer):
def LoginCheck(self, request, context):
print(request.uid)
print(request.password)
response = LoginResponse()
response.status = self._check(request.uid, request.password)
return response
def _check(self, uid, password):
# 简单模拟server保存的用户名和密码
users = {
'AAA': '123456',
'BBB': '666666'
}
for key in users.keys():
if key == uid:
if users[key] == password:
return LoginResponse.Status.SUCCESS
else:
return LoginResponse.Status.FAILED_PASSWORD_ERROR
return LoginResponse.Status.FAILED_UID_NOT_EXIST
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
add_LoginServicer_to_server(check(), server)
server.add_insecure_port('[::]:54321')
server.start()
try:
while True:
time.sleep(60 * 60 * 24)
except KeyboardInterrupt:
server.stop(0)
if __name__ == '__main__':
serve()
由编译工具自动生成的文件login_pb2_grpc.py中包含LoginServicer类(即login.proto中所设定的服务),编写服务器代码时需要先继承该类并具体实现服务的内容,随后使用add_LoginServicer_to_server函数将该服务添加到gRPC的服务器中。
参考资料
Protobuf官方文档Protocol Buffers Documentation
Protobuf源码GitHub - protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format
gRPC官方文档Documentation | gRPC