認識Protobuf
Protocol buffers are a flexible, efficient, automated mechanism for serializing structured data – think XML, but smaller, faster, and simpler.
Protobuf是Google推出的一種輕量&高效的結構化數據存儲格式,是一款跨平臺、跨語言、可擴展的序列化結構數據的方法;可用作通信協議、數據存儲,etc...
特性
【優點】
- 使用簡單
- 跨平臺、跨語言、可擴展性
- 維護成本低
- 向後兼容性好
- 加密性好:二進制不可讀
- 性能好
- 體積比XML小3倍
- 序列化速度比XML快20倍
- 傳輸速度快
【缺點】
- 自解釋性差:二進制不可讀
- 需預定義結構
適用場景
- 傳輸數據量大
- 網絡環境不穩定
看個例子
message Person {
/** required and optional
* 1.required:必填
* 2.optional:可選
*/
// field_type field_name = field_number
required string name = 1;
required int32 id = 2;
}
【測試代碼】
@Test
public void testProtoBuf() {
// encode bytes
byte[] bytes = Person.newBuilder().setName("java").setId(1).build().toByteArray();
System.out.println(Arrays.toString(bytes));
// decode bytes
for (byte b : bytes) {
System.out.println(getBinString(b));
}
}
private String getBinString(byte b) {
return String.format("%8s", Integer.toBinaryString(b)).replace(' ', '0');
}
【生成的字節數組及二進制】
// 字節數組
[10, 4, 106, 97, 118, 97, 16, 1]
// 二進制
00001010
00000100
01101010
01100001
01110110
01100001
00010000
00000001
下面我們通過上述二進制的輸出瞭解下protobuf是怎麼進行序列化的
序列化
protobuf序列化採用的Tag-Length-Value結構的存儲方式
- Tag:通過一個字節(8位)來存儲field_number(前5位)和field_type(後3位)
- Length:可選值,存儲Value的長度,Length-delimited需要存儲Length
- Value:對應字段值的二進制表示
重點介紹一下Tag裡面field_type表示,其決定了value是怎麼表示;
【Wire Type表】
![聊一聊序列化-Protobuf](http://p2.ttnews.xyz/loading.gif)
通過Wire Type再來看上文的二進制表示
# set name = "java"
# Tag:field_number=1,field_type=2
00001010
# Length:4
00000100
# Value:"java"
01101010
01100001
01110110
01100001
# set id = 1
# Tag:field_number=2,field_type=0
00010000
# Value:1
00000001
Varint
Varint是一種特殊的整型,可變長的數字;其類型主要包含以下幾個
![聊一聊序列化-Protobuf](http://p2.ttnews.xyz/loading.gif)
它用一個或多個字節來表示一個數字,值越小的數字使用越少的字節數。這能減少用來表示數字的字節數。
如:
1. 對於 int32 類型的數字,一般需要 4個字節 表示; 若採用 Varint編碼,對於很小的 int32 類型 數字,則可以用 1個字節 來表示
2.雖然大的數字會需要 5 個 字節 來表示,但大多數情況下,消息都不會有很大的數字,所以採用 Varint方法總是可以用更少的字節數來表示數字
我們看一下寫int32的源碼
private void writeVarint32(int n) {
int idx = 0;
while (true) {
// 如果只有一個字節,直接中斷
if ((n & ~0x7F) == 0) {
i32buf[idx++] = (byte)n;
break;
} else {
// 取出字節串末7位,在最高位添加1構成一個字節
i32buf[idx++] = (byte)((n & 0x7F) | 0x80);
// 無符號右移7位
n >>>= 7;
}
}
trans_.write(i32buf, 0, idx);
}
從以上源碼我們能得出
- 如果最高位為1,表示後續的 字節 也是該數字的一部分
- 如果是 0,表示這是最後一個字節,且剩餘 7位 都用來表示數字
因此:
- 小於 128 的數字 都可以用 1個字節 表示;
- 大於 128 的數字,比如 300,會用兩個字節來表示:10101100 00000010
【負數的特殊處理】
我們知道,在二進制表示中,如果最高位為1,則代表該數為負數;當然Protobuf也很好的解決了這個問題,
`Protobuf`定義了 `sint32 / sint64` 類型表示負數,通過先採用 `Zigzag` 編碼(將`有符號數`轉換成`無符號數`),再採用 Varint編碼,從而用於減少編碼後的字節數
【Zigzag】
Zigzag是一種變長的編碼方式,使得絕對值小的數字都可以採用較少字節來表示;其編碼解碼過程為
- 編碼過程,代碼為(n <<1) ^ (n >>31)
- 將n左移1位
- 將n右移31位
- 前兩個結果異或操作
- 解碼過程,代碼為(n >>> 1) ^ -(n & 1)
- 無符號右移1位
- 對(n & 1) 取反
- 兩者異或
FYI
- Protocol Buffer 序列化原理大揭秘 - 為什麼Protocol Buffer性能這麼好?
- Google Protocol Buffers 序列化算法分析
- Protocol Buffers
關聯閱讀
閱讀更多 一隻懶懶的coder 的文章