protocol-buffer3语言指南-01

语言指南

这份指南描述如何使用protocol buffer语言来构建你的protocol buffer数据,包括.proto文件语法和如何从.proto文件生成数据访问类. 覆盖protocol buffers语言的proto3版本

拆分之后的章节列表:

  • 定义消息类型
  • Scalar值类型
  • 默认值
  • 枚举
  • 使用其它消息类型
  • 内嵌类型
  • 更新消息类型
  • Any
  • Oneof
  • Maps
  • 定义服务
  • JSON映射
  • 选项
  • 生成类

定义消息类型

首先我们来看一个非常简单的例子. 让我们假设你想定义一个搜索请求消息格式, 每个搜索请求有一个查询字符串, 搜索结果的第几页/每页结果数量. 这里是你用来定义消息类型的.proto文件:

syntax = "proto3";

option go_package = "./;proto";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  1. 第一行指明当前使用的是proto3语法,如果不指定默认为proto2,必须是.proto文件的除空行和注释内容之外的第一行
  2. SearchRequest消息定义指明了3个字段,每个字段有名字和类型

定义字段类型

上面例子中, 所有字段都是Scalar Types/简单类型(下面会详细讲述), 包括两个int和一个string. 此外还可以支持复合类型包括枚举类型.

指定标签

可以看到消息定义的每个字段都有一个唯一的数字型标签. 这个标签用于在消息的二进制格式中标识字段, 一旦消息类型被使用后不可以再修改.

注意标签的值在1和15之间时编码只需一个字节, 包括标识值和字段类型(可以在Protocol Buffer Encoding中找到更多信息). 标签在16到2047之间将占用两个字节. 因此应该将从1到15的标签分派给最频繁出现的消息元素. 记得保留一些空间给未来可能添加的频繁出现的元素.

可以指定的最小的标签值是1, 最大的是2的29次方-1, 即536870911. 另外19000到19999(FieldDescriptor::kFirstReservedNumber through FieldDescriptor::kLastReservedNumber)不能使用, 因为Protocol Buffers的实现自己保留这个标签段.

指定字段规则

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

  1. 单数:一个定义良好的消息可以有0个或1个此字段(但是不能超过1个)
  2. 重复:这个字段可以在定义良好的消息中重复任意次(包括0次).重复值的顺序将被维持原状.

由于历史原因, 简单数字类型的重复字段并没有编码为最有效率的方式. 新的代码应该使用特别选项[packed=true]来得到更有效率的编码. 例如:
repeated int32 samples = 4 [packed=true];

添加更多消息类型

可以在单个 .proto 文件中定义多个消息类型. 适用于定义多个相关消息, 例如, 如果你想定义应答消息格式来相应你的SearchResponse消息类型,你应该将它添加到同一个.proto文件.

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  repeated int32 samples = 4 [packed=true];
}
message SearchResponse {
  // ...
}

添加注释

可以使用go风格的”//“语法在.proto文件中添加注释.

保留字段

当你更新消息类型,需要彻底删除一个字段时, 或者注释掉它, 未来的用户在实现他们对这个类型的更新时可以重用这个标签数字. 这将导致言中的问题, 如果他们后来装载同一个.proto文件的老版本, 败落数据冲突, 隐私缺陷以及其他. 为了确保不发生这样的事情, 有一个方法时指明这个要删除的字段为保留字段. 如果未来任何用户试图私用这个字段标识符protocol buffer的编译器就是告警.

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

注意:不能在同一个保留字段声明中混合使用名字和标签数字

.proto可以生成什么?

对.proto文件执行protocol buffer的编译器时, 编辑器会生成你所选语言的代码. 你可以用来处理你在文件中定义的消息类型, 包括读写字段值, 序列化消息到输出流, 和从输入流中解析消息.

  • 对于c++, 编译器为每个.proto文件生成.h和.cc文件, 并为每个在文件中描述的消息类型生成一个类
  • 对于Java, 编译器为每个消息类型生成一个.java文件, 还有用于创建消息实例的Builder类.
  • Python小有不同 - Python编译器为.proto文件中的每个消息类型生成带有静态描述符的模块, 这些模块将和metaclass一起在运行时用来创建必须的Python数据访问类.
  • 对于go:编译器生成一个.pb.go文件, .proto文件中的每个消息类型在这个文件中都有一个类型.
  • 对于Ruby, 编译器生成一个.rb文件, 带用包含消息类型的Ruby模块.
  • 对于JavaNano, 编译器输出类似java但是没有builder类.
  • 对于Objective-C, 编译器从每个.proto文件生成一个pbobjc.h和pbobjc.m文件,文件中描述的每个消息类型都有一个类.
  • 对于C#, 编译器从每个.proto文件生成一个.cs文件,文件中描述的每个消息类型都有一个类.

Scalar值类型

默认值

当消息被解析时, 如果被编码的消息没有包含特定的简单元素, 被解析的对象对应的字段被设置为默认值. 默认值是和类型有关的:

  • 对于strings, 默认值是空字符串(注, 是””, 而不是null)
  • 对于bytes, 默认值是空字节(注, 应该是byte[0], 注意这里也不是null)
  • 对于boolean, 默认值是false.
  • 对于数字类型, 默认值是0.
  • 对于枚举, 默认值是第一个定义的枚举值, 而这个值必须是0.
  • 对于消息字段, 默认值是null.

对于重复字段, 默认值是空(通常都是空列表)

注意: 对于简单字段, 当消息被解析后, 是没有办法知道这个字段到底是有设置值然后恰巧和默认值相同(例如一个boolean设置为false)还是这个字段没有没有设置值而取了默认值. 例如, 不要用一个boolean值然后当设置为false时来切换某些行为, 而你又不希望这个行为默认会发生. 同样请注意: 如果一个简单消息字段被设置为它的默认值, 这个值不会被序列化.

枚举

当你定义消息类型时, 你可能希望某个字段只能有预先定义的多个值中的一个. 例如, 假设你想为每个SearchRequest添加一个corpus字段, 而corpus可以是UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS 或 VIDEO. 你可以简单的添加一个枚举到消息定义, 为每个可能的值定义常量.

在下面的例子中, 我们添加一个名为Corpus的枚举类型, 定义好所有可能的值, 然后添加一个类型为Corpus的字段:

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;
}

如你所见, Corpus 枚举的第一个常量设置到0: 每个枚举定义必须包含一个映射到0的常量作为它的第一个元素. 这是因为:

  • 必须有一个0值, 这样我们才能用0来作为数值默认值.
  • 0值必须是第一个元素, 兼容proto2语法,在proto2中默认值总是第一个枚举值

可以通过将相同值赋值给不同的枚举常量来定义别名. 为此需要设置allow_alias选项为true, 否则当发现别名时protocol编译器会生成错误消息.

enum EnumAllowingAlis {
  option allow_alias = true;  // 如果不加这句话,将会报错
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;
}

枚举常量必须在32位整形的范围内. 由于枚举值使用varint encoding, 负值是效率低下的因此不推荐使用.你可以在消息定义中定义枚举, 如前面例子那样, 或者在外部 - 这些枚举可以在.proto文件的任意消息定义中重用. 你也可以用在一个消息中声明的枚举类型作为别的消息的字段类型, 需要使用语法MessageType.EnumType.

当你运行protocol buffer 编译器处理使用枚举的.proto文件时, 生成的代码将会有java或c++的对应枚举, 对于Python, 在生成的运行时类中会有一个特别的EnumDescriptor用于创建带有整型值的symbolic常量集合.

在反序列过程中, 未被识别的枚举值将被保留在消息中, 但是当消息被反序列号时将会如何表现是和语言有关的. 在支持开放枚举类型可以用定义范围之外的值的语言中, 例如C++和Go, 未知的枚举值被简单保存为它底层整型描述. 在封闭枚举类型的语言例如Java中, 一个枚举的特例用于表示这个未识别的值, 底层整型可以被特殊的访问器访问. 在其他案例中, 如果消息被序列化, 这个未识别的值将和消息一起被序列化.

使用其它消息类型

可以使用其他消息类型作为字段类型. 例如, 假设你想在每个SearchResponse消息中包含Result消息 - 为了做到这点, 你可以在相同的.proto文件中定义Result消息类型然后具体指定SearchResponse中的一个字段为Result类型:

syntax = "proto3";

option go_package = "./;proto";

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

导入定义

在上面的例子中, Result消息类型是定义在和SearchResponse同一个文件中 - 如果你想要作为字段类型使用的消息类型已经在其他的.proto文件中定义了呢?

你可以通过导入来使用来自其他.proto文件的定义. 为了导入其他.proto的定义, 需要在文件的顶端增加导入声明:

参考文档

内嵌类型

可以在消息类型内部定义和使用消息类型,如下面的例子所示 - 这里Result消息被定义在SearchResponse消息内部:

syntax = "proto3";

package hello;
option go_package = "test_go/my_protobuf/hello";

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

如果想在父消息类型之外重用消息类型,可以使用Parent.Type来引用:

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

只要愿意可以内嵌的更深:

message Outer {
  message MiddleAA {
    message Inner {
      int32 ival = 1;
      bool booly = 2;
    }
  }
  message MiddleBB {
    message Inner {
      int32 ival = 1;
      bool booly = 2;
    }
  }
}

更新消息类型

如果一个已有的消息类型不再满足需要- 例如, 想增加额外的字段 - 但是依然希望原有格式创建的代码可以继续使用, 不用担心. 可以很简单的更新消息类型而不用打破现有的代码.仅仅需要记住下面的规则:

  • 不要改动任何现有字段的数字标签

  • 如果添加新的字段时, 使用”老”消息格式系列化后的任何消息都可以被新生成的代码解析. 你需要留意这些元素的默认值以便新的代码可以正确和老代码生成的消息交互. 类似的, 新代码创建的消息可以被老代码解析: 解析时新的字段被简单的忽略. 注意当消息反序列化时未知字段会失效, 因此如果消息被传递给新代码, 新的字段将不再存在(这个行为和proto2不同, 在proto2中未知字段会和消息一起序列化).

  • 字段可以被删除, 但是要求在更新后的消息类型中原来的标签数字不再使用.可以考虑重命名这个字段, 或者添加前缀OBSOLETE_, 或者保留标签, 以便你的.proto文件未来的用户不会不小心重用这个数字.

  • int32, uint32, int64, uint64, 和 bool 是完全兼容的 – 这意味着可以将一个字段的类型从这些类型中的一个修改为另外一个而不会打破向前或者向后兼容.如果一个数字解析时不匹配相应的类型, 那么效果会和在c++里面做类型转换一样(例如64位数字被作为32位整型读取, 将被转换为32位).

  • sint32 和 sint64 是彼此兼容的,但是和其他整型类型不兼容.

  • string 和 bytes 是兼容的, 如果bytes是有效的UTF-8.

  • 嵌入式的消息和bytes兼容, 如果bytes包含这个消息的编码后的内容.

  • fixed32 兼容 sfixed32, 而 fixed64 兼容 sfixed64.

posted @ 2022-09-21 10:28  专职  阅读(93)  评论(0编辑  收藏  举报