語言指南 (proto 2)

說明如何在您的專案中使用 Protocol Buffers 語言的 proto2 修訂版本。

本指南說明如何使用 Protocol Buffer 語言來架構您的 Protocol Buffer 資料,包括 .proto 檔案語法,以及如何從 .proto 檔案產生資料存取類別。內容涵蓋 Protocol Buffer 語言的 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 修訂版本。

    • syntax 必須是檔案的第一個非空白、非註解行。
    • 如果未指定 syntax,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:這是成對的鍵/值欄位類型。如需此欄位類型的詳細資訊,請參閱Maps

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

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

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

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

強烈建議停用 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 檔案

  • 建議在 .proto 程式碼元素之前的行使用 C/C++/Java 行尾樣式註解「//」

  • 也接受 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 metadata 檔案。metadata 檔案用於將有效的訊息類型載入描述元集區。
  • 針對 **Dart**,編譯器會產生一個 .pb.dart 檔案,其中包含檔案中每個訊息類型的類別。

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

純量值類型

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

Proto 類型註解
double
float
int32使用變長度編碼。編碼負數的效率不高 - 如果您的欄位可能會有負值,請改用 sint32。
int64使用變長度編碼。編碼負數的效率不高 - 如果您的欄位可能會有負值,請改用 sint64。
uint32使用變長度編碼。
uint64使用變長度編碼。
sint32使用變長度編碼。帶正負號的 int 值。這些編碼負數比一般 int32 更有效率。
sint64使用變長度編碼。帶正負號的 int 值。這些編碼負數比一般 int64 更有效率。
fixed32永遠是四個位元組。如果值通常大於 228,則比 uint32 更有效率。
fixed64永遠是八個位元組。如果值通常大於 256,則比 uint64 更有效率。
sfixed32永遠是四個位元組。
sfixed64永遠是八個位元組。
bool
string字串必須一律包含 UTF-8 編碼或 7 位元 ASCII 文字,且長度不得超過 232
bytes可能包含任何任意位元組序列,長度不得超過 232
Proto 類型C++ 類型Java/Kotlin 類型[1]Python 類型[3]Go 類型Ruby 類型C# 類型PHP 類型Dart 類型Rust 類型
doubledoubledoublefloat*float64Floatdoublefloatdoublef64
floatfloatfloatfloat*float32Floatfloatfloatdoublef32
int32int32_tintintint32Fixnum 或 Bignum (視需要)intinteger*int32i32
int64int64_tlongint/long[4]*int64Bignumlonginteger/string[6]Int64i64
uint32uint32_tint[2]int/long[4]*uint32Fixnum 或 Bignum (視需要)uintintegerintu32
uint64uint64_tlong[2]int/long[4]*uint64Bignumulonginteger/string[6]Int64u64
sint32int32_tintintint32Fixnum 或 Bignum (視需要)intinteger*int32i32
sint64int64_tlongint/long[4]*int64Bignumlonginteger/string[6]Int64i64
fixed32uint32_tint[2]int/long[4]*uint32Fixnum 或 Bignum (視需要)uintintegerintu32
fixed64uint64_tlong[2]int/long[4]*uint64Bignumulonginteger/string[6]Int64u64
sfixed32int32_tintint*int32Fixnum 或 Bignum (視需要)intintegerinti32
sfixed64int64_tlongint/long[4]*int64Bignumlonginteger/string[6]Int64i64
boolboolbooleanbool*boolTrueClass/FalseClassboolbooleanboolbool
stringstringStringunicode (Python 2)、str (Python 3)*stringString (UTF-8)stringstringStringProtoString
bytesstringByteStringbytes[]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。
  • 對於數值類型,預設值為零。
  • 對於訊息欄位,欄位未設定。其確切值取決於語言。如需詳細資訊,請參閱您語言的產生程式碼指南
  • 對於列舉,預設值是**第一個定義的列舉值**,應為 0 (建議與開放式列舉相容)。請參閱列舉預設值

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

map 欄位的預設值為空 (通常是適當語言中的空地圖)。

覆寫預設純量值

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

optional int32 results_per_page = 3 [default = 10];

如果傳送者未指定 results_per_page,接收者將會觀察到下列狀態

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

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

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

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

列舉

當您定義訊息類型時,您可能希望其欄位之一僅具有預先定義的值清單中的其中一個值。例如,假設您要為每個 SearchRequest 新增 corpus 欄位,其中 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 中定義的第一個值。

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

也建議第一個預設值除了「此值未指定」之外,不應具有任何語意含義。

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

  optional Corpus corpus = 4 [default = CORPUS_UNIVERSAL];

列舉值別名

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

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 上執行 Protocol Buffer 編譯器時,產生的程式碼會針對 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。如果任何未來的使用者嘗試使用這些識別碼,Protocol Buffer 編譯器會發出警告。您可以使用 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 陳述式新增至檔案頂端

import "myproject/other_protos.proto";

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

請注意,公開匯入功能在 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

Protocol Buffer 編譯器會在一組目錄中搜尋匯入的檔案,這些目錄是在 Protocol Buffer 編譯器命令列上使用 -I/--proto_path 旗標指定的。如果沒有指定旗標,則會在叫用編譯器的目錄中尋找。一般來說,您應該將 --proto_path 旗標設定為專案的根目錄,並對所有匯入項目使用完整名稱。

使用 proto3 訊息類型

您可以匯入 proto3edition 2023 訊息類型,並在 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 彼此相容,但與其他整數類型相容。
  • 只要位元組是有效的 UTF-8,stringbytes 就相容。
  • 如果嵌入訊息的 bytes 包含訊息的編碼執行個體,則嵌入訊息與 bytes 相容。
  • fixed32sfixed32 相容,fixed64sfixed64 相容。
  • 對於 stringbytes 和訊息欄位,singular 與 repeated 相容。如果提供 repeated 欄位的序列化資料作為輸入,預期此欄位為 singular 的用戶端將採用最後一個輸入值 (如果是基本類型欄位),或合併所有輸入元素 (如果是訊息類型欄位)。請注意,這對於數值類型 (包括布林值和列舉) 通常是安全的。數值類型的 repeated 欄位可能會以 packed 格式序列化,當預期 singular 欄位時,將無法正確剖析。
  • 變更預設值通常是可以的,只要您記住預設值永遠不會透過線路傳送即可。因此,如果程式收到訊息,其中未設定特定欄位,程式將會看到預設值,如同在該程式版本的協定中定義的一樣。它將不會看到在傳送者程式碼中定義的預設值。
  • 就線路格式而言,enumint32uint32int64uint64 相容 (請注意,如果值不符合,則會被截斷)。但是,請注意,當訊息還原序列化時,用戶端程式碼可能會以不同的方式處理它們。特別是,不明的 enum 值會在訊息還原序列化時捨棄,這會使欄位的 has.. 存取子傳回 false,而其 getter 會傳回 enum 定義中列出的第一個值,或者如果指定了預設值,則傳回預設值。在 repeated enum 欄位的情況下,任何不明的值都會從清單中移除。但是,整數欄位將永遠保留其值。因此,當將整數升級為 enum 以接收線路上超出範圍的列舉值時,您需要非常小心。
  • 在目前的 Java 和 C++ 實作中,當不明的 enum 值被移除時,它們會與其他不明欄位一起儲存。請注意,如果此資料被序列化,然後由可辨識這些值的用戶端重新剖析,則可能會導致奇怪的行為。在選填欄位的情況下,即使在原始訊息還原序列化之後寫入新值,原始值仍然會由辨識它的用戶端讀取。在 repeated 欄位的情況下,原始值將會出現在任何已辨識且新加入的值之後,這表示順序將不會被保留。
  • 將單一 optional 欄位或擴充功能變更為新的 oneof 的成員是二進位相容的,但是對於某些語言 (特別是 Go),產生的程式碼的 API 將會以不相容的方式變更。基於這個原因,Google 不會在其公開 API 中進行此類變更,如 AIP-180 中所述。關於來源相容性的相同警告,如果確定沒有程式碼一次設定多個欄位,則將多個欄位移至新的 oneof 中可能是安全的。將欄位移至現有的 oneof 中是不安全的。同樣地,將單一欄位 oneof 變更為 optional 欄位或擴充功能是安全的。
  • map<K, V> 與對應的 repeated 訊息欄位之間變更欄位是二進位相容的 (請參閱下方的 Map,以取得訊息版面配置和其他限制)。但是,變更的安全性取決於應用程式:當還原序列化和重新序列化訊息時,使用 repeated 欄位定義的用戶端將產生語意相同的結果;但是,使用 map 欄位定義的用戶端可能會重新排序項目並捨棄具有重複鍵的項目。

未知欄位

不明欄位是格式正確的 Protocol Buffer 序列化資料,表示剖析器無法辨識的欄位。例如,當舊的二進位檔剖析由具有新欄位的新二進位檔傳送的資料時,這些新欄位會變成舊二進位檔中的不明欄位。

最初,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)

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

但是,由於擴充功能是在容器訊息之外定義的,因此不會產生專用的存取子來取得和設定特定的擴充功能欄位。對於我們的範例,Protocol Buffer 編譯器不會產生 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 的新宣告。

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

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

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

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

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

不建議使用未驗證的擴充功能欄位編號配置策略,因為重複使用欄位編號的後果會落在訊息的所有擴充者身上 (而不僅僅是未遵循建議的開發人員)。如果您的使用案例需要極低的協調性,請考慮改用 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 的範圍內宣告;它只是一個靜態成員。

常見的模式是在擴充功能欄位類型的範圍內定義擴充功能 – 例如,以下是 puppies.Photo 類型的 media.UserContent 的擴充功能,其中擴充功能定義為 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 關鍵字。如果您需要將 repeated 欄位新增至 oneof,您可以使用包含 repeated 欄位的訊息。

在您產生的程式碼中,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 中一樣

      • 基本類型將覆寫任何已設定的值
      • 訊息將合併到任何已設定的值中
  • 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 欄位有類似的問題。

Maps

如果您想要建立關聯式 map 作為資料定義的一部分,Protocol Buffer 提供了方便的捷徑語法

map<key_type, value_type> map_field = N;

…其中 key_type 可以是任何整數或字串類型 (因此,除了浮點類型和 bytes 之外的任何純量類型)。請注意,列舉和 proto 訊息都不能作為有效的 key_typevalue_type 可以是除另一個 map 之外的任何類型。

因此,例如,如果您想要建立專案的 map,其中每個 Project 訊息都與字串鍵關聯,您可以像這樣定義它

map<string, Project> projects = 3;

Maps 功能

  • Map 不支援擴充功能。
  • Map 不能是 repeatedoptionalrequired
  • Map 值的線路格式排序和 map 反覆運算排序是未定義的,因此您不能依賴您的 map 項目以特定順序排列。
  • 當為 .proto 產生文字格式時,map 會依鍵排序。數值鍵會以數值方式排序。
  • 當從線路剖析或合併時,如果有重複的 map 鍵,則會使用最後一個看到的鍵。當從文字格式剖析 map 時,如果存在重複的鍵,剖析可能會失敗。
  • 如果您為 map 欄位提供鍵但沒有值,則欄位序列化時的行為取決於語言。在 C++、Java、Kotlin 和 Python 中,會序列化類型的預設值,而在其他語言中則不會序列化任何內容。
  • 在與 map foo 相同的範圍中,不能存在符號 FooEntry,因為 FooEntry 已被 map 的實作使用。

產生的 map API 目前適用於所有支援的語言。您可以在相關的API 參考資料中找到有關您選擇的語言的 map API 的更多資訊。

回溯相容性

map 語法在線路上等同於以下內容,因此不支援 map 的 Protocol Buffer 實作仍然可以處理您的資料

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

repeated MapFieldEntry map_field = N;

任何支援 map 的 Protocol Buffer 實作都必須產生和接受可以被較早定義接受的資料。

套件

您可以將選填的 package 指定詞新增至 .proto 檔案,以防止 Protocol 訊息類型之間的名稱衝突。

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 無法移植到其他語言。

套件和名稱解析

Protocol Buffer 語言中的類型名稱解析與 C++ 的運作方式類似:首先搜尋最內層的範圍,然後搜尋次內層,依此類推,每個套件都被視為其父套件的「內部」。前導 ‘.’ (例如,.foo.bar.Baz) 表示從最外層的範圍開始。

Protocol Buffer 編譯器會透過剖析匯入的 .proto 檔案來解析所有類型名稱。每種語言的程式碼產生器都知道如何以該語言參照每個類型,即使它具有不同的範圍規則。

定義服務

如果您想要將您的訊息類型與 RPC (遠端程序呼叫) 系統搭配使用,您可以在 .proto 檔案中定義 RPC 服務介面,並且 Protocol Buffer 編譯器將在您選擇的語言中產生服務介面程式碼和 Stub。因此,例如,如果您想要定義具有一個方法的 RPC 服務,該方法接受您的 SearchRequest 並傳回 SearchResponse,您可以在您的 .proto 檔案中將其定義如下

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

預設情況下,協定編譯器接著會產生一個名為 SearchService 的抽象介面,以及一個對應的「樁」實作 (stub implementation)。這個樁會將所有呼叫轉送到 RpcChannel,而 RpcChannel 本身也是一個抽象介面,您必須根據自己的 RPC 系統來定義。舉例來說,您可以實作一個 RpcChannel,將訊息序列化,然後透過 HTTP 將其傳送到伺服器。換句話說,產生的樁為進行以協定緩衝區為基礎的 RPC 呼叫提供了一個型別安全 (type-safe) 的介面,而不會將您鎖定在任何特定的 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 與協定緩衝區搭配使用效果特別好,並且讓您可以直接從 .proto 檔案中使用特殊的協定緩衝區編譯器外掛程式來產生相關的 RPC 程式碼。然而,由於使用 proto2 和 proto3 產生的用戶端和伺服器之間可能存在相容性問題,我們建議您使用 proto3 或 edition 2023 來定義 gRPC 服務。您可以在Proto3 語言指南中找到更多關於 proto3 語法的資訊,並在Edition 2023 語言指南中找到關於 edition 2023 的資訊。

除了 gRPC 之外,還有許多持續進行的第三方專案正在開發協定緩衝區的 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 (預設值):協定緩衝區編譯器將會產生程式碼,用於序列化、剖析以及對您的訊息型別執行其他常見操作。此程式碼經過高度最佳化。
    • CODE_SIZE:協定緩衝區編譯器將會產生最小的類別,並且將依賴共用的、基於反射的程式碼來實作序列化、剖析和各種其他操作。因此,產生的程式碼會比使用 SPEED 小得多,但操作會比較慢。類別仍將實作與 SPEED 模式中完全相同的公開 API。此模式在包含大量 .proto 檔案且不需要所有檔案都極快速度運作的應用程式中最有用。
    • LITE_RUNTIME:協定緩衝區編譯器將會產生僅依賴「lite」執行階段程式庫 (libprotobuf-lite 而不是 libprotobuf) 的類別。lite 執行階段比完整程式庫小得多 (大約小一個數量級),但省略了某些功能,例如描述符和反射。這對於在受限平台 (例如手機) 上執行的應用程式特別有用。編譯器仍將產生所有方法的快速實作,就像在 SPEED 模式中一樣。產生的類別將僅在每種語言中實作 MessageLite 介面,該介面僅提供完整 Message 介面的方法子集。
    option optimize_for = CODE_SIZE;
    
  • cc_generic_servicesjava_generic_servicespy_generic_services (檔案選項):通用服務已被棄用。 協定緩衝區編譯器是否應分別根據 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 allocation)

  • 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);

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

自訂選項

協定緩衝區也允許您定義和使用自己的選項。請注意,這是一項進階功能,大多數人不需要。由於選項是由 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]

可以為協定緩衝區語言中的每種建構定義自訂選項。以下是一個使用每種選項的範例

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";
}

產生您的類別

若要產生 Java、Kotlin、Python、C++、Go、Ruby、Objective-C 或 C# 程式碼,您需要使用協定緩衝區編譯器 protoc 來處理 .proto 檔案,才能使用 .proto 檔案中定義的訊息型別。如果您尚未安裝編譯器,請下載套件並依照 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 情境中使用時,例如用於測試。

支援的平台

如需以下資訊