(62)通訊協議之一protobuf

 

 Protobuf協議特色分析php

                                                            KingKa.吳永聰

1protobuf是什麼?

protobuf(Google Protocol Buffers)是Google提供一個具備高效的協議數據交換格式工具庫(相似JsonXml),但相比於Json,Protobuf有更高的轉化效率,時間效率和空間效率都是JSON的3-5倍。其最大的特色是基於二進制,所以比傳統的一些XML表示的效率要高出很多。雖然protobuf是二進制的數據格式,可是並無由於這樣變得複雜,咱們能夠經過它的語法來定義結構化的消息格式,而後根據命令行工具編譯生成相關的.proto文件,支持的語言有java、C++、Python等語言。生成的.proto文件直接放在項目裏面,就能夠調用相關方法來完成業務消息的序列化和反序列化的工做整個佔據大小在

二、Protobuf的主要特色:

 (1)跨平臺,支持大多數語言開發(Java、object-c、c++、c、php、python、go等),代碼開源,運行穩定可靠,谷歌內部使用(後臺強)

  (2)性能好,效率高,佔據空間和運行時間相比json和xml小,二進制序列化格式,數據壓縮緊湊,佔據字節數小

  (3) 支持向後兼容和向前兼容

 :「向後兼容」(backward compatible),就是說,當模塊B升級了以後,它可以正確識別模塊A發出的老版本的協議。因爲老版本沒有「狀態」這個屬性,在擴充協議時,能夠考 慮把「狀態」屬性設置成非必填 的,或者給「狀態」屬性設置一個缺省值。「向前兼容」(forward compatible),就是說,當模塊A升級了以後,模塊B可以正常識別模塊A發出的新版本的協議。這時候,新增長的「狀態」屬性會被忽略。】

(4)適合對數據大和傳輸速率比較敏感的場合使用。

(5)Protobuf 語義更清晰,無需相似 XML 解析器的東西(由於 Protobuf 編譯器會將 .proto 文件編譯生成對應的數據訪問類以對 Protobuf 數據進行序列化、反序列化操做)。

6在項目工程中只須要添加一個由編譯器庫生成的.proto文件該文件至關於肯定數據協議,數據結構中存在哪些數據,數據類型是怎麼樣),該文件大小與編譯前定義多少結構化數據正相關,以c語言爲例,在工程中的protobuf表現形式就是一個.pb-c.c 和 .pb-c.h兩個文件包含進工程便可,跟普通的c文件和h文件同樣,編譯器編譯前大小在5k左右。

7)支持絕大數數據類型,以下圖所示:

 

8)數據結構化定義靈活,可嵌套定義

 

3、相比jsonxml的優點和不足

 谷歌官方測試對比優點:

 (1) 運行時間

(2)壓縮後佔據空間字節大小

(3) XML 相比, Protobuf 的主要優勢在於性能高。它以高效的二進制方式存儲,比 XML 小 3 到 10 倍,快 20 到 100 倍。

 

Protobuf的不足:

(1)二進制可讀性差

(2)缺少自描述,二進制的協議內容必須配合.proto文件的定義纔有含義不然不能知道定義的數據內容是幹嗎用的。

其餘連接:

1protobuf 性能對比測試:http://agapple.iteye.com/blog/859052

2谷歌官方protobuf說明:

https://developers.google.com/protocol-buffers/docs/proto

                                                      protobuf格式初探

轉自:http://blog.csdn.net/cchd0001/article/details/50669079

定義一個消息

首先來看一個簡單的例子,定義一個搜索請求的消息格式,每一個消息包含一個請求字符串,你感興趣的頁數和每頁的結果數。下面是在.proto 文件中定義的消息。css

 

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

 

 

SearchRequest消息定義了3個特殊的字段(名字/值 對)對應着我須要的的消息內容。每一個字段有一個名字和類型。java

特定字段類型

在上面的例子中,全部的字段都是標量類型 : 兩個整形(page_number result_per_page)和一個字符串query。 固然你也可使用其餘組合類型,好比枚舉或者其餘 消息類型。python

分配標籤

如你所見,消息中的每個字段都被定義了一個獨一無二的數字標籤。這個標籤是用來在二進制的消息格式中區分字段的,一旦你的消息開始被使用,這些標籤就不該該在被修改了。注意 1 到 15 標籤在編碼的時候僅佔用1 byte ,16 - 2047 佔用 2 byte 。所以你應該將 1 - 15 標籤保留給最常常被使用的消息元素。另外爲將來可能添加的經常使用元素預留位子。 
你能定義的最小的標籤是1, 最大是 2的29次方 -1 , 另外 19000 到 19999 (FieldDescriptor::kFirstReservedNumber through FieldDescriptor::kLastReservedNumber) 也不能用。他們是protobuf 的編譯預留標籤。另外你也不能使用被 reserved的標籤。c++

特定字段規則

消息是字段必須是下面的一種編程

  • required 格式正確的消息必須有一個這個字段。
  • optional 格式正確的消息能夠有一個或者零個這樣的消息。
  • repeated 這個字段能夠有任意多個。字段值的順序被保留。

因爲歷史緣由, repeated字段的標量編碼效率沒有應有的效率高,新的代碼可使用[packet=true]來得到更高效的編碼, 好比 :json

repeated int32 samples = 4 [packet=true]
  • 1

Required 字段意味着永久,當你要標記一個字段爲required 的時候你必須很是當心 —– 若是某個時刻你想要再也不使用這個字段,當你把它改爲optional的時候就會出問題 : 使用舊的協議的人會由於認爲這個字段缺失而認爲消息不完整,進而拒收或者丟棄這個消息。谷歌的一些工程師得出這樣的結論:使用required形成的傷害比他們的好處多,他們更傾向於使用optional的和repeated的。然而,這種觀點不是絕對的。安全

添加更多的消息

多個消息類型能夠在一個.proto文件中定義。當你定義多個相關聯的消息的時候就用的上了 —— 好比我要定義一個返回消息格式來回應SearchRequest消息,那麼我在同一個文件中 :bash

 
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字段爲枚舉類型,若是這個字段給出了一個再也不枚舉中的值,那麼解析器就會把它看成一個未知的字段。

 
 1 message SearchRequest {
 2   required string query = 1;
 3   optional int32 page_number = 2;
 4   optional int32 result_per_page = 3 [default = 10];
 5   enum Corpus {
 6     UNIVERSAL = 0;
 7     WEB = 1;
 8     IMAGES = 2;
 9     LOCAL = 3;
10     NEWS = 4;
11     PRODUCTS = 5;
12     VIDEO = 6;
13   }
14   optional Corpus corpus = 4 [default = UNIVERSAL];
15 }
 

 

 

只須要將相同的值賦值給不一樣的枚舉項名字,你就在枚舉中你能夠定義別名 。固然你得先將allow_alias選項設置爲true, 不然編譯器遇到別名的時候就報錯。

 
 1 enum EnumAllowingAlias {
 2   option allow_alias = true;
 3   UNKNOWN = 0;
 4   STARTED = 1;
 5   RUNNING = 1;
 6 }
 7 enum EnumNotAllowingAlias {
 8   UNKNOWN = 0;
 9   STARTED = 1;
10   // RUNNING = 1;  //取消這一行的屏蔽的話,編譯器報錯。
11 }
 

 

 

枚舉常數必須是一個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中定義的 :

 
1 message SearchResponse {
2   message Result {
3     required string url = 1;
4     optional string title = 2;
5     repeated string snippets = 3;
6   }
7   repeated Result result = 1;
8 }
 

 

 

若是你打算在這個消息的父消息以外重用這個消息的話,你能夠這樣引用它 : Parent.Type

message SomeOtherMessage { optional SearchResponse.Result result = 1; }

你想嵌套多深就嵌套多深,沒有限制 :

 
 1 message Outer {                  // Level 0
 2   message MiddleAA {  // Level 1
 3     message Inner {   // Level 2
 4       required int64 ival = 1;
 5       optional bool  booly = 2;
 6     }
 7   }
 8   message MiddleBB {  // Level 1
 9     message Inner {   // Level 2
10       required int32 ival = 1;
11       optional bool  booly = 2;
12     }
13   }
14 }
 

 

 

Groups

注意這是一個被廢棄的特性,若是你建立一個新的消息的話,不要使用這個,請直接使用內嵌消息。 
Groups是另外的一種在你的消息中內嵌信息的方式。例如 :

 
1 message SearchResponse {
2   repeated group Result = 1 {
3     required string url = 2;
4     optional string title = 3;
5     repeated string snippets = 4;
6   }
7 }
 

 

 

 

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消息如今有一個叫作barint32字段了。在編碼的時候,通信格式和使用者定義的新的消息同樣。不過你的程序訪問擴展字段的方式和訪問常規字段的方式不太同樣, 這裏以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;
  • 1

這裏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 的任何實現細節。所以最後你的代碼大致是這樣的 :

 
 1 using google::protobuf;
 2 
 3 protobuf::RpcChannel* channel;
 4 protobuf::RpcController* controller;
 5 SearchService* service;
 6 SearchRequest request;
 7 SearchResponse response;
 8 
 9 void DoSearch() {
10   // 你本身提供MyRpcChannel和MyRpcController兩個類,這兩個類分別實現了純虛接口
11   // s protobuf::RpcChannel 和protobuf::RpcController.
12   channel = new MyRpcChannel("somehost.example.com:1234");
13   controller = new MyRpcController;
14   service = new SearchService::Stub(channel);
15 
16   // Set up the request.
17   request.set_query("protocol buffers");
18 
19   // Execute the RPC.
20   service->Search(controller, request, response, protobuf::NewCallback(&Done));
21 }
22 
23 void Done() {
24   delete service;
25   delete channel;
26   delete controller;
27 }
 

 

 

全部的服務器類一樣實現服務接口。這提供了一種在不知道方法名字和參數的狀況下調用方法的途徑。在服務器這邊,你須要實現一個能夠註冊服務的PRC服務器。

 
 1 using google::protobuf;
 2 
 3 class ExampleSearchService : public SearchService {
 4  public:
 5   void Search(protobuf::RpcController* controller,
 6               const SearchRequest* request,
 7               SearchResponse* response,
 8               protobuf::Closure* done) {
 9     if (request->query() == "google") {
10       response->add_result()->set_url("http://www.google.com");
11     } else if (request->query() == "protocol buffers") {
12       response->add_result()->set_url("http://protobuf.googlecode.com");
13     }
14     done->Run();
15   }
16 };
17 
18 int main() {
19 //你本身提供的MyRpcServer類,它不須要實現任何接口,這裏意思意思就行。
20   MyRpcServer server;
21 
22   protobuf::Service* service = new ExampleSearchService;
23   server.ExportOnPort(1234, service);
24   server.Run();
25 
26   delete service;
27   return 0;
28 }
 

 

 

若是你不想嵌入你本身的已經存在的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;
  • 1
  • cc_generic_services,java_generic_servicespy_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];
  • 1
  • deprecated (field option): 若是被設置爲true,那麼這個字段被標記爲廢棄,新的代碼不該該使用它。在大多數語言中這個沒有實際的意義,Java會使用@Deprecated.
optional int32 old_field = 6 [deprecated=true];
  • 1

自定義選項

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 指定的某個路徑下!
相關文章
相關標籤/搜索