語言指南 (proto 2)

介紹如何在專案中使用 Protocol Buffers 語言的 proto2 版本。

本指南描述瞭如何使用協議緩衝區語言來構建協議緩衝區資料,包括 .proto 檔案語法以及如何從 .proto 檔案生成資料訪問類。它涵蓋了協議緩衝區語言的 proto2 版本。

有關 editions 語法的詳細資訊,請參閱 Protobuf Editions 語言指南

有關 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,協議緩衝區編譯器將假定您使用的是 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 線路格式是精簡的,沒有提供檢測使用一種定義編碼的欄位並用另一種定義解碼的方法。

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

  • 開發者浪費時間除錯
  • 解析/合併錯誤(最好的情況)
  • 個人身份資訊/敏感個人身份資訊洩露
  • 資料損壞

欄位編號重用的常見原因

  • 重編號欄位(有時為了實現欄位編號順序更美觀)。重編號實際上是刪除並重新新增所有涉及的欄位,導致不相容的線路格式變更。

  • 刪除一個欄位並且沒有保留其編號以防止未來重用。

欄位編號限制為 29 位而不是 32 位,因為有三位用於指定欄位的線路格式。有關更多資訊,請參閱編碼主題

指定欄位基數

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

  • 單一 (Singular):

    在 proto2 中,有兩種型別的單一欄位:

    • optional:(推薦)optional 欄位處於兩種可能狀態之一。

      • 該欄位已設定,幷包含一個被顯式設定或從線路中解析出的值。它將被序列化到線路中。
      • 該欄位未設定,並將返回預設值。它將不會被序列化到線路中。

      你可以檢查該值是否被顯式設定。

    • required請勿使用。Required 欄位問題太多,已從 proto3 和 editions 中移除。Required 欄位的語義應在應用層實現。當它被使用時,一個格式良好的訊息必須有且只有一個此欄位。

  • repeated:此欄位型別在一個格式良好的訊息中可以重複零次或多次。重複值的順序將被保留。

  • map:這是一個鍵值對欄位型別。有關此欄位型別的更多資訊,請參閱對映

對新的重複欄位使用 Packed Encoding(打包編碼)

出於歷史原因,標量數字型別(例如,int32int64enum)的 repeated 欄位編碼效率不高。新程式碼應使用特殊選項 [packed = true] 以獲得更高效的編碼。例如:

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

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

Required 被強烈棄用

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

格式良好的訊息

術語“格式良好”在應用於 protobuf 訊息時,指的是序列化/反序列化的位元組。protoc 解析器會驗證給定的 proto 定義檔案是否可解析。

Singular 欄位可以在有線格式位元組中出現多次。解析器會接受輸入,但只有該欄位的最後一個例項可以透過生成的繫結訪問。有關此主題的更多資訊,請參閱後者為準

新增更多訊息型別

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

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

message SearchResponse {
 ...
}

組合訊息會導致臃腫 雖然可以在單個 .proto 檔案中定義多種訊息型別(如 message、enum 和 service),但當在單個檔案中定義大量具有不同依賴關係的訊息時,也可能導致依賴臃腫。建議在每個 .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、一個可空欄位訪問器和一個複製函式。
  • Python 有點不同——Python 編譯器會生成一個模組,其中包含您 .proto 檔案中每種訊息型別的靜態描述符,然後該描述符與一個*元類 (metaclass)* 一起使用,以在執行時建立必要的 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 型別說明
double
float
int32使用可變長度編碼。對於編碼負數效率低下——如果你的欄位可能包含負值,請改用 sint32。
int64使用可變長度編碼。對於編碼負數效率低下——如果你的欄位可能包含負值,請改用 sint64。
uint32使用可變長度編碼。
uint64使用可變長度編碼。
sint32使用可變長度編碼。有符號整數值。這些比常規的 int32 更高效地編碼負數。
sint64使用可變長度編碼。有符號整數值。這些比常規的 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] 為了確保在混合的 Java/Kotlin 程式碼庫中的相容性,Kotlin 即使對於無符號型別也使用 Java 對應的型別。

[2] 在 Java 中,無符號 32 位和 64 位整數使用其有符號的對應型別來表示,最高位僅儲存在符號位中。

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

[4] 64 位或無符號 32 位整數在解碼時總是表示為 long,但在設定欄位時如果給定的是 int,也可以是 int。在所有情況下,設定的值必須符合所表示的型別。參見 [2]。

[5] Proto2 通常不檢查字串欄位的 UTF-8 有效性。但是,不同語言之間的行為有所不同,不應在字串欄位中儲存無效的 UTF-8 資料。

[6] 在 64 位機器上使用 Integer,在 32 位機器上使用 string。

你可以在 Protocol Buffer 編碼中瞭解更多關於這些型別在序列化訊息時的編碼方式。

欄位預設值

當解析訊息時,如果編碼的訊息位元組中不包含某個特定欄位,那麼在解析的物件中訪問該欄位將返回該欄位的預設值。預設值是型別特定的:

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

對於 repeated 欄位,預設值是空的(通常是相應語言中的空列表)。

對於 map 欄位,預設值是空的(通常是相應語言中的空 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
  • results_per_page 的值(從“getter”返回)是 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;
}

列舉值加字首

在列舉值加字首時,剝離字首後的其餘名稱仍應是合法且符合樣式的列舉名稱。例如,避免以下情況:

enum DeviceTier {
  DEVICE_TIER_UNKNOWN = 0;
  DEVICE_TIER_1 = 1;
  DEVICE_TIER_2 = 2;
}

相反,使用像 DEVICE_TIER_TIER1 這樣的值名稱,其中 DEVICE_TIER_ 部分被視為對列舉值進行作用域限定,而不是作為單個列舉值名稱的一部分。一些 Protobuf 實現會自動剝離與包含列舉名稱匹配的字首(在安全的情況下),但在本例中不能,因為裸露的 1 不是合法的列舉值名稱。

我們計劃在未來的 Edition 中增加對作用域列舉的支援,這將消除手動為每個列舉值加字首的需要,並使其能夠簡潔地寫成 TIER1 = 1

列舉預設值

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`。否則,當發現別名時,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 檔案並更新所有呼叫點,而是在舊位置放置一個佔位符 .proto 檔案,使用 import public 概念將所有匯入轉發到新位置。

請注意,公共匯入功能在 Java、Kotlin、TypeScript、JavaScript、GCL 以及使用 protobuf 靜態反射的 C++ 目標中不可用。

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";
// You use definitions from old.proto and new.proto, but not other.proto

協議編譯器在協議編譯器命令列上使用 -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;
    }
  }
}

Groups

請注意,group 功能已棄用,在建立新的訊息型別時不應使用。請改用巢狀訊息型別。

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

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

一個 group 只是將巢狀訊息型別和欄位合併到一個宣告中。在您的程式碼中,您可以像處理具有名為 resultResult 型別欄位一樣處理此訊息(後者的名稱會轉換為小寫,以免與前者衝突)。因此,此示例與之前的 SearchResponse 完全等效,只是訊息具有不同的線格式

更新訊息型別

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

請查閱 Proto 最佳實踐和以下規則:

二進位制線路非安全變更

有線不安全 (Wire-unsafe) 的更改是指,如果您使用新的 schema 解析器來解析使用舊 schema 序列化的資料(或反之),將會導致中斷的 schema 更改。只有在您知道資料的所有序列化器和反序列化器都使用新的 schema 時,才進行有線不安全的更改。

  • 更改任何現有欄位的欄位編號是不安全的。
    • 更改欄位編號等同於刪除該欄位並新增一個具有相同型別的新欄位。如果你想重編號一個欄位,請參閱刪除欄位的說明。
  • 將欄位移入一個現有的 oneof 是不安全的。

二進位制線路安全變更

線路安全的變更是指完全安全地演進模式,而不會有資料丟失或新的解析失敗的風險。

請注意,任何有線安全 (wire-safe) 的更改對於給定語言的應用程式碼來說都可能是破壞性更改。例如,向一個已有的列舉中新增一個值,對於任何對該列舉進行窮盡式 switch 的程式碼來說,都會導致編譯中斷。因此,Google 可能會避免在公共訊息上進行這類更改:AIPs 中包含了關於哪些更改是安全的指導意見。

  • 新增新欄位是安全的。
    • 如果您新增新欄位,任何由使用您的“舊”訊息格式的程式碼序列化的訊息仍然可以被您的新生成程式碼解析。您應該記住這些元素的預設值,以便新程式碼可以正確地與舊程式碼生成的訊息互動。同樣,由您的新程式碼建立的訊息可以被您的舊程式碼解析:舊的二進位制檔案在解析時會簡單地忽略新欄位。有關詳細資訊,請參閱未知欄位部分。
  • 移除欄位是安全的。
    • 在您更新的訊息型別中,不得再次使用相同的欄位編號。您可能需要重新命名欄位,例如新增字首“OBSOLETE_”,或者將欄位編號設為保留,這樣您 .proto 的未來使用者就不會意外地重用該編號。
  • 向列舉新增額外的值是安全的。
  • 將一個單一的顯式存在欄位或擴充套件變更為一個新的 oneof 的成員是安全的。
  • 將一個只包含一個欄位的 oneof 更改為顯式存在欄位是安全的。
  • 將一個欄位更改為具有相同編號和型別的擴充套件是安全的。

二進位制線路相容變更(有條件安全)

與有線安全 (Wire-safe) 的更改不同,有線相容 (wire-compatible) 意味著相同的資料在給定更改前後都可以被解析。然而,在這種型別的更改下,資料的解析可能會是有損的。例如,將一個 int32 更改為 int64 是一個相容的更改,但是如果寫入了一個大於 INT32_MAX 的值,一個將其作為 int32 讀取的客戶端將丟棄該數字的高位位元。

只有在您仔細管理系統推出過程的情況下,才能對您的 schema 進行相容性更改。例如,您可以將 int32 更改為 int64,但要確保在新的 schema 部署到所有端點之前,您繼續只寫入合法的 int32 值,然後在之後才開始寫入更大的值。

如果你的模式是在組織外部發布的,通常不應該進行線路相容的更改,因為你無法管理新模式的部署,從而無法知道何時使用不同範圍的值是安全的。

  • int32uint32int64uint64bool 都是相容的。
    • 如果從線路中解析出一個不適合相應型別的數字,你將得到與在 C++ 中將該數字強制轉換為該型別相同的效果(例如,如果一個 64 位數字被作為 int32 讀取,它將被截斷為 32 位)。
  • sint32sint64 彼此相容,但與其他整數型別相容。
    • 如果寫入的值在 INT_MIN 和 INT_MAX(含)之間,那麼用任一型別解析都會得到相同的值。如果寫入了一個超出該範圍的 sint64 值,並被解析為 sint32,則 varint 會被截斷為 32 位,然後進行 zigzag 解碼(這將導致觀察到不同的值)。
  • 只要位元組是有效的 UTF-8,stringbytes 就是相容的。
  • 如果位元組包含訊息的編碼例項,則嵌入式訊息與 bytes 相容。
  • fixed32sfixed32 相容,fixed64sfixed64 相容。
  • 對於 stringbytes 和訊息欄位,singular 與 repeated 相容。
    • 對於重複欄位的序列化資料作為輸入,期望該欄位為 singular 的客戶端,如果它是原始型別欄位,將取最後一個輸入值;如果它是訊息型別欄位,將合併所有輸入元素。請注意,這對於數字型別(包括布林值和列舉)通常是安全的。數字型別的重複欄位預設以打包格式序列化,當期望的是 singular 欄位時,將無法正確解析。
  • enumint32uint32int64uint64 相容。
    • 請注意,當訊息被反序列化時,客戶端程式碼可能會以不同的方式處理它們:例如,未識別的 proto3 `enum` 值將保留在訊息中,但當訊息被反序列化時,其表示方式是依賴於語言的。
  • map<K, V> 和相應的 repeated 訊息欄位之間更改欄位是二進位制相容的(有關訊息佈局和其他限制,請參閱下面的對映)。
    • 然而,更改的安全性取決於應用程式:在反序列化和重新序列化訊息時,使用 `repeated` 欄位定義的客戶端將產生語義上相同的結果;但是,使用 `map` 欄位定義的客戶端可能會重新排序條目並丟棄具有重複鍵的條目。

未知欄位

未知欄位是格式良好的 protocol buffer 序列化資料,表示解析器無法識別的欄位。例如,當一箇舊的二進位制檔案解析一個由新的二進位制檔案傳送的帶有新欄位的資料時,這些新欄位在舊的二進位制檔案中就成為未知欄位。

Proto2 訊息保留未知欄位,並在解析和序列化輸出中包含它們。

保留未知欄位

一些操作可能導致未知欄位丟失。例如,如果你執行以下操作之一,未知欄位將丟失:

  • 將 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
    }
  ];
}

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 生態系統之外的機制在所選的擴充套件範圍內分配擴充套件欄位號。一個示例可以是使用 monorepo 的提交號。從 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。要使用 `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

  • 反射 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() 兩個帶有 oneofs 的訊息,每個訊息最終會得到對方的 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 欄位有類似的問題。

對映

如果你想在資料定義中建立關聯對映,protocol buffers 提供了一種方便的快捷語法:

map<key_type, value_type> map_field = N;

...其中 `key_type` 可以是任何整數或字串型別(即,除浮點型別和 `bytes` 之外的任何標量型別)。請注意,列舉和 proto 訊息都不能作為 `key_type`。`value_type` 可以是除另一個 map 之外的任何型別。

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

map<string, Project> projects = 3;

對映特性

  • Map 不支援擴充套件。
  • Map 不能是 repeatedoptionalrequired
  • 對映值的線路格式順序和對映迭代順序是未定義的,所以你不能依賴你的對映項會以特定的順序排列。
  • .proto 生成文字格式時,對映按鍵排序。數字鍵按數值排序。
  • 從線路解析或合併時,如果存在重複的對映鍵,則使用最後看到的鍵。從文字格式解析對映時,如果存在重複的鍵,解析可能會失敗。
  • 如果你為一個 map 欄位提供了鍵但沒有提供值,該欄位被序列化時的行為取決於語言。在 C++、Java、Kotlin 和 Python 中,會序列化該型別的預設值,而在其他語言中則什麼也不序列化。
  • 符號 FooEntry 不能與 map foo 存在於同一作用域,因為 FooEntry 已經被 map 的實現使用了。

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

向後相容性

map 語法線上路上等同於以下內容,因此不支援 map 的 protocol buffers 實現仍然可以處理你的資料:

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

repeated MapFieldEntry map_field = N;

任何支援對映的 protocol buffers 實現都必須既能生成也能接受可被先前定義接受的資料。

包(Packages)

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

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

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

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

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

  • C++ 中,生成的類被包裹在一個 C++ 名稱空間內。例如,Open 會在名稱空間 foo::bar 中。
  • JavaKotlin 中,除非你在你的 .proto 檔案中明確提供一個 option java_package,否則該包將用作 Java 包。
  • Python 中,package 指令被忽略,因為 Python 模組是根據它們在檔案系統中的位置來組織的。
  • Go 中,`package` 指令被忽略,生成的 `.pb.go` 檔案位於以相應 `go_proto_library` Bazel 規則命名的包中。對於開源專案,您**必須**提供一個 `go_package` 選項或設定 Bazel `-M` 標誌。
  • Ruby 中,生成的類被包裝在巢狀的 Ruby 名稱空間內,並轉換為所需的 Ruby 大寫風格(首字母大寫;如果第一個字元不是字母,則字首為 `PB_`)。例如,`Open` 將位於 `Foo::Bar` 名稱空間中。
  • PHP 中,除非您在 .proto 檔案中明確提供了 `option php_namespace`,否則 package 在轉換為 PascalCase 後被用作名稱空間。例如,`Open` 將位於 `Foo\Bar` 名稱空間中。
  • C# 中,除非您在 `.proto` 檔案中明確提供 `option csharp_namespace`,否則 package 在轉換為 PascalCase 後將用作名稱空間。例如,`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 的抽象介面和一個相應的“存根”實現。該存根將所有呼叫轉發到一個 RpcChannel,而 RpcChannel 又是一個抽象介面,您必須根據自己的 RPC 系統自行定義。例如,您可以實現一個 RpcChannel,它將訊息序列化並透過 HTTP 傳送到伺服器。換句話說,生成的存根提供了一個型別安全的介面,用於進行基於協議緩衝區的 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 與協議緩衝區配合得非常好,並且允許您使用特殊的協議緩衝區編譯器外掛直接從 .proto 檔案生成相關的 RPC 程式碼。但是,由於使用 proto2 和 proto3 生成的客戶端和伺服器之間可能存在相容性問題,我們建議您使用 proto3 或 edition 2023 來定義 gRPC 服務。您可以在 Proto3 語言指南中找到有關 proto3 語法的更多資訊,並在 Edition 2023 語言指南中找到有關 edition 2023 的更多資訊。

除了 gRPC,還有一些正在進行的第三方專案致力於開發 Protocol Buffers 的 RPC 實現。有關我們所知的專案連結列表,請參閱第三方附加元件 Wiki 頁面

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_services, java_generic_services, py_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 = RETENTION_SOURCE` 來指定一個選項(或選項中的欄位)在執行時不得被保留。這被稱為*原始碼保留期*。

選項保留是一個高階功能,大多數使用者不需要擔心,但如果您想使用某些選項而不想為在二進位制檔案中保留它們付出程式碼大小的代價,它可能很有用。具有源保留的選項對於 `protoc` 和 `protoc` 外掛仍然可見,因此程式碼生成器可以使用它們來自定義其行為。

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

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# 程式碼來處理在 .proto 檔案中定義的訊息型別,您需要在 .proto 檔案上執行 protocol buffer 編譯器 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 的簡稱。

注意: 相對於其 `proto_path` 的檔案路徑在給定的二進位制檔案中必須是全域性唯一的。例如,如果您有 `proto/lib1/data.proto` 和 `proto/lib2/data.proto`,這兩個檔案不能與 `-I=proto/lib1 -I=proto/lib2` 一起使用,因為 `import "data.proto"` 的含義將不明確。相反,應該使用 `-Iproto/`,全域性名稱將是 `lib1/data.proto` 和 `lib2/data.proto`。

如果您正在釋出一個庫,並且其他使用者可能會直接使用您的訊息,您應該在他們預期使用的路徑中包含一個唯一的庫名,以避免檔名衝突。如果您在一個專案中有多個目錄,最好的做法是傾向於將一個 `-I` 設定為專案的頂層目錄。

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

    為了方便起見,如果 `DST_DIR` 以 `.zip` 或 `.jar` 結尾,編譯器會將輸出寫入一個具有給定名稱的 ZIP 格式的存檔檔案中。`.jar` 輸出也會被賦予一個 Java JAR 規範所要求的清單檔案。請注意,如果輸出存檔已經存在,它將被覆蓋。

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

檔案位置

最好不要將 .proto 檔案與其他語言的原始檔放在同一個目錄中。考慮在你的專案根包下為 .proto 檔案建立一個子包 proto

位置應與語言無關

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

這條規則的例外是當很明顯 protos 只會在 Java 上下文中使用時,例如用於測試。

支援的平臺

有關資訊