Protocol Buffers学习指南
一、什么是Protocol Buffers
Protocol Buffers是谷歌的语言无关、平台无关、可扩展的机制,用于序列化结构化数据(比如XML),但更小、更快、更简单。您只需定义数据的结构化方式,然后就可以使用特殊生成的源代码轻松地向各种数据流写入和读取结构化数据,并使用各种语言。目前支持Java、Python、Objective-C和c++中生成的代码。在我们新的proto3语言版本中,你也可以使用Kotlin, Dart, Go, Ruby和c#,以及更多的语言。
比如使用Protocol Buffers来定义一个文件:
- protobuf_test.proto
syntax = "proto3"; // 说明使用的是proto3版本 message Person { string name = 1; // 定义name字段,类型是string,数字1表明指定的编号 int32 age = 2; // 定义age字段,类型是2,数字2表明指定的编号 }
上面就是定义一个简单的proto文件,可以利用该文件生成对应语言的源码,然后进行操作,但是在生成对应源码之前需要先安装编译工具,因为后期使用grpc所以这里可以一起安装:
python -m pip install grpcio #安装grpc python -m pip install grpcio-tools #安装grpc tools
- 生成Python源码
在该文件下执行:
python -m grpc_tools.protoc --python_out=. -I. protobuf_test.proto
生成protobuf_test_pb2.py文件:
# -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: protobuf_test.proto """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import message as _message from google.protobuf import reflection as _reflection from google.protobuf import symbol_database as _symbol_database # @@protoc_insertion_point(imports) _sym_db = _symbol_database.Default() DESCRIPTOR = _descriptor.FileDescriptor( name='protobuf_test.proto', package='', syntax='proto3', serialized_options=None, create_key=_descriptor._internal_create_key, serialized_pb=b'\n\x13protobuf_test.proto\"#\n\x06Person\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0b\n\x03\x61ge\x18\x02 \x01(\x05\x62\x06proto3' ) _PERSON = _descriptor.Descriptor( name='Person', full_name='Person', filename=None, file=DESCRIPTOR, containing_type=None, create_key=_descriptor._internal_create_key, fields=[ _descriptor.FieldDescriptor( name='name', full_name='Person.name', index=0, number=1, type=9, cpp_type=9, label=1, has_default_value=False, default_value=b"".decode('utf-8'), message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), _descriptor.FieldDescriptor( name='age', full_name='Person.age', index=1, number=2, type=5, cpp_type=1, label=1, has_default_value=False, default_value=0, message_type=None, enum_type=None, containing_type=None, is_extension=False, extension_scope=None, serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), ], extensions=[ ], nested_types=[], enum_types=[ ], serialized_options=None, is_extendable=False, syntax='proto3', extension_ranges=[], oneofs=[ ], serialized_start=23, serialized_end=58, ) DESCRIPTOR.message_types_by_name['Person'] = _PERSON _sym_db.RegisterFileDescriptor(DESCRIPTOR) Person = _reflection.GeneratedProtocolMessageType('Person', (_message.Message,), { 'DESCRIPTOR' : _PERSON, '__module__' : 'protobuf_test_pb2' # @@protoc_insertion_point(class_scope:Person) }) _sym_db.RegisterMessage(Person) # @@protoc_insertion_point(module_scope)
此时可以使用生成的源码文件进行序列化和反序列化数据:
import protobuf_test_pb2 # 序列化 request = protobuf_test_pb2.Person() request.name = "barry" request.age = 20 req_str = request.SerializeToString() print(req_str) # 反序列化 res = protobuf_test_pb2.Person() res.ParseFromString(req_str) print(res)
二、数据类型
1、基本数据类型
proto类型 |
说明 |
Python类型 |
Go类型 |
double |
float |
float64 |
|
float |
float |
float32 |
|
int32 |
使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 |
int |
int32 |
uint32 |
使用变长编码 |
int |
uint32 |
uint64 |
使用变长编码 |
int |
uint64 |
sint32 |
使用变长编码,这些编码在负值时比int32高效的多 |
int |
int32 |
sint64 |
使用变长编码,有符号的整型值。编码时比通常的int64高效。 |
int |
int64 |
fixed32 |
总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 |
int |
uint32 |
fixed64 |
总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 |
int |
uint64 |
sfixed32 |
总是4个字节 |
int |
int32 |
sfixed64 |
总是8个字节 |
int |
int64 |
bool |
bool |
bool |
|
string |
一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 |
str |
string |
bytes |
可能包含任意顺序的字节数据。 |
str |
[]byte |
上面是proto中的类型以及通过proto生成的Python或者Go语言源码对应的数据类型,那么如果不给对应的数据类型默认值,就会有一个默认值:
- string 默认为空
- bytes默认是一个空的bytes
- bool默认是false
- int系列默认是0
- 枚举默认是第一个定义的枚举值,必须是0
2、 其它数据类型
2.1 枚举类型
syntax = "proto3"; // 说明使用的是proto3版本 enum Gender { MALE = 0; FEMALE = 1; } message Person { string name = 1; // 定义name字段,类型是string,数字1表明指定的编号 int32 age = 2; // 定义age字段,类型是2,数字2表明指定的编号 Gender gender = 3; // 使用枚举类型 }
枚举的第一个常量映射为0:每个枚举类型必须将其第一个类型映射为0,这是因为:
- 必须有有一个0值,我们可以用这个0值作为默认值。
- 这个零值必须为第一个元素,为了兼容proto2语义,枚举类的第一个值总是默认值。
通过命令行生成Python源码:
python -m grpc_tools.protoc --python_out=. -I. protobuf_test.proto
然后进行使用:
import protobuf_test_pb2 # 序列化 request = protobuf_test_pb2.Person() ... request.gender = protobuf_test_pb2.MALE req_str = request.SerializeToString() print(req_str)
2.2 map类型
syntax = "proto3"; // 说明使用的是proto3版本 enum Gender { MALE = 0; FEMALE = 1; } message Person { string name = 1; // 定义name字段,类型是string,数字1表明指定的编号 int32 age = 2; // 定义age字段,类型是2,数字2表明指定的编号 Gender gender = 3; // 使用枚举类型 map<string, string> hobby = 4; // 使用map类型 }
map类型map<string, string> map_field,第一个string是key类型,第二个string是value类型,map_field是字段名称。
通过命令行生成Python源码:
python -m grpc_tools.protoc --python_out=. -I. protobuf_test.proto
然后进行使用:
import protobuf_test_pb2 # 序列化 request = protobuf_test_pb2.Person() ... request.hobby["hobby1"] = "music" request.hobby["hobby2"] = "book" req_str = request.SerializeToString() print(req_str)
三、进阶使用
1、message嵌套使用
syntax = "proto3"; // 说明使用的是proto3版本 message ShopCar { int32 id = 1; message Goods { int32 id = 1; string name = 2; } repeated Goods data = 2; // 说明嵌套的Good对象可以有多个 }
通过命令行生成Python源码:
python -m grpc_tools.protoc --python_out=. -I. protobuf_test.proto
然后进行使用:
import protobuf_test_pb2 shop_car = protobuf_test_pb2.ShopCar() # 序列化 shop_car.id = 1 for i in range(2): goods = shop_car.Goods() goods.id = 1 goods.name = f"苹果{i}" shop_car.data.extend([goods]) req_str = shop_car.SerializeToString() print(req_str) # 反序列化 res = protobuf_test_pb2.ShopCar() res.ParseFromString(req_str) print(res)
2、go_package的作用
对于go_package对于Go语言来说是起作用的,指明了当前proto文件生成的go源码在什么包下:
syntax = "proto3"; // 说明使用的是proto3版本 option go_package = "common/v1"; // 表示当前输出的go代码放到的包的位置,注意这个地方需要至少带”/“,否则报错 message Person { string name = 1; // 定义name字段,类型是string,数字1表明指定的编号 int32 age = 2; // 定义age字段,类型是2,数字2表明指定的编号 }
如果需要转成go语言源码需要先安装对应的工具:
- protoc工具
下载对应平台的protoc工具,这里我选择下载windows系统64位protoc-3.19.4-win64.zip,然后将其解压后并将其配置在系统环境变量中:
C:\Program Files\protoc-3.19.4-win64\bin
-
go依赖包
go get github.com/golang/protobuf/protoc-gen-go
此时可以将protobuf转成Go语言代码:
protoc -I . protobuf_test.proto --go_out=.
进入到proto文件所在目录,其中protoc命令就是我们安装的protoc工具,-I是import的意思,将后面 "."当前目录下的proto文件导入,后面的--go_out将proto文件转成go源码导出到"."当期目录下,此时会调用protoc-gen-go.exe命令,这就是我们安装的go依赖,这也就意味着protoc-gen-go下载的可执行文件protoc-gen-go.exe配置到环境变量中,或者放到go sdk的bin目录下即可。
3、proto文件的互相引入
如果一个proto文件被多个proto文件共用,那么可以通过导入的方式被其它文件使用,比如:
- base.proto
syntax = "proto3"; option go_package="common/v1"; message Empty { }
- protobuf_test.proto
syntax = "proto3"; // 说明使用的是proto3版本 import "base.proto"; // 引入其它proto文件 option go_package = "common/v1"; // 表示当前目录下proto类型的文件 message Person { string name = 1; // 定义name字段,类型是string,数字1表明指定的编号 int32 age = 2; // 定义age字段,类型是2,数字2表明指定的编号 repeated Empty empty = 3; //使用其它proto文件中定义的message }