Google資料交換格式:ProtoBuf
本文轉自:https://www.cnblogs.com/chenny7/p/5157335.html
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):
- 向後相容,比如說,當接收方升級了之後,它能夠正確識別傳送方發出的老版本的協議。由於老版本沒有“狀態”這個屬性,在擴充協議時,可以考 慮把“狀態”屬性設定成非必填 的(optional),或者給“狀態”屬性設定一個預設值;
- 向前相容,比如說,當傳送方升級了之後,接收方能夠正常識別傳送方發出的新版本的協議。這時候,新增加的“狀態”屬性會被忽略;
向後相容和向前相容有啥用捏?俺舉個例子:當你維護一個很龐大的分散式系統時,由於你無法同時 升級所有 模組,為了保證在升級過程中,整個系統能夠儘可能不受影響,就需要儘量保證通訊協議的向後相容或向前相容。
proto檔案
如上面的例子,使用protobuf,首先需要在一個 .proto 檔案中定義你需要做序列化的資料結構資訊。每個ProtocolBuffer資訊是一小段邏輯記錄,包含一系列的鍵值對。
例如:
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("[email protected]"); 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作為通訊協議,你可以無須擔心破壞現有程式碼的情況下擴充套件協議。
protobuf 訊息
message由至少一個欄位組合而成,類似於C語言中的結構。每個欄位都有一定的格式:
限定修飾符 | 資料型別 | 欄位名稱 | = | 欄位編碼值 | [欄位預設值]
限定修飾符:
- Required:表示是一個必須欄位,必須相對於傳送方,在傳送訊息之前必須設定該欄位的值;對於接收方,必須能夠識別該欄位的意思。傳送之前沒有設定required欄位或者無法識別required欄位都會引發編解碼異常,導致訊息被丟棄。
- Optional:表示是一個可選欄位,可選對於傳送方,在傳送訊息時,可以有選擇性的設定或者不設定該欄位的值;對於接收方,如果能夠識別可選欄位就進行相應的處理,如果無法識別,則忽略該欄位,訊息中的其它欄位正常處理。很多介面在升級版本中都把後來新增的欄位都統一的設定為optional欄位,這樣老的版本無需升級程式也可以正常的與新的軟體進行通訊,只不過新的欄位無法識別而已,因為並不是每個節點都需要新的功能,因此可以做到按需升級和平滑過渡。
- Repeated:表示該欄位可以包含0 ~ N個元素,可以看作是在傳遞陣列。
欄位名稱:
欄位名稱的命名與C、C++、Java等語言的變數命名方式幾乎是相同的。
protobuf建議欄位的命名採用以下劃線分割的駝峰式,例如 first_name 而不是firstName。
欄位編碼值:
編碼值的取值範圍為 1~2^32(4294967296)。
訊息中的欄位的編碼值無需連續,只要是合法的,並且不能在同一個訊息中有欄位包含相同的編碼值。
protobuf 建議把經常要傳遞的值把其欄位編碼設定為1-15之間的值。
欄位預設值:
傳送資料時,對於required資料型別,如果使用者沒有設定值,則使用預設值傳遞到對端;
接收資料時,對於optional欄位,如果沒有接收到optional欄位,則設定為預設值。
另外:
- message訊息支援巢狀定義,訊息可以包含另一個訊息作為其欄位,也可以在訊息內定義一個新的訊息;
- proto定義檔案支援import匯入其它proto定義檔案;
- 每個proto檔案指定一個package名稱,對於java解析為java中的包。對於C++則解析為名稱空間。
protobuf 資料型別
.proto型別 |
C++型別 |
備註 |
double |
double |
|
float |
float |
|
int32 |
int32 |
變長編碼,編碼負數時不夠高效,負數最好使用sint32 |
int64 |
int64 |
變長編碼,編碼負數時不夠高效,負數最好使用sint64 |
uint32 |
uint32 |
變長編碼 |
uint64 |
uint64 |
變長編碼 |
sint32 |
int32 |
變長編碼,有符號的整型值,對負數編碼效率高於int32s |
sint64 |
int64 |
變長編碼,有符號的整型值,對負數編碼效率高於int64s |
fixed32 |
uint32 |
4位元組,如果數值總是比總是比228大的話,這個型別會比uint32高效 |
fixed64 |
uint64 |
8位元組,如果數值總是比總是比256大的話,這個型別會比uint64高效 |
sfixed32 |
int32 |
4位元組定長編碼 |
sfixed64 |
int64 |
8位元組定長編碼 |
bool |
bool |
布林值 |
string |
string |
一個字串必須是UTF-8編碼或者7-bit ASCII編碼的文字 |
bytes |
string |
可能包含任意順序的位元組資料 |
enum |
enum |
分類 |
含義 |
範圍 |
0 |
Varint |
int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 |
64-bit |
fixed64, sfixed64, double |
2 |
Length-delimited |
string, bytes, embedded messages, packed repeated fields |
3 |
Start group |
groups (deprecated) |
4 |
End group |
groups (deprecated) |
5 |
32-bit |
fixed32, sfixed32, float |
其中:
varint(type=0),動態型別:
- 每個位元組第一位表示有無後續位元組,有為1,無為0,(雙位元組,低位元組在前,高位元組在後);
- 剩餘7位倒序合併。
舉例: 300 的二進位制為 10 0101100 第一位:1(有後續) + 0101100 第二位:0(無後續) + 0000010 最終結果: 101011000000010
======專注高效能web伺服器架構和開發=====