protobuf
要点
- 二进制序列化结构数据,时间和空间效率都比XML和json高。
- proto3比proto2支持更多语言,且更简洁,去掉一些复杂的语法和特性。建议使用proto3
- 基于现有proto进行自定义扩展如新增字段是可行的,但删除旧字段需要小心
- 使用基于
SerializeToString
和ParseFromString
两个核心操作
什么是protobuf
- protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。
- 可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
- 可自定义数据的结构,使用各种语言进行编写和读取结构数据。也可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。
protobuf版本说明
- proto3比proto2支持更多语言如go、ruby等,且更简洁,去掉一些复杂的语法和特性。
- 在第一行非空白非注释行写:
syntax = "proto3"
- proto3移除了
required
,新增Any
类型 - 移除
default
选项:字段默认值根据字段类型由系统生成。注意:默认值不会参与序列化 - 枚举第一个字段为必须为0
- ……
使用proto3
syntax = "proto3"; // 必须加上,否则默认使用proto2
message SearchRequest {
string query = 1; // 字段编号为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;
}
message SearchResponse {
...
}
- 注意:每一个字段都有一个字段编号,用于二进制格式标识字段。1-15编号需要1个byte编码,16-2047编号需要两个字节。所以高频使用字段放在1~15编号中
singular
是默认字段规则,具有0/1个;repeated
重复0~任意次reserved
预留字段;或enum
中的保留值import
导入其他文件的定义,注意:导入proto2是可行的,但enum
不能直接使用- 支持嵌套类型
Any
需要import "google/protobuf/any.proto"
,可以在没有指定proto定义情况下作为一个嵌套类型使用oneof
:针对多个可选字段且只有一个字段会被设置时使用,可以节省内存map
:关联映射如map<string, Project> projects = 3;
package
:可选添加,防止不同消息类型有命名冲突- ……
对现有proto的扩展
可以在不破坏现有代码的情况下扩展proto,只需要满足以下条件:
- 不改变任何现有字段号
- 如果添加新字段,则在新代码中注意旧消息的默认值;旧代码中会视为未知字段并包含在序列化输出中(3.5及更晚版本)
- 如果删除某个字段,确保该字段不在新proto中使用
int32
、uint32
int64
、uint64
、bool
是兼容的,可以直接转换而不会破坏兼容性。(注意:64位数字用32位读取会产生截断)sint32
、sint64
是兼容的但不与其他int
类型兼容string
和bytes
是兼容的,只要bytes
是utf-8
编码- 嵌套消息和
bytes
是兼容的,只要bytes
包含该消息的一个编码过的版本 fixed32
、sfixed32
是兼容的,fixed64
、sfixed64
是兼容的- 对于
string
、bytes
、message
,optional
与repeated
兼容 enum
与int32
、uint32
int64
、uint64
兼容(注意如果值不相兼容则会被截断),但客户端反序列化后可能需要不同的处理方式。- 给
oneof
添加一个单独字段是安全且二进制兼容的,添加多个字段则需要小心只能有一个字段被设置。给任何现有的oneof
添加任何字段都需要谨慎,这未必安全。
示例:电话簿应用(python)
- 编写proto文件
// adb.proto
syntax = "proto3";
message Person {
string name = 1;
int32 id = 2;
string email = 3;
// 电话类型枚举
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
// 电话号码嵌套message
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
// 一个人可能有多个电话,使用repeated
repeated PhoneNumber phones = 4;
}
// 电话簿
message AddressBook {
repeated Person people = 1;
}
-
进行编译
protoc --python_out=./ adb.proto
-
写测试
import adb_pb2
def PromptForAddress(person):
person.id = 1
person.name = "ywt"
person.email = "ywt@sensetime.com"
phone_number = person.phones.add()
phone_number.number = "12345678912"
phone_number.type = adb_pb2.Person.MOBILE
def write_test():
address_book = adb_pb2.AddressBook()
address_book_file = "./book.txt"
try:
f = open(address_book_file, "rb")
address_book.ParseFromString(f.read())
f.close()
except IOError:
print(address_book_file + "Could not open file. Creating a new one.")
PromptForAddress(address_book.people.add())
f = open(address_book_file, "wb")
f.write(address_book.SerializeToString())
f.close()
if __name__ == "__main__":
write_test()
- 读测试
import adb_pb2
def ListPeople(address_book):
for person in address_book.people:
print("Person ID:", person.id)
print(" Name:", person.name)
print(" E-mail address:", person.email)
for phone_number in person.phones:
if phone_number.type == adb_pb2.Person.MOBILE:
print(" Mobile phone #: ", phone_number.number)
elif phone_number.type == adb_pb2.Person.HOME:
print(" Home phone #: ", phone_number.number)
elif phone_number.type == adb_pb2.Person.WORK:
print(" Work phone #: ", phone_number.number)
def read_test():
address_book = adb_pb2.AddressBook()
address_book_file = "./book.txt"
f = open(address_book_file, "rb")
address_book.ParseFromString(f.read())
f.close()
ListPeople(address_book)
if __name__ == "__main__":
read_test()