定義一個消息
首先來看一個簡單的例子,定義一個搜索請求的消息格式,每個消息包含一個請求字符串,你感興趣的頁數和每頁的結果數。下面是在.proto
文件中定義的消息。
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
SearchRequest
消息定義了3個特殊的字段(名字/值 對)對應着我需要的的消息內容。每個字段有一個名字和類型。
特定字段類型
在上面的例子中,所有的字段都是標量類型 : 兩個整形(page_number result_per_page
)和一個字符串query
。 當然你也可以使用其他組合類型,比如枚舉或者其他 消息類型。
分配標籤
如你所見,消息中的每一個字段都被定義了一個獨一無二的數字標籤。這個標籤是用來在二進制的消息格式中區分字段的,一旦你的消息開始被使用,這些標籤就不應該在被修改了。注意 1 到 15 標籤在編碼的時候僅佔用1 byte ,16 - 2047 佔用 2 byte 。因此你應該將 1 - 15 標籤保留給最經常被使用的消息元素。另外爲未來可能添加的常用元素預留位子。
你能定義的最小的標籤是1, 最大是 2的29次方 -1 , 另外 19000 到 19999 (FieldDescriptor::kFirstReservedNumber through FieldDescriptor::kLastReservedNumber
) 也不能用。他們是protobuf 的編譯預留標籤。另外你也不能使用被 reserved
的標籤。
特定字段規則
消息是字段必須是下面的一種
required
格式正確的消息必須有一個這個字段。optional
格式正確的消息可以有一個或者零個這樣的消息。repeated
這個字段可以有任意多個。字段值的順序被保留。
由於歷史原因, repeated
字段的標量編碼效率沒有應有的效率高,新的代碼可以使用[packet=true]
來獲得更高效的編碼, 比如 :
repeated int32 samples = 4 [packet=true]
Required 字段意味着永久,當你要標記一個字段爲required 的時候你必須非常小心 —– 如果某個時刻你想要不再使用這個字段,當你把它改成optional的時候就會出問題 : 使用舊的協議的人會因爲認爲這個字段缺失而認爲消息不完整,進而拒收或者丟棄這個消息。谷歌的一些工程師得出這樣的結論:使用required
造成的傷害比他們的好處多,他們更傾向於使用optional
的和repeated
的。然而,這種觀點不是絕對的。
添加更多的消息
多個消息類型可以在一個.proto
文件中定義。當你定義多個相關聯的消息的時候就用的上了 —— 比如我要定義一個返回消息格式來回應SearchRequest
消息,那麼我在同一個文件中 :
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
message SearchResponse {
//。。。
}
添加註釋
在.proto
文件中添加註釋,使用C/C++風格的 //
語法
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;// Which page number do we want?
optional int32 result_per_page = 3;// Number of results to return per page.
}
保留字段
當你在某次更新消息中屏蔽或者刪除了一個字段的話,未來的使用着可能在他們的更新中重用這個標籤數字來標記他們自己的字段。然後當他們加載舊的消息的時候就會出現很多問題,包括數據衝突,隱藏的bug等等。指定這個字段的標籤數字(或者名字,名字可能在序列化爲JSON的時候可能衝突)標記爲reserved
來保證他們不會再次被使用。如果以後的人試用的話protobuf編譯器會提示出錯。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
注意一個reserved字段不能既有標籤數字又有名字。
.proto
文件最終生成什麼
當你使用protoc
來編譯一個.proto
文件的時候,編譯器將利用你在文件中定義的類型生成你打算使用的語言的代碼文件。生成的代碼包括getting setting
接口和序列化,反序列化接口。
- 對於C++,編譯器對每個
.proto
文件生成一個.h
和一個.cc
文件。 每個消息生成一個class。 - 對於Java , 編譯器爲每個消息生成一個
.java
文件,外加一個特殊的Builder
類來生成消息實例。 - 對於Python , 一點點不同 —– Python編譯器生成有一個靜態的對每個消息的描述器的模塊。然後,用一個元類在運行時創建必要的Python數據訪問類。
- 對於Go , 編譯器對文件中的每個消息生成一個
.pb.go
文件。
標量
proto | Note | C++ | Java | Python | Go |
---|---|---|---|---|---|
float | float | float | float | *float32 | |
double | double | double | float | *float64 | |
int32 | 變長編碼. 編碼負數效率底下– 打算使用負數的話請使用 sint32. | int32 | int | int | *int32 |
int64 | 變長編碼. 編碼負數效率底下– 打算使用負數的話請使用 sint64. | int64 | long | int/long | *int64 |
uint32 | 變長編碼. | uint32 | int | int/long | *uint32 |
uint64 | 變長編碼. | uint64 | long | int/long | *uint64 |
sint32 | U變長編碼. 數值有符號,負數編碼效率高於int32 | int32 | int | int | *int32 |
sint64 | U變長編碼. 數值有符號,負數編碼效率高於int64 | int64 | long | int/long | *int64 |
fixed32 | 固定4byte, 如果數值經常大於2的28次方的話效率高於uint32. | uint32 | int | int | *uint32 |
fixed64 | 固定8byte, 如果數值經常大於2的56次方的話效率高於uint64. | uint64 | long | int/long | *uint64 |
sfixed32 | 固定4byte. | int32 | int | int | *int32 |
sfixed64 | 固定8byte. | int64 | long | int/long | *int64 |
bool | bool | boolean | bool | *bool | |
string | 字符串內容應該是 UTF-8 編碼或者7-bit ASCII 文本. | string | String | str/unicode | *string |
bytes | 任意二進制數據. | string | ByteString | str | []byte |
optional
字段和默認初始值
按照上面提到的,元素可以被標記爲optional
的。一個正確格式的消息可以有也可以沒有包含這個可選的字段。再解析消息的時候,如果個可選的字段沒有被設置,那麼他的值就會被設置成默認值。默認值可以作爲消息描述的一不部分 :
optional int32 result_per_page = 3 [default = 10];
如果沒有明確指明默認值,那麼這個字段的值就是這個字段的類型默認值。比如 : 字符串的默認值就是空串。數字類型的默認值就是0。枚舉類型的默認值是枚舉定義表的第一個值,這意味着枚舉的第一個值需要被格外注意。
枚舉
當你定義一個消息的時候,你可能希望它其中的某個字段一定是預先定義好的一組值中的一個。你如說我要在SearchRequest
中添加corpus
字段。它只能是 UNIVERSAL, WEB , IMAGES , LOCAL, NEWS ,PRODUCTS, 或者 VIDEO
。你可以很簡單的在你的消息中定義一個枚舉並且定義corpus
字段爲枚舉類型,如果這個字段給出了一個不再枚舉中的值,那麼解析器就會把它當作一個未知的字段。
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = UNIVERSAL];
}
只需要將相同的值賦值給不同的枚舉項名字,你就在枚舉中你可以定義別名 。當然你得先將allow_alias
選項設置爲true
, 否則編譯器遇到別名的時候就報錯。
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; //取消這一行的屏蔽的話,編譯器報錯。
}
枚舉常數必須是一個32爲的整數。由於枚舉值在通訊的時候使用變長編碼,所以負數的效率很低,不推薦使用。你可以在(像上面這樣)在一個消息內定義枚舉,也可以在消息外定義 —– 這樣枚舉就在全文件可見了。如果你想要使用在消息內定義的枚舉的話,使用語法 MessageType.EnumType
。
在你編譯帶有枚舉的.proto
文件的時候,如果生成的是C++或者Java代碼, 那麼生成的代碼中會有對應的枚舉。
使用其他的消息類型
你可以使用其他的消息類型作爲字段的類型。比如我們打算在SearchResponse
消息中包含一個Result
類型的消息 :
message SearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
導入定義
在上面的例子中, Result
消息類型是和SearchResponse
定義在同一個文件中,如果你想使用的消息類型已經在另一個.proto
文件中定義的話怎麼辦 ?
只要你導入一個文件就可以使用這個文件內定義的消息。在你的文件頭部加上這樣的語句來導入其他文件:
import "myproject/other_protos.proto";
默認情況下你只能使用直接導入的文件中的定義。然而有的時候你需要將一個文件從一個路徑移動到另一個路徑的時候,與其將所有的引用這個文件的地方都更新到新的路徑,不如在原來的路徑上留下一個假的文件,使用import public
來指向新的路徑。import public
語句可以將它導入的文件簡介傳遞給導入本文減的文件。比如 :
// new.proto
// 新的定義都在這裏
// old.proto
// 其他的文件其實導入的都是這個文件
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// 你可以使用 old.proto 和 new.proto 的定義, 但是不能使用other.proto的定義
在命令行中試用-I/--proto_path
來指定一系列的編譯器搜索路徑,如果這個參數沒有被設置,那麼默認在命令執行的路徑查找。通常情況下使用-I/--proto_path
來指定到你項目的根目錄,然後使用完整的路徑來導入所需的文件。
導入proto 3 的消息類型
你可以將proto3的消息類型導入並在proto2的消息中使用,反之亦然。不過proto2的枚舉不能在proto3中使用。
內嵌類型
你可以在一個消息中定義並使用其他消息類型,比如下面的例子 —— Result
消息是在SearchResponse
中定義的 :
message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
如果你打算在這個消息的父消息之外重用這個消息的話,你可以這樣引用它 : Parent.Type
message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}
你想嵌套多深就嵌套多深,沒有限制 :
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
required int64 ival = 1;
optional bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
required int32 ival = 1;
optional bool booly = 2;
}
}
}
Groups
注意這是一個被廢棄的特性,如果你創建一個新的消息的話,不要使用這個,請直接使用內嵌消息。
Groups是另外的一種在你的消息中內嵌信息的方式。例如 :
message SearchResponse {
repeated group Result = 1 {
required string url = 2;
optional string title = 3;
repeated string snippets = 4;
}
}
Group其實將內嵌消息的定義和字段聲明合併在一起了。在你的生成代碼中,你會發現這個消息有一個Result類型的result字段(字段名字自動小寫來防止衝突)。 因此這個例子和上面的第一個內嵌的例子是等價的。除了這個消息的通訊格式不大一樣外。
更新一個消息
如果一個現有的消息類型不再滿足你的需求,比如你需要額外的字段,但是你仍然希望兼容舊代碼生成的消息的話,不要擔心! 在不破壞現有代碼的前提下更新消息是很簡單的。請銘記下面的規則 :
- 不要改變任何已有的數字標籤
- 你新添加的字段需要是
optional
或者repeated
。由於任何required
字段都沒有丟失,這意味着你的舊代碼序列化的消息能夠被新代碼解析通過。你應該給新的字段設置合理的默認值,這樣新的代碼可以合適解析使用舊的消息。同樣的,新的代碼產生的消息包也可以被舊的代碼解析通過,舊的代碼在解析的時候會忽略新的字段。不過新的字段並沒有被丟棄,如果這個消息在舊的代碼中再次被序列化,這些未知的字段還會在裏面 —— 這樣這些消息被傳遞迴新的代碼的時候,解析仍然有效。 - 非
required
字段可以被移除,但是對應的數字標籤不能被重用。或許你可以通過重命名這個字段,加上前綴OBSOLETE_
來表示廢棄。或者你可以標記reserverd
。這樣你未來就不會不小心重用這些字段了。 - 只要保證標籤數字一致,一個非
required
字段可以被轉化擴展字段,反之亦然。 int32, uint32, int64, uint64, 和 bool
這些類型是兼容的 —— 這意味着你可以將一個字段的類型從其中的一種轉化爲另一種,不會打破向前向後兼容! 如果通信的時候傳輸的數字不符合對應類型的那麼你會得到和C++中強制類型轉化一樣的效果(64bit數字會被截斷)。sint32 sint64
相互兼容,但是不和其他的數字類型兼容。string bytes
相互兼容 ,前提是二進制內容是有效的UTF-8 。optional repeated
是兼容的。當給定的輸入字段是repeated
的時候,如果接收方期待的是一個optional
的字段的話,對與原始類型的字段,他會取最後一個值,對於消息類型的字段,他會將所有的輸入合併起來。- 你可以改變一個默認初始值,反正這個初始值從來不再通訊中傳遞。因此, 如果一個字段沒有被設置,那麼解析程序就將它賦值爲解析程序所使用的版本的默認初始值,而不是發送方的默認初始值。
- 枚舉類型和
int32, uint32, int64, and uint64
在傳輸格式中相互兼容(注意如果不合適會被 截斷),但是接收方在發序列化的時候處理他們可不大一樣。請注意: 反序列化的時候不正確的枚舉數字會被丟棄,這樣這個字段的has_xxx
接口就返回false
並且get_xxx
接口返回枚舉的第一個值。不過如果是一個整形字段的話,這個數值會一致保留。所以當你打算把一個整形更新爲枚舉的時候,請務必注意整數的值不要超出接收方枚舉的值。
擴展 extemsions
extensions
讓你定義一段可用的數字標籤來供第三方擴展你的消息。其他人可以在他們自己的文件裏面使用這些標籤數字來擴展你的下消息(無需修改你的消息文件)。 舉個例子:
message Foo {
//,,,
extensions 100 to 199;
}
這意味着Foo
消息在[ 100 , 199 ]區間的標籤數字被保留做擴展使用。其他的使用者可以在他們自己的文件中導入你的文件,然後在他們自己的文件中給你的消息添加新的字段 :
extend Foo {
optional int32 bar = 126;
}
這樣就意味着Foo
消息現在有一個叫做bar
的int32
字段了。在編碼的時候,通訊格式和使用者定義的新的消息一樣。不過你的程序訪問擴展字段的方式和訪問常規字段的方式不太一樣, 這裏以C++代碼爲例 :
Foo foo;
foo.SetExtension(bar, 15);
類似的,Foo
類有以下接口HasExtension(), ClearExtension(), GetExtension(), MutableExtension(), and AddExtension()
。
注意擴展字段可以是除了oneof
或者map
外的其他任何類型,包括消息類型。
內嵌擴展
你可以在其他類型的作用域內定義擴展字段 :
message Baz {
extend Foo {
optional int32 bar = 126;
}
//。。。
}
在這種情況下,擴展的字段如下訪問 ( C++ )
Foo foo;
foo.SetExtension(Baz::bar, 15);
這裏有一個很常見的疑惑 : 在一個消息類型內定義另一個類型的擴展並不會導致被擴展消息類型和包含類型的任何關係。實際上,在上面的例子中,Baz類不是Foo類的子類。上面僅僅意味着bar
這個變量實際上是Baz
的一個static變量,僅此而已。
一個常規的使用方法是當我們要擴展一個類型的字段的時候,將它寫在這個類型裏面, 比如我要擴展一個Baz類型的Foo字段的時候 :
message Baz {
extend Foo {
optional Baz foo_ext = 127;
}
...
}
然而,這並不是必要的。你完全可以這樣做 :
message Baz {
...
}
// This can even be in a different file.
extend Foo {
optional Baz foo_baz_ext = 127;
}
事實上這個語法是用來避免疑惑的。正如上面提到的,嵌套語法經常會不熟悉擴展的人被誤以爲是子類。
選擇擴展標籤數字
重要的是,要確保兩個使用者不會向同一個消息內擴展同一個數字的字段。否則如果類型恰好不兼容的話數據就混亂了。你需要爲你的項目定義合適的擴展數字來避免這種事。
如果你打算使用一些非常大的數字來作爲你的擴展的話,你可以讓你的擴展字段區間一直到最大值,你可以max
關鍵字 :
message Foo {
extensions 1000 to max;
}
max 是 2的29次方 - 1, 536,870,911.
同樣的你不能使用19000-19999 。 你可以定義擴展空間包含他們,不過當你定義擴展字段的時候不能真的使用這些數字。
Oneof 類似union
如果你的消息中有很多可選字段,而同一個時刻最多僅有其中的一個字段被設置的話,你可以使用oneof
來強化這個特性並且節約存儲空間。
oneof
字段類似optional
字段只不過oneof
裏面所有的字段共享內存,而且統一時刻只有一個字段可以被設着。設置其中任意一個字段都自動清理其他字段。在你的代碼中,你可以使用case()或者 WhichOneOf()
接口來查看到底是哪個字段被設置了。
使用 Oneof
使用Oneof特性你只需要在oneof
關鍵字後面加上它的名字就行 :
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
你可以在oneof
中使用oneof
, 你可以使用任何類型的字段,但是你不能使用required, optional, 或者 repeated
關鍵字。
在你的代碼中,oneof內的字段和其他常規字段有一樣的getter setter 接口。你還可以通過接口(取決於你的語言)判斷哪個字段被設置。
Oneof特性
- 設置一個oneof字段會自動清理其他的oneof字段。如果你設置了多個oneof字段,只有最後一個有效。
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); //清理name字段.
CHECK(!message.has_name());
- 如果解析器發現多個oneof字段被設置了,最後一個讀到的算數。
- 擴展字段不能被設置爲oneof類型。
- oneof字段不能是repeated。
- 反射API對oneof字段有效。
- 如果你使用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++而言, 如果你對兩個帶有oneof的消息的使用
Swap()
接口的話,每個消息會帶有對方的oneof字段。
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到底哪裏不一樣了。
重用的注意事項:
- 將opttional字段移入或者移除oneof的話,在(被舊的版本代碼)將消息序列化或者反序列化的時候,有些字段肯能會丟失。
- 先刪除一個oneof中的字段再加回去:在(被舊的版本代碼)將消息序列化或者反序列化的時候,當前設置可能被清理。.
- 合併或者拆分oneof : 同移入移除optional.
Maps
如果你打算在你的數據結構中創建一個關聯表的話,我們提供了很方便的語法:
map<key_type, value_type> map_field = N;
這裏key_type可以是任意整形或者字符串。而value_tpye 可以是任意類型。
舉個例子,如果你打算創建一個Project表,每個Project關聯到一個字符串上的話 :
map<string, Project> projects = 3;
現在生成Map的API對於所有支持proto2的語言都可用了。
Maps 特性
- 擴展項不能是map.
- Maps不能使
repeated, optional, 或者 required
. - 通訊格式中的順序或者Map迭代器的順序是未知的,你不能指望Map保存你的錄入順序。
- 在文本模式下,Map由Key排序。
向後兼容
在通訊中,map等價與下面的定義, 這樣不支持Map的版本也可以解析你的消息:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
Packages概念
爲了防止不同消息之間的命名衝突,你可以對特定的.proto
文件提指定packet 名字 。
package foo.bar;
message Open { ... }
在定義你的消息字段類型的時候你可以指定包的名字:
message Foo {
...
required foo.bar.Open open = 1;
...
}
包名字的實現取決於你工作的具體編程語言:
- 在C++中 ,生成的消息被包被在一個包名字的命名空間中,比如上面的代碼中Bar類是 : foo::bar。
- 在 Java中,除非你指定了選項
java_package
,否則這個包名字就是Java的包名字。 - 在 Python中,由於Python的模塊是由它的文件系統來管理的,所以包名被忽略。
包和名字解析
protobuf的名字解析方式和C++很像。首先是最裏面的作用域被搜索,然後是外面的一層。。。 沒一個包都從他自己到它的父輩。但是如果前面有.
號的話就(比如foo.bar.Baz
)意味着從最外面開始。
protobuf 編譯器通過所有導入.proto
文件來解析所有的名字。代碼生成器爲每個語言生成對應的合適的類型。
定義服務 ( Services )
如果打算將你的消息配合一個RPC(Remote Procedure Call 遠程調用)系統聯合使用的話,你可以在.proto
文件中定義一個RPC 服務接口然後protobuf就會給你生成一個服務接口和其他必要代碼。比如你打算定義一個遠程調用,接收SearchRequest返回SearchResponse, 那麼你在你的文件中這樣定義 :
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
默認情況下,編譯器給你生成一個純虛接口名叫SearchRequest
和一個對應的樁實現。這個樁實現直接調用RpcChannel,這個是你自己實現的具體RPC代碼。比如你打算實現一個RpcChannel來序列化消息並且使用HTTP發送。換句話說,生成的代碼提供了一個基於你的RPC的類型的安全的協議接口實現,它 不需要知曉你的PRC 的任何實現細節。因此最後你的代碼大體是這樣的 :
using google::protobuf;
protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;
void DoSearch() {
// 你自己提供MyRpcChannel和MyRpcController兩個類,這兩個類分別實現了純虛接口
// s protobuf::RpcChannel 和protobuf::RpcController.
channel = new MyRpcChannel("somehost.example.com:1234");
controller = new MyRpcController;
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;
}
所有的服務器類同樣實現服務接口。這提供了一種在不知道方法名字和參數的情況下調用方法的途徑。在服務器這邊,你需要實現一個可以註冊服務的PRC服務器。
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() {
//你自己提供的MyRpcServer類,它不需要實現任何接口,這裏意思意思就行。
MyRpcServer server;
protobuf::Service* service = new ExampleSearchService;
server.ExportOnPort(1234, service);
server.Run();
delete service;
return 0;
}
如果你不想嵌入你自己的已經存在的RPC系統,你現在可以使用gRPC : 這是一種谷歌開發的語言和平臺無關的開源RPC系統。gPRC和protobuf配合的格外方便。在添加了特定的插件後,它可以從你的.proto
文件直接生成對應的RPC代碼。不過由於proto2和proto3之間存在兼容問題,我們推薦你使用proto3來定義你的gPRC服務。如果你打算使用gPRC配合protobuf , 你需要3.0.0以上的版本。
選項
每個.proto
文件中的獨立的定義都可以被一系列的選項說明。選項不改變任何定義的整體意義,但是在特定的上下文下它們能有特定的效果。選項列表在google/protobuf/descriptor.proto
中.
有的選項是文件等級的,意味着它必須在文件最頂端寫,不能在任何消息,枚舉或者服務的定義中。也有寫選項是消息級別的,意味着它們應該寫在消息定義內,有些選項是字段級別的,意味着他們應該被寫在字段定義中。選項可以被寫在枚舉,服務中,但是目前還沒有對應的有意義的選項。
這是一些常用的選項:
java_package
(file option): 生成的Java的包名字。如果沒有指定這個選項那麼使用packet關鍵字的參數。不過packet關鍵字沒有辦法生成優雅的Java包名字,因爲packet關鍵字不支持.
號。非Java語言忽略。
option java_package = "com.example.foo";
java_outer_classname
(file option): Java最外圍的類名字和文件名。如果沒有設置,文件名就死協議文件名轉化成駝峯式的名字 : (foo_bar.proto 變成 FooBar.java
) , 非java語言忽略。
option java_outer_classname = "Ponycopter";
optimize_for
(file option): 可以是SPEED, CODE_SIZE, or LITE_RUNTIME
. 對 C++ 、Java (或者其他三方代碼生成器)代碼生成有如下影響:
- SPEED (default): 生成序列化,解析代碼,生成其他常用代碼。默認配置,代碼經過很好的優化。
- CODE_SIZE: 編譯器生成很少的類,依賴共享,反射等實現序列化,解析等其他操作。生成的代碼比SPEED小的多,也慢了些。生成的API和SPEED一樣。當你有 大量的協議而且不指望他們太快的時候這個就比較合適了。
- LITE_RUNTIME: 生成代碼僅僅依賴輕量級運行庫 (libprotobuf-lite 而不是 libprotobuf)。 輕量運行庫要小的多,而且有必要的描述和反射特性。這個尤其對移動開發有效。API接口和SPEED的一樣塊但是僅僅提供SPEED模式的一個子集API。
option optimize_for = CODE_SIZE;
cc_generic_services
,java_generic_services
,py_generic_services
(file options): 是否生成抽象的服務代碼 分別對應C++, Java, 和Python。 由於歷史遺留原因,這些被默認設置爲true。
// 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
(file option): 允許 arena allocation ,C++有效.packed
(field option): 當你對一個repeated的整形字段設置true 的時,會使用一種更有效的編碼方式。 沒有壞處。不過在2.3.0之前的版本,如果解析器發現期待這個字段不是packed而接收的數據是packed,那麼數據會被忽略。之後的版本是安全的。如果你使用很久的版本的話請小心。
repeated int32 samples = 4 [packed=true];
deprecated
(field option): 如果被設置爲true,那麼這個字段被標記爲廢棄,新的代碼不應該使用它。在大多數語言中這個沒有實際的意義,Java會使用@Deprecated
.
optional int32 old_field = 6 [deprecated=true];
自定義選項
Protocol Buffers 甚至允許你自定義你自己的選項。注意這是高級用法,大多數人用不到。既然選項是在google/protobuf/descriptor.proto (like FileOptions or FieldOptions)
中定義的,你只需要擴展他們定義你自己的選項。比如:
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
message MyMessage {
option (my_option) = "Hello world!";
}
這裏我們通過擴展MessageOptions
定義了一個消息級別的選項。我們在C++中這樣讀取這個選項的值:
string value = MyMessage::descriptor()->options().GetExtension(my_option);
這裏,MyMessage::descriptor()->options()
返回了MessageOptions消息。讀取擴展選項和讀取其他的擴展字段沒什麼區別。
Java代碼:
String value = MyProtoFile.MyMessage.getDescriptor().getOptions()
.getExtension(MyProtoFile.myOption);
Python代碼:
value = my_proto_file_pb2.MyMessage.DESCRIPTOR.GetOptions()
.Extensions[my_proto_file_pb2.my_option]
各種類型的選項都能被擴展。
import "google/protobuf/descriptor.proto";
extend google.protobuf.FileOptions {
optional string my_file_option = 50000;
}
extend google.protobuf.MessageOptions {
optional int32 my_message_option = 50001;
}
extend google.protobuf.FieldOptions {
optional float my_field_option = 50002;
}
extend google.protobuf.EnumOptions {
optional bool my_enum_option = 50003;
}
extend google.protobuf.EnumValueOptions {
optional uint32 my_enum_value_option = 50004;
}
extend google.protobuf.ServiceOptions {
optional MyEnum my_service_option = 50005;
}
extend google.protobuf.MethodOptions {
optional MyMessage my_method_option = 50006;
}
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;
}
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!";
}
生成你的代碼
如果你要用.proto
文件生成 C++ , Java, Python的代碼的話,你需要使用protoc來編譯.proto
文件。如果你還沒安裝這個編譯器的話,去下載一個吧。
如下執行協議的編譯:
protoc –proto_path=IMPORT_PATH –cpp_out=DST_DIR –java_out=DST_DIR –python_out=DST_DIR path/to/file.proto
- IMPORT_PATH 指定查找
.proto
文件的搜索目錄,默認是當前的工作目錄。可以多次使用這個參數來指定多個目錄,他們會按照順序被檢索,-I=IMPORT_PATH
是--proto_path
的簡寫。 - 你可以指定特定的輸出路徑:
--cpp_out
C++ code in DST_DIR.--java_out
generates Java code in DST_DIR.--python_out
generates Python code in DST_DIR.
作爲一個額外的便利,如果DST_DIR
以.zip
或者.jar
來結尾的話,編譯器會自動給你打包。注意如果指定路徑已經存在的話會被覆蓋。
- 你必須提供一個或多個
.proto
文件。多個文件可以一次全給定。文件名必須是相對當前目錄的相對路徑名。每個文件都應該在IMPORT_PATHs
指定的某個路徑下!