Files
rikako-note/protobuf/protobuf.md
2025-03-01 21:41:40 +08:00

24 KiB
Raw Blame History

protobuf

language guideproto3

定义message type

如下为一个定义search request message format的示例

synatx="proto3"

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

上述示例含义如下:

  • synatx = "proto3":
    • 代表当前protobuf的language版本为proto3
    • 如果没有指定syntax那么protocol buffer compiler默认会假设在使用proto2
  • Search Request消息定义了3个fields每个field都代表希望包含在message中的一部分数据

Assign Field Numbers

可以为message中的每个field定义一个整数范围为[1,536,870,911],并有如下约束

  • message中所有field的给定数字必须唯一
  • field number [19,000, 19,999]是为Protocol Buffer实现保留的如果使用这些数字protocol buffer compiler将会报错

一旦消息类型被使用后field number就不能被改变field number代表message wire format中的field。

如果对field的field number进行了修改代表删除旧的field并且新建一个相同类型的field。

filed number不应该被重用。

对于频繁被设置的fields应该将其的field number设置为[1,15]。在wire format中field number的值越小占用空间越小。

例如,[1,15]在编码时只占用1字节[16, 2047]则会占用2字节。

重复使用filed number的后果

如果重复使用field number将会造成解码wire-format message的二义性。

对于protobuf wire format其在编码和解码过程中fields的定义必须一致。

field number被限制为29bit故而field number的最大值为536870911

指定字段基数

在protobuf协议中field可以为如下的集中类型

Singular

在proto3中有两种singular field

  • optional(推荐使用): 一个optional field可能有如下两种状态
    • 如果optional field值被设置那么其将会被序列化到wire
    • 如果optional field值未被设置那么该field将会返回一个默认值并且其不会被序列化到wire中
  • implict(不推荐使用):一个隐式字段没有显式基数标签,并且行为如下:
    • 如果field为一个message type那么其行为和optional相同
    • 如果field不是message那么其有两种状态
      • 如果field被设置为非默认值non-zero其会被序列化到wire中
      • 如果field被设置为默认值那么其不会被序列化到wire中

相比于implict,更推荐使用optional,使用optional能更好与proto2相兼容

optionalimplicit的区别是如果scalar field被设置为默认值optional场景下其会被序列化到wire中implicit则不会对其进行序列化

repeated

代表该field可以在消息中出现0次或多次消息出现的顺序也将被维护

map

代表field为成对的键值对

Message Type Files Always have Field Presence

在proto3中message-type field永远都存在field presence。故而对于message-type field添加optional修饰符并不会改变该field的field presence。

例如,如下示例中定义的Message2Message3对所有的语言都会生成相同的code并且在binary json、text format格式下如下两种定义的数据展示都不会有任何区别

syntax="proto3";

package foo.bar;

message Message1 {}

message Message2 {
  Message1 foo = 1;
}

message Message3 {
  optional Message1 bar = 1;
}

well-formed messages

well-formed来修饰protobuf message其代表被序列化或反序列化的bytes。在对bytes进行转化时protoc parser将会校验是否proto定义文件是可转化的。

对于singular field其可以在wire-format bytes中出现多次parser会接收该输入但是在转化过程中只有field的最后一次出现才有效。

在相同.proto中定义多个message type

可以在相同.proto文件中定义多个message type示例如下所示

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

message SearchResponse {
 ...
}

应尽可能的在每个proto文件中包含较少的message type定义在同一proto问文件中包含过多message type可能会造成依赖的膨胀。

删除Fields

当不再需要某个field并且所有对该field的references都被从client code中移除时可以从message type definition中移除该field。但是该field对应的field number必须被reserved防止field number后续被重用

该field对应的field name也应该被reserved以允许按json或text-format进行编码的消息你能偶被正常的转换。

reversed field number

在将field注释或删除时将来使用者可能仍会对field number进行重用。为了避免该问题可以将被删除field的field number添加到reversed列表中,示例如下:

message Foo {
  reversed 2, 15, 9 to 11;
}

上述示例中,9 to 11代表9,10,11

reversed field names

对被删除field的field name通常是安全的除非使用TextProc或json的编码格式在使用这些格式时field name也会参与序列化。为了避免该问题可以将deleted field name添加到reversed列表中。

reversed names只会影响protoc compiler的行为并不会对rumtime behavior造成影响但是存在一个例外

  • 在parse过程中TextProto实现会丢弃reversed中包含的未知fields而不会抛出异常
  • runtime json parse过程不会受到reversed names影响

使用reversed names示例如下

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

上述示例中将field numbers和field names分为了两个reversed语句,实际上,可以在同一行reversed语句中包含它们

what generated from .proto

当针对.proto文件运行protocol buffer compilercompiler将会生成所选中编程语言对应的和message type进行交互的代码,生成的交互代码包括如下部分:

  • getting and setting field values
  • serialize message to outputstream
  • parse message from input stream

对于常用变成语言,其生成文件内容如下:

  • java:
    • 对于java其会为每个message type生成其对应的class
    • 除了clas外还会生成对应的Builder类用于生成class实例
  • go:
    • 对于go其会为每个message type生成.pb.go文件

Scalar Value Types

一个scalar message field可以是如下类型

Proto TypeNotes
double
float
int32Uses variable-length encoding. Inefficient for encoding negative numbers if your field is likely to have negative values, use sint32 instead.
int64Uses variable-length encoding. Inefficient for encoding negative numbers if your field is likely to have negative values, use sint64 instead.
uint32Uses variable-length encoding.
uint64Uses variable-length encoding.
sint32Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.
sint64Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.
fixed32Always four bytes. More efficient than uint32 if values are often greater than 228.
fixed64Always eight bytes. More efficient than uint64 if values are often greater than 256.
sfixed32Always four bytes.
sfixed64Always eight bytes.
bool
stringA string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232.
bytesMay contain any arbitrary sequence of bytes no longer than 232.

上述scalar type在各个编程语言中对应的类型如下所示

Proto TypeC++ TypeJava/Kotlin Type[1]Python Type[3]Go TypeRuby TypeC# TypePHP TypeDart TypeRust Type
doubledoubledoublefloatfloat64Floatdoublefloatdoublef64
floatfloatfloatfloatfloat32Floatfloatfloatdoublef32
int32int32_tintintint32Fixnum or Bignum (as required)intintegerinti32
int64int64_tlongint/long[4]int64Bignumlonginteger/string[6]Int64i64
uint32uint32_tint[2]int/long[4]uint32Fixnum or Bignum (as required)uintintegerintu32
uint64uint64_tlong[2]int/long[4]uint64Bignumulonginteger/string[6]Int64u64
sint32int32_tintintint32Fixnum or Bignum (as required)intintegerinti32
sint64int64_tlongint/long[4]int64Bignumlonginteger/string[6]Int64i64
fixed32uint32_tint[2]int/long[4]uint32Fixnum or Bignum (as required)uintintegerintu32
fixed64uint64_tlong[2]int/long[4]uint64Bignumulonginteger/string[6]Int64u64
sfixed32int32_tintintint32Fixnum or Bignum (as required)intintegerinti32
sfixed64int64_tlongint/long[4]int64Bignumlonginteger/string[6]Int64i64
boolboolbooleanboolboolTrueClass/FalseClassboolbooleanboolbool
stringstringStringstr/unicode[5]stringString (UTF-8)stringstringStringProtoString
bytesstringByteStringstr (Python 2), bytes (Python 3)[]byteString (ASCII-8BIT)ByteStringstringListProtoBytes

default field values

当message执行反序列化操作时如果encoded message bytes中并不包含指定的field那么对反序列化后的对象访问field时将会返回一个默认值。

各种类型的默认值都不一样:

  • 对于string类型,默认值为空字符串
  • 对于bytes默认值为empty bytes
  • 对于bool类型默认值为bool
  • 对于numeric types默认值为0
  • 对于message fields默认值为该field没有被设置
  • 对于enum默认值为first defined enum value

对于repeated fields其默认值为emptyempty list

对于map fields其默认值为emtpy(empty map)。

implicit-presence

对于implicit presence scalar fields, 当消息被反序列化后,没有方法区分该field是被显式设置为default value还是该field根本未被设置

enumerations

在proto中定义枚举的示例如下

enum Corpus {
  CORPUS_UNSPECIFIED = 0;
  CORPUS_UNIVERSAL = 1;
  CORPUS_WEB = 2;
  CORPUS_IMAGES = 3;
  CORPUS_LOCAL = 4;
  CORPUS_NEWS = 5;
  CORPUS_PRODUCTS = 6;
  CORPUS_VIDEO = 7;
}

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
  Corpus corpus = 4;
}

在proto3中first value defined in enum其值必须为0并且其名称必须为{ENUM_TYPE_NAME}_UNSPECIFIED{ENUM_TYPE_NAME}_UNKNOWN

enum value alias

当开启allow_alias option时允许为相同的枚举值指定不同的枚举项。在反序列化时所有的alias值都有效但是只有第一个会被用于反序列化。

enum alias示例如下

enum EnumAllowingAlias {
  option allow_alias = true;
  EAA_UNSPECIFIED = 0;
  EAA_STARTED = 1;
  EAA_RUNNING = 1;
  EAA_FINISHED = 2;
}

enum EnumNotAllowingAlias {
  ENAA_UNSPECIFIED = 0;
  ENAA_STARTED = 1;
  // ENAA_RUNNING = 1;  // Uncommenting this line will cause a warning message.
  ENAA_FINISHED = 2;
}

在使用枚举时可以在一个message type中定义枚举然后在另一个message type中使用枚举语法如下 _MessageType_._EnumType_

修改message Type需要遵循的原则

如果旧的message type不再满足需求想要添加新的field且在修改message type后仍想和旧代码保持兼容则必须要遵守如下原则

  • 不要修改field number
  • 在添加新field后使用旧meesage type序列化到的消息格式仍然能被新的message type反序列化。同样的,新的message type序列化的消息同样能被旧的message type定义反序列化
    • 即是,若service Aservice B通过message type进行通信service A为调用方而service B为被调用方,如果service B修改了message type定义向message中添加了field但是serivce A仍然使用的是旧的message定义那么service A使用旧的message type定义序列化的数据仍然能被service B反序列化
    • 同样的,service B使用新的message type定义序列化的新消息数据仍然能被service A反序列化,对于旧的消息定义,其会忽略新添加的字段
  • field可以被删除但是被删除的field其field number不能被重用。
  • int32, uint32, int64, uint64, bool这些类型的值都是兼容的代表可以将field的类型从一个修改为另一个并且不会打破向前或向后兼容。
  • sint32和sint64能够彼此兼容,但是和其他数值类型不相兼容
  • string和bytes能够互相兼容但要求bytes为有效的utf8字符串
  • 嵌套的消息能够和bytes相互兼容但是要求bytes内容为序列化后的message实例
  • fixed32和sfixed32相互兼容,fixed64sfixed64相互兼容
  • 对于string, bytesmessage fields, singular和repeated能够相互兼容。
    • 假如被序列化的数据中包含repeated field并且client期望该field为singluar那么在反序列化时
      • 若field为primitive typeclient会取repeated field的最后一个值
      • 如果field为message type那么client会对所有的field element进行merge操作
  • enumint32, uint32, int64, uint64能够互相兼容

Unknown Fields

如果被序列化的数据中包含parser无法识别的fields时其被称为unknown fields。例如old parser针对new sender发送的数据进行反序列化时如果new sender发送的数据中包含new field那么new field即为unknown field。

proto3会对unknown fields进行保存并在序列化和反序列化时包含它们该行为和proto2一致。

unknown fields丢失

一些行为可能会造成unknown fields丢失示例如下

  • 将消息序列化为json
  • 遍历消息中的field并将其注入给新的message

为了避免unknown fields的丢失遵循如下规则

  • 使用binary格式在数据交换时避免使用text-format
  • 使用message-oriented api来拷贝消息例如CopyFromMergeFrom不要使用field-by-field的拷贝方式

Any

any的使用类似于泛型允许在使用嵌套类型时无需声明其.proto定义,Any将会包含如下内容

  • 任意被序列化为bytes的消息
  • 一个唯一标识消息类型的url用于对消息的反序列化

为了使用Any类型需要import google/protobuf/any.proto,示例如下:

import "google/protobuf/any.proto";

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

对于给定消息类型的默认type url为type.googleapis.com/_packagename_._messagename_.

OneOf

如果消息中包含多个singluar fields并且在同一时刻最多只能有一个被设置则可以使用OneOf特性。

OneOf fields类似于optional fields但是所有oneof fields都保存在oneof共享内存中在同一时间最多只能有一个field被设置。对任一oneof member进行设置都会自动清空其他oneof members

可以通过cause()或WhichoneOf()方法来得知哪一个field被设置。

oneof使用如下所示

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

在使用oneof field时可以向oneof中添加任意类型的field除了repeatedmap

OneOf Feature

  • 当parser对数据进行转换时如果存在多个oneof member被设置那么只有最后一个oneof member才会被使用
  • oneof不能为repeated
  • 反射api对oneof field也适用

向后兼容问题

在向oneof中添加field时应该要注意如果oneof的值返回None/NOT_SET,其可能代表oneof没有被设置其有可能被设置但是设置的member不包含在旧版本的消息类型中

Maps

如果想要创建一个map作为data definition的一部分可以使用如下语法

map<key_type, value_type> map_field = N;

上述示例中,key_type可以是任意整数类型或string类型。value_type可以是除了map外的任何类型。

创建示例如下所示;

map<string, Project> projects = 3;

map feature

  • map fields 不能被repeated修饰
  • map中值的遍历顺序以及值在format中的顺序是未定义的
  • 在merge或反序列化过程中如果存在多个相同的key那么最后出现的key将会被使用

向后兼容性

map synatx声明等价于如下声明

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

repeated MapFieldEntry map_field = N;

故而不支持maps的protocol buffer实现也能够接收map数据。

package

可以在.proto文件中指定一个package name用于避免message type冲突示例如下

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

Service In Rpc System

如果想要在rpc系统中使用message type类型可以在proto文件中定义rpc service interface。protocol buffer compiler将会生成service interface code和stub示例如下

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

gRPC

与protocol buffer一起使用的最简单的rpc系统为gRPC。其是语言和平台中立的,其能够直接通过.proto文件产生rpc代码。

json

binary wire format为protobuf的首选格式但protobuf支持json编码规范。

Options

.proto文件中的声明可以添加options。options并不会改变声明的含义但是会影响特定上下文下对声明的处理。

消息级别

  • 部分option是文件级别的其应当被写在文件作用域的最上方而不应该在message type、enum、service之内
  • 部分option则是message级别的其应该被写在消息中
  • 部分option是field级别的其应该被写在field 定义之内

常用options如下

java_package (file option)

java_package option代表生成classes的package name。如果没有指定该选项会默认使用proto文件的package。

使用示例如下:

option java_package = "com.example.foo";

java_outer_class_name (file option)

生成class文件的outer classname。如果该选项没有指定默认为proto文件的文件名,使用示例如下:

option java_outer_classname = "Ponycopter";

java_multiple_files (file option)

如果该选项被指定为false那么所有.proto文件产生的内容都会被嵌套在outer class中。

如果选项被指定为true那么会为每个message type单独生成一个java文件。

该选项默认为false使用示例如下

option java_multiple_files = true;

optimize_for (file option)

该选项可以被设置为SPEED, CODE_SIZE, LIFE_RUNTIME其将会影响java和c++生成器:

  • SPEED: SPEED为默认值protocol buffer compiler将会生成序列化、反序列化、执行其他常用操作的代码。生成的代码是高度优化的
  • CODE_SIZE: protocol buffer compiler将会生成最小的类代码类代码中依赖共享、反射等操作来实现序列化、反序列化以及其他常用操作指定该选项后生成的代码长度要比SPEED小得多但是操作可能会更慢。
  • LIFE_RUNTIME

使用示例如下所示:

option optimize_for = CODE_SIZE;

deprecated (field option)

如果该option为true代表该field已经被废弃并不应该被新代码使用。在java中其代表生成的代码中field将会被标注为@Deprecated

使用示例如下所示:

 int32 old_field = 6 [deprecated = true];

生成代码

在安装完protocol buffer compiler后可以通过运行protoc命令来生成类文件,示例如下:

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 --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto

其中IMPORT_PATH代表当使用import指令时查找.proto文件的路径,如果省略,将会使用当前目录。如果要指定多个目录时,可以传递多个--proto_path选项。

在指定输入时也可以指定多个proto文件。