1. 程式人生 > >Google資料交換格式:ProtoBuf

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):

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

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

 

 

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. 每個位元組第一位表示有無後續位元組,有為1,無為0,(雙位元組,低位元組在前,高位元組在後);
  2. 剩餘7位倒序合併。

複製程式碼

舉例: 300 的二進位制為 10 0101100

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

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

最終結果: 101011000000010

複製程式碼

 

======專注高效能web伺服器架構和開發=====