Protobuf 简介与入门

1,537 阅读4分钟

protobuf

要点

  1. 二进制序列化结构数据,时间和空间效率都比XML和json高。
  2. proto3比proto2支持更多语言,且更简洁,去掉一些复杂的语法和特性。建议使用proto3
  3. 基于现有proto进行自定义扩展如新增字段是可行的,但删除旧字段需要小心
  4. 使用基于SerializeToStringParseFromString两个核心操作

什么是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中使用
  • int32uint32 int64uint64bool是兼容的,可以直接转换而不会破坏兼容性。(注意:64位数字用32位读取会产生截断)
  • sint32sint64是兼容的但不与其他int类型兼容
  • stringbytes是兼容的,只要bytesutf-8编码
  • 嵌套消息和bytes是兼容的,只要bytes包含该消息的一个编码过的版本
  • fixed32sfixed32是兼容的,fixed64sfixed64是兼容的
  • 对于stringbytesmessageoptionalrepeated兼容
  • enumint32uint32 int64uint64兼容(注意如果值不相兼容则会被截断),但客户端反序列化后可能需要不同的处理方式。
  • 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()

参考资料

google protobuf官方介绍

github 示例原型