個人小站,正在持續整理中,歡迎訪問:http://shitouer.cn
小站博文地址:Google Protocol Buffers 語法指南
推薦閱讀順序,希望給你帶來收獲~
《Google Protocol Buffers 編碼(Encoding)》
1. 概述
前兩篇文章,我們概括介紹《Google Protocol Buffers 概述》以及帶領大家簡單的《Google Protocol Buffers 入門》,接下來,再稍微詳細一點介紹Protocol Buffers書寫語言。該篇文章主要講解如何使用PB語言構建數據,包括.proto文件語法及如果使用.proto文件生成數據存取類。
本篇主要包括:
- 定義一個PB message類型
- 介紹PB 數據類型
- Optional字段及其默認值
- 枚舉類型
- 使用其他Message類型作為filed類型
- 嵌套類型
- 更新Message
2. 定義一個PB message類型
假如現在需要定義搜索請求的message格式,每條message包含三個字段:搜索語句(query string),需要的返回結果頁數(page_number),以及該頁上的結果數。可如下定義.proto文件。
1
2
3
4
5
|
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
|
該message定義聲明三個字段(name/value pairs),每個字段有一個名字和類型。
2.1 聲明字段類型
上例中,所有的字段類型均為標准類型:兩個整型和一個字符串類型。當然,也可以指定復合類型:枚舉類型和其他自定義message類型。
2.2 給字段賦值數字標簽
從上例中可以發現,message中定義的每個字段都有一個唯一的數字標簽。該標簽的作用是在二進制message中唯一標示該字段,一旦定義該字 段的值就不能夠再更改。有一點需要強調:1~15的數字標簽編碼后僅占一個字節(byte),包括數字標簽和字段類型。16~2047的數字標簽占兩個字 節(byte)。因此,1~15的數字標簽應該用於最頻繁出現的元素。設計時要考慮到不要一次用完1~15的標簽,要考慮到將來也可能出現頻繁出現的元 素。
最小的數字標簽是1,最大的數字標簽是2的29次方-1,也即 536,870,911。但是並不是這之間所有的數字標簽你都能用,例如 19000~19999。這個區間的數字標簽就像是java中的保留字一樣,他們是PB的保留數字標簽。如果該區間的數字標簽出現在.proto文件 中,PB編譯器會出錯。
2.3 字段標示符
字段標示符有三個:
message的沒一個字段,都要用如下的三個修飾符(modifier)來聲明:
- required:必須賦值,不能為空,否則該條message會被認為是“uninitialized”。build一個 “uninitialized” message會拋出一個RuntimeException異常,解析一條“uninitialized” message會拋出一條IOException異常。除此之外,“required”字段跟“optional”字段並無差別。
- optional:字段可以賦值,也可以不賦值。假如沒有賦值的話,會被賦上默認值。對於簡單類型,默認值可以自己設定,例如上例的 PhoneNumber中的PhoneType字段。如果沒有自行設定,會被賦上一個系統默認值,數字類型會被賦為0,String類型會被賦為空字符 串,bool類型會被賦為false。對於內置的message,默認值為該message的默認實例或者原型,即其內所有字段均為設置。當獲取沒有顯式 設置值的optional字段的值時,就會返回該字段的默認值。
- repeated:該字段可以重復任意次數,包括0次。重復數據的順序將會保存在protocol buffer中,將這個字段想象成一個可以自動設置size的數組就可以了。
由於一些歷史原因,數字類型的repeated字段性能有些不盡人意,但是,PB已經做了改進,但是需要再添加一點改動,即在聲明后添加[packed=true]例如:
1
|
repeated int32 samples =
4
[packed=
true
];
|
Notice:應該格外小心定義Required字段。當因為某原因要把Required字段改為 Optional字段是,會有問題,老版本讀取器會認為消息中沒有該字段不完整,可能會拒絕或者丟棄該字段(Google文檔是這么說的,但是我試了一 下,將required的改為optional的,再用原來required時候的解析代碼去讀,如果字段賦值的話,並不會出錯,但是如果字段未賦值,會 報這樣錯誤:Exception in thread “main” com.google.protobuf.InvalidProtocolBufferException: Message missing required fields:fieldname)。在設計時,盡量將這種驗證放在應用程序端的完成。Google的一些工程師對此也很困惑,他們覺 得,required類型壞處大於好處,應該盡量僅適用optional或者repeated的。但也並不是所有的人都這么想。
2.4 同一.proto文件定義多個message
PB支持同一.proto文件定義多個message。這在需要定義相關message的時候非常有用,例如:除了搜索請求message,還需要定義搜索響應message,可以再同一.proto文件中定義:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
message SearchRequest {
required string query =
1
;
optional int32 page_number =
2
;
optional int32 result_per_page =
3
;
}
message SearchResponse {
...
}
|
2.5 添加評論
使用C/C++風格的注釋 // syntax,如下例子:
1
2
3
4
5
6
7
8
9
|
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;// Which page number do we want?
optional int32 result_per_page = 3;// Number of results to return per page.
}
|
2.6 編譯.proto文件后產生了什么?
用PB 編譯器運行.proto文件后,會按照定義的格式,生成指定語言的一系列代買,這些代碼的功能包括:字段值的getter,setter,序列化message並寫入到輸出流,從輸入流接寫成message等。
對於Java,編譯器生成一個.java文件,該java文件內包含幾個內部類,分別對應.proto文件中定義的message 類型,以及將來用於創建message類實例的Builder類。
3. 標准值類型
.proto Type | Notes | C++ Type | Java Type | Python Type[2] |
---|---|---|---|---|
double | double | double | float | |
float | float | float | float | |
int32 | 使用可變長編碼. 對於負數比較低效,如果負數較多,請使用sint32 | int32 | int | int |
int64 | 使用可變長編碼. 對於負數比較低效,如果負數較多,請使用sint64 | int64 | long | int/long |
uint32 | 使用可變長編碼 | uint32 | int |
int/long |
uint64 | 使用可變長編碼 | uint64 | long |
int/long |
sint32 | 使用可變長編碼. Signed int value. 編碼負數比int32更高效 | int32 | int | int |
sint64 | 使用可變長編碼. Signed int value. 編碼負數比int64更高效 | int64 | long | int/long |
fixed32 | 恆定四個字節。如果數值幾乎總是大於2的28次方,該類型比unit32更高效。 | uint32 | int |
int |
fixed64 | 恆定四個字節。如果數值幾乎總是大於2的56次方,該類型比unit64更高效。 | uint64 | long |
int/long |
sfixed32 | 恆定四個字節 | int32 | int | int |
sfixed64 | 恆定八個字節 | int64 | long | int/long |
bool | bool | boolean | boolean | |
string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode |
bytes | 包含任意數量順序的字節 | string | ByteString | str |
4. Optional字段及其默認值
上面提到,PB允許設置可選字段(optional)。顧名思義,在一條message中,該字段可設值也可不設。假如沒有設置,那么在解析該字段 的時候,會根據該字段類型,給其賦一個類型默認值。除此之外,也可以在定義message格式的時候,就為optional字段設置一個默認值,如下:
1
|
optional int32 result_per_page = 3 [default = 10];
|
假如沒有賦值的話,會被賦上默認值。對於簡單類型,默認值可以自己設定,例如上例的PhoneNumber中的PhoneType字段。如果沒有自 行設定,會被賦上一個系統默認值,數字類型會被賦為0,String類型會被賦為空字符串,bool類型會被賦為false。對於枚舉類型,默認值是枚舉 列表中第一個值。
5. 枚舉類型
在定義message類型的時候,也許會有這樣一種需求:其中的一個字段僅需要包含預定義的若干個值即可。比如,對於每一個搜索請求,現需要增加一 個分類字段,分類包含:UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS or VIDEO。要實現該功能,僅需要增加一個枚舉類型字段。如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = UNIVERSAL];
}
|
還可以給枚舉值設置別名,僅需將相同的數字標簽設置給不同的名稱即可。這里,必須得設置allow_alias為true,否則PB編譯器會報錯。
1
2
3
4
5
6
7
8
9
10
11
12
|
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.
}
|
可以定義枚舉在一個message內部,如上例。也可以定義在message的外部,這樣的枚舉可以被其他任何.proto文件內的message復用。
6. 使用其他Message類型作為filed類型
PB允許使用message類型作為filed類型。例如,在搜索相應message中,包含一個結果message。此時,只需要定義一個結果 message,然后再.proto文件中,在搜索結果message中新增一個字段,該字段的類型設置為結果message即可。如下:
1
2
3
4
5
6
7
8
9
|
message SearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
|
6.1 導入定義
在上例中,Result message類型與SearchResponse 定義在同一個文件中,假如有這么一種情況,這里所要使用的Resultmessage已經在其他的.proto文件中定義了呢?
可以通過導入其他.proto文件來使用其內的定義。為達此目的,需要在現.proto文件前增加一條import語句:
1
|
import "myproject/other_protos.proto";
|
7. 嵌套類型
PB支持message內嵌套message,如下例子中,Result message 定義在了SearchResponse內:
1
2
3
4
5
6
7
8
|
message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
|
如果想要在父Message外復用該message的話,可以用Parent.Type格式來引用。
1
2
3
|
message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}
|
PB支持無限深層次的message嵌套:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
required int64 ival = 1;
optional bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
required int32 ival = 1;
optional bool booly = 2;
}
}
}
|
8. 更新Message類型
如果現有message類型不能在滿足業務需求,例如,需要新增一個字段,但是我們卻希望依然能夠使用原來的.proto生成的代碼。完全沒有問題,僅需記住如下規則:
- 千萬不要修改現有字段后邊的數值標簽
- 只能新增optional或者repeated字段
- 可以刪除非必須字段,但是他們的數字標簽不能再被使用。最好的方法是不刪除,而是修改名字,比如在前綴上加OBSOLETE_,這樣就可以避免后人盡量少的出錯。
- 非required字段可以轉化成extension字段,反之亦然,同時保留原類型和數字標簽
- int32, uint32, int64, uint64, 和bool是兼容的。即這些字段可以相互切換,在代碼處理的時候,不會出錯,但是小心范圍小的數據接收范圍大的數據會發生截斷
- sint32, sint64是相互兼容的,但是不與其他整型類型兼容
- string和bytes是兼容的,因為bytes也是合法的UTF-8
- Embedded messages are compatible with bytes if the bytes contain an encoded version of the message(不知道怎么翻譯了)
- fixed32與 sfixed32兼容, fixed64 與sfixed64兼容
- optional與repeated兼容,也存在數據截斷,假如講一個repeated的序列化后的數據作為輸入給客戶端,客戶端會截取最后一個原子類型的字節。或者,如果是一個message類型的字段的話,合並所有的元素。
- 可以修改字段默認值
9. Package
PB建議在.proto文件開頭添加一個package說明符來避免不同message類型的名字沖突:
1
2
3
|
package foo.bar;
message Open { ... }
|
這樣,就可以使用該package標示符來定義該message類型的字段:
1
2
3
4
5
6
7
8
9
|
message Foo {
...
required foo.bar.Open open = 1;
...
}
|
不同語言,因為添加package標示符,生成的代碼也會有所不同,Java中,該package將會被用作java文件的package。如果不想這樣的話,也可在.proto文件中顯式指明package,該字段是:java_package。
譯自:https://developers.google.com/protocol-buffers/docs/proto
說實話,翻譯下來整個文章非常辛苦,而且都要敲代碼去親自試驗能否通過,所以如果您想轉載,非常歡迎,但請注明出處,也算是對俺辛苦的尊重~