本指南說明如何使用通訊協定緩衝區語言來建構通訊協定緩衝區資料,包括 .proto
檔案語法,以及如何從 .proto
檔案產生資料存取類別。其中涵蓋通訊協定緩衝區語言的 proto3 版本:如要進一步瞭解 proto2 語法,請參閱 Proto2 語言指南。
這是參考指南 – 如需使用本文件中描述許多功能的逐步範例,請參閱所選教學課程的教學課程 (目前僅適用於 proto2,即將推出更多 proto3 說明文件)。
定義訊息類型
首先來看看一個簡單的範例。假設您想定義搜尋要求訊息格式,其中每個搜尋要求都有查詢字串、您感興趣的搜尋結果頁面,以及每個網頁的結果數量。以下是用來定義訊息類型的 .proto
檔案。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 檔案的第一行指定您正在使用
proto3
語法:如果您不這麼做,通訊協定緩衝區編譯器會假設您使用的是 proto2。這必須是檔案的第一個非空白、非註解行。 SearchRequest
訊息定義會指定三個欄位 (名稱/值組合),每個要納入這類訊息的資料。每個欄位都有名稱和類型。
指定欄位類型
在上述範例中,所有欄位都是「純量類型」:兩個整數 (page_number
和 result_per_page
) 和字串 (query
)。不過您也可以為欄位指定複合類型,包含列舉和其他訊息類型。
指派欄位號碼
如您所見,訊息定義中的每個欄位都有專屬編號。這些欄位值用於識別訊息二進位格式的欄位,且在您使用訊息類型後,請勿進行變更。請注意,範圍 1 到 15 中的欄位編號會佔用一個位元組進行編碼,包括欄位號碼和欄位類型 (詳情請參閱通訊協定緩衝區編碼)。16 到 2047 範圍內的欄位編號需要兩個位元組。因此,您應針對經常發生的訊息元素保留數字 1 到 15。請記得為日後會經常更新的元素放置一些空間。
您可以指定的最小欄位值為 1,最大為 229 - 1,或 536,870,911。此外,您也無法使用 19000 到 19999 (FieldDescriptor::kFirstReservedNumber
到 FieldDescriptor::kLastReservedNumber
) 號碼,因為這些號碼已保留供通訊協定緩衝區實作使用;如果您在 .proto
中使用其中一項保留號碼,通訊協定緩衝區編譯器將會編譯。同樣地,您無法使用先前保留的欄位號碼。
指定欄位規則
訊息欄位可能是下列其中一種:
singular
:格式正確的訊息可以有零或一個欄位 (但只能有一個)。使用 proto3 語法時,如果沒有為特定欄位指定其他欄位規則,則此為預設欄位規則。您無法判斷其是否已從電匯進行剖析。除非它是預設值,否則會序列化到電匯。如要進一步瞭解這個主題,請參閱欄位狀態。optional
:與singular
相同,但您可以檢查這個值是否已明確設定。optional
欄位可能處於下列兩種狀態之一:- 欄位已設定,且包含明確從電匯設定或剖析的值。如此一來,該線就會序列化在電線上。
- ,則系統會傳回預設值。因此不會序列化到連接線。
repeated
:這個欄位格式可以在重複的訊息中重複出現零次或多次。系統會保留重複值的順序。map
:此為配對的鍵/值欄位類型。如要進一步瞭解這個欄位類型,請參閱地圖。
在 proto3 中,純量數值類型的 repeated
欄位預設會使用 packed
編碼。如要進一步瞭解 packed
編碼,請參閱通訊協定緩衝區編碼一文。
新增更多訊息類型
您可以在單一 .proto
檔案中定義多種訊息類型。如果您要定義多個相關訊息,這就可以派上用場;舉例來說,如果您想定義與 SearchResponse
訊息類型相對應的回覆訊息格式,就可以將它加入同一個 .proto
:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
新增註解
如要在 .proto
檔案中新增註解,請使用 C/C++ 樣式的 //
和 /* ... */
語法。
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want?
int32 result_per_page = 3; // Number of results to return per page.
}
保留欄位
如果您將欄位類型完全移除或加註,藉此更新訊息類型,日後使用者只要對類型進行更新,就能重複使用欄位編號。如果日後載入相同 .proto
的舊版本 (包括資料毀損、隱私權錯誤等),可能會造成嚴重問題。確保這種情況不會發生,方法是為已刪除欄位指定欄位編號 (和/或名稱,也可能會造成 JSON 序列化問題)。reserved
日後使用者若嘗試使用這些欄位 ID,通訊協定緩衝區編譯器就會提出申訴。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
請注意,您無法在同一個 reserved
陳述式中混用欄位名稱和欄位名稱。
你的 .proto
是哪些內容?
在 .proto
上執行通訊協定緩衝區編譯器時,編譯器會以您選擇的語言產生程式碼,您需要使用您在檔案中所描述的訊息類型,包括取得和設定欄位值、將訊息序列化到輸出串流,以及從輸入串流剖析訊息。
- 針對 C++,編譯器會從各個
.proto
產生.h
和.cc
檔案,其中包含檔案所描述的各種訊息類型的類別。 - 針對 Java,編譯器會產生每個訊息類型的類別的
.java
檔案,以及用於建立訊息類別執行個體的特殊Builder
類別。 - 針對 Kotlin,除了 Java 產生的程式碼以外,編譯器還會為每種訊息類型產生
.kt
檔案,其中包含可用於簡化訊息建立作業的 DSL。 - Python 略有不同,Python 編譯器會產生模組,其中包含
.proto
中每種訊息類型的靜態描述元,然後與 metaclass 搭配使用,以便在執行階段建立必要的 Python 資料存取類別。 - 針對 Go,編譯器會產生
.pb.go
檔案,其中包含檔案中每個訊息類型的類型。 - 針對 Ruby,編譯器會產生包含訊息類型的 Ruby 模組的
.rb
檔案。 - 針對 Objective-C,編譯器會從每個
.proto
產生pbobjc.h
和pbobjc.m
檔案,其中包含檔案中說明的各種訊息類型類別。 - 針對 C#,編譯器會從每個
.proto
產生.cs
檔案,以及檔案中說明的各種訊息類型適用的類別。 - 針對 Dart,編譯器會產生
.pb.dart
檔案,其中包含檔案中每個訊息類型的類別。
您可按照所選語言的教學課程 (proto3 版本即將推出),進一步瞭解如何為每種語言使用 API。如需更多 API 詳細資料,請參閱相關的 API 參考資料 (proto3 版本即將推出)。
純量值類型
純量訊息欄位可以是下列其中一種類型:這個表格會顯示 .proto
檔案中指定的類型,以及自動產生的類別中的對應類型:
.proto 類型 | Notes | C++ 類型 | Java/Kotlin 類型[1] | Python 類型[3] | Go 類型 | Ruby 類型 | C# 類型 | PHP 類型 | 飛鏢類型 |
---|---|---|---|---|---|---|---|---|---|
雙精度值 | 雙精度值 | 雙精度值 | 浮動 | 浮點值 44 | 浮點值 | 雙精度值 | 浮動 | 雙精度值 | |
浮動 | 浮動 | 浮動 | 浮動 | 浮點值 32 | 浮點值 | 浮動 | 浮動 | 雙精度值 | |
int32 | 使用長度長度編碼。對負數的編碼作業效率不佳 – 如果您的欄位可能有負值,請改用 sint32。 | int32 | int | int | int32 | Fixnum 或 Bignum (如有必要) | int | 整數 | int |
int64 | 使用長度長度編碼。對負數的編碼作業效率不佳 – 如果您的欄位可能有負值,請改用 sint64。 | int64 | long | int/long[4] | int64 | 比格文 | long | 整數/字串[6] | Int64 |
uint32 | 使用長度長度編碼。 | uint32 | 整數 [2] | int/long[4] | uint32 | Fixnum 或 Bignum (如有必要) | Uint | 整數 | int |
烏特文 64 | 使用長度長度編碼。 | 烏特文 64 | 長[2] | int/long[4] | 烏特文 64 | 比格文 | Ulong | 整數/字串[6] | Int64 |
Sint32 | 使用長度長度編碼。帶正負號的值。相較於一般的 int32,這些編碼的編碼數值會更有效率。 | int32 | int | int | int32 | Fixnum 或 Bignum (如有必要) | int | 整數 | int |
Sint64 | 使用長度長度編碼。帶正負號的值。比起一般的 int64,這些數字較有效率地編碼出負數。 | int64 | long | int/long[4] | int64 | 比格文 | long | 整數/字串[6] | Int64 |
固定 32 | 一律為 4 個位元組。如果值通常大於 228,則比 uint32 更有效率。 | uint32 | 整數 [2] | int/long[4] | uint32 | Fixnum 或 Bignum (如有必要) | Uint | 整數 | int |
已修正 64 | 一律八個位元組。如果值通常大於 256,則比 uint64 更有效率。 | 烏特文 64 | 長[2] | int/long[4] | 烏特文 64 | 比格文 | Ulong | 整數/字串[6] | Int64 |
Sfixed32 | 一律為 4 個位元組。 | int32 | int | int | int32 | Fixnum 或 Bignum (如有必要) | int | 整數 | int |
Sfixed64 | 一律八個位元組。 | int64 | long | int/long[4] | int64 | 比格文 | long | 整數/字串[6] | Int64 |
bool | bool | 布林值 | bool | bool | TrueClass/FalseClass | bool | 布林值 | bool | |
string | 字串一律須包含 UTF-8 編碼或 7 位元 ASCII 文字,而且長度不得超過 232。 | string | 字串 | Str/unicode [5] | string | 字串 (UTF-8) | string | string | 字串 |
位元組 | 可包含任意長度的位元組序列,不超過 232。 | string | ByteString | str (Python 2) bytes (Python 3) |
[]位元組 | 字串 (ASCII-8BIT) | ByteString | string | 清單 |
如要進一步瞭解這些類型編碼的方式,請參閱通訊協定緩衝區編碼訊息。
[1] Kotlin 會使用 Java 的對應類型 (即使未簽署的類型) 也能確保混合 Java/Kotlin 程式碼集的相容性。
[2] 在 Java 中,未簽署的 32 位元和 64 位元整數會以已簽署的對應項目表示,頂端位元只會儲存在簽署位元中。
[3] 在所有情況下,設定欄位的值都會執行類型檢查,確保值有效。
[4] 解碼 64 位元或未簽署的 32 位元整數時,一律會解碼為長整數,但如果在設定欄位時指定了 int,則可以是整數。不論在何種情況下,該值都必須符合設定時所指定的類型。請參閱 [2]。
[5] Python 字串在解碼中以萬國碼 (Unicode) 字串表示,但如果您提供了 ASCII 字串,則可能適用於此字串 (隨時可能變更)。
[6] 64 位元機器會使用整數,32 位元機器則使用字串。
預設值
剖析訊息時,如果編碼的訊息未包含特定的單一元素,剖析物件中的對應欄位就會設為該欄位的預設值。這些是僅限特定類型的預設值:
- 如果是字串,預設值為空白字串。
- 以位元組來說,預設值為空白位元組。
- 如果是布林值,預設值為 false。
- 對於數字類型,預設值為 0。
- 對於 enums,預設值是第一個定義的列舉值,必須為 0。
- 系統不會針對訊息欄位設定這個欄位。確切的值因語言而異。詳情請參閱產生的程式碼指南。
重複欄位的預設值是空白的 (通常是採用適當語言的空白清單)。
請注意,如果是純量訊息欄位,剖析訊息後,就無法知道欄位是否已明確設為預設值 (例如,布林值是否設為 false
) 或完全不設定:在定義訊息類型時,請留意這點。例如,如果不想在預設情況下同時執行某些行為,則不應使用設為 false
的布林。另請注意,如果純量訊息欄位已設為預設值,該值就不會在線上序列化。
請參閱所選程式碼指南,瞭解所選語言對預設程式碼的運作方式。
列舉
定義訊息類型時,您可能會希望其中一個欄位僅有一個預先定義的值清單。例如,假設您想為每個 SearchRequest
新增 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 result_per_page = 3;
Corpus corpus = 4;
}
如您所見,Corpus
列舉的第一個常數對應至零:每個列舉定義都「必須」包含對應至零的第一常數。可能的原因如下:
將相同的值指派給不同的列舉常數,即可定義別名。
方法是將 allow_alias
選項設為 true
,否則通訊協定編譯器會在找到別名時產生錯誤訊息。雖然在序列化時所有的別名值都是有效的,但序列化時會一律使用第一個值。
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
ENAA_UNSPECIFIED = 0;
ENAA_STARTED = 1;
// ENAA_RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
ENAA_FINISHED = 2;
}
列舉常數必須在 32 位元整數的範圍內。由於 enum
值在傳輸上使用變數編碼,因此負值的效率較低,因此不建議使用。您可以在訊息定義中定義 enum
,如以上範例或在外部定義。這些 enum
可以在 .proto
檔案中的任何訊息定義中重複使用。您也可以使用 enum
語法,在一則訊息中宣告的 enum
類型做為其他訊息中欄位的類型。
當您在採用 enum
的 .proto
上執行通訊協定緩衝區編譯器時,產生的程式碼將會有對應的 Java、Kotlin 或 C++ 專用 enum
,或用於 Python 的特殊 EnumDescriptor
類別,用於在執行階段產生的類別中建立具有整數值的符號化常數。
在反序列化期間,訊息中無法辨識的列舉值將保留在訊息中,不過,當訊息反序列化時,如何表示該語言,則取決於語言。在支援開放式列舉類型且值與其指定符號範圍外 (例如 C++ 和 Go) 的語言中,不明列舉值會直接儲存為基礎整數表示法。在 Java 具有封閉式列舉類型的語言中,列舉中的案例會表示無法辨識的值,而特殊整數可由特殊存取子存取。無論是哪一種情況,如果訊息序列化,無法辨識的訊息仍會透過訊息序列化。
如要進一步瞭解如何在應用程式中使用訊息 enum
,請參閱所選語言的產生的程式碼指南。
保留值
如果您透過完全移除列舉項目或為註解加上註解更新列舉類型,日後使用者可在對類型進行更新時,可以重複使用數值。如果日後載入相同 .proto
的舊版本 (包括資料毀損、隱私權錯誤等),可能會造成嚴重問題。確保這種情況不會發生,方法是為已刪除項目指定數值 (和/或名稱,也可能會造成 JSON 序列化問題) 為 reserved
。日後使用者若使用這些 ID,通訊協定緩衝區編譯器就會提出申訴。您可以使用 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;
}
匯入定義
在上述範例中,您要在與 SearchResponse
相同的檔案內定義 Result
訊息類型,如果另一個 .proto
檔案已定義要做為欄位類型的訊息類型,該怎麼辦?
如要使用其他 .proto
檔案的定義,請匯入這些檔案。如要匯入其他 .proto
的定義,請在檔案頂端新增匯入陳述式:
import "myproject/other_protos.proto";
根據預設,您只能使用直接匯入的 .proto
檔案來定義定義。不過,有時您可能需要將 .proto
檔案移至新位置。
您不必一次移動 .proto
檔案,並且一次更新所有呼叫網站,則可將預留位置 .proto
檔案放入舊位置,使用 import public
標記將所有匯入項目轉送至新的位置。
請注意,Java 不支援公開匯入功能。
任何匯入 import public
陳述式的 proto 的程式碼都可以遞移依賴 import public
依附元件。例如:
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
通訊協定編譯器使用 -I
/--proto_path
標記,在通訊協定編譯器指令列所指定的目錄中搜尋已匯入的檔案。如未提供任何旗標,則會在叫用編譯器的目錄中查看。一般來說,您應該將 --proto_path
標記設為專案的根目錄,並為所有匯入作業使用完整名稱。
使用 proto2 訊息類型
您可以匯入 proto2 訊息類型並在 proto3 訊息中使用這些類型,反之亦然。不過,proto2 列舉不能直接用於 proto3 語法中 (如果匯入的 proto2 訊息則使用它們的話)。
巢狀類型
您可以在其他訊息類型中定義及使用訊息類型,如以下範例所示:Result
訊息是在 SearchResponse
訊息內定義:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果您要在父項訊息類型以外重複使用此訊息類型,則稱為 _Parent_._Type_
:
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
您可以視需要建立訊息巢狀結構:
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}
更新訊息類型
如果現有訊息類型無法滿足您的所有需求 (例如,您希望訊息格式提供額外的欄位),但您仍想使用舊格式建立的程式碼,請別擔心!在不中斷任何現有程式碼的情況下更新訊息類型非常簡單。只要記住下列規則即可:
- 請勿變更任何現有欄位的欄位編號。
- 如果您新增欄位,使用新程式碼產生的程式碼將任何由「old」訊息格式序列化的訊息仍可剖析。請謹記這些元素的預設值,讓新程式碼能正確與舊程式碼產生的訊息互動。同樣地,新程式碼建立的訊息都可以由舊程式碼剖析:剖析時,舊版二進位檔會忽略新欄位。詳情請參閱未知欄位一節。
- 只要在更新的訊息類型中未重複使用欄位名稱,即可移除欄位。您可以改為重新命名欄位,例如新增前置字串「OBSOLETE_」,或將欄位編號設為 保留,這樣
.proto
的未來使用者就無法意外重複使用號碼。 int32
、uint32
、int64
、uint64
和bool
都相容,這表示您可以將欄位從其中一種類型變更為其他類型,而不會破壞前向或回溯相容性。如果從電線剖析不符合對應類型的行數,您將產生與使用 C++ 將類型投放到該類型的效果相同 (例如,如果將 64 位元的數字讀取為 int32,則會被截斷為 32 位元)。sint32
和sint64
彼此相容,但無法與其他整數類型相容。string
和bytes
相容,只要位元組有效,即符合 UTF-8 標準。- 如果位元組含有已編碼的訊息,則內嵌訊息與
bytes
相容。 fixed32
與sfixed32
相容,fixed64
則與sfixed64
相容。- 對於
string
、bytes
和訊息欄位,單一欄位與repeated
欄位相容。以重複欄位的序列化資料做為輸入內容,如果希望這個欄位為單數,就會將其視為最後一個輸入值,如果是原始類型欄位,則合併所有輸入元素 (如果是訊息類型欄位)。請注意,對數值類型 (包括布林值和列舉) 通常並不安全。數值類型的重複欄位可透過 packed 格式序列化,如果預期發生單一欄位,系統就無法正確剖析該欄位。 enum
與電匯、int32
、uint32
、int64
和uint64
的電匯格式相容 (請注意,如果值不符合,則會被截斷)。 但請注意,當用戶端解除序列化時,用戶端程式碼的處理方式可能不同:例如,系統會保留無法辨識的 proto3enum
類型訊息,但該訊息在訊息反序列化時會如何表示語言。int 欄位只會保留其值。- 將單一
optional
欄位或擴充功能變更為新oneof
的成員與二進位檔相容,但在某些語言 (特別是 Go) 中,產生的程式碼的 API 會以不相容的方式變更。因此,Google 不會在其公開 API 中做出這類變更,如 AIP-180 中所述。如果對原始碼相容性有相同的注意事項,但如果您確定沒有一次設定多個程式碼,則將多個欄位移到新的oneof
可能安全無虞。無法將欄位移至現有的oneof
。同樣地,將單一欄位oneof
變更為optional
欄位或擴充功能是安全的。
不明欄位
未知的欄位是格式正確的通訊協定緩衝區序列化資料,代表剖析器無法辨識的欄位。舉例來說,當舊二進位檔剖析新欄位傳送的新欄位時,這些新欄位會成為舊二進位檔中的不明欄位。
最初,proto3 訊息在剖析時一律會捨棄不明欄位,但在 3.5 版中,我們重新導入了保留 proto2 行為的不明欄位。在 3.5 版及更高版本中,剖析期間會保留未知的欄位,並納入序列化輸出。
不限
Any
訊息類型可讓您在沒有 .proto 定義的情況下,使用訊息做為嵌入類型。Any
包含任意序列化序列化訊息做為 bytes
,以及一個網址做為全域唯一識別碼,用於解析該訊息的類型。如要使用 Any
類型,您必須匯入 google/protobuf/any.proto
。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
特定訊息類型的預設類型網址為 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
類型的執行階段程式庫正在開發中。
如果您已熟悉 proto2 語法,Any
可存放任意的 proto3 訊息,類似可允許擴充功能的 proto2 訊息。
單人
如果訊息包含許多欄位且同時要設定最多一個欄位,您可以使用其中一項功能來強制執行這項行為並節省記憶體。
其中一個欄位就像一般欄位一樣,除了一個共用記憶體中所有的欄位之外,但一次只能設定一個欄位。設定其中一個成員後,系統會自動清除所有其他成員。您可以使用特別的 case()
或 WhichOneof()
方法 (可視語言) 檢查其中一個設定的值 (如果有的話)。
請注意,如果設定了多個值,由 proto 中的順序決定的最後設定值將會覆寫所有先前的值。
使用 Oneof
如要在 .proto
中定義其中一種,請使用 oneof
關鍵字,後接您的名字,此例中為 test_oneof
:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然後將其中一個欄位新增至其中一項定義。您可以新增任何類型的欄位,但 map
欄位和 repeated
欄位除外。
在產生的程式碼中,其中一個欄位與一般欄位具有相同的 getter 和 setter。此外,您也提供了特殊的方法,用於檢查哪個值 (如果有的話) 已設定。如要進一步瞭解所選語言的其中一種 API,請前往相關的 API 參考資料。
其中一項功能
設定其中一個欄位會自動清除其中一個其他所有成員。因此,如果您設定了多個欄位,則只有 last 欄位仍具有值。
SampleMessage message; message.set_name("name"); CHECK_EQ(message.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.name().empty());
如果剖析器遇到有線同個的多位成員,剖析訊息中只會使用最後偵測到的成員。
其中一個值不得為
repeated
。反思 API 適用於其中一個欄位。
如果您將其中一個欄位設為預設值 (例如將 int32 其中一個欄位設為 0),系統就會設定該其中一個欄位的「case」,並將該值在線上序列化。
如果您使用 C++,請確認程式碼不會造成記憶體當機。下列程式碼範例停止運作,因為呼叫
set_name()
方法已刪除sub_message
。SampleMessage message; SubMessage* sub_message = message.mutable_sub_message(); message.set_name("name"); // Will delete sub_message sub_message->set_... // Crashes here
再次在 C++ 中,如果
Swap()
有兩個訊息與其中一個訊息,對每則訊息而言,最後一則訊息會與最後其中一個情況產生。在以下範例中,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_EQ(msg2.name(), "");
回溯相容性問題
新增或移除其中一個欄位時,請務必小心謹慎。若是檢查其中一個項目的值為傳回 None
/NOT_SET
,可能表示該值尚未設定,或者已經設定在另一個版本的欄位中。無法分辨差異,因為無法知道傳輸上的未知欄位是否為其中任何欄位。
標記重複使用問題
- 將欄位移入或移出任一欄位:訊息序列化並剖析後,您可能會遺失部分資訊 (部分欄位將會遭到清除)。不過,您可以放心將單一欄位安全移到「新的」欄位,而且如果知道多個欄位只設定為一個欄位,則可移動多個欄位。詳情請參閱更新訊息類型。
- 刪除其中一個欄位並加回:在訊息序列化和剖析後,這可能會清除您目前設定的其中一個欄位。
- 分割或合併其中:這與移動一般欄位有類似的問題。
地圖
如果您想要在資料定義中建立關聯地圖,通訊協定緩衝區會提供實用的捷徑語法:
map<key_type, value_type> map_field = N;
...其中 key_type
可以是任何整數或字串類型 (因此,除了浮點類型和 bytes
之外,任何「純量」類型)。請注意,列舉是無效的「key_type
」。value_type
可以是其他地圖以外的任何類型。
例如,如果您想建立專案地圖,其中每個 Project
訊息都與字串鍵相關聯,您可以定義如下:
map<string, Project> projects = 3;
- 地圖欄位不得為
repeated
。 - 對地圖值的有線順序排序和地圖疊代順序是未定義,因此您無法依賴地圖項目的順序。
- 為
.proto
產生文字格式時,地圖會依照金鑰排序。數字鍵會依數字排序。 - 從電線剖析或合併時,如果有重複的地圖金鑰,則會使用上次看到的金鑰。以文字格式剖析地圖時,如果重複的索引鍵,剖析可能會失敗。
- 如果您為地圖欄位提供了鍵但沒有值,在欄位序列化時的行為會因語言而有所不同。在 C++ 中,Java、Kotlin 和 Python 該類型的預設值會序列化,而其他語言則沒有進行序列化。
產生的 Maps API 目前適用於所有支援 proto3 的語言。如要進一步瞭解所選語言的 Maps API,請參閱相關的 API 參考資料。
回溯相容性
地圖語法相當於電匯上的下列內容,因此不支援通訊協定的通訊協定緩衝區實作仍可處理您的資料:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何支援地圖的通訊協定緩衝區實作都必須產生,並接受上述定義可接受的資料。
包裹
您可以在 .proto
檔案中加入選用的 package
指定碼,以避免通訊協定訊息類型之間的名稱衝突。
package foo.bar;
message Open { ... }
定義訊息類型的欄位時,您可以使用套件指定碼:
message Foo {
...
foo.bar.Open open = 1;
...
}
套件指定碼對產生的程式碼產生的方式取決於您選擇的語言:
- 在 C++ 中,產生的類別會納入 C++ 命名空間。例如,
Open
位於命名空間foo::bar
中。 - 在 Java 和 Kotlin 中,除非您在
.proto
檔案中明確提供option java_package
,否則系統會用套件做為 Java 套件。 - 在 Python 中,系統會忽略套件指令,因為 Python 模組在檔案系統中的位置是依位置整理。
- 在 Go 中,除非您在
.proto
檔案中明確提供option go_package
,否則系統會使用套件做為 Go 套件名稱。 - 在 Ruby 中,產生的類別會包裝在巢狀 Ruby 命名空間中,並轉換為必要的 Ruby 大寫字母樣式 (第一個字母大寫;如果第一個字元不是字母,則在前面加上
PB_
)。例如,Open
位於命名空間Foo::Bar
中。 - 在 C# 中,除非您在
.proto
檔案中明確提供option csharp_namespace
,否則系統會將套件轉換成 PascalCase 首字母大寫格式。例如,Open
位於命名空間Foo.Bar
中。
套件和名稱解析
通訊協定緩衝區語言中的類型名稱解析與 C++ 類似:先搜尋最內部範圍,然後搜尋到最內層,依此類推,每個套件都視為其父項套件的「內部」。開頭的「.」(例如 .foo.bar.Baz
) 表示從最外層開始。
通訊協定緩衝區編譯器會剖析匯入的 .proto
檔案,以解析所有類型名稱。每種語言的程式碼產生器都知道如何參照該語言的每種類型,即使其具有不同的範圍規則。
定義服務
如果您想使用 RPC (遠端程序呼叫) 系統的訊息類型,可以在 .proto
檔案中定義 RPC 服務介面,而通訊協定緩衝區編譯器將會以您選擇的語言產生服務介面程式碼和虛設常式。例如,如果您想使用可接受 SearchRequest
並傳回 SearchResponse
的方法來定義 RPC 服務,您可以在 .proto
檔案中定義該服務,如下所示:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
與通訊協定緩衝區搭配使用的最簡單遠端程序呼叫 (RPC) 系統是 gRPC:Google 開發的語言和平台中立開放原始碼 RPC 系統。gRPC 與通訊協定緩衝區特別搭配運作,並可讓您透過特殊的通訊協定緩衝區編譯器外掛程式,直接從 .proto
檔案產生相關的 RPC 程式碼。
如果您不想使用 gRPC,也可以將通訊協定緩衝區與自己的 RPC 實作搭配使用。詳情請參閱 Proto2 語言指南。
還有一些進行中的第三方專案,用於開發通訊協定緩衝區的 RPC 實作。如需已知專案連結清單,請參閱第三方外掛程式維基頁面。
JSON 對應
Proto3 支援 JSON 的標準編碼,讓您可以更輕鬆地在系統之間共用資料。下表將按照類型按類型說明編碼。
將 JSON 編碼的資料剖析至通訊協定緩衝區時,如果缺少值或值是 null
,系統會將資料解譯為對應的預設值。
從通訊協定緩衝區產生 JSON 編碼輸出時,如果 protobuf 欄位具有預設值,且該欄位不支援欄位,則根據預設,系統會從輸出中省略該欄位。實作可能會提供在輸出中包含預設值的欄位選項。
使用 optional
關鍵字定義的 proto3 欄位可支援欄位。具有值且支援欄位存在的欄位,一律會使用 JSON 編碼的欄位值,即使該欄位是預設值。
proto3 | JSON | JSON 範例 | Notes |
---|---|---|---|
訊息 | 物件 | {"fooBar": v, "g": null, …} |
產生 JSON 物件。訊息欄位名稱會對應至小寫的 CamelCase 並成為 JSON 物件金鑰。如果指定了 json_name 欄位選項,則會使用指定值做為索引鍵。剖析器接受小寫 CamelCase 名稱 (或由 json_name 選項指定的名稱) 和原始 proto 欄位名稱。null 是所有欄位類型可接受的值,且視為對應欄位類型的預設值。 |
列舉 | string | "FOO_BAR" |
使用 proto 中指定的列舉值名稱。剖析器接受列舉名稱和整數值。 |
map<K,V> | 物件 | {"k": v, …} |
所有鍵都會轉換為字串。 |
重複播放 V | 陣列 | [v, …] |
null 接受了空白清單 [] 。 |
bool | true/false | true, false |
|
string | string | "Hello World!" |
|
位元組 | base64 字串 | "YWJjMTIzIT8kKiYoKSctPUB+" |
JSON 值會是以字串形式的標準 Base64 編碼,以字串編碼的資料。可接受標準或網址安全 Base64 編碼,無論是否加上邊框間距。 |
int32、fix32、uint32 | 數字 | 1, -10, 0 |
JSON 值將是十進位數字。接受數字或字串。 |
int64、fix64、uint64 | string | "1", "-10" |
JSON 值將是十進位字串。接受數字或字串。 |
浮動, 雙精度浮點數 | 數字 | 1.1, -10.0, 0, "NaN", "Infinity" |
JSON 值會是數字或以下特殊字串值「NaN」、「Infinity」和「-Infinity」。接受數字或字串。系統也接受指數標記法。-0 等於 0。 |
不限 | object |
{"@type": "url", "f": v, … } |
如果 Any 包含的值含有特殊的 JSON 對應,系統會將其轉換如下:{"@type": xxx, "value": yyy} 。否則,系統會將值轉換為 JSON 物件,然後插入 "@type" 欄位來表示實際資料類型。 |
時間戳記 | string | "1972-01-01T10:00:20.021Z" |
使用 RFC 3339,其中生成的為為為 Z 正規化,使用 0、3、6 或 9 片。我們也接受「Z」以外的偏移值。 |
時間長度 | string | "1.000340012s", "1s" |
根出所需的精度,生成的為輸出為 0、3、6 或 9 位小位,然後是字尾“s”。可接受以任何整數表示的小數位數 (也可以是任何數字),但必須符合「奈秒」精確度,且必須使用後置字串「s」。 |
結構 | object |
{ … } |
任何 JSON 物件。詳情請參閱《struct.proto 》。 |
包裝函式類型 | 各種類型 | 2, "2", "foo", true, "true", null, 0, … |
包裝函式會使用與已包裝的原始類型相同的 JSON 表示法來表示,但 null 可以在資料轉換和傳輸期間允許並保留。 |
FieldMask | string | "f.fooBar,h" |
詳情請參閱《field_mask.proto 》。 |
ListValue | 陣列 | [foo, bar, …] |
|
值 | 值 | 任何 JSON 值。詳情請參閱 google.protobuf.Value。 | |
NullValue | 空值 | JSON 空值 | |
空白 | 物件 | {} |
空白的 JSON 物件 |
JSON 選項
proto3 JSON 實作可提供下列選項:
- 輸出含有預設值的欄位:proto3 JSON 輸出預設會省略含有預設值的欄位。實作可能會提供覆寫此行為的選項,以及使用其預設值輸出欄位。
- 忽略不明欄位:Proto3 JSON 剖析器預設應拒絕不明欄位,但可能提供在剖析時忽略不明欄位的選項。
- 使用 proto 欄位名稱而非 LowerCamelCase 名稱:根據預設,proto3 JSON 印表機應將欄位名稱轉換為小寫 CamelCase,並將其做為 JSON 名稱。實作可能會提供將原型欄位名稱當做 JSON 名稱的選項。必須使用 Proto3 JSON 剖析器,以便接受轉換後的 LowerCamelCase 和 proto 欄位名稱。
- 以列舉值的形式傳送列舉值,而不是字串:根據預設,JSON 輸出會使用列舉值的名稱。您可以提供選項,改用列舉值的數字值。
選項
.proto
檔案中的個別宣告可以利用多項選項加上註解。選項不會變更宣告的整體意義,但可能會影響在特定情境中處理的方式。/google/protobuf/descriptor.proto
定義了可用選項的完整清單。
有些選項是檔案層級選項,表示應該在頂層範圍寫入,而非在訊息、列舉或服務定義中撰寫。有些選項是訊息層級選項,表示應寫入訊息定義。有些選項是欄位層級選項,表示應寫入欄位定義。您也可以透過列舉類型、列舉值、其中一個欄位、服務類型及服務方法,撰寫選項。不過,這些方法目前並沒有適用的選項。
以下是幾個最常用的選項:
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
選項,則在外部外包裝函式 Java 類別「之間」產生的所有其他類別/列舉等項目將以巢狀類別/列舉等形式產生。如果未產生 Java 程式碼,這個選項就不會產生任何作用。option java_outer_classname = "Ponycopter";
java_multiple_files
(檔案選項):如果設為 false,這個.proto
檔案將只會產生一個.java
檔案,且為頂層訊息、服務和列舉建立的所有 Java 類別/enum 等均將在外部類別內建立 (請參閱java_outer_classname
)。如果為 true,系統會針對這個類別和類別的.java
類別,如果未產生 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
)。精簡版執行階段比完整程式庫小得多 (大約比其規模少),但會省略描述元、反射等特定功能。這對於在手機等受限平台上執行的應用程式特別實用。編譯器仍會產生所有方法的快速實作,就像在SPEED
模式中一樣。產生的類別只會實作每種語言的MessageLite
介面,而這僅提供完整Message
介面方法的子集。
option optimize_for = CODE_SIZE;
cc_enable_arenas
(檔案選項):針對 C++ 產生的程式碼啟用區域配置。objc_class_prefix
(檔案選項):設定 Objective-C 類別前置字串,此 附加在 .proto 產生的所有 Objective-C 類別和列舉前面。這項屬性沒有預設值。您應該使用由 3 到 5 個大寫字母的 Apple 推薦的前置字串。請注意,所有 2 個字母的前置字元都是 Apple 保留。deprecated
(欄位選項):如果設為true
,表示該欄位已不適用,不應由新程式碼使用。在大多數語言中,這並沒有實際影響。在 Java 中,這會成為@Deprecated
註解。對於 C++,每次使用已淘汰的欄位時,clang-tidy 都會產生警告。日後,其他語言專用的程式碼產生器可能會為該欄位的存取子產生淘汰註解,如此一來,在編譯嘗試使用該欄位的程式碼時,即會發出警告。如果有任何人未使用該欄位,且想要避免新使用者使用該欄位,請考慮將欄位宣告替換為 serveserve 陳述式。int32 old_field = 6 [deprecated = true];
自訂選項
通訊協定緩衝區也可讓您定義並使用自己的選項。這項進階功能是大多數使用者不需要的功能。如果您需要建立自己的選項,請參閱 Proto2 語言指南瞭解詳情。請注意,建立自訂選項會使用 Extension,只有 proto3 中的自訂選項才允許使用。
產生課程
如要產生 .proto
檔案中定義的訊息類型所需的 Java、Kotlin、Python、C++、Go、Ruby、Objective-C 或 C# 程式碼,您必須在 .proto
上執行通訊協定緩衝區編譯器 protoc
。如果您尚未安裝編譯器,請下載套件並按照 README 的指示操作。針對 Go,您也必須安裝編譯器專用的特殊程式碼產生器外掛程式:您可以在 GitHub 的 golang/protobuf 存放區中找到這個程式碼和安裝操作說明。
通訊協定編譯器的叫用方式如下:
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
IMPORT_PATH
會指定解析import
指令時要尋找.proto
檔案的目錄。如果省略,則會使用目前的目錄。您可以透過多次傳遞--proto_path
選項來指定多個匯入目錄,系統會按順序搜尋這些目錄。-I=_IMPORT_PATH_
可做為--proto_path
的簡短形式。您可以提供一或多個輸出指令:
--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 格式封存檔案。也會按照 Java JAR 規格的要求,提供.jar
輸出的資訊清單檔案。請注意,如果輸出封存檔已存在,系統會覆寫這個檔案,導致編譯器不夠聰明,無法將檔案新增至現有的封存檔。您必須提供一或多個
.proto
檔案做為輸入內容。可以一次指定多個.proto
檔案。雖然檔案是以目前的目錄命名,但每個檔案都必須位於其中一個IMPORT_PATH
中,編譯器才能判斷其標準名稱。
檔案位置
最好不要將 .proto
檔案放在其他語言來源所在的目錄中。請考慮在專案的根套件下為 .proto
檔案建立子套件 proto
。
地點應通用
使用 Java 程式碼時,建議您將相關的 .proto
檔案放在與 Java 來源相同的目錄中。不過,如有任何非 Java 程式碼都使用相同的 proto,路徑前置字串就不再合理。因此,通常將原型放在相關的通用語言目錄中,例如 //myteam/mypackage
。
這項規則的例外狀況是表明 proto 只會用於 Java 內容,例如用於測試。
支援平台
如需以下資訊:
- 支援的作業系統、編譯器、建構系統和 C++ 版本,請參閱基礎 C++ 支援政策。
- 支援的 PHP 版本,請參閱支援的 PHP 版本。