Protocol Buffers
介紹主題
l Protocol Buffers簡介
l 定義一個.proto文件
l Message的使用
l 消息的編碼機制
l 使用時注意事項
什麼是ProtocolBuffers?
l Google定義的一種序列化的協議格式;
l Google內部幾乎所有的RPC調用及文件格式;
(據稱當前google已經定義了12,183個.proto文件,共有48,162種不同的message類型。它們用於RPC系統或各種存儲系統中進行數據的存儲)
l 目標:
Ø 簡單性
Ø 兼容性
Ø 高性能
XML與Protobuf的比較
易讀性 <->二進制格式;
自描述語言 <->沒有.proto文件根據就是無用的;
文件大<-> 文件小(3-10倍);
解析及序列化較慢<->快(20- 100倍);
.xsd(複雜)<->.proto(簡單,無二義性);
訪問簡單<->訪問容易;
示例如下:
<person> <name>John Doe</name> <email>[email protected]</email> </person> (== 69 bytes, 510ms to parse) System.out.println(person.getElementsByTagName("name").getElementText()); System.out.println(person.getElementsByTagName("email").getElementText()); |
Person { name: "John Doe" email: "[email protected]" } (== 28 bytes, 100200ns to parse) System.out.println(person.name()); System.out.println(person.email()); |
message示例
package tutorial; option java_package = "com.example.tutorial"; option java_outer_classname = "AddressBookProtos";
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 AddressBook { repeated Person person = 1; } |
從.proto文件到運行時
在.proto文件中定義消息;
用protoc編譯器將其編譯成源代碼
Ø C++
Ø Java
Ø Python
在代碼中直接使用接口
通過網絡進行傳輸或存儲
Message的定義
在.proto文件中定義Message消息;
語法格式:message [message name] { … }
消息可以內嵌(枚舉或消息)
將會被轉化爲其它語言;
消息的內容
每個消息的內容又包含如下的格式:
消息類型;
枚舉類型:
Enum<name> {
Valuename = value;
}
域;
域的格式定義如下:
<rule><type> <name> = <id> {[options]}
域的修飾符 rules
Required
該值是必須要傳的,具有唯一性。(msg.fieldname())
Optional
該值可以有零個或一個,可以查詢其存在與否。(msg.has_fieldname())
Repeated
該值相當於一個數組或有序列表,查詢時可取其長度。(msg.fieldname_size())
可以使用選項packed = true來進行高效的編碼。
Required是必須的
在用required修飾符時一定要謹慎;
一旦域被required修飾,該值就必須要進行傳遞,在版本升級或兼容時可能存在問題;
Googe工程師不建議使用required修飾符;
域id(標識)
每個域都有唯一的標識(id) (1-2^29)
注:不可以使用其中的[19000-19999]的標識號, Protobuf協議實現中對這些進行了預留。
變量採用的是可變長的編碼方式
[1,15]之內的標識號在編碼的時候會佔用一個字節。[16,2047]之內的標識號則佔用2個字節。應該爲那些頻繁出現的消息元素保留[1,15]之內的標識號。
在二進制格式的數據中唯一標識該域
域的名字在數據編碼時不會使用到,編碼中完全採用id來進行域的標識。
選項,命名空間及消息導入
Options:
[default = value] -> 爲該域設置一個默認值 (默認值是不需編碼的)
如:
optional uint32 ad_bid_count = 4[default = 2];
[packed =false / true]->採用更緊湊的編碼方式
如:
repeated int32 samples = 4[packed=true];
[deprecated =false/true]->標識該域是否已經被棄用
如:
optional int32 old_field = 6[deprecated=true];
[optimize_for= SPEED/CODE/LITE_RUNTIME]:影響代碼生成
Package:
命名空間,影響java的包名及生成的類名;
如:packagecom.example.message
Import:
導入其它文件中的message
如:import “myfile/message1.proto”
Message的使用
從.proto到具體代碼
Protoc編碼器根據.proto文件產生約定語言對應的代碼;
如:protoc -I=C:\protobuf\test\ --java_out=C:\protobuf\test\C:\protobuf\test\addressbook.proto
運行完上述命令後,會在C:\protobuf\test\目錄下生成一個類文件,即com.example.tutorial.AddressBookProtos.java,該類中有關於People和AddressBook的類文件;
爲消息設置具體的值
public static Person addPerson() { Person.Builder person = Person.newBuilder(); Person.PhoneNumber.Builder phoneNumber=Person.PhoneNumber.newBuilder(); person.setId(Integer.parseInt("123456")); person.setName("zhaozheng"); person.setEmail("[email protected]"); phoneNumber.setNumber("15926467660"); phoneNumber.setType(PhoneType.valueOf("MOBILE")); person.addPhone(phoneNumber); return person.build(); }
|
序列化及解析數據
序列化:
addressBook.build().writeTo(OutputStream); addressBook.build().toByteArray(); |
解析:
addressBook.mergeFrom(InputStream); addressBook.parseFrom(InputStream) |
獲取消息的具體值
public void Print(AddressBook addressBook) { for (Person person : addressBook.getPersonList()) { System.out.println("Person ID: " + person.getId()); System.out.println(" Name: " + person.getName()); if (person.hasEmail()) { System.out.println(" E-mail address: " + person.getEmail()); } for (Person.PhoneNumber phoneNumber : person.getPhoneList()){ switch (phoneNumber.getType()) { case MOBILE: System.out.print(" Mobile phone #: "); break; case HOME: System.out.print(" Home phone #: "); break; case WORK: System.out.print(" Work phone #: "); break; } System.out.println(phoneNumber.getNumber()); } } } |
消息編碼機制
l 一個簡單的消息編碼;
l 基於128的Varints;
l 消息結構
l 其它值類型
一個簡單的消息編碼
消息格式定義如下:
message Test1 { required int32 a = 1; } |
在一個應用程序中,創建了一個Test1消息,並將其中的a設置爲150。序列化該消息將可以看到3個字節。
0896 01
如何將該消息序列化爲該格式呢?
Varints是一種將整數採用1個或多個字節序列的方法。越小的數據需要採用更少的字節。
每個Byte的最高位(msb)是標誌位,如果該位爲1,表示該Byte後面還有其它Byte,如果該位爲0,表示該Byte是最後一個Byte。每個Byte的低7位是用來存數值的位。
Varints方法用Litte-Endian(小端)字節序。
示例:
1:0000 0001
300:1010 1100 0000 0010
1010 1100 0000 0010 → 010 1100 000 0010 (去msb) 000 0010 010 1100 (反轉) → 000 0010 ++ 010 1100 (拼接) → 100101100 (計算) → 256 + 32 + 8 + 4 = 300 |
消息的結構
每條消息(message)都是由一系列的key-value對組成的。
key由兩部分組成,一部分是在定義消息時字段的編號(field_num),另一部分是字段的類型(wire_type)。
類型 |
含義 |
使用場景 |
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 |
對於流消息中的每個key值,也是採用varint方式來表示其值的,計算格式如下(field_number << 3) | wire_type),也就是說最後的2位表示的是字段類型信息。
分析之前的示例編碼
08
它採用varint的方式來存儲key,其值爲08,由於是丟棄了msb,所以它的表示如下:
0000 1000 ->000 1000(去msb) ->000 0001(向右移3位) |
將最低3位數據取開,並將剩餘的bit向右移3位,將得到0001,即表示的是該域對應的標識號。
96 01 = 1001 0110 0000 0001 → 000 0001 ++ 001 0110 (丟棄msb 並按7 bits進行反轉) → 10010110 → 2 + 4 + 16 + 128 = 150 |
消息編碼-ZigZag
Int32來存儲負整數時,會使得編碼特別長;
有符號整型可以採用ZigZag機制進行編碼;
ZigZag編碼是將有符號整型映射成爲無符號整型,對於絕對值小的負數將使用小的varint進行編碼。
0 -> 0 -1-> 1 1 -> 2 -2-> 3 …… 2147483647 -> 4294967294 -2147483648 -> 4294967295 |
sint32類型的值的編碼如下:
(n << 1) ^ (n >> 31) |
sint64類型的值的編碼如下:
(n << 1) ^ (n >> 63) |
注意事項
使用message的建議:
在protobuf協議中切記message的兼容性是首要的;
只有在非常必要時,才使用required關鍵詞;
對於常用的值可以選擇域爲1-15的標識號(有效編碼)
根據可能出現的期望值選擇合適的數據類型;
更新message的建議:
將域聲明爲repeated或optional時,設置一些默認值(向後兼容性);
不要隨意更改域標識,不能循環使用域標識;
有一些數據類型是可以改變的;(如ints)
當修改default時,切記默認值是不進行編碼的,但在.proto文件中設置;