語言指南 (版本)
本指南說明如何使用 Protocol Buffer 語言來建構您的 Protocol Buffer 資料,包括 .proto
檔案語法,以及如何從您的 .proto
檔案產生資料存取類別。本指南涵蓋 Protocol Buffers 語言的2023 版本。如需版本在概念上與 proto2 和 proto3 有何不同的相關資訊,請參閱Protobuf 版本總覽。
如需 proto2 語法的相關資訊,請參閱Proto2 語言指南。
如需 proto3 語法的相關資訊,請參閱Proto3 語言指南。
這是一份參考指南 – 如需逐步範例,其中使用了本文件中描述的許多功能,請參閱您所選語言的教學課程。
定義訊息類型
首先,讓我們來看一個非常簡單的範例。假設您想要定義搜尋請求訊息格式,其中每個搜尋請求都有一個查詢字串、您感興趣的特定結果頁面,以及每頁結果數。以下是您用來定義訊息類型的 .proto
檔案。
edition = "2023";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
檔案的第一行指定您正在使用 protobuf 語言規格的 2023 版本。
edition
(或 proto2/proto3 的syntax
) 必須是檔案中的第一個非空白、非註解行。- 如果未指定
edition
或syntax
,Protocol Buffer 編譯器會假設您正在使用 proto2。
SearchRequest
訊息定義指定了三個欄位 (名稱/值組),每個欄位對應到您想要包含在此訊息類型中的一項資料。每個欄位都有一個名稱和一個類型。
指定欄位類型
在先前的範例中,所有欄位都是純量類型:兩個整數 (page_number
和 results_per_page
) 和一個字串 (query
)。您也可以為您的欄位指定列舉和複合類型,例如其他訊息類型。
指派欄位編號
您必須為訊息定義中的每個欄位指定一個介於 1
和 536,870,911
之間的數字,並具有以下限制
- 給定的數字在該訊息的所有欄位中必須是唯一的。
- 欄位編號
19,000
到19,999
保留給 Protocol Buffers 實作使用。如果您在訊息中使用這些保留的欄位編號之一,Protocol Buffer 編譯器將會發出警告。 - 您不能使用任何先前保留的欄位編號,或已分配給擴充功能的任何欄位編號。
此編號一旦您的訊息類型開始使用就不能變更,因為它會識別訊息線路格式中的欄位。「變更」欄位編號相當於刪除該欄位並建立一個具有相同類型但編號不同的新欄位。請參閱刪除欄位,以瞭解如何正確執行此操作。
欄位編號永遠不應重複使用。永遠不要從保留清單中取出欄位編號,以便與新的欄位定義重複使用。請參閱重複使用欄位編號的後果。
您應該將欄位編號 1 到 15 用於最常設定的欄位。較低的欄位編號值在線路格式中佔用的空間較小。例如,編號在 1 到 15 範圍內的欄位需要一個位元組來編碼。編號在 16 到 2047 範圍內的欄位需要兩個位元組。您可以在Protocol Buffer 編碼中找到更多相關資訊。
重複使用欄位編號的後果
重複使用欄位編號會使解碼線路格式訊息變得模稜兩可。
protobuf 線路格式很精簡,並且沒有提供方法來偵測使用一個定義編碼並使用另一個定義解碼的欄位。
使用一個定義編碼欄位,然後使用不同的定義解碼同一個欄位可能會導致
- 開發人員時間浪費在偵錯上
- 剖析/合併錯誤 (最好的情況)
- PII/SPII 洩漏
- 資料損毀
欄位編號重複使用的常見原因
重新編號欄位 (有時是為了使欄位的編號順序更美觀而進行)。重新編號實際上會刪除並重新新增所有參與重新編號的欄位,導致不相容的線路格式變更。
刪除欄位,但未保留編號以防止未來重複使用。
欄位編號限制為 29 位元而不是 32 位元,因為有三個位元用於指定欄位的線路格式。如需更多相關資訊,請參閱編碼主題。
指定欄位基數
訊息欄位可以是下列其中一種
單數:
單數欄位沒有明確的基數標籤。它有兩種可能的狀態
- 欄位已設定,並且包含明確設定或從線路剖析的值。它將序列化到線路。
- 欄位未設定,並且將傳回預設值。它將不會序列化到線路。
您可以檢查以查看值是否已明確設定。
已遷移到版本的 Proto3隱含欄位將使用
field_presence
特性設定為IMPLICIT
值。已遷移到版本的 Proto2
required
欄位也將使用field_presence
特性,但設定為LEGACY_REQUIRED
。repeated
:此欄位類型在格式正確的訊息中可以重複零次或多次。重複值的順序將會保留。map
:這是一種配對的鍵/值欄位類型。請參閱Map,以瞭解有關此欄位類型的更多資訊。
重複欄位預設為已封裝
在 proto 版本中,純量數值類型的 repeated
欄位預設使用 packed
編碼。
您可以在Protocol Buffer 編碼中找到更多有關 packed
編碼的資訊。
格式正確的訊息
術語「格式正確」在應用於 protobuf 訊息時,指的是序列化/還原序列化的位元組。protoc 剖析器會驗證給定的 proto 定義檔案是否可剖析。
單數欄位可以在線路格式位元組中出現多次。剖析器將接受輸入,但只有該欄位的最後一個實例可以透過產生的繫結存取。請參閱後勝出,以瞭解有關此主題的更多資訊。
新增更多訊息類型
可以在單個 .proto
檔案中定義多個訊息類型。如果您要定義多個相關訊息,這會很有用 – 例如,如果您想要定義與您的 SearchResponse
訊息類型對應的回覆訊息格式,您可以將其新增到同一個 .proto
message SearchRequest {
string query = 1;
int32 page_number = 2;
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 {
string query = 1;
// Which page number do we want?
int32 page_number = 2;
// Number of results to return per page.
int32 results_per_page = 3;
}
刪除欄位
如果未正確完成刪除欄位,可能會導致嚴重問題。
當您不再需要某個欄位,並且已從用戶端程式碼中刪除所有參考時,您可以從訊息中刪除欄位定義。但是,您必須保留已刪除的欄位編號。如果您不保留欄位編號,開發人員未來可能會重複使用該編號。
您也應該保留欄位名稱,以允許訊息的 JSON 和 TextFormat 編碼繼續剖析。
保留的欄位編號
如果您透過完全刪除欄位或註解掉欄位來更新訊息類型,未來的開發人員可以在對類型進行自己的更新時重複使用該欄位編號。這可能會導致嚴重問題,如重複使用欄位編號的後果中所述。為了確保不會發生這種情況,請將您刪除的欄位編號新增到 reserved
清單中。
如果未來的任何開發人員嘗試使用這些保留的欄位編號,protoc 編譯器將會產生錯誤訊息。
message Foo {
reserved 2, 15, 9 to 11;
}
保留的欄位編號範圍是包含性的 (9 to 11
與 9, 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.h
和pbobjc.m
檔案,其中每個訊息類型在您的檔案中描述一個類別。 - 對於 C#,編譯器會從每個
.proto
產生一個.cs
檔案,其中每個訊息類型在您的檔案中描述一個類別。 - 對於 PHP,編譯器會為您檔案中描述的每個訊息類型產生一個
.php
訊息檔案,並為您編譯的每個.proto
檔案產生一個.php
метаdata 檔案。 метаdata 檔案用於將有效的訊息類型載入描述符池。 - 對於 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 類型 |
---|---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | double | f64 |
float | float | float | float | float32 | Float | float | float | double | f32 |
int32 | int32_t | int | int | int32 | Fixnum 或 Bignum (視需要) | int | integer | int | i32 |
int64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
uint32 | uint32_t | int[2] | int/long[4] | uint32 | Fixnum 或 Bignum (視需要) | uint | integer | int | u32 |
uint64 | uint64_t | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
sint32 | int32_t | int | int | int32 | Fixnum 或 Bignum (視需要) | int | integer | int | i32 |
sint64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
fixed32 | uint32_t | int[2] | int/long[4] | uint32 | Fixnum 或 Bignum (視需要) | uint | integer | int | u32 |
fixed64 | uint64_t | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
sfixed32 | int32_t | int | int | int32 | Fixnum 或 Bignum (視需要) | int | integer | int | i32 |
sfixed64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | bool |
string | string | String | str/unicode[5] | string | String (UTF-8) | string | string | String | ProtoString |
bytes | string | ByteString | str (Python 2), bytes (Python 3) | []byte | String (ASCII-8BIT) | ByteString | string | List | ProtoBytes |
[1] Kotlin 使用 Java 中的對應類型,即使對於無符號類型也是如此,以確保混合 Java/Kotlin 程式碼庫中的相容性。
[2] 在 Java 中,無符號 32 位元和 64 位元整數使用其帶符號的對應項表示,其中最高位元只是儲存在符號位元中。
[3] 在所有情況下,將值設定為欄位時都會執行類型檢查,以確保其有效。
[4] 64 位元或無符號 32 位元整數在解碼時始終表示為 long,但如果在設定欄位時給定 int,則可以是 int。在所有情況下,值都必須符合設定時表示的類型。請參閱 [2]。
[5] Python 字串在解碼時表示為 unicode,但如果給定 ASCII 字串,則可以是 str (這可能會變更)。
[6] 在 64 位元電腦上使用整數,在 32 位元電腦上使用字串。
您可以在Protocol Buffer 編碼中找到更多關於當您序列化訊息時,這些類型如何編碼的資訊。
預設欄位值
當剖析訊息時,如果編碼的訊息位元組不包含特定欄位,則存取剖析物件中的該欄位會傳回該欄位的預設值。預設值是類型特定的
- 對於字串,預設值為空字串。
- 對於位元組,預設值為空位元組。
- 對於布林值,預設值為 false。
- 對於數值類型,預設值為零。
- 對於訊息欄位,欄位未設定。其確切值取決於語言。請參閱產生的程式碼指南以取得詳細資訊。
- 對於列舉,預設值為第一個定義的列舉值,該值必須為 0。請參閱列舉預設值。
重複欄位的預設值為空 (通常是適當語言中的空清單)。
map 欄位的預設值為空 (通常是適當語言中的空 map)。
覆寫預設純量值
在 protobuf 版本中,您可以為單數非訊息欄位指定明確的預設值。例如,假設您想要為 SearchRequest.result_per_page
欄位提供預設值 10
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」傳回傳送者的值。
請參閱您所選語言的產生的程式碼指南,以取得有關預設值如何在產生的程式碼中運作的更多詳細資訊。
無法為 field_presence
特性設定為 IMPLICIT
的欄位指定明確的預設值。
列舉
當您定義訊息類型時,您可能希望其欄位之一僅具有預定義值清單中的其中一個值。例如,假設您想要為每個 SearchRequest
新增一個 corpus
欄位,其中 corpus 可以是 UNIVERSAL
、WEB
、IMAGES
、LOCAL
、NEWS
、PRODUCTS
或 VIDEO
。您可以透過將 enum
新增到訊息定義中,並為每個可能的值新增一個常數來非常簡單地完成此操作。
在以下範例中,我們新增了一個名為 Corpus
的 enum
,其中包含所有可能的值,以及一個 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 {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
Corpus corpus = 4;
}
列舉預設值
SearchRequest.corpus
欄位的預設值為 CORPUS_UNSPECIFIED
,因為這是 enum 中定義的第一個值。
在 2023 版本中,列舉定義中定義的第一個值必須具有值零,並且應具有名稱 ENUM_TYPE_NAME_UNSPECIFIED
或 ENUM_TYPE_NAME_UNKNOWN
。這是因為
- 零值需要成為第一個元素,以便與 proto2 語意相容,其中第一個列舉值是預設值,除非明確指定了不同的值。
- 必須有一個零值,以便與 proto3 語意相容,其中零值用作使用此列舉類型的所有隱含存在性欄位的預設值。
也建議第一個預設值除了「此值未指定」之外,沒有其他語意含義。
可以像這樣明確覆寫列舉欄位 (例如 SearchRequest.corpus
欄位) 的預設值
Corpus corpus = 4 [default = CORPUS_UNIVERSAL];
如果列舉類型已使用 option features.enum_type = CLOSED;
從 proto2 遷移而來,則對列舉中的第一個值沒有限制。不建議變更這些類型列舉的第一個值,因為它會變更任何使用該列舉類型且沒有明確欄位預設值的欄位的預設值。
列舉值別名
您可以透過為不同的列舉常數指派相同的值來定義別名。若要執行此操作,您需要將 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
類別,該類別用於在執行階段產生的類別中建立一組具有整數值的符號常數。
重要事項
產生的程式碼可能會受到語言特定的列舉器數量限制 (一種語言的低數千個)。請檢閱您計畫使用的語言的限制。在還原序列化期間,不明的列舉值將保留在訊息中,儘管當訊息還原序列化時如何表示這是語言相關的。在支援具有超出指定符號範圍的值的開放式列舉類型的語言中,例如 C++ 和 Go,不明的列舉值只是以其基礎整數表示形式儲存。在具有封閉式列舉類型 (例如 Java) 的語言中,列舉中的一個 case 用於表示不明值,並且可以使用特殊的存取器存取基礎整數。在任何一種情況下,如果訊息被序列化,不明的值仍將與訊息一起序列化。
重要事項
如需列舉應如何運作與目前在不同語言中的運作方式的對比資訊,請參閱列舉行為。如需有關如何在您的應用程式中使用訊息 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 {
string url = 1;
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
相依性可以被任何匯入包含 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
標記設定為專案的根目錄,並對所有匯入使用完整限定名稱。
使用 proto2 和 proto3 訊息類型
可以匯入 proto2 和 proto3 訊息類型,並在您的 editions 2023 訊息中使用它們,反之亦然。
巢狀類型
您可以在其他訊息類型內定義和使用訊息類型,如下列範例所示 – 此處 Result
訊息是在 SearchResponse
訊息內定義的
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果您想在其父訊息類型外部重複使用此訊息類型,您可以將其稱為 _Parent_._Type_
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
您可以根據需要深度巢狀訊息。在下面的範例中,請注意兩個名為 Inner
的巢狀類型是完全獨立的,因為它們是在不同的訊息中定義的
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}
更新訊息類型
如果現有的訊息類型不再滿足您的所有需求 – 例如,您希望訊息格式有一個額外的欄位 – 但您仍然希望使用舊格式建立的程式碼,請別擔心!當您使用二進位線路格式時,更新訊息類型而不會破壞任何現有程式碼非常簡單。
注意
如果您使用 JSON 或 proto 文字格式 來儲存您的協定緩衝區訊息,則您可以在 proto 定義中進行的變更會有所不同。請查看 Proto 最佳實務 和以下規則
- 請勿變更任何現有欄位的欄位編號。「變更」欄位編號相當於刪除欄位並新增具有相同類型的新欄位。如果您想重新編號欄位,請參閱 刪除欄位 的說明。
- 如果您新增新的欄位,使用「舊」訊息格式的程式碼序列化的任何訊息仍然可以被您新的產生程式碼解析。您應該記住這些元素的 預設值,以便新程式碼可以正確地與舊程式碼產生的訊息互動。同樣地,由您新程式碼建立的訊息可以被您的舊程式碼解析:舊的二進位檔在解析時只會忽略新的欄位。有關詳細資訊,請參閱 未知欄位 章節。
- 欄位可以被移除,只要欄位編號在您更新的訊息類型中不再使用即可。您可能想要重新命名欄位,或許新增前綴「OBSOLETE_」,或者將欄位編號 保留,以便您
.proto
的未來使用者不會意外地重複使用該編號。 int32
、uint32
、int64
、uint64
和bool
都是相容的 – 這表示您可以將欄位從這些類型之一變更為另一個類型,而不會破壞向前或向後相容性。如果從線路上解析的數字不符合相應的類型,您將獲得與在 C++ 中將數字轉換為該類型相同的效果(例如,如果將 64 位元數字讀取為 int32,它將被截斷為 32 位元)。sint32
和sint64
彼此相容,但與其他整數類型不相容。string
和bytes
只要位元組是有效的 UTF-8 就相容。- 如果嵌入式訊息的位元組包含訊息的編碼實例,則嵌入式訊息與
bytes
相容。 fixed32
與sfixed32
相容,而fixed64
與sfixed64
相容。- 對於
string
、bytes
和訊息欄位,singular 與repeated
相容。給定 repeated 欄位的序列化資料作為輸入,期望此欄位為 singular 的用戶端將採用最後一個輸入值(如果它是原始類型欄位),或者合併所有輸入元素(如果它是訊息類型欄位)。請注意,這對於數值類型(包括布林值和列舉)通常是不安全的。數值類型的 Repeated 欄位預設以 packed 格式序列化,當期望 singular 欄位時,將無法正確解析。 enum
在線路格式方面與int32
、uint32
、int64
和uint64
相容(請注意,如果值不符合,則會被截斷)。但是,請注意,當訊息被反序列化時,用戶端程式碼可能會以不同的方式對待它們:例如,無法辨識的enum
值將保留在訊息中,但當訊息被反序列化時,這如何表示是與語言相關的。Int 欄位始終只保留它們的值。- 將單個
optional
欄位或擴充功能變更為新的oneof
的成員是二進位相容的,但是對於某些語言(特別是 Go),產生的程式碼的 API 將以不相容的方式變更。因此,Google 不會在其公共 API 中進行此類變更,如 AIP-180 中所述。關於來源相容性的相同警告,如果您確定沒有程式碼一次設定多個欄位,則將多個欄位移動到新的oneof
中可能是安全的。將欄位移動到現有的oneof
中是不安全的。同樣地,將單個欄位oneof
變更為optional
欄位或擴充功能是安全的。 - 在
map<K, V>
和相應的repeated
訊息欄位之間變更欄位是二進位相容的(請參閱下面的 地圖 以了解訊息佈局和其他限制)。但是,變更的安全性取決於應用程式:當反序列化和重新序列化訊息時,使用repeated
欄位定義的用戶端將產生語義上相同的結果;但是,使用map
欄位定義的用戶端可能會重新排序條目並刪除具有重複鍵的條目。
不明欄位
未知欄位是格式良好的協定緩衝區序列化資料,表示解析器無法辨識的欄位。例如,當舊的二進位檔解析由具有新欄位的新二進位檔傳送的資料時,這些新欄位在舊的二進位檔中會變成未知欄位。
Editions 訊息保留未知欄位,並在解析和序列化輸出期間包含它們,這與 proto2 和 proto3 行為一致。
保留不明欄位
某些動作可能會導致未知欄位遺失。例如,如果您執行以下操作之一,則未知欄位會遺失
- 將 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.Video
的 repeated
擴充功能欄位使用。若要了解有關擴充功能宣告的更多資訊,請參閱 擴充功能宣告。
請注意,容器訊息的檔案 (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 及以上只能與擴充功能宣告一起使用。
指定擴充功能類型
擴充功能可以是任何欄位類型,但 oneof
和 map
除外。
巢狀擴充功能 (不建議)
您可以在另一個訊息的範圍內宣告擴充功能
import "common/user_profile.proto";
package puppies;
message Photo {
extend common.UserProfile {
int32 likes_count = 111;
}
...
}
在這種情況下,存取此擴充功能的 C++ 程式碼是
UserProfile user_profile;
user_profile.SetExtension(puppies::Photo::likes_count, 42);
換句話說,唯一的效果是 likes_count
是在 puppies.Photo
的範圍內定義的。
這是一個常見的混淆來源:在訊息類型內巢狀宣告 extend
區塊並不暗示外部類型和擴充類型之間有任何關係。特別是,先前的範例並不表示 Photo
是 UserProfile
的任何一種子類別。它僅表示符號 likes_count
是在 Photo
的範圍內宣告的;它只是一個靜態成員。
常見的模式是在擴充功能的欄位類型範圍內定義擴充功能 - 例如,這是 media.UserContent
類型為 puppies.Photo
的擴充功能,其中擴充功能被定義為 Photo
的一部分
import "media/user_content.proto";
package puppies;
message Photo {
extend media.UserContent {
Photo puppy_photo = 127;
}
...
}
但是,沒有要求訊息類型的擴充功能必須在該類型內定義。您也可以使用標準定義模式
import "media/user_content.proto";
package puppies;
message Photo {
...
}
// This can even be in a different file.
extend media.UserContent {
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
如果您有一個具有許多 singular 欄位的訊息,並且最多一次只會設定一個欄位,則可以使用 oneof 功能來強制執行此行為並節省記憶體。
Oneof 欄位類似於 singular 欄位,除了 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
欄位和 repeated
欄位除外。如果您需要將 repeated 欄位新增到 oneof,您可以使用包含 repeated 欄位的訊息。
在您產生的程式碼中,oneof 欄位具有與常規欄位相同的 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()
具有 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 的成員。
標籤重複使用問題
- 將 singular 欄位移入或移出 oneof:在訊息序列化和解析後,您可能會遺失某些資訊(某些欄位將被清除)。但是,您可以安全地將單個欄位移動到新的 oneof 中,並且如果已知一次只設定一個欄位,則可以移動多個欄位。有關更多詳細資訊,請參閱 更新訊息類型。
- 刪除 oneof 欄位並將其重新新增:這可能會在訊息序列化和解析後清除您目前設定的 oneof 欄位。
- 拆分或合併 oneof:這與移動 singular 欄位有類似的問題。
Map
如果您想建立關聯式地圖作為資料定義的一部分,協定緩衝區提供了一個方便的捷徑語法
map<key_type, value_type> map_field = N;
…其中 key_type
可以是任何整數或字串類型(因此,任何 純量 類型,但浮點類型和 bytes
除外)。請注意,列舉和 proto 訊息對於 key_type
都是無效的。value_type
可以是除另一個地圖之外的任何類型。
因此,例如,如果您想建立一個專案地圖,其中每個 Project
訊息都與字串鍵關聯,您可以像這樣定義它
map<string, Project> projects = 3;
Map 特性
- 地圖不支援擴充功能。
- 地圖欄位不能是
repeated
。 - 地圖值的線路格式排序和地圖迭代排序是未定義的,因此您不能依賴地圖項目以特定順序排列。
- 當為
.proto
產生文字格式時,地圖會按鍵排序。數字鍵會按數字排序。 - 當從線路解析或合併時,如果存在重複的地圖鍵,則使用最後一個看到的鍵。當從文字格式解析地圖時,如果存在重複的鍵,則解析可能會失敗。
- 如果您為地圖欄位提供鍵但沒有值,則欄位序列化時的行為與語言相關。在 C++、Java、Kotlin 和 Python 中,該類型的預設值會被序列化,而在其他語言中則不會序列化任何內容。
- 符號
FooEntry
不能與地圖foo
存在於同一個範圍內,因為FooEntry
已被地圖的實作使用。
產生的地圖 API 目前適用於所有支援的語言。您可以在相關的 API 參考 中找到有關您選擇的語言的地圖 API 的更多資訊。
回溯相容性
地圖語法在線路上等效於以下內容,因此不支援地圖的協定緩衝區實作仍然可以處理您的資料
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何支援地圖的協定緩衝區實作都必須產生和接受可以被較早定義接受的資料。
套件
您可以將選用的 package
指定符新增到 .proto
檔案,以防止協定訊息類型之間的名稱衝突。
package foo.bar;
message Open { ... }
然後,您可以在定義訊息類型的欄位時使用 package 指定符
message Foo {
...
foo.bar.Open open = 1;
...
}
package 指定符影響產生的程式碼的方式取決於您選擇的語言
- 在 C++ 中,產生的類別包裝在 C++ 命名空間內。例如,
Open
將在命名空間foo::bar
中。 - 在 Java 和 Kotlin 中,package 用作 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 中,package 在轉換為 PascalCase 後用作命名空間,除非您在
.proto
檔案中明確提供option php_namespace
。例如,Open
將在命名空間Foo\Bar
中。 - 在 C# 中,package 在轉換為 PascalCase 後用作命名空間,除非您在
.proto
檔案中明確提供option csharp_namespace
。例如,Open
將在命名空間Foo.Bar
中。
請注意,即使 package
指令不會直接影響產生的程式碼(例如在 Python 中),仍然強烈建議為 .proto
檔案指定 package,因為否則可能會導致描述器中的命名衝突,並使 proto 無法在其他語言之間移植。
套件和名稱解析
協定緩衝區語言中的類型名稱解析就像 C++:首先搜尋最內層範圍,然後搜尋下一個最內層範圍,依此類推,每個套件都被認為是其父套件的「內部」。前導「.」(例如,.foo.bar.Baz
)表示從最外層範圍開始。
協定緩衝區編譯器透過解析匯入的 .proto
檔案來解析所有類型名稱。每種語言的程式碼產生器都知道如何以該語言引用每種類型,即使它具有不同的範圍規則。
定義服務
如果您想將您的訊息類型與 RPC(遠端程序呼叫)系統一起使用,您可以在 .proto
檔案中定義 RPC 服務介面,並且協定緩衝區編譯器將以您選擇的語言產生服務介面程式碼和 Stub。因此,例如,如果您想定義一個 RPC 服務,其方法接受您的 SearchRequest
並傳回 SearchResponse
,您可以在您的 .proto
檔案中將其定義如下
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
與協定緩衝區一起使用的最直接的 RPC 系統是 gRPC:一個在 Google 開發的語言和平台中立的開放原始碼 RPC 系統。gRPC 與協定緩衝區特別相容,並可讓您使用特殊的協定緩衝區編譯器外掛程式直接從您的 .proto
檔案產生相關的 RPC 程式碼。
如果您不想使用 gRPC,也可以將協定緩衝區與您自己的 RPC 實作一起使用。您可以在 Proto2 語言指南 中找到有關此內容的更多資訊。
還有許多正在進行的第三方專案,用於開發協定緩衝區的 RPC 實作。如需我們所知專案的連結清單,請參閱 第三方附加元件 Wiki 頁面。
JSON 對應
標準 protobuf 二進位線路格式是使用 protobuf 的兩個系統之間進行通訊的首選序列化格式。對於與使用 JSON 而不是 protobuf 線路格式的系統進行通訊,Protobuf 支援 ProtoJSON 中的標準編碼。
選項
.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
(檔案選項):可以設定為SPEED
、CODE_SIZE
或LITE_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_services
、java_generic_services
、py_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 配置。objc_class_prefix
(檔案選項):設定 Objective-C 類別前綴,此字首會附加到由此 .proto 產生之所有 Objective-C 類別和列舉。沒有預設值。您應使用介於 3 到 5 個大寫字元之間的前綴,如 Apple 建議。請注意,所有 2 個字母的前綴都由 Apple 保留。packed
(欄位選項):在 protobuf 版本中,此選項鎖定為true
。若要使用 unpacked wireformat,您可以使用版本功能覆寫此選項。這提供與 2.3.0 版之前剖析器的相容性(極少需要),如下列範例所示repeated int32 samples = 4 [features.repeated_field_encoding = EXPANDED];
deprecated
(欄位選項):若設定為true
,表示該欄位已棄用,不應由新程式碼使用。在大多數語言中,這實際上沒有任何作用。在 Java 中,這會變成@Deprecated
註解。對於 C++,每當使用已棄用的欄位時,clang-tidy 都會產生警告。未來,其他語言專屬的程式碼產生器可能會在欄位的存取器上產生棄用註解,這反過來會在編譯嘗試使用該欄位的程式碼時發出警告。如果該欄位沒有人使用,且您想防止新使用者使用它,請考慮將欄位宣告替換為 reserved 陳述式。int32 old_field = 6 [deprecated = true];
列舉值選項
支援列舉值選項。您可以使用 deprecated
選項來指示不應再使用某個值。您也可以使用擴充功能建立自訂選項。
以下範例顯示新增這些選項的語法
import "google/protobuf/descriptor.proto";
extend google.protobuf.EnumValueOptions {
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 也允許您定義和使用自己的選項。請注意,這是一項進階功能,大多數人不需要。如果您確實認為需要建立自己的選項,請參閱 Proto2 語言指南 以取得詳細資訊。請注意,建立自訂選項會使用擴充功能。
選項保留
選項具有保留的概念,它控制選項是否保留在產生的程式碼中。選項預設具有執行階段保留,表示它們保留在產生的程式碼中,因此在產生的描述符池中於執行階段可見。但是,您可以設定 retention = RETENTION_SOURCE
以指定選項(或選項內的欄位)不得在執行階段保留。這稱為來源保留。
選項保留是一項進階功能,大多數使用者不必擔心,但如果您想使用某些選項,而又不想付出在二進位檔中保留它們的程式碼大小成本,則它可能很有用。具有來源保留的選項仍然對 protoc
和 protoc
外掛程式可見,因此程式碼產生器可以使用它們來自訂其行為。
保留可以直接在選項上設定,如下所示
extend google.protobuf.FileOptions {
int32 source_retention_option = 1234
[retention = RETENTION_SOURCE];
}
它也可以在一般欄位上設定,在這種情況下,它僅在該欄位出現在選項內部時才生效
message OptionsMessage {
int32 source_retention_field = 1 [retention = RETENTION_SOURCE];
}
您可以視需要設定 retention = RETENTION_RUNTIME
,但這沒有任何作用,因為這是預設行為。當訊息欄位標記為 RETENTION_SOURCE
時,其所有內容都會被捨棄;其中的欄位無法透過嘗試設定 RETENTION_RUNTIME
來覆寫它。
注意
截至 Protocol Buffers 22.0 版,對選項保留的支援仍在進行中,且僅支援 C++ 和 Java。Go 從 1.29.0 版開始提供支援。Python 支援已完成,但尚未發布。選項目標
欄位具有 targets
選項,此選項控制當欄位用作選項時,該欄位可能套用的實體類型。例如,如果欄位具有 targets = TARGET_TYPE_MESSAGE
,則該欄位無法在列舉 (或任何其他非訊息實體) 的自訂選項中設定。Protoc 會強制執行此操作,如果違反目標限制,則會引發錯誤。
乍看之下,此功能似乎不必要,因為每個自訂選項都是特定實體選項訊息的擴充,這已經將選項限制為該單一實體。但是,當您有一個套用至多個實體類型的共用選項訊息,並且想要控制該訊息中個別欄位的使用方式時,選項目標就很有用。例如
message MyOptions {
string file_only_option = 1 [targets = TARGET_TYPE_FILE];
int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE,
targets = TARGET_TYPE_ENUM];
}
extend google.protobuf.FileOptions {
MyOptions file_options = 50000;
}
extend google.protobuf.MessageOptions {
MyOptions message_options = 50000;
}
extend google.protobuf.EnumOptions {
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
檔案上執行協定緩衝區編譯器 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
的簡短形式。您可以提供一個或多個輸出指令
--cpp_out
在DST_DIR
中產生 C++ 程式碼。如需更多資訊,請參閱 C++ 產生程式碼參考。--java_out
在DST_DIR
中產生 Java 程式碼。如需更多資訊,請參閱 Java 產生程式碼參考。--kotlin_out
在DST_DIR
中產生額外的 Kotlin 程式碼。如需更多資訊,請參閱 Kotlin 產生程式碼參考。--python_out
在DST_DIR
中產生 Python 程式碼。如需更多資訊,請參閱 Python 產生程式碼參考。--go_out
在DST_DIR
中產生 Go 程式碼。如需更多資訊,請參閱 Go 產生程式碼參考。--ruby_out
在DST_DIR
中產生 Ruby 程式碼。如需更多資訊,請參閱 Ruby 產生程式碼參考。--objc_out
在DST_DIR
中產生 Objective-C 程式碼。如需更多資訊,請參閱 Objective-C 產生程式碼參考。--csharp_out
在DST_DIR
中產生 C# 程式碼。如需更多資訊,請參閱 C# 產生程式碼參考。--php_out
在DST_DIR
中產生 PHP 程式碼。如需更多資訊,請參閱 PHP 產生程式碼參考。
為了額外方便起見,如果
DST_DIR
以.zip
或.jar
結尾,編譯器會將輸出寫入具有指定名稱的單一 ZIP 格式封存檔。.jar
輸出也將獲得 Java JAR 規格要求的資訊清單檔。請注意,如果輸出封存檔已存在,它將被覆寫。您必須提供一個或多個
.proto
檔案作為輸入。可以一次指定多個.proto
檔案。雖然檔案名稱是相對於目前目錄,但每個檔案都必須位於其中一個IMPORT_PATH
中,以便編譯器可以判斷其標準名稱。
檔案位置
最好不要將 .proto
檔案與其他語言來源放在同一個目錄中。考慮在專案的根套件下,為 .proto
檔案建立子套件 proto
。
位置應與語言無關
當使用 Java 程式碼時,將相關的 .proto
檔案放在與 Java 來源相同的目錄中很方便。但是,如果任何非 Java 程式碼曾經使用相同的 protos,則路徑前綴將不再有意義。因此,一般而言,請將 protos 放在相關的語言中立目錄中,例如 //myteam/mypackage
。
此規則的例外情況是,當明確知道 protos 只會在 Java 環境中使用時,例如用於測試。
支援的平台
有關以下資訊
- 支援的作業系統、編譯器、建置系統和 C++ 版本,請參閱 Foundational C++ Support Policy。
- 支援的 PHP 版本,請參閱 Supported PHP versions。