Protobuf 語言指南(proto3)

穆書偉發表於2018-10-04

Protobuf 語言指南(proto3)

前言

Protocol Buffer是Google的語言中立的,平臺中立的,可擴充套件機制的,用於序列化結構化資料 - 對比XML,但更小,更快,更簡單。您可以定義資料的結構化,然後可以使用特殊生成的原始碼輕鬆地在各種資料流中使用各種語言編寫和讀取結構化資料。

定義訊息型別

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

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
複製程式碼
  • 該檔案的第一行指定您正在使用proto3語法:如果您不這樣做,protobuf 編譯器將假定您正在使用proto2。這必須是檔案的第一個非空的非註釋行。
  • 所述SearchRequest訊息定義指定了三個欄位(名稱/值對),一個用於要在此型別的訊息中包含的每個資料片段。每個欄位都有一個名稱和型別。

指定欄位型別

在上面的示例中,所有欄位都是標量型別:兩個整數(page_numberresult_per_page)和一個字串(query)。但是,您還可以為欄位指定合成型別,包括列舉和其他訊息型別。

分配標識號

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

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

指定欄位規則

訊息欄位可以是以下之一:

  • 單數:格式良好的訊息可以包含該欄位中的零個或一個(但不超過一個)。
  • repeated:此欄位可以在格式良好的訊息中重複任意次數(包括零)。將保留重複值的順序。

在proto3中,repeated數字型別的欄位預設使用packed編碼。

packed您可以在協議緩衝區編碼中找到有關編碼的更多資訊。

新增更多訊息型別

可以在單個.proto檔案中定義多種訊息型別。如果要定義多個相關訊息,這很有用 - 例如,如果要定義與SearchResponse訊息型別對應的回覆訊息格式,可以將其新增到相同的訊息.proto

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

message SearchResponse {
 ...
}
複製程式碼

新增註釋

要為.proto檔案新增註釋,請使用C / C ++ - 樣式///* ... */語法。

/ * SearchRequest表示搜尋查詢,帶有分頁選項
 *表明響應中包含哪些結果。* /

message SearchRequest {
  string query = 1;
  int32 page_number = 2; //我們想要哪個頁碼?
  int32 result_per_page = 3; //每頁返回的結果數。
}
複製程式碼

保留欄位

如果通過完全刪除欄位或將其註釋來更新訊息型別,則未來使用者可以在對型別進行自己的更新時重用欄位編號。如果以後載入相同的舊版本,這可能會導致嚴重問題.proto,包括資料損壞,隱私錯誤等。確保不會發生這種情況的一種方法是指定已刪除欄位的欄位編號(和/或名稱,這也可能導致JSON序列化問題)reserved。如果將來的任何使用者嘗試使用這些欄位識別符號,協議緩衝編譯器將會抱怨。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}
複製程式碼

請注意,您不能在同一reserved語句中混合欄位名稱和欄位編號。

你的生成是什麼.proto

當您在a上執行協議緩衝區編譯器.proto,編譯器會生成您所選語言的程式碼,您需要使用您在檔案中描述的訊息型別,包括獲取和設定欄位值,將訊息序列化為輸出流,並從輸入流解析您的訊息。

  • 對於C ++,編譯器會從每個檔案生成一個.h和一個.cc檔案.proto,併為您檔案中描述的每種訊息型別提供一個類。
  • 對於Java,編譯器生成一個.java檔案,其中包含每種訊息型別的類,以及Builder用於建立訊息類例項的特殊類。
  • Python有點不同 - Python編譯器生成一個模組,其中包含每個訊息型別的靜態描述符,.proto然後與元類一起使用,以在執行時建立必要的Python資料訪問類。
  • 對於Go,編譯器會為.pb.go檔案中的每種訊息型別生成一個型別的檔案。
  • 對於Ruby,編譯器生成一個.rb包含訊息型別的Ruby模組的檔案。
  • 對於Objective-C,編譯器從每個檔案生成一個pbobjc.h和一個pbobjc.m檔案.proto,其中包含檔案中描述的每種訊息型別的類。
  • 對於C#,編譯器會.cs從每個檔案生成一個檔案.proto,其中包含檔案中描述的每種訊息型別的類。

您可以按照所選語言的教程(即將推出的proto3版本)瞭解有關為每種語言使用API的更多資訊。有關更多API詳細資訊,請參閱相關API參考(proto3版本即將推出)。

標量值型別

標量訊息欄位可以具有以下型別之一 - 該表顯示.proto檔案中指定的型別,以及自動生成的類中的相應型別:

.proto type notes C ++ type Java type Python type [2] Type Ruby type C# type PHP type
double double double float float64 float double float
float float float float FLOAT32 float float float
INT32 使用可變長度編碼。編碼負數的效率低 - 如果您的欄位可能有負值,請改用sint32。 INT32 INT INT INT32 Fixnum or Bignum (as needed) INT Integer
Int64 使用可變長度編碼。編碼負數的效率低 - 如果您的欄位可能有負值,請改用sint64。 Int64 long int / long [3] Int64 TWINS long Integer/string[5]
UINT32 使用可變長度編碼。 UINT32 int [1] int / long [3] UINT32 Fixnum or Bignum (as needed) UINT Integer
UINT64 使用可變長度編碼。 UINT64 Long [1] int / long [3] UINT64 TWINS ULONG Integer/string[5]
SINT32 使用可變長度編碼。簽名的int值。這些比常規int32更有效地編碼負數。 INT32 INT INT INT32 Fixnum or Bignum (as needed) INT Integer
sint64 使用可變長度編碼。簽名的int值。這些比常規int64更有效地編碼負數。 Int64 long int / long [3] Int64 TWINS long Integer/string[5]
fixed32 總是四個位元組。如果值通常大於2 28,則比uint32更有效。 UINT32 int [1] int / long [3] UINT32 Fixnum or Bignum (as needed) UINT Integer
fixed64 總是八個位元組。如果值通常大於2 56,則比uint64更有效。 UINT64 Long [1] int / long [3] UINT64 TWINS ULONG Integer/string[5]
sfixed32 總是四個位元組。 INT32 INT INT INT32 Fixnum or Bignum (as needed) INT Integer
sfixed64 總是八個位元組。 Int64 long int / long [3] Int64 TWINS long Integer/string[5]
Boolean Boolean Boolean Boolean Boolean TrueClass / FalseClass Boolean Boolean
string 字串必須始終包含UTF-8編碼或7位ASCII文字。 string string str / unicode[4] string String (UTF-8) string string
byte 可以包含任意位元組序列。 string Byte string Strait []byte String (ASCII-8BIT) Byte string string

協議緩衝區編碼中序列化訊息時,您可以找到有關如何編碼這些型別的更多資訊。

[1]在Java中,無符號的32位和64位整數使用它們的帶符號對應表示,最高位只是儲存在符號位中。

[2]在所有情況下,將值設定為欄位將執行型別檢查以確保其有效。

[3] 64位或無符號32位整數在解碼時始終表示為long,但如果在設定欄位時給出int,則可以為int。在所有情況下,該值必須適合設定時表示的型別。見[2]。

[4] Python字串在解碼時表示為unicode,但如果給出了ASCII字串,則可以是str(這可能會發生變化)。

[5] Integer用於64位計算機,字串用於32位計算機。

預設值

解析訊息時,如果編碼訊息不包含特定的單數元素,則解析物件中的相應欄位將設定為該欄位的預設值。這些預設值是特定於型別的:

  • 對於字串,預設值為空字串。
  • 對於位元組,預設值為空位元組。
  • 對於bools,預設值為false。
  • 對於數字型別,預設值為零。
  • 對於列舉,預設值是第一個定義的列舉值,該必須為0。
  • 對於訊息欄位,未設定該欄位。它的確切值取決於語言。有關詳細資訊, 請參閱生成的程式碼指

重複欄位的預設值為空(通常是相應語言的空列表)。

請注意,對於標量訊息欄位,一旦解析了訊息,就無法確定欄位是否顯式設定為預設值(例如,是否設定了布林值false)或者根本沒有設定:您應該記住這一點在定義訊息型別時。例如,false如果您不希望預設情況下也發生這種行為,那麼在設定為時,沒有一個布林值可以啟用某些行為。還要注意的是,如果一個標訊息欄位設定為預設值,該值將不會在電線上連載。

有關預設值如何在生成的程式碼中工作的更多詳細資訊,請參閱所選語言的生成程式碼指南

列舉

在定義訊息型別時,您可能希望其中一個欄位只有一個預定義的值列表。例如,假設你想新增一個 corpus欄位每個SearchRequest,其中語料庫可以 UNIVERSALWEBIMAGESLOCALNEWSPRODUCTSVIDEO。您可以非常簡單地通過enum為每個可能的值新增一個常量來定義訊息定義。

在下面的示例中,我們新增了一個帶有所有可能值的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;
}
複製程式碼

如您所見,Corpus列舉的第一個常量對映為零:每個列舉定義必須包含一個對映到零的常量作為其第一個元素。這是因為:

  • 必須有一個零值,以便我們可以使用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.
}
複製程式碼

列舉器常量必須在32位整數範圍內。由於enum值線上上使用varint編碼,因此負值效率低,因此不建議使用。您可以enum在訊息定義中定義s,如上例所示,enum也可以在外部定義 - 這些可以在.proto檔案的任何訊息定義中重用。您還可以使用enum語法將一個訊息中宣告的型別用作另一個訊息中的欄位型別。 *MessageType*.*EnumType*

當你在.proto使用a的協議緩衝編譯器上執行時enum,生成的程式碼將具有enumJava或C ++ 的相應程式碼,這EnumDescriptor是Python的一個特殊類,用於在執行時生成的類中建立一組帶有整數值的符號常量。

在反序列化期間,將在訊息中保留無法識別的列舉值,但是當反序列化訊息時,如何表示這種值取決於語言。在支援具有超出指定符號範圍的值的開放列舉型別的語言中,例如C ++和Go,未知的列舉值僅作為其基礎整數表示儲存。在具有封閉列舉型別(如Java)的語言中,列舉中的大小寫用於表示無法識別的值,並且可以使用特殊訪問器訪問基礎整數。在任何一種情況下,如果訊息被序列化,則仍然會使用訊息序列化無法識別的值。

有關如何enum在應用程式中使用訊息的詳細資訊,請參閱所選語言的生成程式碼指南

保留值

如果通過完全刪除列舉條目或將其註釋掉來更新列舉型別,則未來使用者可以在對型別進行自己的更新時重用該數值。如果以後載入相同的舊版本,這可能會導致嚴重問題.proto,包括資料損壞,隱私錯誤等。確保不會發生這種情況的一種方法是指定已刪除條目的數值(和/或名稱,這也可能導致JSON序列化問題)reserved。如果將來的任何使用者嘗試使用這些識別符號,協議緩衝編譯器將會抱怨。您可以使用max關鍵字指定保留的數值範圍達到最大可能值。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}
複製程式碼

請注意,您不能在同一reserved語句中混合欄位名稱和數值。

使用其他訊息型別

您可以使用其他訊息型別作為欄位型別。例如,假設你想包括Result每個訊息的SearchResponse訊息-要做到這一點,你可以定義一個Result在同一個訊息型別.proto,然後指定型別的欄位ResultSearchResponse

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}
複製程式碼

匯入定義

在上面的示例中,Result訊息型別在同一檔案中定義SearchResponse- 如果要用作欄位型別的訊息型別已在另一個.proto檔案中定義,該怎麼辦?

您可以.proto通過匯入來使用其他檔案中的定義。要匯入其他.proto人的定義,請在檔案頂部新增import語句:

import“myproject / other_protos.proto”;
複製程式碼

預設情況下,您只能使用直接匯入.proto檔案中的定義。但是,有時您可能需要將.proto檔案移動到新位置。.proto現在,您可以.proto在舊位置放置一個虛擬檔案,以使用該import public概念將所有匯入轉發到新位置,而不是直接移動檔案並在一次更改中更新所有呼叫站點。import public任何匯入包含該import public語句的proto的人都可以傳遞依賴關係。例如:

// 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";
//您使用old.proto和new.proto中的定義,但不使用other.proto
複製程式碼

協議編譯器使用-I/ --proto_pathflag 在協議編譯器命令列中指定的一組目錄中搜尋匯入的檔案 。如果沒有給出標誌,它將查詢呼叫編譯器的目錄。通常,您應該將--proto_path標誌設定為專案的根目錄,並對所有匯入使用完全限定名稱。

使用proto2訊息型別

可以匯入proto2訊息型別並在proto3訊息中使用它們,反之亦然。但是,proto2列舉不能直接用於proto3語法(如果匯入的proto2訊息使用它們就可以了)。

巢狀型別

您可以在其他訊息型別中定義和使用訊息型別,如下例所示 - 此處Result訊息在訊息中定義SearchResponse

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}
複製程式碼

如果要在其父訊息型別之外重用此訊息型別,請將其稱為: *Parent*.*Type*

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}
複製程式碼

您可以根據需要深入巢狀訊息:

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;
    }
  }
}
複製程式碼

更新訊息型別

如果現有的訊息型別不再滿足您的所有需求 - 例如,您希望訊息格式具有額外的欄位 - 但您仍然希望使用使用舊格式建立的程式碼,請不要擔心!在不破壞任何現有程式碼的情況下更新訊息型別非常簡單。請記住以下規則:

  • 請勿更改任何現有欄位的欄位編號。
  • 如果新增新欄位,則使用“舊”訊息格式按程式碼序列化的任何訊息仍可由新生成的程式碼進行解析。您應該記住這些元素的預設值,以便新程式碼可以正確地與舊程式碼生成的訊息進行互動。同樣,您的新程式碼建立的訊息可以由舊程式碼解析:舊的二進位制檔案在解析時只是忽略新欄位。有關詳細資訊,請參閱“ 未知欄位”部分
  • 只要在更新的訊息型別中不再使用欄位編號,就可以刪除欄位。您可能希望重新命名該欄位,可能新增字首“OBSOLETE_”,或者保留欄位編號,以便您的未來使用者.proto不會意外地重複使用該編號。
  • int32uint32int64uint64,和bool都是相容的-這意味著你可以改變這些型別到另一個的一個場不破壞forwards-或向後相容。如果從導線中解析出一個不符合相應型別的數字,您將獲得與在C ++中將該數字轉換為該型別相同的效果(例如,如果將64位數字作為int32讀取,它將被截斷為32位)。
  • sint32並且sint64彼此相容但與其他整數型別相容。
  • string``bytes只要位元組是有效的UTF-8 ,它們是相容的。
  • bytes如果位元組包含訊息的編碼版本,則嵌入訊息是相容的。
  • fixed32與相容sfixed32,並fixed64sfixed64
  • enum與相容int32uint32int64,和uint64電線格式條款(注意,如果他們不適合的值將被截斷)。但請注意,在反序列化訊息時,客戶端程式碼可能會以不同方式對待它們:例如,enum將在訊息中保留未識別的proto3 型別,但在反序列化訊息時如何表示這種型別取決於語言。Int欄位總是保留它們的價值。
  • 將單個值更改為 成員oneof是安全且二進位制相容的。oneof如果您確定沒有程式碼一次設定多個欄位,則將多個欄位移動到新欄位可能是安全的。將任何欄位移動到現有欄位oneof並不安全。

未知欄位

未知欄位是格式良好的協議緩衝區序列化資料,表示解析器無法識別的欄位。例如,當舊二進位制檔案解析具有新欄位的新二進位制檔案傳送的資料時,這些新欄位將成為舊二進位制檔案中的未知欄位。

最初,proto3訊息在解析期間總是丟棄未知欄位,但在3.5版本中,我們重新引入了儲存未知欄位以匹配proto2行為。在版本3.5及更高版本中,未知欄位在解析期間保留幷包含在序列化輸出中。

任何

Any訊息型別,可以使用郵件作為嵌入式型別,而不必自己.proto定義。一個Any含有任意的序列化訊息bytes,以充當一個全域性唯一識別符號和解析到該訊息的型別的URL一起。要使用該Any型別,您需要匯入google/protobuf/any.proto

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}
複製程式碼

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

不同的語言實現將支援執行時庫傭工型別安全的方式打包和解包的任何值-例如,在Java中,任何型別都會有特殊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 ...
  }
}
複製程式碼

目前,正在開發用於處理Any型別的執行時庫

如果您已熟悉proto2語法,則Any型別將替換擴充套件

Oneof

如果您有一個包含許多欄位的訊息,並且最多隻能同時設定一個欄位,則可以使用oneof功能強制執行此行為並節省記憶體。

除了一個共享記憶體中的所有欄位之外,其中一個欄位類似於常規欄位,並且最多可以同時設定一個欄位。設定oneof的任何成員會自動清除所有其他成員。您可以使用特殊case()WhichOneof()方法檢查oneof中的哪個值(如果有),具體取決於您選擇的語言。

使用Oneof

要在您中定義oneof,請.proto使用oneof關鍵字後跟您的oneof名稱,在這種情況下test_oneof

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}
複製程式碼

然後,將oneof欄位新增到oneof定義中。您可以新增任何型別的欄位,但不能使用repeated欄位。

在生成的程式碼中,oneof欄位與常規欄位具有相同的getter和setter。您還可以使用特殊方法檢查oneof中的值(如果有)。您可以在相關API參考中找到有關所選語言的oneof API的更多資訊。

其中一個特點

  • 設定oneof欄位將自動清除oneof的所有其他成員。因此,如果您設定了多個欄位,則只有您設定的

    最後一個

    欄位仍然具有值。

    SampleMessage message;
    message.set_name("name");
    CHECK(message.has_name());
    message.mutable_sub_message();   // Will clear name field.
    CHECK(!message.has_name());
    複製程式碼
  • 如果解析器線上路上遇到同一個oneof的多個成員,則在解析的訊息中僅使用看到的最後一個成員。

  • 一個不可能repeated

  • Reflection API適用於其中一個欄位。

  • 如果您使用的是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 
    複製程式碼
  • 同樣在C ++中,如果你有Swap()兩個訊息與oneofs,每個訊息最終將與另一個訊息結果:在下面的例子中,msg1將有一個sub_messagemsg2並將有一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的被設定為一個欄位。沒有辦法區分,因為沒有辦法知道線上的未知欄位是否是其中一個成員。

標籤重用問題

  • 將欄位移入或移出oneof:在序列化和解析訊息後,您可能會丟失一些資訊(某些欄位將被清除)。但是,您可以安全地將單個欄位移動到新的 oneof中,並且如果已知只有一個欄位被設定,則可以移動多個欄位。
  • 刪除oneof欄位並將其新增回:在序列化和解析訊息後,這可能會清除當前設定的oneof欄位。
  • 拆分或合併oneof:這與移動常規欄位有類似的問題。

地圖

如果要在資料定義中建立關聯對映,協議緩衝區提供了一種方便的快捷方式語法:

map < key_type ,value_type > map_field = N ;
複製程式碼

...其中key_type可以是任何整數或字串型別(因此,除了浮點型別之外的任何標量型別bytes)。請注意,列舉不是有效的key_type。的value_type可以是任何型別的除另一地圖。

因此,例如,如果要建立專案對映,其中每條Project訊息都與字串鍵相關聯,則可以像下面這樣定義它:

map < string ,Project > projects = 3 ;  
複製程式碼
  • 地圖欄位不能repeated
  • 地圖值的有線格式排序和地圖迭代排序未定義,因此您不能依賴於特定順序的地圖專案。
  • 為a生成文字格式時.proto,地圖按鍵排序。數字鍵按數字排序。
  • 從線路解析或合併時,如果有重複的對映鍵,則使用最後看到的鍵。從文字格式解析對映時,如果存在重複鍵,則解析可能會失敗。
  • 如果為對映欄位提供鍵但沒有值,則欄位序列化時的行為取決於語言。在C ++,Java和Python中,型別的預設值是序列化的,而在其他語言中沒有任何序列化。

生成的地圖API目前可用於所有proto3支援的語言。您可以在相關API參考中找到有關所選語言的地圖API的更多資訊。

向後相容性

對映語法線上上等效於以下內容,因此不支援對映的協議緩衝區實現仍可處理您的資料:

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

repeated MapFieldEntry map_field = N;
複製程式碼

任何支援對映的協議緩衝區實現都必須生成和接受上述定義可以接受的資料。

您可以向.proto檔案新增package可選說明符,以防止協議訊息型別之間的名稱衝突。

package foo.bar;
message Open { ... }
複製程式碼

然後,您可以在定義訊息型別的欄位時使用包說明符:

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}
複製程式碼

包說明符影響生成的程式碼的方式取決於您選擇的語言:

  • C ++中,生成的類包含在C ++名稱空間中。例如,Open將在名稱空間中foo::bar
  • Java中,該包用作Java包,除非您option java_package.proto檔案中明確提供了該包。
  • Python中,package指令被忽略,因為Python模組是根據它們在檔案系統中的位置進行組織的。
  • Go中,該包用作Go包名稱,除非您option go_package.proto檔案中明確提供。
  • Ruby中,生成的類包含在巢狀的Ruby名稱空間內,轉換為所需的Ruby大寫形式(首字母大寫;如果第一個字元不是字母,PB_則前置)。例如,Open將在名稱空間中Foo::Bar
  • C#中,包轉換為PascalCase後用作名稱空間,除非您option csharp_namespace.proto檔案中明確提供。例如,Open將在名稱空間中Foo.Bar

包和名稱解析

協議緩衝區語言中的型別名稱解析與C ++類似:首先搜尋最裡面的範圍,然後搜尋下一個範圍,依此類推,每個包被認為是其父包的“內部”。一個領先的'。' (例如,.foo.bar.Baz)意味著從最外層的範圍開始。

protobuf 編譯器通過解析匯入的.proto檔案來解析所有型別名稱。每種語言的程式碼生成器都知道如何使用該語言引用每種型別,即使它具有不同的範圍規則。

定義服務

如果要將訊息型別與RPC(遠端過程呼叫)系統一起使用,則可以在.proto檔案中定義RPC服務介面,protobuf 編譯器將使用您選擇的語言生成服務介面程式碼和存根。因此,例如,如果要定義RPC服務請求方法為:SearchRequest和返回方法為:SearchResponse,可以.proto按如下方式在檔案中定義它:

service SearchService {
  rpc Search(SearchRequest)returns(SearchResponse);
}
複製程式碼

與協議緩衝區一起使用的最簡單的RPC系統是gRPC:一種由Google開發的,平臺中立的開源RPC系統。gRPC特別適用於protobuf,並允許在您的.proto檔案中使用特殊的protobuf 編譯器外掛直接生成相關的RPC程式碼。

如果您不想使用gRPC,也可以將protobuf與您自己的RPC實現一起使用。您可以在Proto2語言指南中找到更多相關資訊。

還有一些正在進行的第三方專案使用Protocol Buffers開發RPC實現。有關我們瞭解的專案的連結列表,請參閱第三方載入項wiki頁面

JSON對映

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

如果JSON編碼資料中缺少值null,或者其值為,則在解析為協議緩衝區時,它將被解釋為適當的預設值。如果欄位在協議緩衝區中具有預設值,則預設情況下將在JSON編碼資料中省略該欄位以節省空間。實現可以提供用於在JSON編碼的輸出中發出具有預設值的欄位的選項。

proto3 JSON JSON示例 筆記
message object {"fooBar": v, "g": null,…} 生成JSON物件。訊息欄位名稱對映到小寫駝峰併成為JSON物件鍵。如果json_name指定了field選項,則指定的值將用作鍵。解析器接受小寫駝峰名稱(或json_name選項指定的名稱)和原始proto欄位名稱。null是所有欄位型別的可接受值,並將其視為相應欄位型別的預設值。
eunm String "FOO_BAR" 使用proto中指定的列舉值的名稱。解析器接受列舉名稱和整數值。
map<K,V> object {"k": v, …} 所有鍵都轉換為字串。
repeated V. array [v, …] null 被接受為空列表[]。
bool true,false true, false
string string "Hello World!"
bytes base64 string "YWJjMTIzIT8kKiYoKSctPUB+" JSON值將是使用帶填充的標準base64編碼編碼為字串的資料。接受帶有/不帶填充的標準或URL安全base64編碼。
int32,fixed32,uint32 string 1, -10, 0 JSON值將是十進位制數。接受數字或字串。
int64,fixed64,uint64 string "1", "-10" JSON值將是十進位制字串。接受數字或字串。
float,double number 1.1, -10.0, 0, "NaN","Infinity" JSON值將是一個數字或一個特殊字串值“NaN”,“Infinity”和“-Infinity”。接受數字或字串。指數表示法也被接受。
any object {"@type": "url", "f": v, … } 如果Any包含具有特殊JSON對映的值,則將按如下方式進行轉換:。否則,該值將轉換為JSON物件,並將插入該欄位以指示實際的資料型別。{"@type": xxx, "value": yyy}``"@type"
Timestamp string "1972-01-01T10:00:20.021Z" 使用RFC 3339,其中生成的輸出將始終被Z標準化並使用0,3,6或9個小數位。也接受“Z”以外的偏移。
Duration string "1.000340012s", "1s" 生成的輸出始終包含0,3,6或9個小數位,具體取決於所需的精度,後跟字尾“s”。接受的是任何小數位(也沒有),只要它們符合納秒精度並且字尾“s”是必需的。
Struct object { … } 任何JSON物件。見。struct.proto
Wrapper types various types 2, "2", "foo", true,"true", null, 0, … 包裝器在JSON中使用與包裝基元型別相同的表示形式,除了null在資料轉換和傳輸期間允許和保留的表示形式。
FieldMask string "f.fooBar,h" 見。field_mask.proto
ListValue array [foo, bar, …]
Value value 任何JSON值
NullValue null JSON null

JSON選項

proto3 JSON實現可以提供以下選項:

  • 使用預設值發出欄位:預設情況下,proto3 JSON輸出中省略了具有預設值的欄位。實現可以提供覆蓋此行為的選項,並使用其預設值輸出欄位。
  • 忽略未知欄位:預設情況下,Proto3 JSON解析器應拒絕未知欄位,但可以提供忽略解析中未知欄位的選項。
  • 使用proto欄位名稱而不是小寫駝峰名稱:預設情況下,proto3 JSON印表機應將欄位名稱轉換為小寫駝峰並將其用作JSON名稱。實現可以提供使用proto欄位名稱作為JSON名稱的選項。Proto3 JSON解析器需要接受轉換後的小寫駝峰名稱和proto欄位名稱。
  • 將列舉值傳送為整數而不是字串:預設情況下,在JSON輸出中使用列舉值的名稱。可以提供選項以使用列舉值的數值。

選項

.proto檔案中的各個宣告可以使用許多選項進行註釋。選項不會更改宣告的整體含義,但可能會影響在特定上下文中處理它的方式。可用選項的完整列表在中定義google/protobuf/descriptor.proto

一些選項是檔案級選項,這意味著它們應該在頂級範圍內編寫,而不是在任何訊息,列舉或服務定義中。一些選項是訊息級選項,這意味著它們應該寫在訊息定義中。一些選項是欄位級選項,這意味著它們應該寫在欄位定義中。選項也可以寫在列舉型別,列舉值,服務型別和服務方法上; 但是,目前沒有任何有用的選擇。

以下是一些最常用的選項:

  • java_package(檔案選項):用於生成的Java類的包。如果.proto檔案中沒有給出顯式選項java_package,則預設情況下將使用proto包(使用檔案中的“package”關鍵字指定 .proto )。但是,proto包通常不能生成好的Java包,因為proto包不會以反向域名開頭。如果不生成Java程式碼,則此選項無效。

    option java_package =“com.example.foo”;
    複製程式碼
  • java_multiple_files (檔案選項):導致在包級別定義頂級訊息,列舉和服務,而不是在.proto檔案之後命名的外部類中。

option java_multiple_files = true;
複製程式碼
  • java_outer_classname(file option):要生成的最外層Java類(以及檔名)的類名。如果 .proto檔案中沒有指定 java_outer_classname,則通過將.proto檔名轉換為駝峰格式(因此 foo_bar.proto 成為FooBar.java)來構造類名。如果不生成Java程式碼,則此選項無效。
  option java_outer_classname =“Ponycopter”;
複製程式碼
  • optimize_for

    (檔案選項):可以設定為SPEEDCODE_SIZELITE_RUNTIME。這會以下列方式影響C ++和Java程式碼生成器(可能還有第三方生成器):

    • SPEED(預設值):protobuf 編譯器將生成用於對訊息型別進行序列化,解析和執行其他常見操作的程式碼。此程式碼經過高度優化。
    • CODE_SIZE:protobuf 編譯器將生成最少的類,並依賴於基於反射的共享程式碼來實現序列化,解析和各種其他操作。因此生成的程式碼比使用SPEED小得多,但操作會更慢。類仍將實現與SPEED模式完全相同的公共API 。此模式在包含非常大數量的.proto檔案的應用程式中最有用,並且不需要所有檔案都非常快速。
    • LITE_RUNTIME:protobuf 編譯器將生成僅依賴於“lite”執行時庫(libprotobuf-lite而不是libprotobuf)的類。精簡版執行時比整個庫小得多(大約小一個數量級),但省略了描述符和反射等特定功能。這對於在行動電話等受限平臺上執行的應用程式尤其有用。編譯器仍然會像在SPEED模式中一樣生成所有方法的快速實現。生成的類將僅實現MessageLite每種語言的介面,該介面僅提供完整Message介面的方法的子集。
    option optimize_for = CODE_SIZE;
    
    複製程式碼
  • cc_enable_arenas(檔案選項):為C ++生成的程式碼啟用競技場分配

  • objc_class_prefix(檔案選項):設定Objective-C類字首,該字首預先新增到此.proto的所有Objective-C生成的類和列舉中。沒有預設值。您應該使用Apple建議的 3-5個大寫字元之間的字首。請注意,Apple保留所有2個字母的字首。

  • deprecated(欄位選項):如果設定為true,則表示該欄位已棄用,新程式碼不應使用該欄位。在大多數語言中,這沒有實際效果。在Java中,這成為一個@Deprecated註釋。將來,其他特定於語言的程式碼生成器可能會在欄位的訪問器上生成棄用註釋,這將導致在編譯嘗試使用該欄位的程式碼時發出警告。如果任何人都沒有使用該欄位,並且您希望阻止新使用者使用該欄位,請考慮使用保留語句替換欄位宣告。

    int32 old_field = 6 [deprecated = true];
    
    複製程式碼

自定義選項

Protocol Buffers還允許您定義和使用自己的選項。這是大多數人不需要的高階功能。如果您確實認為需要建立自己的選項,請參閱Proto2語言指南以獲取詳細資訊。請注意,建立自定義選項使用的副檔名僅允許用於proto3中的自定義選項。

生成您的類

根據實際工作需要,生成以下對應語言的自定義訊息型別Java,Python,C ++,Go, Ruby, Objective-C,或C#的.proto檔案,你需要執行protobuf 編譯器protoc.proto。如果尚未安裝編譯器,請下載該軟體包並按照自述檔案中的說明進行操作。對於Go,您還需要為編譯器安裝一個特殊的程式碼生成器外掛:您可以在GitHub上的golang / protobuf儲存庫中找到這個和安裝說明。

Protobuf 編譯器的呼叫如下:

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 --objc_out = DST_DIR --csharp_out = DST_DIR  path / to / file .proto

複製程式碼
  • IMPORT_PATH指定.proto解析import指令時在其中查詢檔案的目錄。如果省略,則使用當前目錄。可以通過--proto_path多次傳遞選項來指定多個匯入目錄; 他們將按順序搜尋。 可以用作簡短的形式。 -I=*IMPORT_PATH*``--proto_path

  • 您可以提供一個或多個輸出指令:

    為了方便起見,如果DST_DIR結束於.zip或.jar,編譯器會將輸出寫入具有給定名稱的單個ZIP格式存檔檔案。.jar輸出還將根據Java JAR規範的要求提供清單檔案。請注意,如果輸出存檔已存在,則會被覆蓋; 編譯器不夠智慧,無法將檔案新增到現有存檔中。

  • 您必須提供一個或多個.proto檔案作為輸入。.proto可以一次指定多個檔案。雖然檔案是相對於當前目錄命名的,但每個檔案必須位於其中一個檔案中,IMPORT_PATH以便編譯器可以確定其規範名稱。

相關文章