Protobuf从入门到“顺手”
Posted AI蜗牛之家
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Protobuf从入门到“顺手”相关的知识,希望对你有一定的参考价值。
文章目录
很多项目采用
Protobuf
进行消息的通讯,还有基于Protobuf
的微服务框架GRPC,最近在使用一些框架的时候,顺手梳理了一下protobuf
的一些语言特性和一些实用技巧。全文基于最新protobuf3
,并用python
举例
1.概述
序列化(serialization、marshalling)的过程是指将数据结构或者对象的状态转换成可以存储(比如文件、内存)或者传输的格式(比如网络)。反向操作就是反序列化(deserialization、unmarshalling)的过程。
JSON是一种更轻量级的基于文本的编码方式,经常用在client/server端的通讯中。YAML类似JSON,新的特性更强大,更适合人类阅读,也更紧凑。
Protobuf是google提出的消息通讯规范,支持很多语言,比如C++、C#、Dart、Go、Java、Python、Rust等,同时也是跨平台的,所以得到了广泛的应用。Protobuf包含序列化格式的定义、各种语言的库以及一个IDL编译器。正常情况下你需要定义proto文件,然后使用IDL编译器编译成你需要的语言。
1.1.proto格式
先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件了:
syntax = "proto3";
message SearchRequest {
string query = 1; // 注释
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
... //这里省略了
}
- 第一行指定
protobuf
的版本,这里是以proto3格式定义。你还可以指定为proto2。如果没有指定,默认以proto2格式定义。 - 它定义了一个message类型: SearchRequest, 它包含三个字段query、page_number、result_per_page,它会被protoc编译成不同的编程语言的相应对象,比如Java中的class、Go中的struct等。在一个.proto文件中可以定义多个消息类型,例如,如果想定义与SearchResponse消息类型对应的回复消息格式的话,你可以将它添加到相同的.proto文件中
- 字段是以
[ "repeated" ] type fieldName "=" fieldNumber [ "[" fieldOptions "]" ] ";"
格式定义的。这个例子是一个简单的例子,采用了type fieldName "=" fieldNumber
格式定义的。 - 指定字段类型 在上面的例子中,所有字段都是标量类型:两个整型(page_number和result_per_page),一个string类型(query)。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型。
- 分配标识号 正如你所见,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。 最小的标识号从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]( (从
FieldDescriptor::kFirstReservedNumber
到FieldDescriptor::kLastReservedNumber
))的标识号
1.2.proto编译
可以将这个proto编译成Python的代码(参考链接参考连接),因为这里我们使用了python_out输出格式。(安装proto编译器)
protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto
其中-I
指定protoc的搜索import的proto的文件夹,可以有多个-I
参数。在MacOS操作系统中protobuf把一些扩展的proto放在了/usr/local/include对应的文件夹中。
从.proto文件生成了什么?
当用protocol buffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。
- 对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
- 对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
- 对Java来说,编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。
- 对go来说,编译器会位每个消息类型生成了一个.pd.go文件。
2.支持类型
2.1.标量数值类型
一个标量消息字段可以含有一个如下的类型:
.proto Type | Notes | C++ Type | Java Type | Python Type[2] | Go Type | Ruby Type | C# Type | php Type |
---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | |
float | float | float | float | float32 | Float | float | float | |
int32 | 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 | int32 | int | int | int32 | Fixnum 或者 Bignum(根据需要) | int | integer |
uint32 | 使用变长编码 | uint32 | int | int/long | uint32 | Fixnum 或者 Bignum(根据需要) | uint | integer |
uint64 | 使用变长编码 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string |
sint32 | 使用变长编码,这些编码在负值时比int32高效的多 | int32 | int | int | int32 | Fixnum 或者 Bignum(根据需要) | int | integer |
sint64 | 使用变长编码,有符号的整型值。编码时比通常的int64高效。 | int64 | long | int/long | int64 | Bignum | long | integer/string |
fixed32 | 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 | uint32 | int | int | uint32 | Fixnum 或者 Bignum(根据需要) | uint | integer |
fixed64 | 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 | uint64 | long | int/long | uint64 | Bignum | ulong | integer/string |
sfixed32 | 总是4个字节 | int32 | int | int | int32 | Fixnum 或者 Bignum(根据需要) | int | integer |
sfixed64 | 总是8个字节 | int64 | long | int/long | int64 | Bignum | long | integer/string |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | |
string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 | string | String | str/unicode | string | String (UTF-8) | string | string |
bytes | 可能包含任意顺序的字节数据。 | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string |
关于默认值的一些说明:
- 如果被编码的信息不包含某个变量,该被解析的对象会自动设置一个默认值:
对于string,默认是一个空string;
对于bytes,默认是一个空的bytes
对于bool,默认是false
对于数值类型,默认是0
对于枚举,默认是第一个定义的枚举值,必须为0;
对于消息类型(message),域没有被设置,确切的消息是根据语言确定的,详见generated code guide- 数据传输时,为尽可能减少传输数据量,如果值是默认值时,在传输的消息里面会省略该字段,所以如果接受端看不到某些字段,可能就是该原因。
2.2.枚举
当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值,也就是我们常说的枚举类型。
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
这里尤其注意,枚举的序号中必须包含0,这里跟变量的序号从1开始不同,注意区分。另外枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,虽支持负数,但是对负数不够高效,不推荐在enum中使用负数。更多详情参考generated code guide
小贴士:
同上一节说明,如果数据传输时值是默认值,该字段就回被省略,这对枚举类型字段分析时很不方便,所以可以用序号0表示一个默认弃用的类型(比如上面例子中把UNIVERSAL = 0
;下标0用DEFAULT
占位,其他枚举类型顺延即可),这样正常情况下就不会出现默认值,该值也会出现在变量列表中
2.3.内置封装类型
2.3.1. Any类型
Any 类型可以表述任何message数据。
# protobuff文件Status.proto
import "google/protobuf/any.proto";
message Status {
string message = 1;
google.protobuf.Any details = 2;
}
message ErrorDetails{
int status;
}
python的用法:
def serial():
errorStatus = Status_pb2.ErrorStatus()
errorStatus.message = "run time error"
errorDetails = Status_pb2.ErrorDetails()
errorDetails.status=-1
errorStatus.details.Pack(errorDetails) # Any类型打包存在details变量中
return errorStatus.SerializeToString()
def parse():
errorStatus = Status_pb2.ErrorStatus()
tmessage.ParseFromString(protobufdata)
message = tmessage.tmessage
errorDetails = Status_pb2.ErrorDetails()
tmessage.details.Unpack(errorDetails)
status = errorDetails.status
更多详情可以参考例子:python protobuf泛型类Any使用
2.3.2. Oneof 类型
如果你的消息中有很多可选字段, 并且同时至多一个字段会被设置, 你可以加强这个行为,使用oneof特性节省内存.
message Foo {
oneof test_oneof {
string name = 1;
int32 serial_number = 2;
}
}
python举例
message = Foo()
message.name = "Bender"
assert message.HasField("name")
message.serial_number = 2716057
assert message.HasField("serial_number")
assert not message.HasField("name")
更多详情参考Python Generated Code
2.3.3.Map 映射类型
proto中的map与python中的字典或者json类似,每个key对应其value存储
message MyMessage {
map<int32, int32> mapfield = 1;
}
Python中Map用法与dict类似
# Assign value to map
m.mapfield[5] = 10
# Read value from map
m.mapfield[5]
# Iterate over map keys
for key in m.mapfield:
print(key)
print(m.mapfield[key])
# Test whether key is in map:
if 5 in m.mapfield:
print(“Found!”)
# Delete key from map.
del m.mapfield[key]
Map的字段可以是repeated。
序列化后的顺序和map迭代器的顺序是不确定的,所以你不要期望以固定顺序处理Map
2.4.类型的引用
Result消息类型与SearchResponse是定义在同一文件中的。如果想要使用的消息类型已经在其他.proto文件中已经定义过了呢?
你可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:
import "myproject/other_protos.proto";
另外,可以在本消息中引用其他消息的字段,在下面的例子中,Result消息就定义在SearchResponse消息内:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
另外,消息类型可以是嵌套的。
3.python中的一些常用用法
3.1.json与message转换
通过Parse可以实现json数据类型到message格式的快速转换,方便一些测试等。
举例如下:
# proto定义
message Thing {
string first = 1;
bool second = 2;
int32 third = 3;
}
json
=> message
使用Parse,同理dict
=> message
使用ParseDict
:
import json
from google.protobuf.json_format import Parse
message = Parse(json.dumps({
"first": "a string",
"second": True,
"third": 123456789
}), Thing())
print(message.first) # "a string"
print(message.second) # True
print(message.third) # 123456789
message
=> dict
,通过MessageToDict
实现,同理message
=> json
也可以通过MessageToJson
实现:
from google.protobuf.json_format import MessageToDict
message_as_dict = MessageToDict(message)
message_as_dict['first'] # == 'a string'
message_as_dict['second'] # == True
message_as_dict['third'] # == 123456789
这里可以注意下,proto中推荐使用驼峰命名格式,如果使用了下划线,proto中在将message转换为json或者dict时,默认会自动转为驼峰,比如原始变量
para_list
会被转换成paraList
,为避免出现如上现象,可以再使用MessageToDict
或者MessageToJson
时,函数中添加preserving_proto_field_name=True
,更多详情参考:google.protobuf.json_format¶ JSON to Protobuf in Python¶
4.更多详情和大神帖参考
- Python Generated Code python关于proto官方教程,其他语言参考overview
- 开发学院中文教程 比较详细的中文教程
- [转]Protobuf3 语法指南
以上是关于Protobuf从入门到“顺手”的主要内容,如果未能解决你的问题,请参考以下文章