標籤:XA 建議 size 欄位 googl 枚舉 理解 project rpc
本文是對官方文檔的翻譯,大部分內容都是引用其他一些作者的優質翻譯使文章內容更加通俗易懂(自己是直譯,讀起來有點繞口難理解,本人英文水平有限),參考的文章連結在文章末尾
這篇指南描述如何使用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.protoimport "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_messagesub_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技術詳解(語言規範)
Protocol Buffers官方文檔(proto3語言指南)