文章內容來源自Google官方文檔翻譯,詳見原文Language Guide。部分內容可能重複,望多見諒。
假設你想定義一個“搜尋請求”的訊息格式,每一個請求含有一個查詢字串、你感興趣的查詢結果所在的頁數,以及每一頁多少條查詢結果。可以採用如下的方式來定義訊息類型的.proto檔案了:
syntax = "proto3";message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3;}
SearchRequest訊息格式有3個欄位,在訊息中承載的資料分別對應於每一個欄位。其中每個欄位都有一個名字和一種類型。
注意:
- 檔案的第一行指定了你正在使用proto3文法:如果你沒有指定這個,編譯器會使用proto2。這個指定文法行必須是檔案的非空非注釋的第一個行。
訊息定義
1. 指定欄位類型
在上面的例子中,所有欄位都是標量類型:兩個整型(page_number
和result_per_page
),一個string類型(query)。當然,你也可以為欄位指定其他的合成類型,包括枚舉(enumerations)或其他訊息類型。
2. 分配標識號
正如上述檔案格式,在訊息定義中,每個欄位都有唯一的一個數位識別碼符。這些標識符是用來在訊息的二進位格式中識別各個欄位的,一旦開始使用就不能夠再改變。
註:[1,15]之內的標識號在編碼的時候會佔用一個位元組。[16,2047]之內的標識號則佔用2個位元組。所以應該為那些頻繁出現的訊息元素保留 [1,15]之內的標識號。切記:要為將來有可能添加的、頻繁出現的標識號預留一些標識號。
最小的標識號可以從1開始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的標識號, Protobuf協議實現中對這些進行了預留。如果非要在.proto檔案中使用這些預留標識號,編譯時間就會警示。
3. 指定欄位規則
所指定的訊息欄位修飾符必須是如下之一:
- required:一個格式良好的訊息一定要含有1個這種欄位。表示該值是必須要設定的;
- optional:訊息格式中該欄位可以有0個或1個值(不超過1個)。
- repeated:在一個格式良好的訊息中,這種欄位可以重複任意多次(包括0次)。重複的值的順序會被保留。表示該值可以重複,相當於java中的List。
由於一些曆史原因,基本數實值型別的repeated的欄位並沒有被儘可能地高效編碼。在新的代碼中,使用者應該使用特殊選項[packed=true]
來保證更高效的編碼。如:
repeated int32 samples = 4 [packed=true];
required是永久性的:在將一個欄位標識為required的時候,應該特別小心。如果在某些情況下不想寫入或者發送一個required的欄位,將原始該欄位修飾符更改為optional可能會遇到問題——舊版本的使用者會認為不含該欄位的訊息是不完整的,從而可能會無目的的拒絕解析。在這種情況下,你應該考慮編寫特別針對於應用程式的、自訂的訊息校正函數。
Google的一些工程師得出了一個結論:使用required弊多於利;他們更 願意使用optional和repeated而不是required。當然,這個觀點並不具有普遍性。
4. 添加更多訊息類型
在一個.proto檔案中可以定義多個訊息類型。在定義多個相關的訊息的時候,這一點特別有用——例如,如果想定義與SearchResponse訊息類型對應的回複訊息格式的話,你可以將它添加到相同的.proto檔案中,如:
message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3;}message SearchResponse { ...}
5. 添加註釋
向.proto檔案添加註釋,可以使用C/C++/java風格的雙斜杠(//) 文法格式,如:
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.}
6. .proto檔案產生了什麼
當用protocolbuffer編譯器來運行.proto檔案時,編譯器將產生所選擇語言的代碼,這些代碼可以操作在.proto檔案中定義的訊息類型,包括擷取、設定欄位值,將訊息序列化到一個輸出資料流中,以及從一個輸入資料流中解析訊息。
- 對go來說,編譯器會位每個訊息類型產生了一個.pd.go檔案。
- 對C++來說,編譯器會為每個.proto檔案產生一個.h檔案和一個.cc檔案,.proto檔案中的每一個訊息有一個對應的類。
- 對Python來說,有點不太一樣——Python編譯器為.proto檔案中的每個訊息類型產生一個含有靜態描述符的模組,,該模組與一個元類(metaclass)在運行時(runtime)被用來建立所需的Python資料訪問類。
- 對於Objective-C來說,編譯器會為每個訊息類型產生了一個pbobjc.h檔案和pbobjcm檔案,.proto檔案中的每一個訊息有一個對應的類。
7. 標量數實值型別
一個標量訊息欄位可以自動產生的訪問類中定義的類型.
更多“序列化訊息時各種類型如何編碼”的資訊請看這裡
8. Optional的欄位和預設值
訊息描述中的一個元素可以被標記為“可選的”(optional)。一個格式良好的訊息可以包含0個或一個optional的元素。當解 析訊息時,如果它不包含optional的元素值,那麼解析出來的對象中的對應欄位就被置為預設值。預設值可以在訊息描述檔案中指定。例如,要為 SearchRequest訊息的result_per_page欄位指定預設值10,在定義訊息格式時如下所示:
optional int32 result_per_page = 3 [default = 10];
如果沒有為optional的元素指定預設值,就會使用與特定類型相關的預設值:對string來說,預設值是Null 字元串。對bool來說,預設值是false。對數實值型別來說,預設值是0。對枚舉來說,預設值是枚舉類型定義中的第一個值。
9. 枚舉
當需要定義一個訊息類型的時候,可能想為一個欄位指定某“預定義值序列”中的一個值。例如,假設要為每一個SearchRequest訊息添加一個 corpus欄位,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一個。
其實可以很容易地實現這一點:通過向訊息定義中添加一個枚舉(enum)就可以了。一個enum類型的欄位只能用指定的常量集中的一個值作為其值(如果嘗 試指定不同的值,解析器就會把它當作一個未知的欄位來對待)。
在下面的例子中,在訊息格式中添加了一個叫做Corpus的枚舉類型——它含有所有可能的值 ——以及一個類型為Corpus的欄位:
message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; enum Corpus { UNIVERSAL = 0; WEB = 1; IMAGES = 2; LOCAL = 3; NEWS = 4; PRODUCTS = 5; VIDEO = 6; } Corpus corpus = 4;}
如果給枚舉常量定義別名, 需要設定allow_alias option 為 true, 否則 protocol編譯器會產生錯誤資訊。
enum EnumAllowingAlias { option allow_alias = true; UNKNOWN = 0; STARTED = 1; RUNNING = 1;}enum EnumNotAllowingAlias { UNKNOWN = 0; STARTED = 1; // RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.}
枚舉常量必須在32位整型值的範圍內。因為enum值是使用可變編碼方式的,對負數不夠高效,因此不推薦在enum中使用負數。
如上例所示,可以在 一個訊息定義的內部或外部定義枚舉——這些枚舉可以在.proto檔案中的任何訊息定義裡重用。當然也可以在一個訊息中聲明一個枚舉類型,而在另一個不同 的訊息中使用它——採用MessageType.EnumType的文法格式。
當對一個使用了枚舉的.proto檔案運行protocol buffer編譯器的時候,產生的程式碼中將有一個對應的enum(對Java或C++來說),或者一個特殊的EnumDescriptor類(對 Python來說),它被用來在運行時產生的類中建立一系列的整型值符號常量(symbolic constants)。
使用
message支援嵌套使用,作為另一message中的欄位類型
message SearchResponse { repeated Result results = 1;}message Result { string url = 1; string title = 2; repeated string snippets = 3;}
匯入定義(import)
可以使用import語句匯入使用其它描述檔案中聲明的類型
import "others.proto";
protocol buffer編譯器會在-I / --proto_path
參數指定的目錄中尋找匯入的檔案,如果沒有指定該參數,預設在目前的目錄中尋找。
Message嵌套
栗子:
message SearchResponse { message Result { string url = 1; string title = 2; repeated string snippets = 3; } repeated Result results = 1;}
內部聲明的message類型名稱只可在內部直接使用,在外部參考需要前置父級message名稱,如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; } }}
Map類型
proto3 支援 map 型別宣告:
map<key_type, value_type> map_field = N;message Project {...}map<string, Project> projects = 1;
- 鍵、實值型別可以是內建的標量類型,也可以是自訂message類型
- 欄位不支援repeated屬性
- 不要依賴map類型的欄位順序
包(Packages)
在.proto檔案中使用package聲明包名,避免命名衝突。
syntax = "proto3";package foo.bar;message Open {...}
在其他的訊息格式定義中可以使用包名+訊息名的方式來使用類型,如:
message Foo { ... foo.bar.Open open = 1; ...}
在不同的語言中,包名定義對編譯後產生的程式碼的影響不同:
- C++ 中:對應C++命名空間,例如Open會在命名空間foo::bar中
- Java 中:package會作為Java包名,除非指定了option jave_package選項
- Python 中:package被忽略
- Go 中:預設使用package名作為包名,除非指定了option go_package選項
- JavaNano 中:同Java
- C# 中:package會轉換為駝峰式命名空間,如Foo.Bar,除非指定了option csharp_namespace選項
定義服務(Service)
如果想要將訊息類型用在RPC(遠程方法調用)系統中,可以在.proto檔案中定義一個RPC服務介面,protocol buffer編譯器會根據所選擇的不同語言產生服務介面代碼。
例如,想要定義一個RPC服務並具有一個方法,該方法接收SearchRequest並返回一個SearchResponse,此時可以在.proto檔案中進行如下定義:
service SearchService { rpc Search (SearchRequest) returns (SearchResponse) {}}
產生的介面代碼作為用戶端與服務端的約定,服務端必須實現定義的所有介面方法,用戶端直接調用同名方法向服務端發起請求。
比較蛋疼的是即便業務上不需要參數也必須指定一個請求訊息,一般會定義一個空message。
選項(Options)
在定義.proto
檔案時可以標註一系列的options。Options並不改變整個檔案聲明的含義,但卻可以影響特定環境下處理方式。完整的可用選項可以查google。
一些選項是檔案層級的,意味著它可以作用於頂層範圍,不包含在任何訊息內部、enum或服務定義中。一些選項是訊息層級的,可以用在訊息定義的內部。當然有些選項可以作用在欄位、enum類型、enum值、服務類型及服務方法中。
但是到目前為止,並沒有一種有效選項能作用於這些類型。
以下是一些常用的選擇:
- java_package (file option):指定產生java類所在的包,如果在.proto檔案中沒有明確的聲明java_package,會使用預設包名。不需要產生java代碼時不起作用
- java_outer_classname (file option):指定產生Java類的名稱,如果在.proto檔案中沒有明確聲明java_outer_classname,產生的class名稱將會根據.proto檔案的名稱採用駝峰式的命名方式進行產生。如(foo_bar.proto產生的java類名為FooBar.java),不需要產生java代碼時不起任何作用
- objc_class_prefix (file option): 指定Objective-C類首碼,會前置在所有類和枚舉類型名之前。沒有預設值,應該使用3-5個大寫字母。注意所有2個字母的首碼是Apple保留的。
基本規範
描述檔案以.proto做為檔案尾碼,除結構定義外的語句以分號結尾
結構定義包括:message、service、enum
rpc方法定義結尾的分號可有可無
Message命名採用駝峰命名方式,欄位命名採用小寫字母加底線分隔方式
message SongServerRequest { required string song_name = 1;}
- Enums類型名採用駝峰命名方式,欄位命名採用大寫字母加底線分隔方式
enum Foo { FIRST_VALUE = 1; SECOND_VALUE = 2;}
編譯
通過定義好的.proto檔案產生Java, Python, C++, Go, Ruby, JavaNano, Objective-C, or C# 代碼,需要安裝編譯器protoc。
參考Github項目 google/protobuf 安裝編譯器,Go語言需要同時安裝一個特殊的外掛程式: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 --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
這裡只做參考就好,具體語言的編譯執行個體請參考詳細文檔,