强大的序列化工具:Protocol Buffers
本文最后更新于 737 天前,其中的信息可能已经有所发展或是发生改变。

概述

Protocol Buffers 为结构化数据的序列化向前兼容,向后兼容,提供了语言中立、平台无关、可扩展机制的途径。类似JSON,但比JSON更小、更快。

通过.proto文件来定义,生成接口代码、特定语言的运行库,以及数据的序列化格式。

解决了什么问题

网络包的序列化格式 ,高达几兆大小的结构化数据,适用于网络传输和长期的数据存储。面对变更,不用修改代码。

程序员只需编写.proto文件

message Person {
  optional string name = 1;
  optional int32 id = 2;
  optional string email = 3;
}

通过.proto文件,可生成各种语言的代码,还包含字段的访问、序列化和反序列化的方法。

Person john = Person.newBuilder()
    .setId(1234)
    .setName("John Doe")
    .setEmail("jdoe@example.com")
    .build();
output = new FileOutputStream(args[0]);
john.writeTo(output);

由于可用于持久化,那么向后兼容就是至关重要的了。Protocol buffers 允许修改、新增、删除字段的同时,不影响现有服务,后面细说。

使用Protocol buffers 的好处

Protocol buffers可实现以下功能:

  • 序列化结构化数据
  • 记录
  • 语言无关、平台无关的数据类型
  • 可扩展

一般用于定义通信协议(同grpc一起使用)和数据存储。

优点:

  • 紧凑型数据存储
  • 快速解析
  • 多语言可用
  • 自动化生成代码

支持跨语言

可使用不同语言序列化和反序列化

支持跨项目

定义一份.proto文件,多个项目都能使用。可用于跨项目之间的接口定义。

更新proto文件后没有更新代码

由于支持跨项目,就要考虑向前兼容和向后兼容。

  • 向前兼容:proto没更新,代码更新了,新加的字段proto文件里没有,这种情况Protocol buffers会提供默认值
  • 向后兼容:proto更新了,代码没有更新,会忽略新加的字段,针对删除的字段,Protocol buffers会提供默认值,删除的是list字段(repeated fields),将被置空。

不适合Protocol Buffers的情况

  1. 针对小文件,Protocol Buffers是一次加载进内存,但体积超过几兆的文件加载过程中会产生多个副本,倒导致瞬间内存出现峰值
  2. 序列化后的二进制文件不能直接比较,也就是同样的数据,序列化后不保证相同。要比较的话需要先解析。
  3. 消息没有压缩
  4. 对于涉及大型多维浮点数数组的许多科学和工程应用,Protocol Buffers message在大小和速度上都没有达到最大效率。
  5. 不支持非面向对象的语言
  6. Protocol Buffers message不能自描述

谁使用了Protocol Buffers

Protocol Buffers 定义文件的语法

字段选项

  • optional:可选字段读取时,如果不存在,就会读取该字段类型的默认值,可主动设置默认值
    • optional int32 result_per_page = 3 [default = 10];
  • repeated:数组,顺序会保留,proto3默认压缩
  • singular
  • required(不建议使用)
    1. 如果必填字段更改为非必填了,但某个项目的code没有及时更新,这时如果不传递该字段就会出现异常。
    2. 针对必填的枚举值,新增枚举值后,未更新code的项目,无法识别新的枚举值,会丢弃掉,导致无法通过必填校验

基础类型

.proto Type Notes C++ Type Java Type Python Type[2] Go Type
double double double float *float64
float float float float *float32
int32 Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. int32 int int *int32
int64 Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. int64 long int/long[3] *int64
uint32 Uses variable-length encoding. uint32 int[1] int/long[3] *uint32
uint64 Uses variable-length encoding. uint64 long[1] int/long[3] *uint64
sint32 Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. int32 int int *int32
sint64 Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. int64 long int/long[3] *int64
fixed32 Always four bytes. More efficient than uint32 if values are often greater than 228. uint32 int[1] int/long[3] *uint32
fixed64 Always eight bytes. More efficient than uint64 if values are often greater than 256. uint64 long[1] int/long[3] *uint64
sfixed32 Always four bytes. int32 int int *int32
sfixed64 Always eight bytes. int64 long int/long[3] *int64
bool bool boolean bool *bool
string A string must always contain UTF-8 encoded text. string String unicode (Python 2) or str (Python 3) *string
bytes May contain any arbitrary sequence of bytes. string ByteString bytes []byte

复合类型

  • message
  • enum
  • oneof:当一条消息有多个可选字段且最多同时设置一个字段时,可以使用该类型
  • map

支持额外的数据类型

简单使用

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 文件第一行非空、非注释的代码,指定了proto的版本,否则默认按照proto2来解析

  • 非配字段序号,二进制文件中字段的唯一标识,不应该改变和复用,会影响兼容性

  • 为避免上诉问题,如果是多系统交互,删除字段后,应该通过reserved来标识该字段序号或者字段名被预留了

    message Foo {
    reserved 2, 15, 9 to 11;
    reserved "foo", "bar";
    }
  • 1-15的字段序号(包含字段类型)需要一个字节存储,16-2047的字段序号需要两个字节存储,频繁使用的字段应放到1-15范围内

  • 多个相关的message可以放到一个proto文件

参数取值

  • method为get时,path及queryString中的参数都会被映射到 reques 对应的 message 字段
    传参字段名可以使用json_name指定名称,但不能与原字段名同时使用
  • method为post时,有的情况会忽略queryString中的传参,只接收 path 及 body 中的传参
    • option中省略了 body,则表示没有http请求体,参数只能通过 path 及 query string 传递,body 被忽略
    • option中指定body为 时,参数只能通过 path 及 body 传递,query string 被忽略,所以如果使用 ,要注意不能再通过 query string 传参了
    • option中指定body指定为指定字段时,path/query string/body中的值都可以被接收
    • json body中字段名可以使用json_name指定名称,但不能与原字段名同时使用

注释

/* SearchRequest represents a search query, with pagination options to
 * indicate which results to include in the response. */

message SearchRequest {
  string query = 1;
  int32 page_number = 2;  // Which page number do we want?
  int32 result_per_page = 3;  // Number of results to return per page.
}
  • 单行注释放到末尾(谷歌的是单独一行)

通过proto文件产生了哪些产物

  • 字段的读写方法

  • 序列化、反序列化方法

  • .pb.go文件

默认值

  • 枚举的默认值是第一个定义的枚举值,并且必须值为0

  • repeated字段的默认值为空的list

  • 实际使用时需注意区分默认值和主动设置的值,例如一个布尔值为false,有可能是主动设置的false,也有可能是没有提供该参数而产生的默认值。这种情况可使用包装类

    import "google/protobuf/wrappers.proto";
    
    google.protobuf.Int32Value status = 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,可用于默认值

  • 重复值需注明,否则编译错误

    message MyMessage1 {
    enum EnumAllowingAlias {
      option allow_alias = true;
      UNKNOWN = 0;
      STARTED = 1;
      RUNNING = 1;
    }
    }
  • 无法识别的枚举值也会被序列化到文件,还会反序列化到message

  • 删除枚举值也会产生兼容性问题,和字段类似,可以通过预留的方式,防止被重新使用

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

引用其他proto文件里的message

import "myproject/other_protos.proto";

嵌套使用

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

外部使用

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

更新proto不更新代码

Protocol Buffers在处理数据时,会自动进行类型转换,所以有的情况下可以达到兼容的效果。例如string的code读取bytes时,只要bytes是utf8编码的,就可以读取为string。int32读取int64的数据,会自动截取32位。

这里主要是体现兼容性,但不建议故意为之。

Unknown Fields

old code parse new binary,new fields become unknown fields

Any message type

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}
  • 代表任意类型,相当于java.object,go.interface{}

Oneof

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}
  • 最终只有一个字段有值,设置多个字段的值,会自动清除已赋值的字段
  • 不支持map、repeated
  • 额外提供检测某个字段是否被被赋值的方法
  • 向后兼容时需要注意,oneof返回值为None/NOT_SET,无法区分是没有设置值,还是因为兼容性问题导致的

map

map<string, Project> projects = 3;
  • key只能是整数和字符串
  • value不能是map
  • 不能使用repeated
  • 不能指定遍历顺序,只能是按照key排序
  • 解析重复key的文件可能会失败
  • 序列化value为空的map item时,C++, Java, Kotlin, and Python 会使用value的默认值,其他语言不会序列化该map item

packages

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

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}
  • 使用package,避免message命名冲突
  • 在go中,生成的文件也是用了上面指定的包名

service

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}
  • 用于RPC

json

json里的空字段转Protocol buffers时,会转成默认值。Protocol buffers里的默认字段转json时会被忽略,但可配置。

proto3 JSON JSON example Notes
message object {"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.
enum string "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 V array [v, …] null is accepted as the empty list [].
bool true, false true, false
string string "Hello World!"
bytes base64 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, uint32 number 1, -10, 0 JSON value will be a decimal number. Either numbers or strings are accepted.
int64, fixed64, uint64 string "1", "-10" JSON value will be a decimal string. Either numbers or strings are accepted.
float, double number 1.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.
Any object {"@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.
Timestamp string "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.
Duration string "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.
Struct object { … } Any JSON object. See struct.proto.
Wrapper types various types 2, "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.
FieldMask string "f.fooBar,h" See field_mask.proto.
ListValue array [foo, bar, …]
Value value Any JSON value. Check google.protobuf.Value for details.
NullValue null JSON null
Empty object {} An empty JSON object

json options

  • 输出默认值的字段
  • 忽略unknown fields:Proto3 JSON parser 默认会报错
  • 转json时使用proto里的字段名,默认会转成小驼峰(标注的proto应该是下划线分隔)
  • 针对枚举,可以输出int值,默认是输出枚举值的name字符串

Options

  • 不同级别的选项:file-level、message-level、field-level、enum types, enum values, oneof fields, service types, and service methods...

  • 级别对应编写的位置

  • 可以自定义option

  • go_packages:指定生成的文件的引用路径,最后一个词作为包名

    option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";
作者:Yuyy
博客:https://yuyy.info
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇