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

468 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

- [protobuf](#protobuf)
- [language guideproto3](#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 guideproto3
### 定义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可以是如下类型
<table><tbody><tr><th>Proto Type</th><th>Notes</th></tr><tr><td>double</td><td></td></tr><tr><td>float</td><td></td></tr><tr><td>int32</td><td>Uses variable-length encoding. Inefficient for encoding negative
numbers if your field is likely to have negative values, use sint32
instead.</td></tr><tr><td>int64</td><td>Uses variable-length encoding. Inefficient for encoding negative
numbers if your field is likely to have negative values, use sint64
instead.</td></tr><tr><td>uint32</td><td>Uses variable-length encoding.</td></tr><tr><td>uint64</td><td>Uses variable-length encoding.</td></tr><tr><td>sint32</td><td>Uses variable-length encoding. Signed int value. These more
efficiently encode negative numbers than regular int32s.</td></tr><tr><td>sint64</td><td>Uses variable-length encoding. Signed int value. These more
efficiently encode negative numbers than regular int64s.</td></tr><tr><td>fixed32</td><td>Always four bytes. More efficient than uint32 if values are often
greater than 2<sup>28</sup>.</td></tr><tr><td>fixed64</td><td>Always eight bytes. More efficient than uint64 if values are often
greater than 2<sup>56</sup>.</td></tr><tr><td>sfixed32</td><td>Always four bytes.</td></tr><tr><td>sfixed64</td><td>Always eight bytes.</td></tr><tr><td>bool</td><td></td></tr><tr><td>string</td><td>A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot
be longer than 2<sup>32</sup>.</td></tr><tr><td>bytes</td><td>May contain any arbitrary sequence of bytes no longer than 2<sup>32</sup>.</td></tr></tbody></table>
上述scalar type在各个编程语言中对应的类型如下所示
<table style="width:100%;overflow-x:scroll"><tbody><tr><th>Proto Type</th><th>C++ Type</th><th>Java/Kotlin Type<sup>[1]</sup></th><th>Python Type<sup>[3]</sup></th><th>Go Type</th><th>Ruby Type</th><th>C# Type</th><th>PHP Type</th><th>Dart Type</th><th>Rust Type</th></tr><tr><td>double</td><td>double</td><td>double</td><td>float</td><td>float64</td><td>Float</td><td>double</td><td>float</td><td>double</td><td>f64</td></tr><tr><td>float</td><td>float</td><td>float</td><td>float</td><td>float32</td><td>Float</td><td>float</td><td>float</td><td>double</td><td>f32</td></tr><tr><td>int32</td><td>int32_t</td><td>int</td><td>int</td><td>int32</td><td>Fixnum or Bignum (as required)</td><td>int</td><td>integer</td><td>int</td><td>i32</td></tr><tr><td>int64</td><td>int64_t</td><td>long</td><td>int/long<sup>[4]</sup></td><td>int64</td><td>Bignum</td><td>long</td><td>integer/string<sup>[6]</sup></td><td>Int64</td><td>i64</td></tr><tr><td>uint32</td><td>uint32_t</td><td>int<sup>[2]</sup></td><td>int/long<sup>[4]</sup></td><td>uint32</td><td>Fixnum or Bignum (as required)</td><td>uint</td><td>integer</td><td>int</td><td>u32</td></tr><tr><td>uint64</td><td>uint64_t</td><td>long<sup>[2]</sup></td><td>int/long<sup>[4]</sup></td><td>uint64</td><td>Bignum</td><td>ulong</td><td>integer/string<sup>[6]</sup></td><td>Int64</td><td>u64</td></tr><tr><td>sint32</td><td>int32_t</td><td>int</td><td>int</td><td>int32</td><td>Fixnum or Bignum (as required)</td><td>int</td><td>integer</td><td>int</td><td>i32</td></tr><tr><td>sint64</td><td>int64_t</td><td>long</td><td>int/long<sup>[4]</sup></td><td>int64</td><td>Bignum</td><td>long</td><td>integer/string<sup>[6]</sup></td><td>Int64</td><td>i64</td></tr><tr><td>fixed32</td><td>uint32_t</td><td>int<sup>[2]</sup></td><td>int/long<sup>[4]</sup></td><td>uint32</td><td>Fixnum or Bignum (as required)</td><td>uint</td><td>integer</td><td>int</td><td>u32</td></tr><tr><td>fixed64</td><td>uint64_t</td><td>long<sup>[2]</sup></td><td>int/long<sup>[4]</sup></td><td>uint64</td><td>Bignum</td><td>ulong</td><td>integer/string<sup>[6]</sup></td><td>Int64</td><td>u64</td></tr><tr><td>sfixed32</td><td>int32_t</td><td>int</td><td>int</td><td>int32</td><td>Fixnum or Bignum (as required)</td><td>int</td><td>integer</td><td>int</td><td>i32</td></tr><tr><td>sfixed64</td><td>int64_t</td><td>long</td><td>int/long<sup>[4]</sup></td><td>int64</td><td>Bignum</td><td>long</td><td>integer/string<sup>[6]</sup></td><td>Int64</td><td>i64</td></tr><tr><td>bool</td><td>bool</td><td>boolean</td><td>bool</td><td>bool</td><td>TrueClass/FalseClass</td><td>bool</td><td>boolean</td><td>bool</td><td>bool</td></tr><tr><td>string</td><td>string</td><td>String</td><td>str/unicode<sup>[5]</sup></td><td>string</td><td>String (UTF-8)</td><td>string</td><td>string</td><td>String</td><td>ProtoString</td></tr><tr><td>bytes</td><td>string</td><td>ByteString</td><td>str (Python 2), bytes (Python 3)</td><td>[]byte</td><td>String (ASCII-8BIT)</td><td>ByteString</td><td>string</td><td>List<int></int></td><td>ProtoBytes</td></tr></tbody></table>
### 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 typeclient会取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<key_type, value_type> map_field = N;
```
上述示例中,`key_type`可以是任意整数类型或string类型。`value_type`可以是除了`map`外的任何类型。
创建示例如下所示;
```proto
map<string, Project> 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文件。