1. 程式人生 > >Protocol Buffer學習教程之語法手冊(二)

Protocol Buffer學習教程之語法手冊(二)

1.說明

此嚮導介紹如何使用protocol buffer language建立一個自己的protocolbuffer檔案,包括語法與如何通過“.proto”檔案生成資料訪問的類,此處只介紹proto2proto3的更多訊息點這裡

這是一個參考指南,一步一步功能描述的示例,請訪問以下連結,並選擇你自己熟悉的開發語言

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區間內的編號佔用一個位元組,在

16~2047區間用兩個位元組,所以,你可以保留1~15的編號給那些比較常用的元素使用,併為將來可能要增加的段預留一些此區間的編號。

最小編號為1,最大編號為229- 1,或者536,870,911,但是19000 ~ 19999區間的編號是保留給Protocol Buffers使用的。

修飾符

required: 一個格式完好的訊息必須最少有一個這種型別的段。被這種修飾符修飾的段,是必須賦值的,否則會被認為“未初始化”,如果未賦值,在debug版本序列化時會丟擲斷言錯誤,release版本能順利通過,但是反序列化(解析)時,必然會失敗的。除此之外,requiredoptional修飾型別就沒有什麼區別了。

optional: 一個格式完好的訊息有N(N0)個這種型別的段。對於此欄位的賦值,不是必須的。如果沒有賦值,它將使用預設值,對於預設資料型別,你可以指定預設值,如虛擬碼中的phone number,如果沒有指定預設值,將使用系統預設值,數字型別為0string為空,bool型為false。對於巢狀型別,預設值為“預設例項”或“原型”。

repeated: 欄位會出現N(0)次,重複的值將按順序儲存在“protocol buffer”中,你只要把它當成一個動態陣列即可。

由於歷史原因,repeated修飾的段的資料型別如果是數字型別的話,不能高效編碼,為提高效果可以使用一個選項[packed=true]來獲得更高的效率,示例如:

repeated int32samples = 4 [packed=true];

關於packed參見這裡

Required 是永久的,使用此種修飾符時,要特別小心,當你不想給此種類型的欄位賦值的話,你需要把它改成Optional型別,它可能會出現一些問題----接受方可能會認為此訊息是非完事的,而拒絕解析。有些google開發者認為required利大於弊,所以他們更喜歡使用optionalrepeated。當然,這種觀點不一定是普遍的。

可以在一個“.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為空,foolsfalse,numberic0enmums為列舉中的第一個值。

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.更新訊息

如果一個訊息因為需求需要進行修改時,只要遵循以下規則,它就可以不影響原來的程式碼的基礎上進行升級修改。

不要修改已經存在的段的編號。

在舊訊息中,新增段時,最好使用optionalrepeated修飾符,這樣那些舊的訊息格式檔案也能新版本的訊息,只要required元素都被賦值了的話。應該給新增的元素加上預設值,這樣新定義的訊息格式將能與舊的訊息格式進行一定的互動,同理,新的訊息資料能被舊的訊息資料解析。

對於舊訊息來說,新增的段是不可識別的,但是新增的段並不會被丟棄,並能被序列化,如果被新的訊息解析的話,它將正確解析。

required段是可以被刪除的,只要它的編號不會再被使用。如果你想對它重新命名,請加上"OBSOLETE_"字首,或者直接對這個編號設定成reserved,這樣就能避免你的“.proto”檔案的使用者使用它。

一個非required欄位可以轉化為一個擴充套件(extension),反之亦然(擴充套件可以轉化為一個非required欄位),只要編號與型別不變。

int32,uint32,int64,uint64,and bool這些型別是可以相互轉換的,同時還能保證向前向後相容。解析的時候,如果不符的話,它將像C++裡的強制轉換一樣(64位整數,被強制轉換成32)

sint32 sint64 是相互相容的,但是與其他型別的整形不相容。

stringbytes是相互相容的,只要bytes是有效的UTF-8編碼。

巢狀訊息與bytes是相容的,只要bytes包含該訊息已經編碼過的版本。

fixed32sfixed32相容,同時ixed64sfixed64相容。

optionalrepeated相容,如果一個序列化的資料串,被使用者預判為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()

關於擴充套件段的更多資訊,請參考你選擇的對應語言的程式碼生成手冊,擴充套件段可以是任何資料型別的段,除oneofsmap外。

巢狀擴充套件

可以宣告一個擴充套件,在其他訊息型別裡面:

message Baz {
  extend Foo {
    optional int32 bar = 126;
  }
  ...
}

C++中,訪問擴充套件示例如下:

Foo foo;
foo.SetExtension(Baz::bar, 15);

換句話說,這唯一能說明的是,foo擴充套件定義在bar訊息裡面

這一般是引起混淆的根源:宣告一個擴充套件塊,並巢狀在一個訊息裡,同時此訊息與擴充套件並沒有任何關係。以上示例並沒有表明barzFoo的子型別,唯一隻表示了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;
}

max229 - 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段有相同的getterssetters方法,同時有特定的方法用於判斷哪個段被賦了值,更多關於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修飾

反射APIsoneof有效

如果你使用的是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, msg1will have a sub_messageand msg2will 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 optionalfields.

MapC++中的對映,以下是定義對映型別的語法:

map<key_type, value_type> map_field = N;

key_type可以是整數字符串型別,value_type可以為任何型別,如定義prOjects的對映表,鍵為string,如例:

map<string,Project> projects =3;

生成的APIproto2版本全支援,更詳細的訊息參見連線

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檔案中定義的所有型別名。對於不同語言的程式碼生成器會知道如何來指向每個具體的型別,即使它們使用了不同的規則。