Protocol buffer---Protobuf3開發指南

FeelTouch發表於2018-05-13

英文原文: 
Language Guide (proto3) 
中文出處: 
Protobuf語言指南 
[譯]Protobuf 語法指南 
中文出處是proto2的譯文,proto3的英文出現後在原來基礎上增改了,水平有限,還請指正

這個指南描述瞭如何使用Protocol buffer 語言去描述你的protocol buffer 資料, 包括 .proto檔案符號和如何從.proto檔案生成類。包含了proto2版本的protocol buffer語言:對於老版本的proto3 符號,請見Proto2 Language Guide(以及中文譯本,抄了很多這裡的感謝下老版本的翻譯者)

本文是一個參考指南——如果要檢視如何使用本文中描述的多個特性的循序漸進的例子,請在教程中查詢需要的語言的教程。

定義一個訊息型別

先來看一個非常簡單的例子。假設你想定義一個“搜尋請求”的訊息格式,每一個請求含有一個查詢字串、你感興趣的查詢結果所在的頁數,以及每一頁多少條查詢結果。可以採用如下的方式來定義訊息型別的.proto檔案了:

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 檔案的第一行指定了你正在使用proto3語法:如果你沒有指定這個,編譯器會使用proto2。這個指定語法行必須是檔案的非空非註釋的第一個行。
  • SearchRequest訊息格式有3個欄位,在訊息中承載的資料分別對應於每一個欄位。其中每個欄位都有一個名字和一種型別。

指定欄位型別

在上面的例子中,所有欄位都是標量型別:兩個整型(page_number和result_per_page),一個string型別(query)。當然,你也可以為欄位指定其他的合成型別,包括列舉(enumerations)或其他訊息型別。

分配標識號

正如你所見,在訊息定義中,每個欄位都有唯一的一個數字識別符號。這些識別符號是用來在訊息的二進位制格式中識別各個欄位的,一旦開始使用就不能夠再改變。注:[1,15]之內的標識號在編碼的時候會佔用一個位元組。[16,2047]之內的標識號則佔用2個位元組。所以應該為那些頻繁出現的訊息元素保留 [1,15]之內的標識號。切記:要為將來有可能新增的、頻繁出現的標識號預留一些標識號。

最小的標識號可以從1開始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]( (從FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber))的標識號, Protobuf協議實現中對這些進行了預留。如果非要在.proto檔案中使用這些預留標識號,編譯時就會報警。同樣你也不能使用早期保留的標識號。

指定欄位規則

所指定的訊息欄位修飾符必須是如下之一:

  • singular:一個格式良好的訊息應該有0個或者1個這種欄位(但是不能超過1個)。
  • repeated:在一個格式良好的訊息中,這種欄位可以重複任意多次(包括0次)。重複的值的順序會被保留。

    在proto3中,repeated的標量域預設情況蝦使用packed。

    你可以瞭解更多的pakced屬性在Protocol Buffer 編碼

新增更多訊息型別

在一個.proto檔案中可以定義多個訊息型別。在定義多個相關的訊息的時候,這一點特別有用——例如,如果想定義與SearchResponse訊息型別對應的回覆訊息格式的話,你可以將它新增到相同的.proto檔案中,如:

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

message SearchResponse {
 ...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

新增註釋

向.proto檔案新增註釋,可以使用C/C++/java風格的雙斜槓(//) 語法格式,如:

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.
}
  • 1
  • 2
  • 3
  • 4
  • 5

保留識別符號(Reserved)

如果你通過刪除或者註釋所有域,以後的使用者可以重用標識號當你重新更新型別的時候。如果你使用舊版本載入相同的.proto檔案這會導致嚴重的問題,包括資料損壞、隱私錯誤等等。現在有一種確保不會發生這種情況的方法就是指定保留識別符號(and/or names, which can also cause issues for JSON serialization不明白什麼意思),protocol buffer的編譯器會警告未來嘗試使用這些域識別符號的使用者。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}
  • 1
  • 2
  • 3
  • 4

注:不要在同一行reserved宣告中同時宣告域名字和標識號

從.proto檔案生成了什麼?

當用protocol buffer編譯器來執行.proto檔案時,編譯器將生成所選擇語言的程式碼,這些程式碼可以操作在.proto檔案中定義的訊息型別,包括獲取、設定欄位值,將訊息序列化到一個輸出流中,以及從一個輸入流中解析訊息。

  • 對C++來說,編譯器會為每個.proto檔案生成一個.h檔案和一個.cc檔案,.proto檔案中的每一個訊息有一個對應的類。
  • 對Java來說,編譯器為每一個訊息型別生成了一個.java檔案,以及一個特殊的Builder類(該類是用來建立訊息類介面的)。
  • 對Python來說,有點不太一樣——Python編譯器為.proto檔案中的每個訊息型別生成一個含有靜態描述符的模組,,該模組與一個元類(metaclass)在執行時(runtime)被用來建立所需的Python資料訪問類。
  • 對go來說,編譯器會位每個訊息型別生成了一個.pd.go檔案。
  • 對於Ruby來說,編譯器會為每個訊息型別生成了一個.rb檔案。
  • javaNano來說,編譯器輸出類似域java但是沒有Builder類
  • 對於Objective-C來說,編譯器會為每個訊息型別生成了一個pbobjc.h檔案和pbobjcm檔案,.proto檔案中的每一個訊息有一個對應的類。
  • 對於C#來說,編譯器會為每個訊息型別生成了一個.cs檔案,.proto檔案中的每一個訊息有一個對應的類。

你可以從如下的文件連結中獲取每種語言更多API(proto3版本的內容很快就公佈)。API Reference

標量數值型別

一個標量訊息欄位可以含有一個如下的型別——該表格展示了定義於.proto檔案中的型別,以及與之對應的、在自動生成的訪問類中定義的型別:

.proto TypeNotesC++ TypeJava TypePython Type[2]Go TypeRuby TypeC# TypePHP Type
double doubledoublefloatfloat64Floatdoublefloat
float floatfloatfloatfloat32Floatfloatfloat
int32使用變長編碼,對於負值的效率很低,如果你的域有可能有負值,請使用sint64替代int32intintint32Fixnum 或者 Bignum(根據需要)intinteger
uint32使用變長編碼uint32intint/longuint32Fixnum 或者 Bignum(根據需要)uintinteger
uint64使用變長編碼uint64longint/longuint64Bignumulonginteger/string
sint32使用變長編碼,這些編碼在負值時比int32高效的多int32intintint32Fixnum 或者 Bignum(根據需要)intinteger
sint64使用變長編碼,有符號的整型值。編碼時比通常的int64高效。int64longint/longint64Bignumlonginteger/string
fixed32總是4個位元組,如果數值總是比總是比228大的話,這個型別會比uint32高效。uint32intintuint32Fixnum 或者 Bignum(根據需要)uintinteger
fixed64總是8個位元組,如果數值總是比總是比256大的話,這個型別會比uint64高效。uint64longint/longuint64Bignumulonginteger/string
sfixed32總是4個位元組int32intintint32Fixnum 或者 Bignum(根據需要)intinteger
sfixed64總是8個位元組int64longint/longint64Bignumlonginteger/string
bool boolbooleanboolboolTrueClass/FalseClassboolboolean
string一個字串必須是UTF-8編碼或者7-bit ASCII編碼的文字。stringStringstr/unicodestringString (UTF-8)stringstring
bytes可能包含任意順序的位元組資料。stringByteStringstr[]byteString (ASCII-8BIT)ByteStringstring

你可以在文章Protocol Buffer 編碼中,找到更多“序列化訊息時各種型別如何編碼”的資訊。

  1. 在java中,無符號32位和64位整型被表示成他們的整型對應形似,最高位被儲存在標誌位中。
  2. 對於所有的情況,設定值會執行型別檢查以確保此值是有效。
  3. 64位或者無符號32位整型在解碼時被表示成為ilong,但是在設定時可以使用int型值設定,在所有的情況下,值必須符合其設定其型別的要求。
  4. python中string被表示成在解碼時表示成unicode。但是一個ASCIIstring可以被表示成str型別。
  5. Integer在64位的機器上使用,string在32位機器上使用

預設值

當一個訊息被解析的時候,如果被編碼的資訊不包含一個特定的singular元素,被解析的物件鎖對應的域被設定位一個預設值,對於不同型別指定如下:

  • 對於strings,預設是一個空string
  • 對於bytes,預設是一個空的bytes
  • 對於bools,預設是false
  • 對於數值型別,預設是0
  • 對於列舉,預設是第一個定義的列舉值,必須為0;
  • 對於訊息型別(message),域沒有被設定,確切的訊息是根據語言確定的,詳見generated code guide

    對於可重複域的預設值是空(通常情況下是對應語言中空列表)。

    注:對於標量訊息域,一旦訊息被解析,就無法判斷域釋放被設定為預設值(例如,例如boolean值是否被設定為false)還是根本沒有被設定。你應該在定義你的訊息型別時非常注意。例如,比如你不應該定義boolean的預設值false作為任何行為的觸發方式。也應該注意如果一個標量訊息域被設定為標誌位,這個值不應該被序列化傳輸。

    檢視generated code guide選擇你的語言的預設值的工作細節。

列舉

當需要定義一個訊息型別的時候,可能想為一個欄位指定某“預定義值序列”中的一個值。例如,假設要為每一個SearchRequest訊息新增一個 corpus欄位,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一個。 其實可以很容易地實現這一點:通過向訊息定義中新增一個列舉(enum)並且為每個可能的值定義一個常量就可以了。

在下面的例子中,在訊息格式中新增了一個叫做Corpus的列舉型別——它含有所有可能的值 ——以及一個型別為Corpus的欄位:

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

如你所見,Corpus列舉的第一個常量對映為0:每個列舉型別必須將其第一個型別對映為0,這是因為:

  • 必須有有一個0值,我們可以用這個0值作為預設值。
  • 這個零值必須為第一個元素,為了相容proto2語義,列舉類的第一個值總是預設值。

    你可以通過將不同的列舉常量指定位相同的值。如果這樣做你需要將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.
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

列舉常量必須在32位整型值的範圍內。因為enum值是使用可變編碼方式的,對負數不夠高效,因此不推薦在enum中使用負數。如上例所示,可以在 一個訊息定義的內部或外部定義列舉——這些列舉可以在.proto檔案中的任何訊息定義裡重用。當然也可以在一個訊息中宣告一個列舉型別,而在另一個不同 的訊息中使用它——採用MessageType.EnumType的語法格式。

當對一個使用了列舉的.proto檔案執行protocol buffer編譯器的時候,生成的程式碼中將有一個對應的enum(對Java或C++來說),或者一個特殊的EnumDescriptor類(對 Python來說),它被用來在執行時生成的類中建立一系列的整型值符號常量(symbolic constants)。

在反序列化的過程中,無法識別的列舉值會被儲存在訊息中,雖然這種表示方式需要依據所使用語言而定。在那些支援開放列舉型別超出指定範圍之外的語言中(例如C++和Go),為識別的值會被表示成所支援的整型。在使用封閉列舉型別的語言中(Java),使用列舉中的一個型別來表示未識別的值,並且可以使用所支援整型來訪問。在其他情況下,如果解析的訊息被序列號,未識別的值將保持原樣。

關於如何在你的應用程式的訊息中使用列舉的更多資訊,請檢視所選擇的語言generated code guide

使用其他訊息型別

你可以將其他訊息型別用作欄位型別。例如,假設在每一個SearchResponse訊息中包含Result訊息,此時可以在相同的.proto檔案中定義一個Result訊息型別,然後在SearchResponse訊息中指定一個Result型別的欄位,如:

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

匯入定義

在上面的例子中,Result訊息型別與SearchResponse是定義在同一檔案中的。如果想要使用的訊息型別已經在其他.proto檔案中已經定義過了呢? 
你可以通過匯入(importing)其他.proto檔案中的定義來使用它們。要匯入其他.proto檔案的定義,你需要在你的檔案中新增一個匯入宣告,如:

import "myproject/other_protos.proto";
  • 1

預設情況下你只能使用直接匯入的.proto檔案中的定義. 然而, 有時候你需要移動一個.proto檔案到一個新的位置, 可以不直接移動.proto檔案, 只需放入一個偽 .proto 檔案在老的位置, 然後使用import public轉向新的位置。import public 依賴性會通過任意匯入包含import public宣告的proto檔案傳遞。例如:

// 這是新的proto
// All definitions are moved here
  • 1
  • 2
// 這是久的proto
// 這是所有客戶端正在匯入的包
import public "new.proto";
import "other.proto";
  • 1
  • 2
  • 3
  • 4
// 客戶端proto
import "old.proto";
// 現在你可以使用新久兩種包的proto定義了。
  • 1
  • 2
  • 3

通過在編譯器命令列引數中使用-I/--proto_pathprotocal 編譯器會在指定目錄搜尋要匯入的檔案。如果沒有給出標誌,編譯器會搜尋編譯命令被呼叫的目錄。通常你只要指定proto_path標誌為你的工程根目錄就好。並且指定好匯入的正確名稱就好。

使用proto2訊息型別

在你的proto3訊息中匯入proto2的訊息型別也是可以的,反之亦然,然後proto2列舉不可以直接在proto3的識別符號中使用(如果僅僅在proto2訊息中使用是可以的)。

巢狀型別

你可以在其他訊息型別中定義、使用訊息型別,在下面的例子中,Result訊息就定義在SearchResponse訊息內,如:

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

如果你想在它的父訊息型別的外部重用這個訊息型別,你需要以Parent.Type的形式使用它,如:

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}
  • 1
  • 2
  • 3

當然,你也可以將訊息巢狀任意多層,如:

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;
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

更新一個訊息型別

如果一個已有的訊息格式已無法滿足新的需求——如,要在訊息中新增一個額外的欄位——但是同時舊版本寫的程式碼仍然可用。不用擔心!更新訊息而不破壞已有程式碼是非常簡單的。在更新時只要記住以下的規則即可。

  • 不要更改任何已有的欄位的數值標識。
  • 如果你增加新的欄位,使用舊格式的欄位仍然可以被你新產生的程式碼所解析。你應該記住這些元素的預設值這樣你的新程式碼就可以以適當的方式和舊程式碼產生的資料互動。相似的,通過新程式碼產生的訊息也可以被舊程式碼解析:只不過新的欄位會被忽視掉。注意,未被識別的欄位會在反序列化的過程中丟棄掉,所以如果訊息再被傳遞給新的程式碼,新的欄位依然是不可用的(這和proto2中的行為是不同的,在proto2中未定義的域依然會隨著訊息被序列化)
  • 非required的欄位可以移除——只要它們的標識號在新的訊息型別中不再使用(更好的做法可能是重新命名那個欄位,例如在欄位前新增“OBSOLETE_”字首,那樣的話,使用的.proto檔案的使用者將來就不會無意中重新使用了那些不該使用的標識號)。
  • int32, uint32, int64, uint64,和bool是全部相容的,這意味著可以將這些型別中的一個轉換為另外一個,而不會破壞向前、 向後的相容性。如果解析出來的數字與對應的型別不相符,那麼結果就像在C++中對它進行了強制型別轉換一樣(例如,如果把一個64位數字當作int32來 讀取,那麼它就會被截斷為32位的數字)。
  • sint32和sint64是互相相容的,但是它們與其他整數型別不相容。
  • string和bytes是相容的——只要bytes是有效的UTF-8編碼。
  • 巢狀訊息與bytes是相容的——只要bytes包含該訊息的一個編碼過的版本。
  • fixed32與sfixed32是相容的,fixed64與sfixed64是相容的。
  • 列舉型別與int32,uint32,int64和uint64相相容(注意如果值不相相容則會被截斷),然而在客戶端反序列化之後他們可能會有不同的處理方式,例如,未識別的proto3列舉型別會被保留在訊息中,但是他的表示方式會依照語言而定。int型別的欄位總會保留他們的

Any

Any型別訊息允許你在沒有指定他們的.proto定義的情況下使用訊息作為一個巢狀型別。一個Any型別包括一個可以被序列化bytes型別的任意訊息,以及一個URL作為一個全域性識別符號和解析訊息型別。為了使用Any型別,你需要匯入import google/protobuf/any.proto

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

對於給定的訊息型別的預設型別URL是type.googleapis.com/packagename.messagename

不同語言的實現會支援動態庫以執行緒安全的方式去幫助封裝或者解封裝Any值。例如在java中,Any型別會有特殊的pack()unpack()訪問器,在C++中會有PackFrom()UnpackTo()方法。

// 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 ...
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

目前,用於Any型別的動態庫仍在開發之中 
如果你已經很熟悉proto2語法,使用Any替換擴充

Oneof

如果你的訊息中有很多可選欄位, 並且同時至多一個欄位會被設定, 你可以加強這個行為,使用oneof特性節省記憶體.

Oneof欄位就像可選欄位, 除了它們會共享記憶體, 至多一個欄位會被設定。 設定其中一個欄位會清除其它欄位。 你可以使用case()或者WhichOneof() 方法檢查哪個oneof欄位被設定, 看你使用什麼語言了.

使用Oneof

為了在.proto定義Oneof欄位, 你需要在名字前面加上oneof關鍵字, 比如下面例子的test_oneof:

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

然後你可以增加oneof欄位到 oneof 定義中. 你可以增加任意型別的欄位, 但是不能使用repeated 關鍵字.

在產生的程式碼中, oneof欄位擁有同樣的 getters 和setters, 就像正常的可選欄位一樣. 也有一個特殊的方法來檢查到底那個欄位被設定. 你可以在相應的語言API指南中找到oneof API介紹.

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());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 如果解析器遇到同一個oneof中有多個成員,只有最會一個會被解析成訊息。
  • oneof不支援repeated.
  • 反射API對oneof 欄位有效.
  • 如果使用C++,需確保程式碼不會導致記憶體洩漏. 下面的程式碼會崩潰, 因為sub_message 已經通過set_name()刪除了
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name");      // Will delete sub_message
sub_message->set_...            // Crashes here
  • 1
  • 2
  • 3
  • 4
  • 在C++中,如果你使用Swap()兩個oneof訊息,每個訊息,兩個訊息將擁有對方的值,例如在下面的例子中,msg1會擁有sub_message並且msg2會有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());
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

向後相容性問題

當增加或者刪除oneof欄位時一定要小心. 如果檢查oneof的值返回None/NOT_SET, 它意味著oneof欄位沒有被賦值或者在一個不同的版本中賦值了。 你不會知道是哪種情況,因為沒有辦法判斷如果未識別的欄位是一個oneof欄位。

Tage 重用問題:

  • 將欄位移入或移除oneof:在訊息被序列號或者解析後,你也許會失去一些資訊(有些欄位也許會被清除)
  • 刪除一個欄位或者加入一個欄位:在訊息被序列號或者解析後,這也許會清除你現在設定的oneof欄位
  • 分離或者融合oneof:行為與移動常規欄位相似。

Map(對映)

如果你希望建立一個關聯對映,protocol buffer提供了一種快捷的語法:

map<key_type, value_type> map_field = N;
  • 1

其中key_type可以是任意Integer或者string型別(所以,除了floating和bytes的任意標量型別都是可以的)value_type可以是任意型別。

例如,如果你希望建立一個project的對映,每個Projecct使用一個string作為key,你可以像下面這樣定義:

map<string, Project> projects = 3;
  • 1
  • Map的欄位可以是repeated。
  • 序列化後的順序和map迭代器的順序是不確定的,所以你不要期望以固定順序處理Map
  • 當為.proto檔案產生生成文字格式的時候,map會按照key 的順序排序,數值化的key會按照數值排序。
  • 從序列化中解析或者融合時,如果有重複的key則後一個key不會被使用,當從文字格式中解析map時,如果存在重複的key。

生成map的API現在對於所有proto3支援的語言都可用了,你可以從API指南找到更多資訊。

向後相容性問題

map語法序列化後等同於如下內容,因此即使是不支援map語法的protocol buffer實現也是可以處理你的資料的:

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

當然可以為.proto檔案新增一個可選的package宣告符,用來防止不同的訊息型別有命名衝突。如:

package foo.bar;
message Open { ... }
  • 1
  • 2

在其他的訊息格式定義中可以使用包名+訊息名的方式來定義域的型別,如:

message Foo {
  ...
  required foo.bar.Open open = 1;
  ...
}
  • 1
  • 2
  • 3
  • 4
  • 5

包的宣告符會根據使用語言的不同影響生成的程式碼。

  • 對於C++,產生的類會被包裝在C++的名稱空間中,如上例中的Open會被封裝在 foo::bar空間中; - 對於Java,包宣告符會變為java的一個包,除非在.proto檔案中提供了一個明確有java_package
  • 對於 Python,這個包宣告符是被忽略的,因為Python模組是按照其在檔案系統中的位置進行組織的。
  • 對於Go,包可以被用做Go包名稱,除非你顯式的提供一個option go_package在你的.proto檔案中。
  • 對於Ruby,生成的類可以被包裝在內建的Ruby名稱空間中,轉換成Ruby所需的大小寫樣式 (首字母大寫;如果第一個符號不是一個字母,則使用PB_字首),例如Open會在Foo::Bar名稱空間中。
  • 對於javaNano包會使用Java包,除非你在你的檔案中顯式的提供一個option java_package
  • 對於C#包可以轉換為PascalCase後作為名稱空間,除非你在你的檔案中顯式的提供一個option csharp_namespace,例如,Open會在Foo.Bar名稱空間中

包及名稱的解析

Protocol buffer語言中型別名稱的解析與C++是一致的:首先從最內部開始查詢,依次向外進行,每個包會被看作是其父類包的內部類。當然對於 (foo.bar.Baz)這樣以“.”分隔的意味著是從最外圍開始的。

ProtocolBuffer編譯器會解析.proto檔案中定義的所有型別名。 對於不同語言的程式碼生成器會知道如何來指向每個具體的型別,即使它們使用了不同的規則。

定義服務(Service)

如果想要將訊息型別用在RPC(遠端方法呼叫)系統中,可以在.proto檔案中定義一個RPC服務介面,protocol buffer編譯器將會根據所選擇的不同語言生成服務介面程式碼及存根。如,想要定義一個RPC服務並具有一個方法,該方法能夠接收 SearchRequest並返回一個SearchResponse,此時可以在.proto檔案中進行如下定義:

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}
  • 1
  • 2
  • 3

最直觀的使用protocol buffer的RPC系統是gRPC一個由谷歌開發的語言和平臺中的開源的PRC系統,gRPC在使用protocl buffer時非常有效,如果使用特殊的protocol buffer外掛可以直接為您從.proto檔案中產生相關的RPC程式碼。

如果你不想使用gRPC,也可以使用protocol buffer用於自己的RPC實現,你可以從proto2語言指南中找到更多資訊

還有一些第三方開發的PRC實現使用Protocol Buffer。參考第三方外掛wiki檢視這些實現的列表。

JSON 對映

Proto3 支援JSON的編碼規範,使他更容易在不同系統之間共享資料,在下表中逐個描述型別。

如果JSON編碼的資料丟失或者其本身就是null,這個資料會在解析成protocol buffer的時候被表示成預設值。如果一個欄位在protocol buffer中表示為預設值,體會在轉化成JSON的時候編碼的時候忽略掉以節省空間。具體實現可以提供在JSON編碼中可選的預設值。

proto3JSONJSON示例注意
messageobject{“fBar”: v, “g”: null, …}產生JSON物件,訊息欄位名可以被對映成lowerCamelCase形式,並且成為JSON物件鍵,null被接受併成為對應欄位的預設值
enumstring“FOO_BAR”列舉值的名字在proto檔案中被指定
mapobject{“k”: v, …}所有的鍵都被轉換成string
repeated Varray[v, …]null被視為空列表
booltrue, falsetrue, false 
stringstring“Hello World!” 
bytesbase64 string“YWJjMTIzIT8kKiYoKSctPUB+” 
int32, fixed32, uint32number1, -10, 0JSON值會是一個十進位制數,數值型或者string型別都會接受
int64, fixed64, uint64string“1”, “-10”JSON值會是一個十進位制數,數值型或者string型別都會接受
float, doublenumber1.1, -10.0, 0, “NaN”, “Infinity”JSON值會是一個數字或者一個指定的字串如”NaN”,”infinity”或者”-Infinity”,數值型或者字串都是可接受的,指數符號也可以接受
Anyobject{“@type”: “url”, “f”: v, … }如果一個Any保留一個特上述的JSON對映,則它會轉換成一個如下形式:{"@type": xxx, "value": yyy}否則,該值會被轉換成一個JSON物件,@type欄位會被插入所指定的確定的值
Timestampstring“1972-01-01T10:00:20.021Z”使用RFC 339,其中生成的輸出將始終是Z-歸一化啊的,並且使用0,3,6或者9位小數
Durationstring“1.000340012s”, “1s”生成的輸出總是0,3,6或者9位小數,具體依賴於所需要的精度,接受所有可以轉換為納秒級的精度
Structobject{ … }任意的JSON物件,見struct.proto
Wrapper typesvarious types2, “2”, “foo”, true, “true”, null, 0, …包裝器在JSON中的表示方式類似於基本型別,但是允許nulll,並且在轉換的過程中保留null
FieldMaskstring“f.fooBar,h”見fieldmask.proto
ListValuearray[foo, bar, …] 
Valuevalue 任意JSON值
NullValuenull JSON null

選項

在定義.proto檔案時能夠標註一系列的options。Options並不改變整個檔案宣告的含義,但卻能夠影響特定環境下處理方式。完整的可用選項可以在google/protobuf/descriptor.proto找到。

一些選項是檔案級別的,意味著它可以作用於最外範圍,不包含在任何訊息內部、enum或服務定義中。一些選項是訊息級別的,意味著它可以用在訊息定義的內部。當然有些選項可以作用在域、enum型別、enum值、服務型別及服務方法中。到目前為止,並沒有一種有效的選項能作用於所有的型別。

如下就是一些常用的選擇:

  • java_package (檔案選項) :這個選項表明生成java類所在的包。如果在.proto檔案中沒有明確的宣告java_package,就採用預設的包名。當然了,預設方式產生的 java包名並不是最好的方式,按照應用名稱倒序方式進行排序的。如果不需要產生java程式碼,則該選項將不起任何作用。如:
option java_package = "com.example.foo";
  • 1
  • java_outer_classname (檔案選項): 該選項表明想要生成Java類的名稱。如果在.proto檔案中沒有明確的java_outer_classname定義,生成的class名稱將會根據.proto檔案的名稱採用駝峰式的命名方式進行生成。如(foo_bar.proto生成的java類名為FooBar.java),如果不生成java程式碼,則該選項不起任何作用。如:
option java_outer_classname = "Ponycopter";
  • 1
  • optimize_for(檔案選項): 可以被設定為 SPEED, CODE_SIZE,或者LITE_RUNTIME。這些值將通過如下的方式影響C++及java程式碼的生成: 
    • SPEED (default): protocol buffer編譯器將通過在訊息型別上執行序列化、語法分析及其他通用的操作。這種程式碼是最優的。
    • CODE_SIZE: protocol buffer編譯器將會產生最少量的類,通過共享或基於反射的程式碼來實現序列化、語法分析及各種其它操作。採用該方式產生的程式碼將比SPEED要少得多, 但是操作要相對慢些。當然實現的類及其對外的API與SPEED模式都是一樣的。這種方式經常用在一些包含大量的.proto檔案而且並不盲目追求速度的 應用中。
    • LITE_RUNTIME: protocol buffer編譯器依賴於執行時核心類庫來生成程式碼(即採用libprotobuf-lite 替代libprotobuf)。這種核心類庫由於忽略了一 些描述符及反射,要比全類庫小得多。這種模式經常在移動手機平臺應用多一些。編譯器採用該模式產生的方法實現與SPEED模式不相上下,產生的類通過實現 MessageLite介面,但它僅僅是Messager介面的一個子集。
option optimize_for = CODE_SIZE;
  • 1
  • cc_enable_arenas(檔案選項):對於C++產生的程式碼啟用arena allocation
  • objc_class_prefix(檔案選項):設定Objective-C類的字首,新增到所有Objective-C從此.proto檔案產生的類和列舉型別。沒有預設值,所使用的字首應該是蘋果推薦的3-5個大寫字元,注意2個位元組的字首是蘋果所保留的。
  • deprecated(欄位選項):如果設定為true則表示該欄位已經被廢棄,並且不應該在新的程式碼中使用。在大多數語言中沒有實際的意義。在java中,這回變成@Deprecated註釋,在未來,其他語言的程式碼生成器也許會在字識別符號中產生廢棄註釋,廢棄註釋會在編譯器嘗試使用該欄位時發出警告。如果欄位沒有被使用你也不希望有新使用者使用它,嘗試使用保留語句替換欄位宣告。
int32 old_field = 6 [deprecated=true];
  • 1

自定義選項

ProtocolBuffers允許自定義並使用選項。該功能應該屬於一個高階特性,對於大部分人是用不到的。如果你的確希望建立自己的選項,請參看 Proto2 Language Guide。注意建立自定義選項使用了擴充,擴充只在proto3中可用。

生成訪問類

可以通過定義好的.proto檔案來生成Java,Python,C++, Ruby, JavaNano, Objective-C,或者C# 程式碼,需要基於.proto檔案執行protocol buffer編譯器protoc。如果你沒有安裝編譯器,下載安裝包並遵照README安裝。對於Go,你還需要安裝一個特殊的程式碼生成器外掛。你可以通過GitHub上的protobuf庫找到安裝過程

通過如下方式呼叫protocol編譯器:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
  • 1
  • IMPORT_PATH宣告瞭一個.proto檔案所在的解析import具體目錄。如果忽略該值,則使用當前目錄。如果有多個目錄則可以多次呼叫--proto_path,它們將會順序的被訪問並執行匯入。-I=IMPORT_PATH--proto_path的簡化形式。
  • 當然也可以提供一個或多個輸出路徑: 
    • --cpp_out 在目標目錄DST_DIR中產生C++程式碼,可以在C++程式碼生成參考中檢視更多。
    • --java_out 在目標目錄DST_DIR中產生Java程式碼,可以在 Java程式碼生成參考中檢視更多。
    • --python_out 在目標目錄 DST_DIR 中產生Python程式碼,可以在Python程式碼生成參考中檢視更多。
    • --go_out 在目標目錄 DST_DIR 中產生Go程式碼,可以在GO程式碼生成參考中檢視更多。
    • --ruby_out在目標目錄 DST_DIR 中產生Go程式碼,參考正在製作中。
    • --javanano_out在目標目錄DST_DIR中生成JavaNano,JavaNano程式碼生成器有一系列的選項用於定製自定義生成器的輸出:你可以通過生成器的README查詢更多資訊,JavaNano參考正在製作中。
    • --objc_out在目標目錄DST_DIR中產生Object程式碼,可以在Objective-C程式碼生成參考中檢視更多。
    • --csharp_out在目標目錄DST_DIR中產生Object程式碼,可以在C#程式碼生成參考中檢視更多。
    • --php_out在目標目錄DST_DIR中產生Object程式碼,可以在PHP程式碼生成參考中檢視更多。

作為一個方便的擴充,如果DST_DIR以.zip或者.jar結尾,編譯器會將輸出寫到一個ZIP格式檔案或者符合JAR標準的.jar檔案中。注意如果輸出已經存在則會被覆蓋,編譯器還沒有智慧到可以追加檔案。 
- 你必須提議一個或多個.proto檔案作為輸入,多個.proto檔案可以只指定一次。雖然檔案路徑是相對於當前目錄的,每個檔案必須位於其IMPORT_PATH下,以便每個檔案可以確定其規範的名稱。

相關文章