Protocol Buffer學習教程之語法手冊(二)
1.說明
此嚮導介紹如何使用protocol buffer language建立一個自己的protocolbuffer檔案,包括語法與如何通過“.proto”檔案生成資料訪問的類,此處只介紹proto2,proto3的更多訊息點這裡。
這是一個參考指南,一步一步功能描述的示例,請訪問以下連結,並選擇你自己熟悉的開發語言。
2.定義訊息型別
首先我們來看一個簡單的示例,定義一個searchrequest訊息格式,每一個search request有一個query字串,頁碼,每頁結果數量。以下是定義的“.proto”檔案:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
訊息指定了三“段”(“名-值”對),每一段,有修飾符、型別、名稱、編號組成,還會有一些可選項組成,如指定預設值呀,後續章節中會介紹到。
段型別
在以上的示例中,所有的段都指定了型別。你也可以用複合型別,包括列舉與其他訊息型別(protobuffer 定義的型別)。
分配標記
從以上示例中可以看到,每一段都指定了唯一編號“= x”,它用於二進位制格式中標記“段”,當你的訊息型別投入使用後,它們的順序不能改變。編碼時編號在1~15區間內的編號佔用一個位元組,在
最小編號為1,最大編號為229- 1,或者536,870,911,但是19000 ~ 19999區間的編號是保留給Protocol Buffers使用的。
修飾符
required: 一個格式完好的訊息必須最少有一個這種型別的段。被這種修飾符修飾的段,是必須賦值的,否則會被認為“未初始化”,如果未賦值,在debug版本序列化時會丟擲斷言錯誤,release版本能順利通過,但是反序列化(解析)時,必然會失敗的。除此之外,required與optional修飾型別就沒有什麼區別了。
optional: 一個格式完好的訊息有N(N≥0)個這種型別的段。對於此欄位的賦值,不是必須的。如果沒有賦值,它將使用預設值,對於預設資料型別,你可以指定預設值,如虛擬碼中的phone number,如果沒有指定預設值,將使用系統預設值,數字型別為0,string為空,bool型為false。對於巢狀型別,預設值為“預設例項”或“原型”。
repeated: 欄位會出現N(≥0)次,重複的值將按順序儲存在“protocol buffer”中,你只要把它當成一個動態陣列即可。
由於歷史原因,repeated修飾的段的資料型別如果是數字型別的話,不能高效編碼,為提高效果可以使用一個選項[packed=true]來獲得更高的效率,示例如:
repeated int32samples = 4 [packed=true];
關於packed參見這裡。
Required 是永久的,使用此種修飾符時,要特別小心,當你不想給此種類型的欄位賦值的話,你需要把它改成Optional型別,它可能會出現一些問題----接受方可能會認為此訊息是非完事的,而拒絕解析。有些google開發者認為required利大於弊,所以他們更喜歡使用optional與repeated。當然,這種觀點不一定是普遍的。
可以在一個“.proto”檔案定義多個訊息,特別是對那些有相互關聯的訊息,比較適用。如你需要給以上示例的請求訊息加一個響應訊息
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
message SearchResponse {
...
}
關於註釋
“.proto”檔案註釋,使用的是C/C++語法“//”,如下:
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.
}
保留段
如果你要對以前定義的訊息中的段刪除,或者註釋。將來使用者可能會更新他們的訊息,並重新使用這些段,或者他們又使用此訊息的舊版本,這將導致資料損壞,隱性錯誤等問題,有一個辦法可以避免這些問題。把這個刪除的段指定為reserved型別,可以通過它的標誌指定,也可以通過名稱(JSON版本會有問題)指定,指定後使用都如果再使用這些段,將會收到錯誤提醒。使用reserved時,同一行,不能混合使用標誌與名稱。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
通過protocol buffer 編譯器對.proto檔案進行編譯後,能生成你選擇的語言的程式碼。你可以通過此程式碼對你在.proto檔案中描述的資料進行提取、給段賦值、把你打包後的資料序列化成流、把接收到的流反序列化成類例項等操作。
C++:對應每一個.proto檔案生成.h與.cpp檔案。每個訊息將生成一個類。可以通過此連結,找到對應語言的API。
3.資料型別
以下列表中是.proto檔案中資料型別與相應的語言之間的資料型別的對應關係。
.proto Type |
Notes |
C++ Type |
Java Type |
Python Type[2] |
Go Type |
double |
double |
double |
float |
*float64 |
|
float |
float |
float |
float |
*float32 |
|
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 |
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[3] |
*int64 |
uint32 |
Uses variable-length encoding. |
uint32 |
int[1] |
int/long[3] |
*uint32 |
uint64 |
Uses variable-length encoding. |
uint64 |
long[1] |
int/long[3] |
*uint64 |
sint32 |
Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. |
int32 |
int |
int |
*int32 |
sint64 |
Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. |
int64 |
long |
int/long[3] |
*int64 |
fixed32 |
Always four bytes. More efficient than uint32 if values are often greater than 228. |
uint32 |
int[1] |
int |
*uint32 |
fixed64 |
Always eight bytes. More efficient than uint64 if values are often greater than 256. |
uint64 |
long[1] |
int/long[3] |
*uint64 |
sfixed32 |
Always four bytes. |
int32 |
int |
int |
*int32 |
sfixed64 |
Always eight bytes. |
int64 |
long |
int/long[3] |
*int64 |
bool |
bool |
boolean |
bool |
*bool |
|
string |
A string must always contain UTF-8 encoded or 7-bit ASCII text. |
string |
String |
str/unicode[4] |
*string |
bytes |
May contain any arbitrary sequence of bytes. |
string |
ByteString |
str |
[]byte |
關於以上資料型別的編碼方式的詳情,點選這裡。
[1]在Java中, unsigned 32-bit and64-bit被解釋成有符合整形,最高位被描述成符號位。
[2]對段進行賦值時,會執行型別檢查。
[3]64-bit orunsigned 32-bit 整形被在解析時都被解析成long型,可以為int型,如果在設定的時候設定成int型的話。總之,值必須與設定的時候一致。參見[2]。
[4]Pythonstrings將解析為寬字元,同時可以是ASCII,當被指定為ASCII的話(主觀指定)。
4.可選欄位與預設值
訊息中的元素可以指定為optional型別,指此段可以不被賦值,在解析時,沒有被賦值的段將被賦預設值。
預設值可以在欄位描述時指定,如給欄位指定一個為10的預設值
optional int32 result_per_page = 3 [default = 10];
沒有指定預設值的optional型別,解析時將被賦型別相關的預設值。string為空,fools為false,numberic為0,enmums為列舉中的第一個值。
5.列舉型別
列舉型別,大家都懂了,不多說。下面是為訊息加一個Corpus列舉型別,以下是示例,定義的時候的資料型別應該是Corpus,而不是整形哦。
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 option 為 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.
}
個人建議採用,常量名稱加列舉名稱為字首,儘量不要重名,省去一些麻煩。
6.自定義資料型別
大家都應該能看明白,就是訊息型別中定義自定義型別,不過,他們應該在同一個”.proto”檔案下,不然要引入,關於引用見下一章節。
message SearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
不在同一個訊息檔案的訊息,可以通過import相互引用,只要在引用的檔案裡,加上以下語句:
import "myproject/other_protos.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
編譯器將要在你編譯指定的路徑----通過-I=proto_path指定的路徑下搜尋引用檔案,如果沒有指定此引數,將在編譯器所在目錄下搜尋。一般情況下,你需要通過-I=proto_path指定路徑。如下:
proto –I=proto檔案路徑 –cpp_out=proto檔案目錄 proto檔案路徑
可以引用proto3版本的訊息型別到proto2版本,反之亦然,但是,proto2的列舉型別不適用於proto3版本的語法。
6.巢狀型別
訊息是可以巢狀的,當一個訊息需要使用另外一個訊息裡面的訊息時,你可以加上其“父”訊息域即可,示例如一:
message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}
同時你可以進行多層巢狀,如下:
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;
}
}
}
組
注意此功能已經被棄用,在定義一個新的訊息時,不建議再使用它,應該使用巢狀訊息來替代它。組是訊息巢狀中的另一種方法,如在SearchResponse巢狀一個Result訊息:
message SearchResponse {
repeated group Result = 1 {
required string url = 2;
optional string title = 3;
repeated string snippets = 4;
}
}
7.更新訊息
如果一個訊息因為需求需要進行修改時,只要遵循以下規則,它就可以不影響原來的程式碼的基礎上進行升級修改。
不要修改已經存在的段的編號。
在舊訊息中,新增段時,最好使用optional或repeated修飾符,這樣那些舊的訊息格式檔案也能新版本的訊息,只要required元素都被賦值了的話。應該給新增的元素加上預設值,這樣新定義的訊息格式將能與舊的訊息格式進行一定的互動,同理,新的訊息資料能被舊的訊息資料解析。
對於舊訊息來說,新增的段是不可識別的,但是新增的段並不會被丟棄,並能被序列化,如果被新的訊息解析的話,它將正確解析。
非required段是可以被刪除的,只要它的編號不會再被使用。如果你想對它重新命名,請加上"OBSOLETE_"字首,或者直接對這個編號設定成reserved,這樣就能避免你的“.proto”檔案的使用者使用它。
一個非required欄位可以轉化為一個擴充套件(extension),反之亦然(擴充套件可以轉化為一個非required欄位),只要編號與型別不變。
int32,uint32,int64,uint64,and bool這些型別是可以相互轉換的,同時還能保證向前向後相容。解析的時候,如果不符的話,它將像C++裡的強制轉換一樣(64位整數,被強制轉換成32位)。
sint32 和 sint64 是相互相容的,但是與其他型別的整形不相容。
string與bytes是相互相容的,只要bytes是有效的UTF-8編碼。
巢狀訊息與bytes是相容的,只要bytes包含該訊息已經編碼過的版本。
fixed32與sfixed32相容,同時ixed64與sfixed64相容。
optional與repeated相容,如果一個序列化的資料串,被使用者預判為optional型別的話,如果它是預設資料型別的話,將解析最後一個值(repeated型別可能會有很多值),如果是訊息型別(自定義訊息)的話,將被全部解析。
修改一個欄位的預設值一般不會有問題的,接收者收到一個某個段沒有賦值的訊息時,接收者是按自己的訊息版本的預設值給它賦值,而不是傳送者的版本的預設值。
列舉型別與int32,uint32,int64,and uint64型別是相容的,當然如果溢位的話,它可能被截斷,但是,需要注意的是,客戶端(使用者,接收者)對他們會區別對待,當反序列化時,不能識別的列舉常量將被丟棄,並會得到“has…”之類的提示,並返回列舉中第一個常量給它賦值,或者是預設值,如果有指定預設值的話。
8.擴充套件
在訊息中宣告一個號段預留給第三方來定義,其他使用者可以在你指定的這個號段裡定義他們自己的訊息檔案,同時不需要重新編譯原始檔案,如例:
message Foo {
// ...
extensions 100 to 199;
}
也就是說[100, 199]之間的號段被保留為擴充套件所用,如例:
extend Foo {
optional int32 bar = 126;
}
也就是說訊息中多了一個bar的段,當對它進行編碼時,在棧格式上與你重新定義一個這樣的bar欄位是無異的,訪問此擴充套件段的訪問標準段的方式很類似,編譯器給生成了擴充套件段的互動方法,給擴充套件段賦值(C++),如例:
Foo foo; foo.SetExtension(bar,15);
類似的,Foo類還下定義了以下介面:
HasExtension()
,ClearExtension()
,GetExtension()
,MutableExtension()
,
and AddExtension()
關於擴充套件段的更多資訊,請參考你選擇的對應語言的程式碼生成手冊,擴充套件段可以是任何資料型別的段,除oneofs和map外。
巢狀擴充套件
可以宣告一個擴充套件,在其他訊息型別裡面:
message Baz {
extend Foo {
optional int32 bar = 126;
}
...
}
在C++中,訪問擴充套件示例如下:
Foo foo;
foo.SetExtension(Baz::bar, 15);
換句話說,這唯一能說明的是,foo擴充套件定義在bar訊息裡面
這一般是引起混淆的根源:宣告一個擴充套件塊,並巢狀在一個訊息裡,同時此訊息與擴充套件並沒有任何關係。以上示例並沒有表明barz是Foo的子型別,唯一隻表示了bar被宣告在Baz訊息內,它只是一個簡單的靜態成員而已。
一般常用的方法是,把擴充套件定義在擴充套件訊息裡面,如定義一個Baz型別的Foo擴充套件,如例:
message Baz {
extend Foo {
optional Baz foo_ext = 127;
}
...
}
當然,這裡並沒有需求,說要把一個擴充套件定義在某個訊息型別裡面,所以,你可以這樣定義,如例:
message Baz {
...
}
// This can even be in a different file.
extend Foo {
optional Baz foo_baz_ext = 127;
}
事實上這種語法可以比較完美的避免困惑,而上面相互巢狀的語法通常會讓人產生他們之間有子類化的誤解,特別對那些對擴充套件不是很熟悉的使用者。
確保兩個使用者不會在同一個訊息中使用相同編號來擴充套件訊息,不然會因為資料型別不一樣可能引發資料損壞。可以約定擴充套件的編號範圍來解決這個問題(譯者注:但是我也沒有看懂怎麼解決這個問題),如例:
message Foo {
extensions 1000 to max;
}
max為229 - 1,或536,870,911。[19000,19999]號段是保留給ProtocolBuffers實現使用的,此號段不能用。
Oneof其實就是C/C++中的Union共用體,當你某個訊息裡,有很多optional屬性的段,同時他們當中同時最多隻有一個需要賦值的時候,它們可以共用記憶體,此功能叫Oneof。可以給所有的段賦值,但是你給其中一個段賦值時,其他段的值自動被清空,你可以通過case()或WhichOneof()方法來檢查哪個段被賦值,取決於你使用的語言。
Oneof用法
以下是語法,用oneof關鍵字後面跟著oneof型別名,如例:
message SampleMessage{ oneof test_oneof { string name =4;SubMessage sub_message =9;} }
在oneof型別(test_oneof)裡可以加任何資料型別的段,當然不能加任何修飾符。在你生成的程式碼時,oneof段有相同的getters與setters方法,同時有特定的方法用於判斷哪個段被賦了值,更多關於oneof的詳細資料參見這裡。
Oneof功能
給oneof賦值,將清空所有其他段的值,所以當你給它賦幾次值後,最後一次的值將保留
SampleMessage message; message.set_name("name"); CHECK(message.has_name()); message.mutable_sub_message(); // Will clear name field. CHECK(!message.has_name());
If the parser encounters multiple members of the same oneof onthe wire, only the last member seen is used in the parsed message.
oneof不支援擴充套件
oneof不支援repeated修飾
反射APIs對oneof有效
如果你使用的是C++語言,請注意記憶體引起的衝突,如下例中的衝突是因為記憶體已經刪除引起的。
SampleMessage message;SubMessage* sub_message = message.mutable_sub_message(); message.set_name("name");// Will delete sub_message sub_message->set_... // Crashes here
· Again in C++, if you Swap()
two
messages with oneofs, each message will end up with theother’s oneof case: in the example below, msg1
will have a sub_message
and msg2
will
have a name
.
SampleMessage msg1; msg1.set_name("name");SampleMessage msg2; msg2.mutable_sub_message(); msg1.swap(&msg2); CHECK(msg1.has_sub_message()); CHECK(msg2.has_name());
增加或者刪除一個oneof段需要小心,當檢查到返回的值為None/NOT_SET時,它可以是oneof沒有被賦值或者使用了不同版本賦值了,這是沒有辦法分辨的。
當訊息已經序列化或者反序列化後,在oneof中移入或者移出一些optinal段,可以丟失一些資訊(某些段將被清空)
當訊息已經序列化或者反序列化後,刪除或者重新恢復某些段,它可能會清除當前設定的某些段。
· Split or merge oneof: This hassimilar issues to moving regular optional
fields.
Map即C++中的對映,以下是定義對映型別的語法:
map<key_type, value_type> map_field = N;
key_type可以是整數字符串型別,value_type可以為任何型別,如定義prOjects的對映表,鍵為string,如例:
map<string,Project> projects =3;
生成的API在proto2版本全支援,更詳細的訊息參見連線。
Maps功能
不支援擴充套件
不能被repeated
,optional
,
or required
修飾
Wire format ordering and map iteration ordering of map values isundefined, so you cannot rely on your map items being in a particular order.(對於值與鍵的排序並沒有定義,所以不能把你的迭代順序依賴於此)
When generating text format for a .proto
, maps are sorted bykey. Numeric keys are sorted numerically.(通過.proto檔案生成檔案格式時,是按鍵的以數字排序)
When parsing from the wire or when merging, if there areduplicate map keys the last key seen is used. When parsing a map from textformat, parsing will fail if there are duplicate keys.(當反序列化或者融合Map時,如果有重新的key將以最後一個為準,如果通過檔案格式反序列化,如果有重複的鍵,將會失敗)
向後相容
Map也可以通過以下方法來實現,所以protobuf並不保證以後都支援map的語法:
message MapFieldEntry{ key_type key = 1; value_type value = 2; } repeated MapFieldEntry map_field = N;
package為了防止命名衝突的關鍵字,功能與namespace類似。如例:
package foo.bar;
message Open { ... }
定義訊息時,可以通過package名來指定域,如例:
message Foo {
...
required foo.bar.Open open = 1;
...
}
package的效果,依賴於你選擇的語言:
對於C++,產生的類會被包裝在C++的名稱空間中,如上例中的Open會被封裝在 foo::bar空間中;
對於Java,包宣告符會變為java的一個包,除非在.proto檔案中提供了一個明確有java_package;
對於 Python,這個包宣告符是被忽略的,因為Python模組是按照其在檔案系統中的位置進行組織的。
Protocol buffer語言中型別名稱的解析與C++是一致的:首先從最內部開始查詢,依次向外進行,每個包會被看作是其父類包的內部類。當然對於(foo.bar.Baz)這樣以“.”分隔的意味著是從最外圍開始的。ProtocolBuffer編譯器會解析.proto檔案中定義的所有型別名。對於不同語言的程式碼生成器會知道如何來指向每個具體的型別,即使它們使用了不同的規則。