Google數據交換格式:ProtoBuf


Protocol Buffer

 

ProtocolBuffer是Google公司的一個開源項目,用於結構化數據串行化的靈活、高效、自動的方法,有如XML,不過它更小、更快、也更簡單。你可以定義自己的數據結構,然后使用代碼生成器生成的代碼來讀寫這個數據結構。你甚至可以在無需重新部署程序的情況下更新數據結構。

 

一個例子

比如有個電子商務的系統(假設用C++實現),其中的模塊A需要發送大量的訂單信息給模塊B,通訊的方式使用socket。

假設訂單包括如下屬性:
--------------------------------
  時間:time(用整數表示)
  客戶id:userid(用整數表示)
  交易金額:price(用浮點數表示)
  交易的描述:desc(用字符串表示)
--------------------------------

如果使用protobuf實現,首先要寫一個proto文件(不妨叫Order.proto),在該文件中添加一個名為"Order"的message結構,用來描述通訊協議中的結構化數據。該文件的內容大致如下:

message Order
{
  required int32 time = 1;
  required int32 userid = 2;
  required float price = 3;
  optional string desc = 4;
}

然后,使用protobuf內置的編譯器編譯該proto。由於本例子的模塊是C++,你可以通過protobuf編譯器讓它生成 C++語言的“訂單包裝類”(一般來說,一個message結構會生成一個包裝類)。

然后你使用類似下面的代碼來序列化/解析該訂單包裝類:

發送方:

Order order;
order.set_time(XXXX);
order.set_userid(123);
order.set_price(100.0f);
order.set_desc("a test order");
string sOrder;
order.SerailzeToString(&sOrder);

然后調用某種socket的通訊庫把序列化之后的字符串sOrder發送出去;

 

接收方:

string sOrder;
// 先通過網絡通訊庫接收到數據,存放到某字符串sOrder
// ......
Order order;
if(order.ParseFromString(sOrder)){ // 解析該字符串
  cout << "userid:" << order.userid() << endl
          << "desc:" << order.desc() << endl;
} else {
  cerr << "parse error!" << endl;
}

 

有了這種代碼生成機制,開發人員再也不用吭哧吭哧地編寫那些協議解析的代碼了。萬一將來需求發生變更,要求給訂單再增加一個“狀態”的屬性,那只需要在Order.proto文件中增加一行代碼。對於發送方,只要增加一行設置狀態的代碼;對於接收方只要增加一行讀取狀態的代碼。另外,如果通訊雙方使用不同的編程語言來實現,使用這種機制可以有效確保兩邊的模塊對於協議的處理是一致的。

從某種意義上講,可以把proto文件看成是描述通訊協議的規格說明書(或者叫接口規范)。這種伎倆其實老早就有了,搞過微軟的COM編程或者接觸過CORBA的同學,應該都能從中看到IDL的影子。它們的思想是相通滴。

ProtoBuf支持向后兼容(backward compatible)和向前兼容(forward compatible):

  1. 向后兼容,比如說,當接收方升級了之后,它能夠正確識別發送方發出的老版本的協議。由於老版本沒有“狀態”這個屬性,在擴充協議時,可以考 慮把“狀態”屬性設置成非必填 的(optional),或者給“狀態”屬性設置一個缺省值;
  2. 向前兼容,比如說,當發送方升級了之后,接收方能夠正常識別發送方發出的新版本的協議。這時候,新增加的“狀態”屬性會被忽略;

向后兼容和向前兼容有啥用捏?俺舉個例子:當你維護一個很龐大的分布式系統時,由於你無法同時 升級所有 模塊,為了保證在升級過程中,整個系統能夠盡可能不受影響,就需要盡量保證通訊協議的向后兼容或向前兼容。

 

 

proto文件

如上面的例子,使用protobuf,首先需要在一個 .proto 文件中定義你需要做串行化的數據結構信息。每個ProtocolBuffer信息是一小段邏輯記錄,包含一系列的鍵值對。

例如:

syntax = "proto3";

message Person { required string name=1; required int32 id=2; optional string email=3; enum PhoneType { MOBILE=0; HOME=1; WORK=2; } message PhoneNumber { required string number=1; optional PhoneType type=2 [default=HOME]; } repeated PhoneNumber phone=4; }

一旦你定義了自己的報文格式(message),你就可以運行ProtocolBuffer編譯器,將你的 .proto 文件編譯成特定語言的類。這些類提供了簡單的方法訪問每個字段(像是 query() 和 set_query() ),像是訪問類的方法一樣將結構串行化或反串行化。例如你可以選擇C++語言,運行編譯如上的協議文件生成類叫做 Person 。隨后你就可以在應用中使用這個類來串行化的讀取報文信息。你可以這么寫代碼:

Person person;
person.set_name("John Doe"); person.set_id(1234); person.set_email("jdoe@example.com"); fstream.output("myfile",ios::out | ios::binary); person.SerializeToOstream(&output);

 

然后,你可以讀取報文中的數據:

fstream input("myfile",ios::in | ios:binary);
Person person;
person.ParseFromIstream(&input); cout << "Name: " << person.name() << endl; cout << "E-mail: " << person.email() << endl;

 

你可以在不影響向后兼容的情況下隨意給數據結構增加字段,舊有的數據會忽略新的字段。所以如果使用ProtocolBuffer作為通信協議,你可以無須擔心破壞現有代碼的情況下擴展協議。

 

當用protocol buffer編譯器來運行.proto文件時,編譯器將生成所選擇語言的代碼,這些代碼可以操作在.proto文件中定義的消息類型,包括獲取、設置字段值,將消息序列化到一個輸出流中,以及從一個輸入流中解析消息。

  • 對C++來說,編譯器會為每個.proto文件生成一個.h文件和一個.cc文件,.proto文件中的每一個消息有一個對應的類。
  • 對Java來說,編譯器為每一個消息類型生成了一個.java文件,以及一個特殊的Builder類(該類是用來創建消息類接口的)。
  • 對Python來說,有點不太一樣——Python編譯器為.proto文件中的每個消息類型生成一個含有靜態描述符的模塊,,該模塊與一個元類(metaclass)在運行時(runtime)被用來創建所需的Python數據訪問類。
  • 對go來說,編譯器會位每個消息類型生成了一個.pd.go文件。

 

 

protobuf3 消息定義

message由至少一個字段組合而成,類似於C語言中的結構。每個字段都有一定的格式:

字段規則 | 數據類型 | 字段名稱 | = | 字段編碼值 | [字段默認值]

 

字段規則:

  • singular:格式良好的消息可以包含該字段中的零個或一個(但不超過一個);
  • repeated:此字段可以在格式良好的消息中重復任意次數(包括零),將保留重復值的順序;

 

字段名稱:

字段名稱的命名與C、C++、Java等語言的變量命名方式幾乎是相同的。
protobuf建議字段的命名采用以下划線分割的駝峰式,例如 first_name 而不是firstName。

 

字段編碼值:

編碼值的取值范圍為 1~2^32(4294967296)。

消息中的字段的編碼值無需連續,只要是合法的,並且不能在同一個消息中有字段包含相同的編碼值。

[1,15]之內的標識號在編碼的時候會占用一個字節。[16,2047]之內的標識號則占用2個字節。所以應該為那些頻繁出現的消息元素保留 [1,15]之內的標識號。

切記:要為將來有可能添加的、頻繁出現的標識號預留一些標識號。

 

字段默認值:

當一個消息被解析的時候,如果被編碼的信息不包含一個特定的singular元素,被解析的對象鎖對應的域被設置位一個默認值,對於不同類型指定如下:

  • 對於string,默認是一個空string
  • 對於bytes,默認是一個空的bytes
  • 對於bool,默認是false
  • 對於數值類型,默認是0
  • 對於枚舉,默認是第一個定義的枚舉值,必須為0;
  • 對於消息類型(message),域沒有被設置,確切的消息是根據語言確定的;

注:對於標量消息域,一旦消息被解析,就無法判斷域釋放被設置為默認值(例如,例如boolean值是否被設置為false)還是根本沒有被設置。你應該在定義你的消息類型時非常注意。例如,比如你不應該定義boolean的默認值false作為任何行為的觸發方式。也應該注意如果一個標量消息域被設置為標志位,這個值不應該被序列化傳輸。

對於可重復域的默認值是空(通常情況下是對應語言中空列表)。

 

 

另外:

  • message消息支持嵌套定義,消息可以包含另一個消息作為其字段,也可以在消息內定義一個新的消息;
  • proto定義文件支持import導入其它proto定義文件;
  • 每個proto文件指定一個package名稱,對於java解析為java中的包。對於C++則解析為名稱空間。

 

 

 標量數值類型

.proto Type Notes C++  Java  Python  Go
double   double double float float64
float   float float float float32
int32 使用變長編碼,對於負值的效率很低,如果你的域有可能有負值,請使用sint64替代 int32 int int int32
uint32 使用變長編碼 uint32 int int/long uint32
uint64 使用變長編碼 uint64 long int/long uint64
sint32 使用變長編碼,這些編碼在負值時比int32高效的多 int32 int int int32
sint64 使用變長編碼,有符號的整型值。編碼時比通常的int64高效。 int64 long int/long int64
fixed32 總是4個字節,如果數值總是比總是比228大的話,這個類型會比uint32高效。 uint32 int int uint32
fixed64 總是8個字節,如果數值總是比總是比256大的話,這個類型會比uint64高效。 uint64 long int/long uint64
sfixed32 總是4個字節 int32 int int int32
sfixed64 總是8個字節 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

 

 其中:

varint(type=0),動態類型

  1. 每個字節第一位表示有無后續字節,有為1,無為0,(雙字節,低字節在前,高字節在后);
  2. 剩余7位倒序合並。
舉例: 300 的二進制為 10 0101100

第一位:1(有后續) + 0101100

第二位:0(無后續) + 0000010

最終結果: 101011000000010

 

 

枚舉類型

在下面的例子中,在消息格式中添加了一個叫做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;
}

如你所見,Corpus枚舉的第一個常量映射為0:每個枚舉類型必須將其第一個類型映射為0,這是因為:

  • 必須有有一個0值,我們可以用這個0值作為默認值。
  • 這個零值必須為第一個元素,為了兼容proto2語義,枚舉類的第一個值總是默認值。

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM