聊一聊序列化-Protobuf

認識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


通過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


它用一個或多個字節來表示一個數字,值越小的數字使用越少的字節數。這能減少用來表示數字的字節數。

如:
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. 如果最高位為1,表示後續的 字節 也是該數字的一部分
  2. 如果是 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

關聯閱讀


分享到:


相關文章: