語言指南 (proto 2)

涵蓋如何在您的專案中使用 Protocol Buffers 語言的 proto2 修訂版。

本指南說明如何使用 protocol buffer 語言來結構化您的 protocol buffer 資料,包括 .proto 檔案語法以及如何從您的 .proto 檔案產生資料存取類別。它涵蓋 protocol buffers 語言的 proto2 修訂版。

如需 版本 語法的相關資訊,請參閱 Protobuf 版本語言指南

如需 proto3 語法的相關資訊,請參閱 Proto3 語言指南

這是一份參考指南 – 如需逐步範例,說明如何使用本文件中描述的許多功能,請參閱您選擇的語言的教學課程

定義訊息類型

首先,讓我們看一個非常簡單的範例。假設您想要定義搜尋要求訊息格式,其中每個搜尋要求都有查詢字串、您感興趣的特定結果頁面,以及每頁的結果數。以下是用來定義訊息類型的 .proto 檔案。

syntax = "proto2";

message SearchRequest {
  optional string query = 1;
  optional int32 page_number = 2;
  optional int32 results_per_page = 3;
}
  • 檔案的第一行指定您正在使用 protobuf 語言規格的 proto2 修訂版。

    • edition (或 proto2/proto3 的 syntax) 必須是檔案的第一個非空白、非註解行。
    • 如果未指定任何 editionsyntax,protocol buffer 編譯器將會假設您正在使用 proto2
  • SearchRequest 訊息定義指定三個欄位(名稱/值組),每個欄位對應您想要包含在此訊息類型中的每一筆資料。每個欄位都有一個名稱和一個類型。

指定欄位類型

在先前的範例中,所有欄位都是純量類型:兩個整數 (page_numberresults_per_page) 和一個字串 (query)。您也可以為您的欄位指定列舉和複合類型,例如其他訊息類型。

指派欄位編號

您必須為訊息定義中的每個欄位指定介於 1536,870,911 之間的數字,但有下列限制

  • 給定的數字在該訊息的所有欄位中必須是唯一的
  • 欄位編號 19,00019,999 保留給 Protocol Buffers 實作使用。如果您在訊息中使用其中一個保留的欄位編號,protocol buffer 編譯器將會抱怨。
  • 您不能使用任何先前保留的欄位編號,或任何已配置給擴充的欄位編號。

此編號一旦您的訊息類型正在使用中就無法變更,因為它會識別訊息線路格式中的欄位。「變更」欄位編號相當於刪除該欄位並建立一個具有相同類型但編號不同的新欄位。請參閱刪除欄位,了解如何正確執行此動作。

欄位編號絕不應重複使用。絕不應將欄位編號從保留清單中取出,以用於新的欄位定義。請參閱重複使用欄位編號的後果

您應將欄位編號 1 到 15 用於最常設定的欄位。較低的欄位編號值在線路格式中佔用的空間較小。例如,範圍 1 到 15 的欄位編號需要一個位元組進行編碼。範圍 16 到 2047 的欄位編號需要兩個位元組。您可以在Protocol Buffer 編碼中找到更多相關資訊。

重複使用欄位編號的後果

重複使用欄位編號會使解碼線路格式訊息變得模糊不清。

protobuf 線路格式很精簡,並未提供方法來偵測使用一個定義編碼並使用另一個定義解碼的欄位。

使用一個定義編碼欄位,然後使用不同的定義解碼同一個欄位可能會導致

  • 開發人員時間浪費在偵錯上
  • 剖析/合併錯誤 (最佳情況)
  • 洩漏 PII/SPII
  • 資料損毀

欄位編號重複使用的常見原因

  • 重新編號欄位 (有時為了達到更美觀的欄位編號順序而執行)。重新編號會有效刪除並重新新增所有牽涉在重新編號中的欄位,導致不相容的線路格式變更。

  • 刪除欄位而未保留該編號以防止未來重複使用。

    • 由於幾個原因,這一直是一個很容易在擴充欄位中發生的錯誤。擴充宣告提供一種保留擴充欄位的機制。

欄位編號限制為 29 位元,而不是 32 位元,因為使用三個位元來指定欄位的線路格式。如需更多相關資訊,請參閱編碼主題

指定欄位基數

訊息欄位可以是下列其中之一

  • 單一:

    在 proto2 中,有兩種單一欄位類型

    • optional:(建議) optional 欄位處於兩種可能的狀態之一

      • 已設定欄位,且包含明確設定或從線路剖析的值。它將序列化到線路。
      • 未設定欄位,並將傳回預設值。它不會序列化到線路。

      您可以檢查該值是否已明確設定。

    • required請勿使用。必要的欄位問題重重,因此已從 proto3 和版本中移除。必要的欄位語意應在應用程式層級實作。當使用時,格式正確的訊息必須正好有一個此欄位。

  • repeated:此欄位類型可以在格式正確的訊息中重複零次或多次。重複值的順序將會保留。

  • map:這是一種成對的鍵/值欄位類型。請參閱對應,了解有關此欄位類型的更多資訊。

針對新的重複欄位使用封裝編碼

由於歷史原因,純量數值類型的 repeated 欄位 (例如,int32int64enum) 的編碼效率不如它們可以的。新的程式碼應使用特殊選項 [packed = true] 來取得更有效率的編碼。例如

repeated int32 samples = 4 [packed = true];
repeated ProtoEnum results = 5 [packed = true];

您可以在Protocol Buffer 編碼中找到更多關於 packed 編碼的資訊。

Required 已強烈不建議使用

required 欄位的第二個問題發生在有人將值新增至列舉時。在這種情況下,無法識別的列舉值會被視為遺失,這也會導致 required 值檢查失敗。

格式正確的訊息

當應用於 protobuf 訊息時,「格式正確」一詞指的是序列化/反序列化的位元組。protoc 剖析器會驗證給定的 proto 定義檔案是否可剖析。

單數欄位在線格式位元組中可能會出現多次。剖析器會接受輸入,但只有該欄位的最後一個實例可透過產生的繫結存取。請參閱後者優先以取得更多相關資訊。

新增更多訊息類型

可以在單一 .proto 檔案中定義多種訊息類型。如果您要定義多個相關的訊息,這會很有用 – 因此,舉例來說,如果您想要定義與您的 SearchResponse 訊息類型相對應的回覆訊息格式,您可以將其新增至相同的 .proto

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

message SearchResponse {
 ...
}

合併訊息會導致臃腫 雖然可以在單一 .proto 檔案中定義多種訊息類型 (例如訊息、列舉和服務),但是當在單一檔案中定義大量具有不同相依性的訊息時,也可能導致相依性臃腫。建議每個 .proto 檔案包含盡可能少的訊息類型。

新增註解

若要在您的 .proto 檔案中新增註解

  • 優先使用 C/C++/Java 行尾樣式的註解 ‘//’,在 .proto 程式碼元素的前一行。

  • 也接受 C 樣式的內嵌/多行註解 /* ... */

    • 使用多行註解時,建議使用 ‘*’ 的邊界線。
/**
 * SearchRequest represents a search query, with pagination options to
 * indicate which results to include in the response.
 */
message SearchRequest {
  optional string query = 1;

  // Which page number do we want?
  optional int32 page_number = 2;

  // Number of results to return per page.
  optional int32 results_per_page = 3;
}

刪除欄位

如果未正確執行,刪除欄位可能會導致嚴重問題。

請勿刪除 required 欄位。這幾乎不可能安全地完成。如果您必須刪除 required 欄位,您應該先將欄位標記為 optionaldeprecated,並確保以新的結構描述部署所有以任何方式觀察訊息的系統。然後,您可以考慮移除欄位 (但請注意,這仍然是一個容易出錯的程序)。

當您不再需要不是 required 的欄位時,請先從用戶端程式碼中刪除對該欄位的所有參考,然後從訊息中刪除欄位定義。但是,您必須 保留已刪除的欄位編號。如果您不保留欄位編號,開發人員可能會在未來重複使用該編號並導致中斷。

您也應該保留欄位名稱,以允許繼續剖析訊息的 JSON 和 TextFormat 編碼。

保留欄位編號

如果您透過完全刪除欄位或將其註解掉來更新訊息類型,則未來開發人員可以在對類型進行自己的更新時重複使用欄位編號。如重複使用欄位編號的後果中所述,這可能會導致嚴重問題。為了確保不會發生這種情況,請將您刪除的欄位編號新增至 reserved 清單。

如果任何未來的開發人員嘗試使用這些保留的欄位編號,protoc 編譯器將會產生錯誤訊息。

message Foo {
  reserved 2, 15, 9 to 11;
}

保留的欄位編號範圍是包含性的 (9 to 119, 10, 11 相同)。

保留欄位名稱

稍後重複使用舊的欄位名稱通常是安全的,除非在使用 TextProto 或 JSON 編碼時,會序列化欄位名稱。若要避免此風險,您可以將已刪除的欄位名稱新增至 reserved 清單。

保留名稱只會影響 protoc 編譯器的行為,而不會影響執行階段行為,但有一個例外:TextProto 實作可能會在剖析時捨棄具有保留名稱的未知欄位 (而不會像其他未知欄位一樣引發錯誤) (目前只有 C++ 和 Go 實作會這樣做)。執行階段 JSON 剖析不受保留名稱的影響。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

請注意,您無法在同一個 reserved 陳述式中混合欄位名稱和欄位編號。

從您的 .proto 產生什麼?

當您在 .proto 上執行protocol buffer 編譯器時,編譯器會在您選擇的語言中產生您需要用來處理您在檔案中描述的訊息類型的程式碼,包括取得和設定欄位值、將訊息序列化到輸出串流,以及從輸入串流剖析訊息。

  • 對於 C++,編譯器會從每個 .proto 產生 .h.cc 檔案,並為您檔案中描述的每個訊息類型產生一個類別。
  • 對於 Java,編譯器會產生一個 .java 檔案,其中包含每個訊息類型的類別,以及一個特殊的 Builder 類別,用於建立訊息類別的實例。
  • 對於 Kotlin,除了產生的 Java 程式碼之外,編譯器還會為每個訊息類型產生一個 .kt 檔案,其中包含經過改良的 Kotlin API。這包括一個簡化建立訊息實例的 DSL、一個可為 null 的欄位存取器和一個複製函式。
  • Python 有點不同 – Python 編譯器會產生一個模組,其中包含您 .proto 中每個訊息類型的靜態描述符,然後會使用元類別在執行階段建立必要的 Python 資料存取類別。
  • 對於 Go,編譯器會產生一個 .pb.go 檔案,其中包含您檔案中每個訊息類型的類型。
  • 對於 Ruby,編譯器會產生一個 .rb 檔案,其中包含一個包含訊息類型的 Ruby 模組。
  • 對於 Objective-C,編譯器會從每個 .proto 產生 pbobjc.hpbobjc.m 檔案,並為您檔案中描述的每個訊息類型產生一個類別。
  • 對於 C#,編譯器會從每個 .proto 產生一個 .cs 檔案,並為您檔案中描述的每個訊息類型產生一個類別。
  • 對於 PHP,編譯器會為您檔案中描述的每個訊息類型產生一個 .php 訊息檔案,並為您編譯的每個 .proto 檔案產生一個 .php 中繼資料檔案。中繼資料檔案用於將有效的訊息類型載入描述符池中。
  • 對於 Dart,編譯器會產生一個 .pb.dart 檔案,其中包含您檔案中每個訊息類型的類別。

您可以按照您選擇的語言的教學課程,找到更多關於使用每種語言 API 的資訊。如需更多 API 詳細資訊,請參閱相關的API 參考

純量值類型

純量訊息欄位可以具有下列類型之一 – 下表顯示 .proto 檔案中指定的類型,以及自動產生類別中的對應類型

.proto 類型註解C++ 類型Java/Kotlin 類型[1]Python 類型[3]Go 類型Ruby 類型C# 類型PHP 類型Dart 類型Rust 類型
doubledoubledoublefloat*float64Floatdoublefloatdoublef64
floatfloatfloatfloat*float32Floatfloatfloatdoublef32
int32使用可變長度編碼。編碼負數效率不高 – 如果您的欄位可能具有負值,請改用 sint32。int32intintint32Fixnum 或 Bignum (視需要)intinteger*int32i32
int64使用可變長度編碼。編碼負數效率不高 – 如果您的欄位可能具有負值,請改用 sint64。int64longint/long[4]*int64Bignumlonginteger/string[6]Int64i64
uint32使用可變長度編碼。uint32int[2]int/long[4]*uint32Fixnum 或 Bignum (視需要)uintintegerintu32
uint64使用可變長度編碼。uint64long[2]int/long[4]*uint64Bignumulonginteger/string[6]Int64u64
sint32使用可變長度編碼。帶正負號的 int 值。這些編碼負數比常規 int32 更有效率。int32intintint32Fixnum 或 Bignum (視需要)intinteger*int32i32
sint64使用可變長度編碼。帶正負號的 int 值。這些編碼負數比常規 int64 更有效率。int64longint/long[4]*int64Bignumlonginteger/string[6]Int64i64
fixed32永遠是 4 個位元組。如果值通常大於 228,則比 uint32 更有效率。uint32int[2]int/long[4]*uint32Fixnum 或 Bignum (視需要)uintintegerintu32
fixed64永遠是 8 個位元組。如果值通常大於 256,則比 uint64 更有效率。uint64long[2]int/long[4]*uint64Bignumulonginteger/string[6]Int64u64
sfixed32永遠是 4 個位元組。int32intint*int32Fixnum 或 Bignum (視需要)intintegerinti32
sfixed64永遠是 8 個位元組。int64longint/long[4]*int64Bignumlonginteger/string[6]Int64i64
boolboolbooleanbool*boolTrueClass/FalseClassboolbooleanboolbool
string字串必須永遠包含 UTF-8 編碼或 7 位元 ASCII 文字,且不能超過 232stringStringunicode (Python 2) 或 str (Python 3)*stringString (UTF-8)stringstringStringProtoString
bytes可能包含任何任意位元組序列,不得超過 232stringByteStringbytes[]byteString (ASCII-8BIT)ByteStringstringListProtoBytes

[1] Kotlin 使用 Java 的對應類型,即使是不帶正負號的類型也是如此,以確保在混合 Java/Kotlin 程式碼庫中的相容性。

[2] 在 Java 中,不帶正負號的 32 位元和 64 位元整數會使用其帶正負號的對應項表示,而最高位元只會儲存在正負號位元中。

[3] 在所有情況下,將值設定為欄位時都會執行類型檢查,以確保其有效。

[4] 64 位元或不帶正負號的 32 位元整數在解碼時永遠會表示為 long,但是如果在設定欄位時給定 int,則可以是 int。在所有情況下,該值必須適合設定時表示的類型。請參閱 [2]。

[5] Proto2 通常永遠不會檢查字串欄位的 UTF-8 有效性。行為因語言而異,不應將無效的 UTF-8 資料儲存在字串欄位中。

[6] 在 64 位元機器上使用整數,而在 32 位元機器上使用字串。

您可以在Protocol Buffer 編碼中找到更多關於當您序列化訊息時如何編碼這些類型的資訊。

預設欄位值

當解析訊息時,如果編碼後的訊息位元組不包含特定的欄位,則存取解析物件中的該欄位會傳回該欄位的預設值。預設值是類型特定的。

  • 對於字串,預設值為空字串。
  • 對於位元組,預設值為空位元組。
  • 對於布林值,預設值為 false。
  • 對於數值類型,預設值為零。
  • 對於訊息欄位,該欄位未設定。其確切的值取決於程式語言。請參閱您所用語言的產生程式碼指南以取得詳細資訊。
  • 對於列舉 (enums),預設值是第一個定義的列舉值,應該為 0(建議與 proto3 相容)。請參閱列舉預設值

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

對應欄位的預設值為空(通常是適當語言中的空對應)。

覆寫預設純量值

在 proto2 中,您可以為單數的非訊息欄位指定明確的預設值。例如,假設您要為 SearchRequest.result_per_page 欄位提供預設值 10。

optional int32 result_per_page = 3 [default = 10];

如果傳送者未指定 result_per_page,則接收者將觀察到以下狀態:

  • result_per_page 欄位不存在。也就是說,has_result_per_page()(hazzer 方法)會傳回 false
  • result_per_page 的值(從「getter」傳回)為 10

如果傳送者確實傳送了 result_per_page 的值,則會忽略預設值 10,並從「getter」傳回傳送者的值。

請參閱您所選語言的產生程式碼指南,以取得有關預設值如何在產生程式碼中運作的更多詳細資訊。

由於列舉的預設值是第一個定義的列舉值,因此在將值新增至列舉值清單的開頭時請務必小心。請參閱更新訊息類型章節,以取得有關如何安全地變更定義的指南。

列舉

當您定義訊息類型時,您可能希望其某個欄位只能具有預先定義的值清單中的一個值。例如,假設您要為每個 SearchRequest 新增一個 corpus 欄位,其中語料庫可以是 UNIVERSALWEBIMAGESLOCALNEWSPRODUCTSVIDEO。您可以透過在您的訊息定義中新增一個 enum,並為每個可能的值新增一個常數,來非常簡單地完成此操作。

在以下範例中,我們新增了一個名為 Corpusenum,其中包含所有可能的值,以及一個 Corpus 類型的欄位。

enum Corpus {
  CORPUS_UNSPECIFIED = 0;
  CORPUS_UNIVERSAL = 1;
  CORPUS_WEB = 2;
  CORPUS_IMAGES = 3;
  CORPUS_LOCAL = 4;
  CORPUS_NEWS = 5;
  CORPUS_PRODUCTS = 6;
  CORPUS_VIDEO = 7;
}

message SearchRequest {
  optional string query = 1;
  optional int32 page_number = 2;
  optional int32 results_per_page = 3;
  optional Corpus corpus = 4;
}

列舉預設值

SearchRequest.corpus 欄位的預設值為 CORPUS_UNSPECIFIED,因為它是列舉中定義的第一個值。

強烈建議將每個列舉的第一個值定義為 ENUM_TYPE_NAME_UNSPECIFIED = 0;ENUM_TYPE_NAME_UNKNOWN = 0;。這是因為 proto2 處理列舉欄位未知值的方式。

也建議第一個預設值除了「此值未指定」之外,沒有其他語義含義。

可以像這樣明確覆寫列舉欄位(如 SearchRequest.corpus 欄位)的預設值。

  optional Corpus corpus = 4 [default = CORPUS_UNIVERSAL];

列舉值別名

您可以透過將相同的值指派給不同的列舉常數來定義別名。若要執行此操作,您需要將 allow_alias 選項設定為 true。否則,當找到別名時,協定緩衝區編譯器會產生警告訊息。雖然在還原序列化期間所有別名值都有效,但在序列化時始終會使用第一個值。

enum EnumAllowingAlias {
  option allow_alias = true;
  EAA_UNSPECIFIED = 0;
  EAA_STARTED = 1;
  EAA_RUNNING = 1;
  EAA_FINISHED = 2;
}

enum EnumNotAllowingAlias {
  ENAA_UNSPECIFIED = 0;
  ENAA_STARTED = 1;
  // ENAA_RUNNING = 1;  // Uncommenting this line will cause a warning message.
  ENAA_FINISHED = 2;
}

列舉器常數必須在 32 位元整數的範圍內。由於 enum 值在傳輸時使用 varint 編碼,因此負值效率低下,因此不建議使用。您可以在訊息定義內(如先前的範例)或外部定義 enum,這些 enum 可以在您的 .proto 檔案中的任何訊息定義中重複使用。您也可以使用在一個訊息中宣告的 enum 類型作為不同訊息中的欄位類型,使用語法 _MessageType_._EnumType_

當您在使用了 enum.proto 上執行協定緩衝區編譯器時,產生的程式碼將針對 Java、Kotlin 或 C++ 具有對應的 enum,或針對 Python 具有特殊的 EnumDescriptor 類別,用於在執行階段產生的類別中建立一組具有整數值的符號常數。

移除列舉值對於持續性 proto 而言是一項破壞性變更。與其移除值,不如使用 reserved 關鍵字標記該值,以防止產生該列舉值的程式碼,或保留該值但使用 deprecated 欄位選項表示稍後將移除該值。

enum PhoneType {
  PHONE_TYPE_UNSPECIFIED = 0;
  PHONE_TYPE_MOBILE = 1;
  PHONE_TYPE_HOME = 2;
  PHONE_TYPE_WORK = 3 [deprecated=true];
  reserved 4,5;
}

如需更多有關如何在您的應用程式中使用訊息 enum 的資訊,請參閱您所選語言的產生程式碼指南

保留值

如果您透過完全移除列舉項目或將其註解掉來更新列舉類型,則未來的使用者可以在對類型進行自己的更新時重複使用數值。如果他們稍後載入相同 .proto 的舊執行個體,這可能會導致嚴重的問題,包括資料損壞、隱私權錯誤等等。確保不會發生這種情況的一種方法是指定您已刪除項目的數值(和/或名稱,這也可能導致 JSON 序列化出現問題)為 reserved。如果任何未來的使用者嘗試使用這些識別碼,協定緩衝區編譯器將會抱怨。您可以使用 max 關鍵字指定保留的數值範圍上升到最大可能值。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

請注意,您無法在同一個 reserved 陳述式中混合欄位名稱和數值。

使用其他訊息類型

您可以使用其他訊息類型作為欄位類型。例如,假設您要在每個 SearchResponse 訊息中包含 Result 訊息 – 若要執行此操作,您可以在相同的 .proto 中定義 Result 訊息類型,然後在 SearchResponse 中指定 Result 類型的欄位。

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  optional string url = 1;
  optional string title = 2;
  repeated string snippets = 3;
}

匯入定義

在先前的範例中,Result 訊息類型是在與 SearchResponse 相同的文件中定義的 – 如果您要用作欄位類型的訊息類型已在另一個 .proto 檔案中定義,該怎麼辦?

您可以透過匯入來使用其他 .proto 檔案中的定義。若要匯入其他 .proto 的定義,請在您的檔案頂部新增一個匯入陳述式。

import "myproject/other_protos.proto";

根據預設,您只能使用直接匯入的 .proto 檔案中的定義。但是,有時您可能需要將 .proto 檔案移至新的位置。與其直接移動 .proto 檔案並在單一變更中更新所有呼叫站點,不如將預留位置 .proto 檔案放在舊位置,以使用 import public 概念將所有匯入轉發到新位置。

請注意,Java、Kotlin、TypeScript、JavaScript、GCL 以及使用 protobuf 靜態反映的 C++ 目標中無法使用公用匯入功能。

任何匯入包含 import public 陳述式的 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 旗標,在協定編譯器命令列上指定的一組目錄中搜尋匯入的檔案。如果未提供任何旗標,它會在叫用編譯器的目錄中尋找。一般而言,您應該將 --proto_path 旗標設定為專案的根目錄,並對所有匯入使用完整合格名稱。

使用 proto3 訊息類型

可以匯入 proto3 訊息類型,並在您的 proto2 訊息中使用它們,反之亦然。但是,proto2 列舉不能直接在 proto3 語法中使用(如果匯入的 proto2 訊息使用它們則沒問題)。

巢狀類型

您可以在其他訊息類型內定義和使用訊息類型,如下列範例所示 – 此處 Result 訊息是在 SearchResponse 訊息內定義的。

message SearchResponse {
  message Result {
    optional string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

如果您想在父訊息類型之外重複使用此訊息類型,請將其稱為 _Parent_._Type_

message SomeOtherMessage {
  optional SearchResponse.Result result = 1;
}

您可以根據自己的喜好巢狀訊息。在以下範例中,請注意,兩個名為 Inner 的巢狀類型完全獨立,因為它們是在不同的訊息內定義的。

message Outer {       // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      optional int64 ival = 1;
      optional bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      optional int32  ival = 1;
      optional bool   booly = 2;
    }
  }
}

群組

請注意,群組功能已被取代,在建立新的訊息類型時不應使用。請改用巢狀訊息類型。

群組是在您的訊息定義中巢狀資訊的另一種方式。例如,指定包含多個 ResultSearchResponse 的另一種方式如下:

message SearchResponse {
  repeated group Result = 1 {
    optional string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
}

群組只是將巢狀訊息類型和欄位組合成單一宣告。在您的程式碼中,您可以將此訊息視為它具有一個名為 resultResult 類型欄位(後者名稱會轉換為小寫,因此它不會與前者衝突)。因此,此範例與先前的 SearchResponse 完全相同,唯一的區別是訊息具有不同的傳輸格式

更新訊息類型

如果現有的訊息類型不再滿足您的所有需求 – 例如,您希望訊息格式具有額外的欄位 – 但您仍然想使用以舊格式建立的程式碼,請別擔心!當您使用二進位傳輸格式時,更新訊息類型而不會破壞任何現有程式碼非常簡單。

請查看Proto 最佳實務和以下規則:

  • 請勿變更任何現有欄位的欄位編號。「變更」欄位編號相當於刪除該欄位並新增一個具有相同類型的新欄位。如果您想要重新編號欄位,請參閱刪除欄位的說明。
  • 您新增的任何新欄位都應該是 optionalrepeated。這表示使用「舊」訊息格式的程式碼序列化的任何訊息仍然可以由您新的產生程式碼解析,因為它們不會遺失任何 required 元素。您應該記住這些元素的預設值,以便新程式碼可以正確地與舊程式碼產生的訊息互動。同樣地,您的新程式碼建立的訊息可以由您的舊程式碼解析:舊二進位檔案在解析時只會忽略新欄位。但是,不會捨棄未知的欄位,如果訊息稍後被序列化,則未知的欄位會與其一起序列化 – 因此,如果訊息被傳遞到新程式碼,新欄位仍然可用。請參閱未知欄位章節以取得詳細資訊。
  • 可以移除非必要欄位,只要您更新的訊息類型不再使用該欄位編號。您可能想要改為重新命名該欄位,或許新增字首「OBSOLETE_」,或將欄位編號保留,以便您 .proto 的未來使用者不會意外地重複使用該編號。
  • 非必要欄位可以轉換為擴充功能,反之亦然,只要類型和編號保持不變即可。
  • int32uint32int64uint64bool 都是相容的 – 這表示您可以將欄位從這些類型中的一種更改為另一種,而不會破壞向前或向後相容性。如果從網路解析的數字不符合對應的類型,您將獲得與在 C++ 中將數字轉換為該類型相同的效果(例如,如果將 64 位元數字讀取為 int32,它將被截斷為 32 位元)。
  • sint32sint64 彼此相容,但與其他整數類型相容。
  • stringbytes 只要位元組是有效的 UTF-8,就相容。
  • 如果位元組包含訊息的編碼實例,則嵌入式訊息與 bytes 相容。
  • fixed32sfixed32 相容,而 fixed64sfixed64 相容。
  • 對於 stringbytes 和訊息欄位,單數與 repeated 相容。如果輸入的是重複欄位的序列化資料,則期望此欄位為單數的客戶端會取得最後一個輸入值(如果它是原始類型欄位),或者合併所有輸入元素(如果它是訊息類型欄位)。請注意,這對於數值類型(包括布林值和列舉值)通常是安全的。數值類型的重複欄位可以採用 packed 格式進行序列化,當預期為單數欄位時,將無法正確解析。
  • 更改預設值通常是可以的,只要您記住預設值永遠不會通過網路傳送。因此,如果程式收到訊息,其中沒有設定特定欄位,程式將看到該程式版本的協定中定義的預設值。它將不會看到發送者程式碼中定義的預設值。
  • 就網路格式而言,enumint32uint32int64uint64 相容(請注意,如果值不符合,則會被截斷)。但是,請注意,當訊息反序列化時,客戶端程式碼可能會以不同的方式處理它們。值得注意的是,當訊息反序列化時,無法識別的 enum 值會被捨棄,這會導致欄位的 has.. 存取器返回 false,並且其 getter 返回 enum 定義中列出的第一個值,如果指定了預設值,則返回預設值。對於重複的列舉欄位,任何無法識別的值都會從清單中移除。但是,整數欄位始終會保留其值。因此,當在網路接收超出範圍的列舉值時,將整數升級為 enum 時需要非常小心。
  • 在目前的 Java 和 C++ 實作中,當無法識別的 enum 值被移除時,它們會與其他未知欄位一起儲存。請注意,如果此資料被序列化,然後由可識別這些值的客戶端重新解析,則可能會導致奇怪的行為。在可選欄位的情況下,即使在原始訊息反序列化後寫入了新值,可識別它的客戶端仍會讀取舊值。在重複欄位的情況下,舊值會出現在任何已識別且新加入的值之後,這表示不會保留順序。
  • 將單個 optional 欄位或擴充功能變更為新的 oneof 的成員是二進位相容的,但是對於某些語言(特別是 Go),產生的程式碼的 API 將以不相容的方式變更。因此,Google 不會在公共 API 中進行此類變更,如 AIP-180 中所述。關於原始碼相容性的相同警告,如果您確定沒有程式碼一次設定多個欄位,則將多個欄位移至新的 oneof 中可能是安全的。將欄位移至現有的 oneof 是不安全的。同樣地,將單個欄位 oneof 變更為 optional 欄位或擴充功能是安全的。
  • map<K, V> 和對應的 repeated 訊息欄位之間變更欄位是二進位相容的(請參閱下面的 映射,了解訊息佈局和其他限制)。但是,變更的安全性取決於應用程式:當反序列化和重新序列化訊息時,使用 repeated 欄位定義的客戶端將產生語義上相同的結果;但是,使用 map 欄位定義的客戶端可能會重新排序項目並刪除具有重複鍵的項目。

未知欄位

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

最初,proto3 訊息在解析期間總是捨棄未知欄位,但在 3.5 版本中,我們重新引入了保留未知欄位的機制,以符合 proto2 的行為。在 3.5 和更高版本中,未知欄位會在解析期間保留,並包含在序列化輸出中。

保留未知欄位

某些動作可能會導致未知欄位遺失。例如,如果您執行下列其中一個動作,則會遺失未知欄位

  • 將 proto 序列化為 JSON。
  • 迭代訊息中的所有欄位以填入新訊息。

為了避免遺失未知欄位,請執行下列動作

  • 使用二進位;避免使用文字格式進行資料交換。
  • 使用面向訊息的 API,例如 CopyFrom()MergeFrom() 來複製資料,而不是逐欄位複製

TextFormat 有點特殊。序列化為 TextFormat 會使用其欄位編號列印未知欄位。但是,如果存在使用欄位編號的項目,則將 TextFormat 資料解析回二進位 proto 會失敗。

擴充

擴充功能是在其容器訊息之外定義的欄位;通常在與容器訊息的 .proto 檔案不同的 .proto 檔案中。

為何使用擴充?

使用擴充功能主要有兩個原因

  • 容器訊息的 .proto 檔案將具有較少的匯入/相依性。這可以縮短建置時間、打破循環相依性,並以其他方式促進鬆散耦合。擴充功能在這方面非常好。
  • 允許系統以最小的相依性和協調將資料附加到容器訊息。擴充功能並不是此方面的絕佳解決方案,因為欄位編號空間有限,以及重複使用欄位編號的後果。如果您的使用案例需要大量擴充功能的極低協調,請考慮改用 Any 訊息類型

擴充範例

讓我們來看一個擴充功能範例

// file kittens/video_ext.proto

import "kittens/video.proto";
import "media/user_content.proto";

package kittens;

// This extension allows kitten videos in a media.UserContent message.
extend media.UserContent {
  // Video is a message imported from kittens/video.proto
  repeated Video kitten_videos = 126;
}

請注意,定義擴充功能的檔案 (kittens/video_ext.proto) 匯入了容器訊息的檔案 (media/user_content.proto)。

容器訊息必須為擴充功能保留其欄位編號的子集。

// file media/user_content.proto

package media;

// A container message to hold stuff that a user has created.
message UserContent {
  // Set verification to `DECLARATION` to enforce extension declarations for all
  // extensions in this range.
  extensions 100 to 199 [verification = DECLARATION];
}

容器訊息的檔案 (media/user_content.proto) 定義了訊息 UserContent,它為擴充功能保留了欄位編號 [100 到 199]。建議為範圍設定 verification = DECLARATION,以要求其所有擴充功能的宣告。

當新增新的擴充功能 (kittens/video_ext.proto) 時,應將對應的宣告新增至 UserContent,並且應移除 verification

// A container message to hold stuff that a user has created.
message UserContent {
  extensions 100 to 199 [
    declaration = {
      number: 126,
      full_name: ".kittens.kitten_videos",
      type: ".kittens.Video",
      repeated: true
    },
    // Ensures all field numbers in this extension range are declarations.
    verification = DECLARATION
  ];
}

UserContent 宣告欄位編號 126 將由具有完整名稱 .kittens.kitten_videos 和完整類型 .kittens.Videorepeated 擴充欄位使用。若要深入了解擴充功能宣告,請參閱 擴充功能宣告

請注意,容器訊息的檔案 (media/user_content.proto) 匯入 kitten_video 擴充功能定義 (kittens/video_ext.proto)

與具有相同欄位編號、類型和基數的標準欄位相比,擴充功能欄位的網路格式編碼沒有差異。因此,只要欄位編號、類型和基數保持不變,將標準欄位從其容器移出作為擴充功能,或將擴充功能欄位移入其容器訊息作為標準欄位都是安全的。

但是,由於擴充功能是在容器訊息之外定義的,因此不會產生專用的存取器來取得和設定特定擴充功能欄位。對於我們的範例,protobuf 編譯器不會產生 AddKittenVideos()GetKittenVideos() 存取器。相反地,擴充功能會透過參數化的函式存取,例如:HasExtension()ClearExtension()GetExtension()MutableExtension()AddExtension()

在 C++ 中,它看起來會像這樣

UserContent user_content;
user_content.AddExtension(kittens::kitten_videos, new kittens::Video());
assert(1 == user_content.GetExtensionCount(kittens::kitten_videos));
user_content.GetExtension(kittens::kitten_videos, 0);

定義擴充範圍

如果您是容器訊息的所有者,您將需要為訊息的擴充功能定義擴充功能範圍。

分配給擴充功能欄位的欄位編號不能重複用於標準欄位。

在定義擴充功能範圍後擴充它是安全的。一個好的預設值是分配 1000 個相對較小的數字,並使用擴充功能宣告密集地填滿該空間

message ModernExtendableMessage {
  // All extensions in this range should use extension declarations.
  extensions 1000 to 2000 [verification = DECLARATION];
}

在實際擴充功能之前新增擴充功能宣告的範圍時,您應該新增 verification = DECLARATION,以強制執行宣告用於此新範圍。一旦新增了實際宣告,就可以移除此預留位置。

將現有的擴充功能範圍拆分為涵蓋相同總範圍的單獨範圍是安全的。對於將舊式訊息類型遷移到 擴充功能宣告 可能有必要這樣做。例如,在遷移之前,範圍可能定義為

message LegacyMessage {
  extensions 1000 to max;
}

遷移後(拆分範圍),它可以是

message LegacyMessage {
  // Legacy range that was using an unverified allocation scheme.
  extensions 1000 to 524999999 [verification = UNVERIFIED];
  // Current range that uses extension declarations.
  extensions 525000000 to max  [verification = DECLARATION];
}

增加開始欄位編號或減少結束欄位編號來移動或縮小擴充功能範圍是不安全的。這些變更可能會使現有的擴充功能失效。

對於大多數 proto 執行個體中填入的標準欄位,最好使用欄位編號 1 到 15。不建議將這些數字用於擴充功能。

如果您的編號慣例可能涉及擴充功能具有非常大的欄位編號,您可以使用 max 關鍵字指定擴充功能範圍達到最大可能的欄位編號

message Foo {
  extensions 1000 to max;
}

max 是 229 - 1 或 536,870,911。

選擇擴充編號

擴充功能只是可以在其容器訊息之外指定的欄位。所有相同的分配欄位編號規則都適用於擴充功能欄位編號。相同的重複使用欄位編號的後果也適用於重複使用擴充功能欄位編號。

如果容器訊息使用擴充功能宣告,則選擇唯一的擴充功能欄位編號很簡單。在定義新的擴充功能時,請從容器訊息中定義的最高擴充功能範圍中選擇高於所有其他宣告的最低欄位編號。例如,如果容器訊息定義如下

message Container {
  // Legacy range that was using an unverified allocation scheme
  extensions 1000 to 524999999;
  // Current range that uses extension declarations. (highest extension range)
  extensions 525000000 to max  [
    declaration = {
      number: 525000001,
      full_name: ".bar.baz_ext",
      type: ".bar.Baz"
    }
    // 525,000,002 is the lowest field number above all other declarations
  ];
}

Container 的下一個擴充功能應新增編號為 525000002 的新宣告。

未驗證的擴充功能編號分配(不建議)

容器訊息的所有者可以選擇放棄擴充功能宣告,而採用他們自己的未驗證擴充功能編號分配策略。

未驗證的分配方案使用 protobuf 生態系統外部的機制,在選定的擴充功能範圍內分配擴充功能欄位編號。一個範例是使用單一儲存庫的提交編號。從 protobuf 編譯器的角度來看,此系統是「未驗證的」,因為沒有辦法檢查擴充功能是否使用了正確取得的擴充功能欄位編號。

未驗證系統比擴充功能宣告等已驗證系統的優點是能夠在不與容器訊息所有者協調的情況下定義擴充功能。

未驗證系統的缺點是 protobuf 編譯器無法保護參與者免於重複使用擴充功能欄位編號。

不建議使用未驗證的擴充功能欄位編號分配策略,因為重複使用欄位編號的後果會落在訊息的所有擴充功能程式 (而不僅僅是不遵循建議的開發人員) 上。如果您的使用案例需要極低的協調,請考慮改用 Any 訊息

未驗證的擴充功能欄位編號分配策略僅限於 1 到 524,999,999 的範圍。欄位編號 525,000,000 及以上只能與擴充功能宣告一起使用。

指定擴充類型

擴充功能可以是任何欄位類型,但 oneofmap 除外。

巢狀擴充 (不建議)

您可以在另一個訊息的範圍內宣告擴充功能

import "common/user_profile.proto";

package puppies;

message Photo {
  extend common.UserProfile {
    optional int32 likes_count = 111;
  }
  ...
}

在此情況下,存取此擴充功能的 C++ 程式碼是

UserProfile user_profile;
user_profile.SetExtension(puppies::Photo::likes_count, 42);

換句話說,唯一的影響是 likes_count 被定義在 puppies.Photo 的作用域內。

這是一個常見的混淆來源:在訊息類型內宣告巢狀的 extend 區塊,並不暗示外部類型和被擴展類型之間有任何關係。特別是,先前的範例並不表示 PhotoUserProfile 的任何子類別。它僅表示符號 likes_countPhoto 的作用域內宣告;它只是一個靜態成員。

常見的模式是在擴展的欄位類型作用域內定義擴展,例如,這裡有一個 media.UserContent 的擴展,類型為 puppies.Photo,其中擴展被定義為 Photo 的一部分。

import "media/user_content.proto";

package puppies;

message Photo {
  extend media.UserContent {
    optional Photo puppy_photo = 127;
  }
  ...
}

然而,並沒有要求訊息類型的擴展必須在該類型內定義。你也可以使用標準的定義模式。

import "media/user_content.proto";

package puppies;

message Photo {
  ...
}

// This can even be in a different file.
extend media.UserContent {
  optional Photo puppy_photo = 127;
}

為了避免混淆,首選這種標準(檔案層級)語法。巢狀語法經常被不熟悉擴展的使用者誤認為是子類別化。

Any

Any 訊息類型讓你可以在不使用其 .proto 定義的情況下,將訊息作為嵌入類型使用。Any 包含一個任意序列化的訊息,以 bytes 形式儲存,並帶有一個 URL,該 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_

不同的語言實作將支援運行時程式庫輔助程式,以類型安全的方式打包和解包 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 google::protobuf::Any& detail : status.details()) {
  if (detail.Is<NetworkErrorDetails>()) {
    NetworkErrorDetails network_error;
    detail.UnpackTo(&network_error);
    ... processing network_error ...
  }
}

如果你想將包含的訊息限制為少量類型,並在將新類型添加到列表之前要求許可,請考慮使用帶有擴展宣告擴展,而不是 Any 訊息類型。

Oneof

如果你的訊息有許多可選欄位,且同時最多只會設定一個欄位,你可以強制執行此行為並透過使用 oneof 功能來節省記憶體。

Oneof 欄位就像可選欄位,只是 oneof 中的所有欄位共用記憶體,且一次最多只能設定一個欄位。設定 oneof 的任何成員會自動清除所有其他成員。你可以使用特殊的 case()WhichOneof() 方法來檢查 oneof 中設定了哪個值(如果有的話),具體取決於你選擇的語言。

請注意,如果設定了多個值,則根據 proto 中的順序,最後設定的值將覆蓋所有先前的值

oneof 欄位的欄位編號必須在封閉訊息內是唯一的。

使用 Oneof

若要在你的 .proto 中定義 oneof,請使用 oneof 關鍵字,後跟你的 oneof 名稱,在此範例中為 test_oneof

message SampleMessage {
  oneof test_oneof {
     string name = 4;
     SubMessage sub_message = 9;
  }
}

然後將你的 oneof 欄位添加到 oneof 定義中。你可以新增除了 map 欄位之外的任何類型欄位,但不能使用 requiredoptionalrepeated 關鍵字。如果你需要在 oneof 中新增重複欄位,你可以使用包含重複欄位的訊息。

在你的產生程式碼中,oneof 欄位具有與常規 optional 欄位相同的 getter 和 setter。你還會獲得一個特殊的方法來檢查 oneof 中設定了哪個值(如果有的話)。你可以在相關的 API 參考中找到更多關於你選擇的語言的 oneof API 資訊。

Oneof 特性

  • 設定 oneof 欄位會自動清除 oneof 的所有其他成員。因此,如果你設定了多個 oneof 欄位,則只有你設定的最後一個欄位仍然有值。

    SampleMessage message;
    message.set_name("name");
    CHECK(message.has_name());
    // Calling mutable_sub_message() will clear the name field and will set
    // sub_message to a new instance of SubMessage with none of its fields set.
    message.mutable_sub_message();
    CHECK(!message.has_name());
    
  • 如果剖析器在傳輸時遇到同一個 oneof 的多個成員,則只會使用最後看到的成員在剖析的訊息中。

  • oneof 不支援擴展。

  • oneof 不能是 repeated

  • Reflection API 適用於 oneof 欄位。

  • 如果你將 oneof 欄位設定為預設值(例如將 int32 oneof 欄位設定為 0),則該 oneof 欄位的「case」將被設定,並且該值將在傳輸時序列化。

  • 如果你使用 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() 交換兩個具有 oneof 的訊息,則每個訊息都將以另一個訊息的 oneof case 結束:在下面的範例中,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());
    

回溯相容性問題

新增或移除 oneof 欄位時請小心。如果檢查 oneof 的值返回 None/NOT_SET,則可能表示 oneof 尚未設定,或者已設定為不同版本的 oneof 中的欄位。沒有辦法區分,因為無法知道傳輸時的未知欄位是否為 oneof 的成員。

標籤重用問題

  • 將可選欄位移入或移出 oneof:在訊息序列化和剖析後,你可能會遺失某些資訊(某些欄位將被清除)。但是,你可以安全地將單個欄位移入新的 oneof,並且如果已知一次只設定一個欄位,則可能可以移動多個欄位。請參閱 更新訊息類型 以取得更多詳細資訊。
  • 刪除 oneof 欄位並將其新增回來:這可能會在訊息序列化和剖析後清除你目前設定的 oneof 欄位。
  • 分割或合併 oneof:這與移動 optional 欄位有類似的問題。

對應

如果想建立一個關聯式映射作為資料定義的一部分,協議緩衝區提供了一個方便的快捷語法

map<key_type, value_type> map_field = N;

…其中 key_type 可以是任何整數或字串類型(因此,任何純量類型,除了浮點數類型和 bytes 之外)。請注意,枚舉和 proto 訊息都不適用於 key_typevalue_type 可以是任何類型,但不能是另一個映射。

因此,舉例來說,如果你想建立一個專案映射,其中每個 Project 訊息都與字串鍵相關聯,你可以這樣定義它

map<string, Project> projects = 3;

對應特性

  • 映射不支援擴展。
  • 映射不能是 repeatedoptionalrequired
  • 映射值的傳輸格式排序和映射迭代排序是未定義的,因此你不能依賴映射項目按特定順序排列。
  • .proto 產生文字格式時,映射會按鍵排序。數值鍵會按數值排序。
  • 從傳輸線路剖析或合併時,如果有重複的映射鍵,則會使用最後看到的鍵。從文字格式剖析映射時,如果有重複的鍵,剖析可能會失敗。
  • 如果你為映射欄位提供鍵但沒有值,則序列化欄位時的行為會依賴於語言。在 C++、Java、Kotlin 和 Python 中,會序列化該類型的預設值,而在其他語言中則不會序列化任何內容。
  • 符號 FooEntry 不能存在於與映射 foo 相同的作用域中,因為 FooEntry 已被映射的實作使用。

目前所有支援的語言都提供了產生的映射 API。你可以在相關的 API 參考中找到更多關於你選擇的語言的映射 API 資訊。

回溯相容性

映射語法在傳輸時等效於以下內容,因此不支援映射的協議緩衝區實作仍然可以處理你的資料

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

repeated MapFieldEntry map_field = N;

任何支援映射的協議緩衝區實作都必須產生並接受可由先前定義接受的資料。

套件

你可以在 .proto 檔案中新增可選的 package 指定符,以防止協議訊息類型之間的名稱衝突。

package foo.bar;
message Open { ... }

然後,你可以在定義訊息類型的欄位時使用封裝指定符

message Foo {
  ...
  optional foo.bar.Open open = 1;
  ...
}

封裝指定符影響產生程式碼的方式取決於你選擇的語言

  • C++ 中,產生的類別會包裝在 C++ 命名空間內。例如,Open 將位於 foo::bar 命名空間中。
  • JavaKotlin 中,該封裝會被用作 Java 封裝,除非你明確地在你的 .proto 檔案中提供 option java_package
  • Python 中,會忽略 package 指令,因為 Python 模組會根據它們在檔案系統中的位置進行組織。
  • Go 中,會忽略 package 指令,並且產生的 .pb.go 檔案位於以相應 go_proto_library Bazel 規則命名的封裝中。對於開源專案,你必須提供 go_package 選項或設定 Bazel -M 標誌。
  • Ruby 中,產生的類別會包裝在巢狀的 Ruby 命名空間中,並轉換為所需的 Ruby 大小寫樣式(第一個字母大寫;如果第一個字元不是字母,則會前置 PB_)。例如,Open 將位於 Foo::Bar 命名空間中。
  • PHP 中,該封裝會轉換為 PascalCase 後用作命名空間,除非你明確地在你的 .proto 檔案中提供 option php_namespace。例如,Open 將位於 Foo\Bar 命名空間中。
  • C# 中,該封裝會轉換為 PascalCase 後用作命名空間,除非你明確地在你的 .proto 檔案中提供 option csharp_namespace。例如,Open 將位於 Foo.Bar 命名空間中。

請注意,即使 package 指令不會直接影響產生的程式碼(例如在 Python 中),仍然強烈建議為 .proto 檔案指定封裝,否則可能會導致描述符中的命名衝突,並使 proto 對其他語言不具可攜性。

套件與名稱解析

協議緩衝區語言中的類型名稱解析與 C++ 類似:首先搜尋最內層的作用域,然後搜尋下一個最內層的作用域,依此類推,每個封裝都被視為對其父封裝「內部」。前導 '.'(例如,.foo.bar.Baz)表示從最外層的作用域開始。

協議緩衝區編譯器會透過剖析匯入的 .proto 檔案來解析所有類型名稱。每種語言的程式碼產生器都知道如何以該語言引用每個類型,即使它有不同的作用域規則。

定義服務

如果您想將您的訊息類型與 RPC(遠端程序呼叫)系統一起使用,您可以在 .proto 檔案中定義一個 RPC 服務介面,並且 protocol buffer 編譯器會以您選擇的語言產生服務介面程式碼和存根。舉例來說,如果您想定義一個 RPC 服務,其方法接受您的 SearchRequest 並回傳 SearchResponse,您可以在您的 .proto 檔案中如下定義:

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

預設情況下,協定編譯器會產生一個名為 SearchService 的抽象介面和一個對應的「存根」實作。存根會將所有呼叫轉發到 RpcChannel,而 RpcChannel 本身是一個抽象介面,您必須根據自己的 RPC 系統來定義。例如,您可以實作一個 RpcChannel,它會序列化訊息並透過 HTTP 將其傳送到伺服器。換句話說,產生的存根為您提供了一個型別安全的介面來進行基於 protocol buffer 的 RPC 呼叫,而不會將您鎖定在任何特定的 RPC 實作中。因此,在 C++ 中,您可能會得到像這樣的程式碼:

using google::protobuf;

protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;

void DoSearch() {
  // You provide classes MyRpcChannel and MyRpcController, which implement
  // the abstract interfaces protobuf::RpcChannel and protobuf::RpcController.
  channel = new MyRpcChannel("somehost.example.com:1234");
  controller = new MyRpcController;

  // The protocol compiler generates the SearchService class based on the
  // definition given earlier.
  service = new SearchService::Stub(channel);

  // Set up the request.
  request.set_query("protocol buffers");

  // Execute the RPC.
  service->Search(controller, &request, &response,
                  protobuf::NewCallback(&Done));
}

void Done() {
  delete service;
  delete channel;
  delete controller;
}

所有服務類別也都實作了 Service 介面,這個介面提供了一種方法,可以在編譯時不知道方法名稱或其輸入和輸出類型的情況下呼叫特定方法。在伺服器端,這可用於實作一個 RPC 伺服器,您可以在該伺服器上註冊服務。

using google::protobuf;

class ExampleSearchService : public SearchService {
 public:
  void Search(protobuf::RpcController* controller,
              const SearchRequest* request,
              SearchResponse* response,
              protobuf::Closure* done) {
    if (request->query() == "google") {
      response->add_result()->set_url("http://www.google.com");
    } else if (request->query() == "protocol buffers") {
      response->add_result()->set_url("http://protobuf.googlecode.com");
    }
    done->Run();
  }
};

int main() {
  // You provide class MyRpcServer.  It does not have to implement any
  // particular interface; this is just an example.
  MyRpcServer server;

  protobuf::Service* service = new ExampleSearchService;
  server.ExportOnPort(1234, service);
  server.Run();

  delete service;
  return 0;
}

如果您不想插入自己現有的 RPC 系統,可以使用 gRPC:這是一個由 Google 開發的、與語言和平台無關的開源 RPC 系統。gRPC 特別適用於 protocol buffer,並允許您使用特殊的 protocol buffer 編譯器外掛程式直接從您的 .proto 檔案產生相關的 RPC 程式碼。然而,由於使用 proto2 和 proto3 產生的客戶端和伺服器之間可能存在相容性問題,我們建議您使用 proto3 來定義 gRPC 服務。您可以在 Proto3 語言指南中找到更多關於 proto3 語法的資訊。如果您確實想將 proto2 與 gRPC 一起使用,則需要使用 3.0.0 或更高版本的 protocol buffer 編譯器和函式庫。

除了 gRPC 之外,還有許多正在進行的第三方專案,旨在為 Protocol Buffers 開發 RPC 實作。有關我們所知專案的連結列表,請參閱第三方附加元件維基頁面

JSON 對應

標準 protobuf 二進制線路格式是使用 protobuf 的兩個系統之間通訊的首選序列化格式。對於與使用 JSON 而非 protobuf 線路格式的系統進行通訊,Protobuf 支援 JSON 中的標準編碼。

選項

.proto 檔案中的個別宣告可以使用多個選項進行註解。選項不會變更宣告的整體含義,但可能會影響在特定情況下的處理方式。可用選項的完整列表在 /google/protobuf/descriptor.proto 中定義。

有些選項是檔案層級的選項,這表示它們應該寫在頂層範圍內,而不是任何訊息、列舉或服務定義內部。有些選項是訊息層級的選項,這表示它們應該寫在訊息定義內部。有些選項是欄位層級的選項,這表示它們應該寫在欄位定義內部。選項也可以寫在列舉類型、列舉值、oneof 欄位、服務類型和服務方法上;然而,目前沒有任何對這些有用的選項。

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

  • java_package(檔案選項):您想要用於產生的 Java/Kotlin 類別的套件。如果在 .proto 檔案中未明確給出 java_package 選項,則預設會使用 proto 套件(使用 .proto 檔案中的「package」關鍵字指定)。然而,proto 套件通常不會成為好的 Java 套件,因為 proto 套件不應該以反向網域名稱開頭。如果沒有產生 Java 或 Kotlin 程式碼,則此選項無效。

    option java_package = "com.example.foo";
    
  • java_outer_classname(檔案選項):您想要產生的包裝 Java 類別的類別名稱(以及因此產生的檔案名稱)。如果在 .proto 檔案中未明確指定 java_outer_classname,則類別名稱將透過將 .proto 檔案名稱轉換為駝峰式大小寫來建構(因此 foo_bar.proto 會變成 FooBar.java)。如果停用 java_multiple_files 選項,則為 .proto 檔案產生的所有其他類別/列舉/等等都將作為巢狀類別/列舉/等等產生於此外部包裝 Java 類別內部。如果沒有產生 Java 程式碼,則此選項無效。

    option java_outer_classname = "Ponycopter";
    
  • java_multiple_files(檔案選項):如果為 false,則只會為此 .proto 檔案產生一個 .java 檔案,並且為頂層訊息、服務和列舉產生的所有 Java 類別/列舉/等等都將巢狀在外部類別內(請參閱 java_outer_classname)。如果為 true,則會為為頂層訊息、服務和列舉產生的每個 Java 類別/列舉/等等產生單獨的 .java 檔案,並且為此 .proto 檔案產生的包裝 Java 類別將不會包含任何巢狀類別/列舉/等等。這是一個布林選項,預設值為 false。如果沒有產生 Java 程式碼,則此選項無效。

    option java_multiple_files = true;
    
  • optimize_for(檔案選項):可以設定為 SPEEDCODE_SIZELITE_RUNTIME。這會以以下方式影響 C++ 和 Java 程式碼產生器(以及可能的第三方產生器):

    • SPEED(預設):protocol buffer 編譯器將會產生用於序列化、剖析和對您的訊息類型執行其他常見操作的程式碼。此程式碼經過高度最佳化。
    • CODE_SIZE:protocol buffer 編譯器將產生最小化的類別,並且將依賴共享的、基於反射的程式碼來實作序列化、剖析和各種其他操作。因此,產生的程式碼會比 SPEED 小得多,但操作速度會較慢。類別仍然會實作與 SPEED 模式下相同的公開 API。此模式在包含大量 .proto 檔案且不需要它們都快如閃電的應用程式中最有用。
    • LITE_RUNTIME:protocol buffer 編譯器將產生只依賴「lite」執行階段函式庫 (libprotobuf-lite 而非 libprotobuf) 的類別。lite 執行階段比完整的函式庫小得多(大約小一個數量級),但省略了描述符和反射等某些功能。這對於在行動電話等受限平台上執行的應用程式特別有用。編譯器仍然會像在 SPEED 模式下一樣產生所有方法的快速實作。產生的類別只會在每種語言中實作 MessageLite 介面,該介面僅提供完整 Message 介面的一部分方法。
    option optimize_for = CODE_SIZE;
    
  • cc_generic_servicesjava_generic_servicespy_generic_services(檔案選項):通用服務已棄用。 protocol buffer 編譯器是否應該分別根據 C++、Java 和 Python 中的服務定義產生抽象服務程式碼。由於歷史原因,這些選項的預設值為 true。然而,從 2.3.0 版(2010 年 1 月)開始,人們認為 RPC 實作最好提供程式碼產生器外掛程式來產生更特定於每個系統的程式碼,而不是依賴「抽象」服務。

    // This file relies on plugins to generate service code.
    option cc_generic_services = false;
    option java_generic_services = false;
    option py_generic_services = false;
    
  • cc_enable_arenas(檔案選項):為 C++ 產生的程式碼啟用arena 配置

  • objc_class_prefix(檔案選項):設定 Objective-C 類別前綴,該前綴會附加到從此 .proto 產生的所有 Objective-C 類別和列舉。沒有預設值。您應該使用 3-5 個大寫字母之間的前綴,如 Apple 建議的那樣。請注意,所有 2 個字母的前綴都由 Apple 保留。

  • message_set_wire_format(訊息選項):如果設定為 true,則訊息使用不同的二進制格式,旨在與 Google 內部使用的名為 MessageSet 的舊格式相容。Google 外部的使用者可能永遠不需要使用此選項。訊息必須完全按照如下方式宣告:

    message Foo {
      option message_set_wire_format = true;
      extensions 4 to max;
    }
    
  • packed(欄位選項):如果在基本數值類型的重複欄位上設定為 true,則會導致使用更緊湊的編碼。不使用此選項的唯一原因是您需要與 2.3.0 版之前的剖析器相容。在這些舊的剖析器在未預期時會忽略打包的資料。因此,無法在不破壞線路相容性的情況下將現有欄位變更為打包格式。在 2.3.0 及更高版本中,此變更是安全的,因為可打包欄位的剖析器將始終接受兩種格式,但如果您必須處理使用舊 protobuf 版本的舊程式,請小心。

    repeated int32 samples = 4 [packed = true];
    
  • deprecated(欄位選項):如果設定為 true,則表示該欄位已棄用,新程式碼不應使用。在大多數語言中,這沒有實際影響。在 Java 中,這會變成 @Deprecated 註解。對於 C++,clang-tidy 會在每次使用已棄用的欄位時產生警告。未來,其他特定語言的程式碼產生器可能會在欄位的存取器上產生棄用註解,這反過來會在編譯嘗試使用該欄位的程式碼時產生警告。如果沒有人使用該欄位,並且您想要防止新使用者使用它,請考慮將欄位宣告取代為保留陳述式。

    optional int32 old_field = 6 [deprecated=true];
    

列舉值選項

支援列舉值選項。您可以使用 deprecated 選項來表示不應再使用某個值。您也可以使用擴充功能建立自訂選項。

以下範例顯示了新增這些選項的語法:

import "google/protobuf/descriptor.proto";

extend google.protobuf.EnumValueOptions {
  optional string string_name = 123456789;
}

enum Data {
  DATA_UNSPECIFIED = 0;
  DATA_SEARCH = 1 [deprecated = true];
  DATA_DISPLAY = 2 [
    (string_name) = "display_value"
  ];
}

讀取 string_name 選項的 C++ 程式碼可能如下所示:

const absl::string_view foo = proto2::GetEnumDescriptor<Data>()
    ->FindValueByName("DATA_DISPLAY")->options().GetExtension(string_name);

請參閱自訂選項以瞭解如何將自訂選項套用到列舉值和欄位。

自訂選項

Protocol Buffers 也允許您定義和使用自己的選項。請注意,這是一個進階功能,大多數人不需要它。由於選項是由 google/protobuf/descriptor.proto 中定義的訊息(例如 FileOptionsFieldOptions)定義的,因此定義您自己的選項只是擴充這些訊息的問題。例如:

import "google/protobuf/descriptor.proto";

extend google.protobuf.MessageOptions {
  optional string my_option = 51234;
}

message MyMessage {
  option (my_option) = "Hello world!";
}

在這裡,我們透過擴充 MessageOptions 定義了一個新的訊息層級選項。當我們隨後使用該選項時,選項名稱必須括在括號中,以表示它是擴充功能。我們現在可以在 C++ 中讀取 my_option 的值,如下所示:

string value = MyMessage::descriptor()->options().GetExtension(my_option);

在這裡,MyMessage::descriptor()->options() 會傳回 MyMessageMessageOptions 協定訊息。從中讀取自訂選項就像讀取任何其他擴充功能一樣。

同樣地,在 Java 中,我們會寫成:

String value = MyProtoFile.MyMessage.getDescriptor().getOptions()
  .getExtension(MyProtoFile.myOption);

在 Python 中,它會是:

value = my_proto_file_pb2.MyMessage.DESCRIPTOR.GetOptions()
  .Extensions[my_proto_file_pb2.my_option]

在 Protocol Buffers 語言中,可以為每種類型的結構定義自訂選項。以下範例示範了如何使用各種選項。

import "google/protobuf/descriptor.proto";

extend google.protobuf.FileOptions {
  optional string my_file_option = 50000;
}
extend google.protobuf.MessageOptions {
  optional int32 my_message_option = 50001;
}
extend google.protobuf.FieldOptions {
  optional float my_field_option = 50002;
}
extend google.protobuf.OneofOptions {
  optional int64 my_oneof_option = 50003;
}
extend google.protobuf.EnumOptions {
  optional bool my_enum_option = 50004;
}
extend google.protobuf.EnumValueOptions {
  optional uint32 my_enum_value_option = 50005;
}
extend google.protobuf.ServiceOptions {
  optional MyEnum my_service_option = 50006;
}
extend google.protobuf.MethodOptions {
  optional MyMessage my_method_option = 50007;
}

option (my_file_option) = "Hello world!";

message MyMessage {
  option (my_message_option) = 1234;

  optional int32 foo = 1 [(my_field_option) = 4.5];
  optional string bar = 2;
  oneof qux {
    option (my_oneof_option) = 42;

    string quux = 3;
  }
}

enum MyEnum {
  option (my_enum_option) = true;

  FOO = 1 [(my_enum_value_option) = 321];
  BAR = 2;
}

message RequestType {}
message ResponseType {}

service MyService {
  option (my_service_option) = FOO;

  rpc MyMethod(RequestType) returns(ResponseType) {
    // Note:  my_method_option has type MyMessage.  We can set each field
    //   within it using a separate "option" line.
    option (my_method_option).foo = 567;
    option (my_method_option).bar = "Some string";
  }
}

請注意,如果您想在定義自訂選項的套件以外的其他套件中使用自訂選項,您必須像使用類型名稱一樣,在選項名稱前加上套件名稱。例如:

// foo.proto
import "google/protobuf/descriptor.proto";
package foo;
extend google.protobuf.MessageOptions {
  optional string my_option = 51234;
}
// bar.proto
import "foo.proto";
package bar;
message MyMessage {
  option (foo.my_option) = "Hello world!";
}

最後一點:由於自訂選項是擴充功能,它們必須像任何其他欄位或擴充功能一樣被分配欄位編號。在前面的範例中,我們使用了 50000-99999 範圍內的欄位編號。這個範圍是保留給個別組織內部使用的,因此您可以自由地將此範圍中的數字用於內部應用程式。但是,如果您打算在公開應用程式中使用自訂選項,那麼請務必確保您的欄位編號是全域唯一的。要取得全域唯一的欄位編號,請傳送請求以在protobuf 全域擴充註冊表中新增一個條目。通常您只需要一個擴充編號。您可以將多個選項放入子訊息中,只用一個擴充編號聲明多個選項。

message FooOptions {
  optional int32 opt1 = 1;
  optional string opt2 = 2;
}

extend google.protobuf.FieldOptions {
  optional FooOptions foo_options = 1234;
}

// usage:
message Bar {
  optional int32 a = 1 [(foo_options).opt1 = 123, (foo_options).opt2 = "baz"];
  // alternative aggregate syntax (uses TextFormat):
  optional int32 b = 2 [(foo_options) = { opt1: 123 opt2: "baz" }];
}

此外,請注意,每個選項類型(檔案級別、訊息級別、欄位級別等)都有自己的數字空間,因此,例如,您可以聲明具有相同編號的 FieldOptions 和 MessageOptions 的擴充功能。

選項保留

選項具有保留的概念,它控制是否在產生的程式碼中保留選項。選項預設具有執行階段保留,這表示它們會保留在產生的程式碼中,因此在產生的描述符池中於執行階段可見。但是,您可以設定 retention = RETENTION_SOURCE 來指定選項(或選項內的欄位)不得在執行階段保留。這稱為來源保留

選項保留是一項進階功能,大多數使用者不需要擔心,但是如果您想使用某些選項而不必付出將它們保留在二進位檔案中的程式碼大小成本,則它會很有用。具有來源保留的選項對 protocprotoc 外掛程式仍然可見,因此程式碼產生器可以使用它們來自訂其行為。

保留可以直接在選項上設定,如下所示:

extend google.protobuf.FileOptions {
  optional int32 source_retention_option = 1234
      [retention = RETENTION_SOURCE];
}

它也可以在普通欄位上設定,在這種情況下,它僅在該欄位出現在選項內部時才生效:

message OptionsMessage {
  optional int32 source_retention_field = 1 [retention = RETENTION_SOURCE];
}

您可以根據需要設定 retention = RETENTION_RUNTIME,但是這不會有任何作用,因為它是預設行為。當訊息欄位被標記為 RETENTION_SOURCE 時,其整個內容都會被刪除;其中的欄位無法嘗試設定 RETENTION_RUNTIME 來覆蓋該設定。

選項目標

欄位具有 targets 選項,該選項控制當該欄位用作選項時,該欄位可以應用於哪些類型的實體。例如,如果欄位具有 targets = TARGET_TYPE_MESSAGE,則該欄位無法在列舉(或任何其他非訊息實體)上的自訂選項中設定。Protoc 會強制執行此操作,如果違反目標約束,將引發錯誤。

乍看之下,此功能似乎不必要,因為每個自訂選項都是特定實體選項訊息的擴充功能,它已經將選項限制為該實體。但是,在將共享選項訊息應用於多個實體類型,並且您想控制該訊息中各個欄位的使用時,選項目標非常有用。例如:

message MyOptions {
  optional string file_only_option = 1 [targets = TARGET_TYPE_FILE];
  optional int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE,
                                              targets = TARGET_TYPE_ENUM];
}

extend google.protobuf.FileOptions {
  optional MyOptions file_options = 50000;
}

extend google.protobuf.MessageOptions {
  optional MyOptions message_options = 50000;
}

extend google.protobuf.EnumOptions {
  optional MyOptions enum_options = 50000;
}

// OK: this field is allowed on file options
option (file_options).file_only_option = "abc";

message MyMessage {
  // OK: this field is allowed on both message and enum options
  option (message_options).message_and_enum_option = 42;
}

enum MyEnum {
  MY_ENUM_UNSPECIFIED = 0;
  // Error: file_only_option cannot be set on an enum.
  option (enum_options).file_only_option = "xyz";
}

產生您的類別

若要產生使用 .proto 檔案中定義的訊息類型所需的 Java、Kotlin、Python、C++、Go、Ruby、Objective-C 或 C# 程式碼,您需要在 .proto 檔案上執行協定緩衝區編譯器 protoc。如果您尚未安裝編譯器,請下載套件並依照 README 中的指示進行操作。對於 Go,您還需要為編譯器安裝一個特殊的程式碼產生器外掛程式;您可以在 GitHub 上的 golang/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 指定一個目錄,在解析 import 指示詞時,會在其中查找 .proto 檔案。如果省略,則使用目前目錄。可以透過多次傳遞 --proto_path 選項來指定多個導入目錄;它們將依序搜尋。-I=_IMPORT_PATH_ 可以用作 --proto_path 的簡短形式。

  • 您可以提供一個或多個輸出指示詞

    為了額外的便利,如果 DST_DIR.zip.jar 結尾,編譯器會將輸出寫入具有給定名稱的單一 ZIP 格式封存檔。.jar 輸出也將獲得 Java JAR 規範所要求的資訊清單檔案。請注意,如果輸出封存檔已存在,它將被覆寫。

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

檔案位置

最好不要將 .proto 檔案與其他語言來源放在同一個目錄中。請考慮在專案的根套件下為 .proto 檔案建立子套件 proto

位置應與語言無關

使用 Java 程式碼時,將相關的 .proto 檔案放在與 Java 來源相同的目錄中很方便。但是,如果有任何非 Java 程式碼使用相同的 proto,則路徑前綴將不再有意義。因此,通常將 proto 放在相關的與語言無關的目錄中,例如 //myteam/mypackage

此規則的例外情況是,當明確知道 proto 只會在 Java 環境中使用時,例如用於測試。

支援的平台

有關以下資訊: