本文是對官方文檔的翻譯,大部分內容都是引用其他一些作者的優質翻譯使文章內容更加通俗易懂(自己是直譯,讀起來有點繞口難理解,本人英文水平有限),參考的文章鏈接在文章末尾
這篇指南描述如何使用protocol buffer語言來組織你的protocol buffer數據,包括.proto文件的語法規則以及如何通過.proto文件來生成數據訪問類代碼。
Defining A Message Type(定義一個消息類型)
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 語法說明(syntax)前只能是空行或者注釋
- 每個字段由字段限制、字段類型、字段名和編號四部分組成
Specifying Field Types(指定字段類型)
在上面的例子中,該消息定義了三個字段,兩個int32類型和一個string類型的字段
Assigning Tags(賦予編號)
消息中的每一個字段都有一個獨一無二的數值類型的編號。1到15使用一個字節編碼,16到2047使用2個字節編碼,所以應該將編號1到15留給頻繁使用的字段。
可以指定的最小的編號為1,最大為2^{29}-1或536,870,911。但是不能使用19000到19999之間的值,這些值是預留給protocol buffer的。
Specifying Field Rules(指定字段限制)
required
:必須賦值的字段optional
:可有可無的字段repeated
:可重復字段(變長字段)
Adding More Message Types(添加更多消息類型)
一個.proto文件可以定義多個消息類型:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
Adding Comments(添加注釋)
.proto
文件也使用C/C++風格的注釋語法//
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.
}
Reserved Fields(預留字段)
如果消息的字段被移除或注釋掉,但是使用者可能重復使用字段編碼,就有可能導致例如數據損壞、隱私漏洞等問題。一種避免此類問題的方法就是指明這些刪除的字段是保留的。如果有用戶使用這些字段的編號,protocol buffer編譯器會發出告警。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
What's Generated From Your .proto?(編譯.proto
文件)
對於C++,每一個.proto
文件經過編譯之后都會對應的生成一個.h
和一個.cc
文件。
Scalar Value Types(類型對照表)
.proto Type | Notes | C++ Type |
---|---|---|
double | double | double |
float | float | float |
int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 |
int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 |
uint32 | Uses variable-length encoding. | uint32 |
uint64 | Uses variable-length encoding. | uint64 |
sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 |
sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 |
fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28 | uint32 |
fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56 | uint64 |
sfixed32 | Always four bytes. | int32 |
sfixed64 | Always eight bytes. | int64 |
bool | bool | boolean |
string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string |
bytes | May contain any arbitrary sequence of bytes. | string |
Default Values(缺省值)
如果沒有指定默認值,則會使用系統默認值,對於string
默認值為空字符串,對於bool
默認值為false,對於數值類型
默認值為0,對於enum
默認值為定義中的第一個元素,對於repeated
默認值為空。
Enumerations(枚舉)
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;
}
通過設置可選參數allow_alias
為true,就可以在枚舉結構中使用別名(兩個值元素值相同)
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
由於枚舉值采用varint編碼,所以為了提高效率,不建議枚舉值取負數。這些枚舉值可以在其他消息定義中重復使用。
Using Other Message Types(使用其他消息類型)
可以使用一個消息的定義作為另一個消息的字段類型。
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
Importing Definitions(導入定義)
就像C++的頭文件一樣,你還可以導入其他的.proto文件
import "myproject/other_protos.proto";
如果想要移動一個.proto
文件,但是又不想修改項目中import
部分的代碼,可以在文件原先位置留一個空.proto
文件,然后使用import public
導入文件移動后的新位置:
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
Nested Types(嵌套類型)
在protocol中可以定義如下的嵌套類型
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果在另外一個消息中需要使用Result
定義,則可以通過Parent.Type
來使用。
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
protocol支持更深層次的嵌套和分組嵌套,但是為了結構清晰起見,不建議使用過深層次的嵌套。
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
Updating A Message Type(更新一個數據類型)
在實際的開發中會存在這樣一種應用場景,既消息格式因為某些需求的變化而不得不進行必要的升級,但是有些使用原有消息格式的應用程序暫時又不能被立刻升級,這便要求我們在升級消息格式時要遵守一定的規則,從而可以保證基於新老消息格式的新老程序同時運行。規則如下:
- 不要修改已經存在字段的標簽號。
- 任何新添加的字段必須是optional和repeated限定符,否則無法保證新老程序在互相傳遞消息時的消息兼容性。
- 在原有的消息中,不能移除已經存在的required字段,optional和repeated類型的字段可以被移除,但是他們之前使用的標簽號必須被保留,不能被新的字段重用。
- int32、uint32、int64、uint64和bool等類型之間是兼容的,sint32和sint64是兼容的,string和bytes是兼容的,fixed32和sfixed32,以及fixed64和sfixed64之間是兼容的,這意味着如果想修改原有字段的類型時,為了保證兼容性,只能將其修改為與其原有類型兼容的類型,否則就將打破新老消息格式的兼容性。
- optional和repeated限定符也是相互兼容的。
Any(任意消息類型)
Any
類型是一種不需要在.proto
文件中定義就可以直接使用的消息類型,使用前import google/protobuf/any.proto
文件即可。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
C++使用PackFrom()
和UnpackTo()
方法來打包和解包Any
類型消息。
// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const Any& detail : status.details()) { if (detail.Is<NetworkErrorDetails>()) { NetworkErrorDetails network_error;
detail.UnpackTo(&network_error); ... processing network_error ... }
}
Oneof(其中一個字段類型)
有點類似C++中的聯合,就是消息中的多個字段類型在同一時刻只有一個字段會被使用,使用case()
或WhichOneof()
方法來檢測哪個字段被使用了。
Using Oneof(使用Oneof)
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
你可以添加除repeated
外任意類型的字段到Oneof
定義中
Oneof Features(Oneof特性)
- oneof字段只有最后被設置的字段才有效,即后面的set操作會覆蓋前面的set操作
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
- oneof不可以是
repeated
的 - 反射API可以作用於oneof字段
- 如果使用C++要防止內存泄露,即后面的set操作會覆蓋之前的set操作,導致前面設置的字段對象發生析構,要注意字段對象的指針操作
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // Will delete sub_message
sub_message->set_... // Crashes her
- 如果使用C++的
Swap()
方法交換兩條oneof消息,兩條消息都不會保存之前的字段
SampleMessage msg1;
msg1.set_name("name");
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK(msg2.has_name());
Backwards-compatibility issues(向后兼容)
添加或刪除oneof
字段的時候要注意,如果檢測到oneof
字段的返回值是None
/NOT_SET
,這意味着oneof
沒有被設置或者設置了一個不同版本的oneof
的字段,但是沒有辦法能夠區分這兩種情況,因為沒有辦法確認一個未知的字段是否是一個oneof
的成員。
Tag Reuse Issues(編號復用問題)
- 刪除或添加字段到oneof:在消息序列化或解析后會丟失一些信息,一些字段將被清空
- 刪除一個字段然后重新添加:在消息序列化或解析后會清除當前設置的oneof字段
- 分割或合並字段:同普通的刪除字段操作
Maps(表映射)
protocol buffers提供了簡介的語法來實現map類型:
map<key_type, value_type> map_field = N;
key_type
可以是除浮點指針或bytes
外的其他基本類型,value_type
可以是任意類型
map<string, Project> projects = 3;
- Map的字段不可以是重復的(repeated)
- 線性順序和map值的的迭代順序是未定義的,所以不能期待map的元素是有序的
- maps可以通過key來排序,數值類型的key通過比較數值進行排序
- 線性解析或者合並的時候,如果出現重復的key值,最后一個key將被使用。從文本格式來解析map,如果出現重復key值則解析失敗。
Backwards compatibility(向后兼容)
map語法下面的表達方式在線性上是等價的,所以即使protocol buffers沒有實現maps數據結構也不會影響數據的處理:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
包
類似C++的命名空間,用來防止名稱沖突
package foo.bar;
message Open { ... }
你可以使用包說明符來定義你的消息字段:
message Foo {
...
foo.bar.Open open = 1;
...
}
定義服務
如果想在RPC系統中使用消息類型,就需要在.proto
文件中定義RPC服務接口,然后使用編譯器生成對應語言的存根。
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
JSON映射
Proto3支持JSON格式的編碼。編碼后的JSON數據的如果沒有值或值為空,解析時protocol buffer將會使用默認值,在對JSON編碼時可以節省空間。
proto3 | JSON | JSON example | Notes |
---|---|---|---|
message | object | {"fBar": v, "g": null, …} | Generates JSON objects. Message field names are mapped to lowerCamelCase and become JSON object keys. null is accepted 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. |
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+" | |
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. |
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,<wbr style="box-sizing: inherit;"> "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. |
Duration | string | "1.000340012s", "1s" | Generated output always contains 0, 3, 6, or 9 fractional digits, depending on required precision. Accepted are any fractional digits (also none) as long as they fit into nano-seconds precision. |
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 fieldmask.proto. |
ListValue | array | [foo, bar, …] | |
Value | value | Any JSON value | |
NullValue | null | JSON null |
選項
Protocol Buffer允許我們在.proto文件中定義一些常用的選項,這樣可以指示Protocol Buffer編譯器幫助我們生成更為匹配的目標語言代碼。Protocol Buffer內置的選項被分為以下三個級別:
文件級別,這樣的選項將影響當前文件中定義的所有消息和枚舉。
消息級別,這樣的選項僅影響某個消息及其包含的所有字段。
字段級別,這樣的選項僅僅響應與其相關的字段。
下面將給出一些常用的Protocol Buffer選項。
optimize_for
(文件選項):可以設置的值有SPEED
、CODE_SIZE
或LITE_RUNTIME
,不同的選項會以下述方式影響C++代碼的生成(option optimize_for = CODE_SIZE;
)。
SPEED (default): protocol buffer編譯器將會生成序列化,語法分析和其他高效操作消息類型的方式.這也是最高的優化選項.確定是生成的代碼比較大.
CODE_SIZE: protocol buffer編譯器將會生成最小的類,確定是比SPEED運行要慢
LITE_RUNTIME: protocol buffer編譯器將會生成只依賴"lite" runtime library (libprotobuf-lite instead of libprotobuf)的類. lite運行時庫比整個庫更小但是刪除了例如descriptors 和 reflection等特性. 這個選項通常用於手機平台的優化.
cc_enable_arenas
(文件選項):生成的C++代碼啟用arena allocation內存管理deprecated
(文件選項):
參考資料
Protocol Buffer官方文檔
Protocol Buffer使用簡介
Protocol Buffer技術詳解(語言規范)