Protocol Buffers 详解

1,086 阅读8分钟

概述

Protocol Buffers 提供了一种语言中立、平台中立和可扩展的机制,用于序列化数据结构体向前兼容或先后兼容。它像json,但比它更小和更快,并且与本地语言绑定。 Protocol Buffers 是一个定义语言(在 .proto 文件中创建),proto 编译器生成数据接口、特定语言运行时的库和序列化格式数据写入文件或通过网络连接发送等代码。

什么问题需要用 Protocol Buffer 来解决

  • 对数据的序列化或反序列化的效率要求高
  • 定义的协议需要被不同的平台进行调用
  • 协议的扩展或升级能容易,可以很容易地引入新的字段,并且不需要检查数据的中间服务器可以简单地解析并传递数据,而无需了解所有字段

使用 Protocol Buffer 的好处

  • 序列化效率高 :Protocol Buffers 为数据结构提供更小(序列化后,数据大小可缩小约3倍)和更快(比xml和JSON快20-100倍,体积缩小后,传输时,带宽也会优化)的序列化和反序列化能力
  • 支持跨平台、多语言 :proto 编译器能将定义的.proto 文件构建生成各种编程语言的代码,来操作相应的Protocol Buffers。多个平台仅需维护一套协议(.proto)\
  • 具备好的扩展性、兼容性 :Protocol Buffers 允许无缝支持对任何协议缓冲区的更改,包括添加新字段和删除现有字段,而不会破坏现有服务。

Protocol Buffer 是怎么工作

image.png

  • 创建.proto文件,定义协议数据结构
  • 根据.proto文件,编译生成对应语言的 PB 源文件
  • 用项目的代码编程 PB 代码, 生成编译后class文件
  • 用对应 PB 的类别进行序列化,分享或反序列化数据

proto3 定义

定义 message

  • 定义一个 message 类别
  // syntax 指定使用的proto3或proto2的协议
  syntax = "proto3";
  
  // 定义的消息结构
  message SearchRequest {
    string query = 1;
    int32 page_number = 2;
    int32 result_per_page = 3;
  }
  • 指定字段编号
    每个消息定义中的每个字段都有唯一的编号。这些字段编号用于标识消息二进制格式中的字段,并且在使用消息类型后不应更改。

  • 字段规则
    singular:可以有零个或一个此字段(但不能超过一个)。 是 proto3 语法的默认字段规则
    repeated:可以有任意个此字段(包含0个),在proto3 语法需要显示使用 repeated 关键字进行修饰

  • 字段类型
    基本类型image.png image.png 其他类型
    Any类型
    Any 消息类型允许您将消息用作嵌入类型,而无需定义它们的 .proto。 Any 包含作为字节的任意序 列化消息,以及充当全局唯一标识符并解析为该消息类型的 URL。 要使用 Any 类型,您需要导入 google/protobuf/any.proto。 image.png

    Map类型
    定义一个map类型

    map<key_type, value_type> map_field = N;
    

    注意:map字段不能使用repeated进行修饰

    类型更新规则

    1、不要更改任何现有字段的字段编号。
    2、添加新的字段,要关注字段的默认,保证老版本的协议和新版本的协议可以正常使用
    3、删除字段或重命名字段,可以通过添加前缀“OBSOLETE_”,或保留字段编号,以便您的 .proto 的未来用户不会意外重用该编号
    4、int32uint32int64uint64, and bool 都相互兼容,可以从一种类别转换为另外一种类型
    5、sint32 和 sint64 是相互兼容的,但是与其他整型是不兼容
    6、string 和 bytes 是相互兼容的,以utf-8进行编码
    7、如果 bytes 包含消息的编码版本,则嵌入消息与 bytes 兼容。
    8、fixed32 兼容 sfixed32,  fixed64 和 sfixed64
    9、stringbytes和 message 字段, optional 和 repeated 是相互兼容的
    10、数据类别(包含bool和enums), optional 和 repeated 是不兼容的
    11、enum 类别和int32, uint32, int64, and uint64 是相互兼容的
    12、将单个值更改为新 oneof 的成员是安全且二进制兼容的。 如果您确定没有代码一次设置多个字段,则将多个字段移动到新的 oneof 中可能是安全的。 将任何字段移动到现有的 oneof 中是不安全的。

  • 保留字段
    如果您通过完全删除某个字段或将其注释掉来更新消息类型,未来的用户可以在对类型进行自己的更新时重用该字段编号。 如果他们稍后加载相同 .proto 的旧版本,这可能会导致严重的问题,包括数据损坏、隐私错误等。 确保不会发生这种情况的一种方法是通过reserved来指定保留已删除字段的字段编号或名称。 如果将来有任何用户尝试使用这些字段标识符,protocol buffer 编译器将会报错。

    message Foo {
      reserved 2, 15, 9 to 11;
      reserved "foo", "bar";
    }
    

    注意:不能在同一个reserved中即有编号又有名称,例如 reserved 2, "foo";

  • 默认值 在解析 message 字段时,若字段没有被赋予特殊的值,则会赋予默认值,以下特定类型的赋予的默认值:

    类型默认值
    strings空字符串
    bytes空字节
    boolsfalse
    数字类别0
    enums第一个默认的枚举值,它必须为0
    message 类别与语言强相关,不同语言默认值不一样

定义Enum

  • 定义一个枚举类别
  enum Corpus {
   UNIVERSAL = 0;
   WEB = 1;
   IMAGES = 2;
   LOCAL = 3;
   NEWS = 4;
   PRODUCTS = 5;
   VIDEO = 6;
 }
  • 元素赋值

    枚举必须包含一个映射到0值作为第一个元素,因为这样才可以将0值作为枚举的默认值和同时为了兼容proto2
    枚举运行用相同的值来代表枚举的元素,但是必须将 allow_alias 选项设置为 true,否则编译时会抛出异常的错误

message MyMessage1 {
  enum EnumAllowingAlias {
    option allow_alias = true;
    UNKNOWN = 0;
    STARTED = 1;
    RUNNING = 1;
  }
}
message MyMessage2 {
  enum EnumNotAllowingAlias {
    UNKNOWN = 0;
    STARTED = 1;
    // RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.
  }
}
  • 保留值

    如果您通过完全删除枚举条目或将其注释掉来更新枚举类型,将来的用户可以在对类型进行自己的更新时重用该数值。 如果他们稍后加载相同 .proto 的旧版本,这可能会导致严重的问题,包括数据损坏、隐私错误等。 确保不会发生这种情况的一种方法是指定保留已删除条目的数值(和/或名称,这也可能导致 JSON 序列化问题)。 如果将来有任何用户尝试使用这些标识符,protocol buffer 编译器会抱怨。 您可以使用 max 关键字指定保留的数值范围达到最大可能值。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

注意:不能在同一个reserved中即有编号又有名称,例如 reserved 2, "foo";

定义 service

  • 定义一个rpc服务
service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

定义包名

为了防止message类别冲突,可以添加包名

package foo.bar;
message Open { ... }

命名规范

  • message 类型命名采用驼峰命名方式,并且首字母大写。字段名采用下划线命名方式
    message EventRequest {
      required string event_name = 1;
    }
    
  • enum 类型命名采用驼峰命名方式,并且首字母大写。枚举值采用下划线命名方式,并且全大写。
    message EventType {
      GIT_EVENT = 0;
      RELEASE_EVENT = 1;
    }
    
  • service 命名和方法命名都是使用驼峰命名方式,并且首字母大写
    service EventService {
      rpc SendEvent(EventRequest) returns (EventResponse);
    }
    

json 映射关系

Proto3 支持 JSON 中的规范编码,从而更容易在系统之间共享数据。 下表中按类型描述了编码

proto3JSONJSON exampleNotes
messageobject{"fooBar": v, "g": null, …}Generates JSON objects. Message field names are mapped to lowerCamelCase and become JSON object keys. If the json_name field option is specified, the specified value will be used as the key instead. Parsers accept both the lowerCamelCase name (or the one specified by the json_name option) and the original proto field name. null is an accepted value for all field types and treated as the default value of the corresponding field type.
enumstring"FOO_BAR"The name of the enum value as specified in proto is used. Parsers accept both enum names and integer values.
map<K,V>object{"k": v, …}All keys are converted to strings.
repeated Varray[v, …]null is accepted as the empty list [].
booltrue, falsetrue, false
stringstring"Hello World!"
bytesbase64 string"YWJjMTIzIT8kKiYoKSctPUB+"JSON value will be the data encoded as a string using standard base64 encoding with paddings. Either standard or URL-safe base64 encoding with/without paddings are accepted.
int32, fixed32, uint32number1, -10, 0JSON value will be a decimal number. Either numbers or strings are accepted.
int64, fixed64, uint64string"1", "-10"JSON value will be a decimal string. Either numbers or strings are accepted.
float, doublenumber1.1, -10.0, 0, "NaN", "Infinity"JSON value will be a number or one of the special string values "NaN", "Infinity", and "-Infinity". Either numbers or strings are accepted. Exponent notation is also accepted. -0 is considered equivalent to 0.
Anyobject{"@type": "url", "f": v, … }If the Any contains a value that has a special JSON mapping, it will be converted as follows: {"@type": xxx, "value": yyy}. Otherwise, the value will be converted into a JSON object, and the "@type" field will be inserted to indicate the actual data type.
Timestampstring"1972-01-01T10:00:20.021Z"Uses RFC 3339, where generated output will always be Z-normalized and uses 0, 3, 6 or 9 fractional digits. Offsets other than "Z" are also accepted.
Durationstring"1.000340012s", "1s"Generated output always contains 0, 3, 6, or 9 fractional digits, depending on required precision, followed by the suffix "s". Accepted are any fractional digits (also none) as long as they fit into nano-seconds precision and the suffix "s" is required.
Structobject{ … }Any JSON object. See struct.proto.
Wrapper typesvarious types2, "2", "foo", true, "true", null, 0, …Wrappers use the same representation in JSON as the wrapped primitive type, except that null is allowed and preserved during data conversion and transfer.
FieldMaskstring"f.fooBar,h"See field_mask.proto.
ListValuearray[foo, bar, …]
ValuevalueAny JSON value. Check google.protobuf.Value for details.
NullValuenullJSON null
Emptyobject{}An empty JSON object