Protocol Buffers使用指南
一、简介
protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。
Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。
总结下,有以下特点
- 语言无关、平台无关。即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台
- 高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单
- 扩展性、兼容性好。你可以更新数据结构,而不影响和破坏原有的旧程序
二、ProtoBuf相关概念
1、定义消息类型
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 文件的第一行指定您正在使用
proto3
语法:如果不这样做,则协议缓冲区编译器将假定您正在使用proto2
。这必须是文件的第一行非空,非注释行。 - 所述
SearchRequest
消息定义指定了三个字段(名称/值对),一个用于每条数据要在此类型的消息包括。每个字段都有一个名称和类型。
字段类型:
上述我们使用了两个整型和一个字符串,你也可以使用复合类型、枚举、或者其他消息类型
字段编号:
消息定义中的每个字段都有一个唯一的编号。这些字段号用于标识
[消息二进制格式的]
字段,一旦使用了消息类型,就不应更改这些字段号
字段规则:
消息字段可以是以下内容之一:
- 单数:格式正确的邮件可以包含零个或一个此字段(但不能超过一个)。这是proto3语法的默认字段规则。
repeated
:在格式正确的邮件中,此字段可以重复任意次(包括零次)。重复值的顺序将保留。- 在proto3中,
repeated
标量数字类型的字段packed
默认情况下使用编码。
添加注释:
要将注释添加到
.proto
文件中,请使用//
样式和/* ... */
语法。
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want?
int32 result_per_page = 3; // Number of results to return per page.
}
2、标量值类型
下表展示了.proto
文件的类型和自动生成的文件的类型
.proto Type | Notes | C++ Type | Java/Kotlin Type[1] | Python Type[3] | Go Type | Ruby Type | C# Type | PHP Type | Dart Type |
---|---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | double | |
float | float | float | float | float32 | Float | float | float | double | |
int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
uint32 | Uses variable-length encoding. | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
uint64 | Uses variable-length encoding. | uint64 | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 |
sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 228. | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 256. | uint64 | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 |
sfixed32 | Always four bytes. | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sfixed64 | Always eight bytes. | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | |
string | A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232. | string | String | str/unicode[5] | string | String (UTF-8) | string | string | String |
bytes | May contain any arbitrary sequence of bytes no longer than 232. | string | ByteString | str | []byte | String (ASCII-8BIT) | ByteString | string | List |
3、默认值
解析消息时,如果编码的消息不包含特定的单数元素,则已解析对象中的相应字段将设置为该字段的默认值。这些默认值是特定于类型的:
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节。
- 对于布尔值,默认值为false。
- 对于数字类型,默认值为零。
- 对于
[枚举]
,默认值为第一个定义的枚举值,必须为0。 - 对于消息字段,未设置该字段。它的确切值取决于语言。
重复字段的默认值是空的(通常是使用适当语言的空列表)。
4、枚举
消息定义我们也可以使用枚举类型
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;
}
5、嵌套类型
我们也可以使用嵌套类型
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果要在消息类型之外重用此消息,则将其称为_Parent_._Type_
:
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
6、Map
如果要创建关联映射作为数据定义的一部分,则协议缓冲区提供了方便的快捷方式语法:
map<key_type, value_type> map_field = N;
其中key_type
可以是任何整数或字符串类型(不能为枚举),map 的value_type
可以是任何类型的但不能为另一map(也就是说不支持嵌套)。
注意点:
- map字段不能为
repeated
。 - map是无序的,你不能根据它的顺序来处理相关业务
- 如果存在重复的键,则使用最后一个。如果
7、定义服务
如果要将消息类型与RPC(远程过程调用)系统一起使用,则可以在.proto
文件中定义RPC服务接口,并且协议缓冲区编译器将以您选择的语言生成服务接口代码和存根。因此,例如,如果您想使用一种方法来定义RPC服务,该方法接受您的方法SearchRequest
并返回SearchResponse
,则可以在.proto
文件中按以下方式定义它:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
与协议缓冲区一起使用的最简单的RPC系统是gRPC:这是Google开发的与语言和平台无关的开源RPC系统。gRPC与协议缓冲区配合使用特别好,它使您可以
.proto
使用特殊的协议缓冲区编译器插件直接从文件中生成相关的RPC代码。
8、JSON对应
Proto3支持JSON中的规范编码,从而使在系统之间共享数据更加容易。下表中按类型对编码进行了描述。
如果JSON编码的数据中缺少某个值,或者如果该值为
null
,则在解析为协议缓冲区时,它将被解释为适当的[默认值]。
proto3 | JSON | JSON example | Notes |
---|---|---|---|
message | object | {"fooBar": v, "g": null, …} |
Generates JSON objects. Message field names are mapped to lowerCamelCase and become JSON object keys. If the json_name field option is specified, the specified value will be used as the key instead. Parsers accept both the lowerCamelCase name (or the one specified by the json_name option) and the original proto field name. null is an accepted value for all field types and treated as the default value of the corresponding field type. |
enum | string | "FOO_BAR" |
The name of the enum value as specified in proto is used. Parsers accept both enum names and integer values. |
map<K,V> | object | {"k": v, …} |
All keys are converted to strings. |
repeated V | array | [v, …] |
null is accepted as the empty list [] . |
bool | true, false | true, false |
|
string | string | "Hello World!" |
|
bytes | base64 string | "YWJjMTIzIT8kKiYoKSctPUB+" |
JSON value will be the data encoded as a string using standard base64 encoding with paddings. Either standard or URL-safe base64 encoding with/without paddings are accepted. |
int32, fixed32, uint32 | number | 1, -10, 0 |
JSON value will be a decimal number. Either numbers or strings are accepted. |
int64, fixed64, uint64 | string | "1", "-10" |
JSON value will be a decimal string. Either numbers or strings are accepted. |
float, double | number | 1.1, -10.0, 0, "NaN", "Infinity" |
JSON value will be a number or one of the special string values “NaN”, “Infinity”, and “-Infinity”. Either numbers or strings are accepted. Exponent notation is also accepted. -0 is considered equivalent to 0. |
Any | object |
{"@type": "url", "f": v, … } |
If the Any contains a value that has a special JSON mapping, it will be converted as follows: {"@type": xxx, "value": yyy} . Otherwise, the value will be converted into a JSON object, and the "@type" field will be inserted to indicate the actual data type. |
Timestamp | string | "1972-01-01T10:00:20.021Z" |
Uses RFC 3339, where generated output will always be Z-normalized and uses 0, 3, 6 or 9 fractional digits. Offsets other than “Z” are also accepted. |
Duration | string | "1.000340012s", "1s" |
Generated output always contains 0, 3, 6, or 9 fractional digits, depending on required precision, followed by the suffix “s”. Accepted are any fractional digits (also none) as long as they fit into nano-seconds precision and the suffix “s” is required. |
Struct | object |
{ … } |
Any JSON object. See struct.proto . |
Wrapper types | various types | 2, "2", "foo", true, "true", null, 0, … |
Wrappers use the same representation in JSON as the wrapped primitive type, except that null is allowed and preserved during data conversion and transfer. |
FieldMask | string | "f.fooBar,h" |
See field_mask.proto . |
ListValue | array | [foo, bar, …] |
|
Value | value | Any JSON value. Check google.protobuf.Value for details. | |
NullValue | null | JSON null | |
Empty | object | {} |
An empty JSON object |
三、ProtoBuf使用
对 ProtoBuf 的基本概念有了一定了解之后,我们来看看具体该如何使用 ProtoBuf。 第一步,创建 .proto 文件,定义数据结构,如下例1所示:
// 例1: 在 xxx.proto 文件中定义 Example1 message
message Example1 {
optional string stringVal = 1;
optional bytes bytesVal = 2;
message EmbeddedMessage {
int32 int32Val = 1;
string stringVal = 2;
}
optional EmbeddedMessage embeddedExample1 = 3;
repeated int32 repeatedInt32Val = 4;
repeated string repeatedStringVal = 5;
}
message xxx {
// 字段规则:required -> 字段只能也必须出现 1 次
// 字段规则:optional -> 字段可出现 0 次或1次
// 字段规则:repeated -> 字段可出现任意多次(包括 0)
// 类型:int32、int64、sint32、sint64、string、32-bit ....
// 字段编号:0 ~ 536870911(除去 19000 到 19999 之间的数字)
字段规则 类型 名称 = 字段编号;
}
在上例中,我们定义了:
- 类型 string,名为 stringVal 的 optional 可选字段,字段编号为 1,此字段可出现 0 或 1 次
- 类型 bytes,名为 bytesVal 的 optional 可选字段,字段编号为 2,此字段可出现 0 或 1 次
- 类型 EmbeddedMessage(自定义的内嵌 message 类型),名为 embeddedExample1 的 optional 可选字段,字段编号为 3,此字段可出现 0 或 1 次
- 类型 int32,名为 repeatedInt32Val 的 repeated 可重复字段,字段编号为 4,此字段可出现 任意多次(包括 0)
- 类型 string,名为 repeatedStringVal 的 repeated 可重复字段,字段编号为 5,此字段可出现 任意多次(包括 0)
四、根据.proto文件生成代码
编译器命令如下:
protoc --proto_path=IMPORT_PATH --java_out=DST_DIR path/to/file.proto
IMPORT_PATH
指定.proto
解析import
指令时要在其中查找文件的目录。如果省略,则使用当前目录。可以通过--proto_path
多次传递选项来指定多个导入目录。将按顺序搜索它们。-I=_IMPORT_PATH_
可以用作的简写形式--proto_path
。- 您可以提供一个或多个输出指令:
--cpp_out
在中生成C ++代码DST_DIR
。--java_out
在中生成Java代码DST_DIR
。--kotlin_out
在中生成其他Kotlin代码DST_DIR
。--python_out
在中生成Python代码DST_DIR
。--go_out
在中生成Go代码DST_DIR
。--ruby_out
在中生成Ruby代码DST_DIR
。Ruby生成的代码参考即将推出!--objc_out
在中生成Objective-C代码DST_DIR
。objective-c-generated)。--csharp_out
在中生成C#代码DST_DIR
。--php_out
在中生成PHP代码DST_DIR
。为方便起见,如果DST_DIR
结尾为.zip
或.jar
,编译器会将输出写入具有给定名称的单个ZIP格式的存档文件。.jar
根据Java JAR规范的要求,还将为输出提供清单文件。请注意,如果输出归档文件已经存在,它将被覆盖;编译器不够智能,无法将文件添加到现有存档中。
- 您必须提供一个或多个
.proto
文件作为输入。.proto
可以一次指定多个文件。尽管这些文件是相对于当前目录命名的,但是每个文件都必须位于IMPORT_PATH
s之一中,以便编译器可以确定其规范名称。
// $SRC_DIR: .proto 所在的源目录文件夹
// --java_out: $DST_DIR: 生成的 java 代码的目标目录
// xxx.proto: 要针对哪个 proto 文件生成接口代码,如果生成全部把`xxx/proto/Member.proto`改为`xxx/proto/*.proto`
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/xxx.proto
// 如下示例
protoc -I=/Users/jarvan/IdeaProjects/workspace-jarvan/spring-boot-grpc/src/main/proto --java_out=/Users/jarvan/ /Users/jarvan/IdeaProjects/workspace-jarvan/spring-boot-grpc/src/main/proto/Member.proto