什么是 protocol buffers?

Google 官方文档是这样子介绍的

Protocol buffers 是 Google 的语言中立的, 平台中立的, 可扩展的序列化结构的数据机制, 类似 XML, 但是更小, 更快, 更简单. 你可以一次性定义数据的结构, 然后你可以使用生成的特殊代码来轻松写入和读取你的各种结构化数据, 并且可以使用多种语言来实现.

语言指南 (proto3)

syntax = "proto3";

这表示使用哪个版本的 protocol . 它必须是文件的非空, 非注释的第一行.如果没写, 则编译器会假设为是 proto2

示例

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

分配标签

可以看到上面的例子, 每个字段都有一个唯一的数字标签( unique numbered tag ).

这些标签是用于在二进制格式消息里标识你的字段的, 并且一旦在你的消息类型中使用了, 就不应该再更改.

1~15, 编码时会占用 1 个字节.(包含标识数字以及字段的类型)

16~2047 则会占用 2 个字节.

所以, 你应该保留 1~15 为那些经常出现的消息元素.

数字范围为 1~2^29-1 或 1~536,870,911.

你也不能使用 19000 ~ 19999, 它们是为 Protocol Buffers 实现保留的. 如果你使用了这些, 编译器会报错.

也不能使用 保留字段 (见下)

指定字段规则

消息字段可以是以下之一:

  • 单数
  • repeated

添加更多的消息类型

可以在单一的 .proto 文件定义多个消息类型.

添加注释

C/C++ 风格的注释一样.

保留字段

如果你通过移除一个字段或注释掉一个字段来更新一条消息, 将来的用户可以重用这个标签数字. 如果它们延迟加载旧版本的 .proto 的话, 这可能导致一些问题. 一种防止这不会出现的方式是, 为这些字段标识为 reserved . 编译器会报错, 如果将来任意有人试图使用这些标识.

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

注意, 在 reserved 语句中, 不能混合数字和名字.

数据类型对应

doc

默认值

  • string : 空字符串 “”
  • bytes : 空字节
  • bool : false
  • 数值类型 : 0
  • enum : 首个枚举值, 必须为 0
  • message 字段 : 这是依赖于具体的语言的
  • repeated 字段: 通常是一个空列表

枚举 enum

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. (也出于兼容 proto2 的考虑), 并且必须是 32 位的 integer 范围

你可以通过定义别名来允许同一个 value 赋值给不同的枚举常量.例如

enum EnumAllowingAlias {
  option allow_alias = true;
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;
}

使用其他的 message 类型

message SearchResponse {
  repeated Result results = 1;
}

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

导入定义

import "myproject/other_protos.proto";

通过编译器参数 -I 或 --proto_path 来定义一系列的目录来搜索 .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;
}

更新一个 message 类型

仅记要遵从以下规则

  • 不要改变当前已存在的字段的 数字标签 (numeric tag)
  • 如果添加了新的字段, 任意的旧的 message 序列化格式都可以被新的代码解析.
  • 可以删除字段, 只要标签的数字不会被重用即可.
  • int32, uint32, int64, uint64 以及 bool 是相互兼容的. 这意味着你可以从一个字段的类型修改为其他类型.
  • sint32, sint64 是相互兼容的, 但与其他的 integer 类型是不兼容的.
  • string 和 bytes 是相互兼容的, 只要 bytes 都是有效的 UTF-8
  • 内嵌的 message 是兼容 bytes 的, 如果 bytes 包含一个 message 的编码版本.
  • fixed32 是兼容于 sfixed32, fixed64 是兼容于 sfixed64 的
  • enum 是兼容于 int32, uint32, int64 以及 uint64 (注意, 如果值不匹配则会直接截断)

未知字段

你不应该依赖于 proto3 对未知字段是保留还是丢弃. 对于绝大部分的 Google Protocol buffers 的实现, 未知字段在 proto3 是不可访问的, 并且在反序列化时会被丢弃.

这不同与 proto2, 未知字段总是会被保留和序列化的

Any

Any message 类型允许 你使用它来作为内嵌的类型而不必需要它们的 .proto 定义. Any 包含一个任意的序列化 message 为 bytes 类型.

为了使用该类型, 你需要导入 google/protobuf/any.proto

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

当前处于开发阶段.

Oneof

类似于 C/C++ 中的共同体. 包含许多字段, 但同一时间, 只允许设置一个字段值.

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

它的特点

  • 设置 oneof 字段时, 会自动清除其他成员. 因此, 当你设置了几次 oneof 字段后, 仅最后一次设置的字段仍保留值.
  • 如果解析时, 只有最后一次的解析是可见的.
  • oneof 字段不能是 repeated
  • 反射 api 可以用于 oneof
  • 如果你正使用C++, 请确保你的代码不会导致内存崩溃.

例如

SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name");      // Will delete sub_message
sub_message->set_...            // Crashes here
  • 还是C++, 如果你 Swap() 两个 oneof 的 message , 每一个 message 将结束于另一个的 oneof 的情况, 看以下例子. msg1 将有一个 sub_message 以及 msg2 将有一个 name

    SampleMessage msg1;
    msg1.set_name("name");
    SampleMessage msg2;
    msg2.mutable_sub_message();
    msg1.swap(&msg2);
    CHECK(msg1.has_sub_message());
    CHECK(msg2.has_name());
    

向后兼容的问题

要注意添加或删除 oneof 字段. 如果检查 oneof 的值返回的是 NoneNOT_SET , 它可能表示 oneof 还没有设置或它已经设置到了不同版本的 oneof 字段.

标签重用的问题

  • 移入或移出 oneof 字段: message 序列化和解析之后, 会丢失一些你的信息(一些字段会被清除)
  • 删除一个 oneof 字段 然后再添加它回来: message 序列化和解析之后, 你可能会清除你当前的 oneof 字段集合
  • 分割或合并 oneof : 这跟迁移普通的字段的问题一样

maps

map<string, Project> projects = 3;
  • map 字段不能是 repeated
  • 不能依赖于 map 的值的顺序.
  • 当为 .proto 生成文本格式时, maps 是根据 key 来排序的. 所有 numeric 的 key 是根据数字来排序的.
  • 当合并 maps 时, 重复的 key 仅最后一个可见的 key 会被使用. 当解析为 text 格式时, 如果有重复的 keys 则会失败

向后兼容问题

map 的语法等同于以下写法. 如果 protocol buffers 的实现不支持 maps , 仍可处理人的数据

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;

packages 包

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

然后可以这样子使用


message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

包和名字解析

工作原理类似 C++.

首先, 它会在最内层范围搜索, 然后到下一范围再搜索, 以此类推.

定义服务

如果你想在 RPC 中使用你的 messages 类型. 你可以在 .proto 文件中定义一个 RPC 服务接口, 这样子编译器就会为你的语言生成服务接口了.例如

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

最直接使用 protocol buffers 的RPC系统就是 Google 开发的 gRPC了. 它是一个语言以及平台中立的开源RPC系统.

json 映射

映射的情况, 参见文档

doc

选项 options

选项并不会影响声明, 但它会在特定的上下文中影响它的处理. 选项级别

  • 文件 级别
  • message 级别
  • 字段 级别

最常用的选项:

option java_package = "com.example.foo";

  • java_package (文件级别) 指定 java 的包名. 默认情况下是 proto 里的 package 的名字.
  • java_multiple_files (文件级别) 将顶层的 message 分割为不同的文件.
  • java_outer_classname (文件级别) 指定最外部的Java类名.否则是 proto 文件名(转为驼峰后的)
  • optimize_for (文件级别) 这会影响C++和Java代码生成

SPEED : (默认) 编译器会为序列化, 解析以及其他 message 类型相关的性能操作生成代码. 这是高级优化后的.

CODE_SIZE : 它的目标是生成最小的类. 因此它会生成比 SPEED 更小的代码, 但是操作上会比较慢. 这个模式最适合于那些包含非常多的 .proto 文件的应用, 并且不需要明显的性能的.

LITE_RUNTIME : 这对于运行有限平台的应用比较好, 例如 mobile phones.

  • cc_enable_arenas : (文件级别) 为 C++ 生成的代码开启 arena allocation
  • objc_class_prefix : (文件级别) object c 类前缀
  • deprecated : (字段级别)

生成你的类

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto

与JSON的映射

所有选项的说明

https://github.com/google/protobuf/blob/master/src/google/protobuf/descriptor.proto

message accessTokenResponse {
    int32 errcode = 1 [json_name="errcode"];
    string errmsg = 2 [json_name="errmsg"];
    string accessToken = 3 [json_name="access_token"];
    int32 expiresIn = 4 [json_name="expires_in"];
}