- [protobuf](#protobuf) - [language guide(proto3)](#language-guideproto3) - [定义message type](#定义message-type) - [Assign Field Numbers](#assign-field-numbers) - [重复使用filed number的后果](#重复使用filed-number的后果) - [指定字段基数](#指定字段基数) - [Singular](#singular) - [repeated](#repeated) - [map](#map) - [Message Type Files Always have Field Presence](#message-type-files-always-have-field-presence) - [well-formed messages](#well-formed-messages) - [在相同`.proto`中定义多个message type](#在相同proto中定义多个message-type) - [删除Fields](#删除fields) - [reversed field number](#reversed-field-number) - [reversed field names](#reversed-field-names) - [what generated from `.proto`](#what-generated-from-proto) - [Scalar Value Types](#scalar-value-types) - [default field values](#default-field-values) - [enumerations](#enumerations) - [enum value alias](#enum-value-alias) - [修改message Type需要遵循的原则](#修改message-type需要遵循的原则) - [Unknown Fields](#unknown-fields) - [unknown fields丢失](#unknown-fields丢失) - [Any](#any) - [OneOf](#oneof) - [OneOf Feature](#oneof-feature) - [向后兼容问题](#向后兼容问题) - [Maps](#maps) - [map feature](#map-feature) - [向后兼容性](#向后兼容性) - [package](#package) - [Service In Rpc System](#service-in-rpc-system) - [gRPC](#grpc) - [json](#json) - [Options](#options) - [消息级别](#消息级别) - [java\_package (file option)](#java_package-file-option) - [java\_outer\_class\_name (file option)](#java_outer_class_name-file-option) - [java\_multiple\_files (file option)](#java_multiple_files-file-option) - [optimize\_for (file option)](#optimize_for-file-option) - [deprecated (field option)](#deprecated-field-option) - [生成代码](#生成代码) # protobuf ## language guide(proto3) ### 定义message type 如下为一个定义search request message format的示例, ```proto 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相兼容 > `optional`和`implicit`的区别是,如果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。 例如,如下示例中定义的`Message2`和`Message3`对所有的语言都会生成相同的code,并且在binary json、text format格式下如下两种定义的数据展示都不会有任何区别 ```proto 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,示例如下所示: ```proto 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`列表中,示例如下: ```proto 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示例如下: ```proto message Foo { reversed 2, 15, 9 to 11; reversed "foo", "bar"; } ``` > 上述示例中,将field numbers和field names分为了两个`reversed`语句,实际上,可以在同一行`reversed`语句中包含它们 ### what generated from `.proto` 当针对`.proto`文件运行`protocol buffer compiler`时,compiler将会生成`所选中编程语言`对应的`和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,其默认值为`empty`(empty list)。 对于map fields,其默认值为emtpy(empty map)。 > #### implicit-presence > 对于implicit presence scalar fields, 当消息被反序列化后,没有方法区分`该field是被显式设置为default value`还是`该field根本未被设置`。 ### enumerations 在proto中定义枚举的示例如下: ```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示例如下: ```proto 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 A`和`service 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`相互兼容,`fixed64`和`sfixed64`相互兼容 - 对于`string`, `bytes`,message fields, singular和`repeated`能够相互兼容。 - 假如被序列化的数据中包含repeated field,并且client期望该field为singluar,那么在反序列化时 - 若field为primitive type,client会取repeated field的最后一个值 - 如果field为message type,那么client会对所有的field element进行merge操作 - `enum`和`int32, 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来拷贝消息,例如`CopyFrom`或`MergeFrom`,不要使用field-by-field的拷贝方式 ### Any any的使用类似于泛型,允许在使用嵌套类型时无需声明其`.proto`定义,`Any`将会包含如下内容 - 任意被序列化为bytes的消息 - 一个唯一标识消息类型的url,用于对消息的反序列化 为了使用Any类型,需要`import google/protobuf/any.proto`,示例如下: ```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使用如下所示: ```protobuf message SampleMessage { oneof test_oneof { string name = 4; SubMessage sub_message = 9; } } ``` 在使用oneof field时,可以向oneof中添加任意类型的field,除了`repeated`和`map`。 #### OneOf Feature - 当parser对数据进行转换时,如果存在多个oneof member被设置,那么只有最后一个oneof member才会被使用 - oneof不能为repeated - 反射api对oneof field也适用 #### 向后兼容问题 在向oneof中添加field时应该要注意,如果oneof的值返回`None/NOT_SET`,其可能代表`oneof没有被设置`或`其有可能被设置,但是设置的member不包含在旧版本的消息类型中`。 ### Maps 如果想要创建一个map作为data definition的一部分,可以使用如下语法: ```proto map map_field = N; ``` 上述示例中,`key_type`可以是任意整数类型或string类型。`value_type`可以是除了`map`外的任何类型。 创建示例如下所示; ```proto map projects = 3; ``` #### map feature - map fields 不能被`repeated`修饰 - map中值的遍历顺序以及值在format中的顺序是未定义的 - 在merge或反序列化过程中,如果存在多个相同的key,那么最后出现的key将会被使用 #### 向后兼容性 map synatx声明等价于如下声明: ```proto 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冲突,示例如下: ```proto package foo.bar; message Open { ... } ``` ```proto 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,示例如下: ```proto 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。 使用示例如下: ```proto option java_package = "com.example.foo"; ``` #### java_outer_class_name (file option) 生成class文件的outer classname。如果该选项没有指定,默认为`proto`文件的文件名,使用示例如下: ```proto option java_outer_classname = "Ponycopter"; ``` #### java_multiple_files (file option) 如果该选项被指定为false,那么所有`.proto`文件产生的内容都会被嵌套在outer class中。 如果选项被指定为true,那么会为每个message type单独生成一个java文件。 该选项默认为false,使用示例如下: ```proto 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 使用示例如下所示: ```proto option optimize_for = CODE_SIZE; ``` #### deprecated (field option) 如果该option为true,代表该field已经被废弃并不应该被新代码使用。在java中,其代表生成的代码中field将会被标注为`@Deprecated`。 使用示例如下所示: ```proto int32 old_field = 6 [deprecated = true]; ``` ### 生成代码 在安装完protocol buffer compiler后,可以通过运行`protoc`命令来生成类文件,示例如下: ```bash 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文件。