Protoc Buffer 是咱們比較經常使用的序列化框架,Protocol Buffer 序列化後的佔空間小,傳輸高效,能夠在不一樣編程語言以及平臺之間傳輸。今天這篇文章主要介紹 Protocol Buffer 使用 VarInt32 減小序列化後的數據大小。編程
VarInt32 (vary int 32),即:長度可變的 32 爲整型類型。通常來講,int 類型的長度固定爲 32 字節。但 VarInt32 類型的數據長度是不固定的,VarInt32 中每一個字節的最高位有特殊的含義。若是最高位爲 1 表明下一個字節也是該數字的一部分。所以,表示一個整型數字最少用 1 個字節,最多用 5 個字節表示。若是某個系統中大部分數字須要 >= 4 字節才能表示,那其實並不適合用 VarInt32 來編碼。下面以一個例子解釋 VarInt32 的編碼方式:框架
以 129 爲例,它的二進制爲 1000 0001 。
因爲每一個字節最高位用於特殊標記,所以只能有 7 位存儲數據。
第一個字節存儲最後 7 位 (000 0001),但並無存下全部的比特,所以最高位置位 1,剩下的部分用後續字節表示。因此,第一個字節爲:1000 0001
第二個字節只存儲一個比特位便可,所以最高位爲 0 ,因此,第二個字節爲:0000 0001
這樣,咱們就沒必要用 4 字節的整型存儲 129 ,能夠節省存儲空間
在 Protoc buffer 中,每個 ProtoBuf 對象都有一個方法 public void writeDelimitedTo(final OutputStream output),該方法將 ProtoBuf 對象序列化後的長度以及序列化數據自己寫入到輸出流 output 中。多個對象調用該方法能夠將序列化後的數據寫入到同一個輸出流。因爲每次寫入都有長度,因此反序列化時先解析長度,在讀取對應長度的字節數據,便可解析出每一個對象。該方法中對序列化後長度的編碼便使用 VarInt32,由於一個 Protobuf 對象序列化後的長度不會太大,所以使用 VarInt32 編碼可以有效的節省存儲空間。接下來咱們看下 Protoc Buffer 中如何實現 VarInt32 編碼,跟進 writeDelimitedTo 方法,能夠看到 VarInt32 編碼的源碼以下:編程語言
/** * Encode and write a varint. {@code value} is treated as * unsigned, so it won't be sign-extended if negative. */ public void writeRawVarint32(int value) throws IOException { while (true) { if ((value & ~0x7F) == 0) {//表明只有低7位有值,所以只需1個字節便可完成編碼 writeRawByte(value); return; } else { writeRawByte((value & 0x7F) | 0x80);//表明編碼不止一個字節,value & 0x7f 只取低 7 位,與 0x80 進行按位或(|)運算爲了將最高位置位 1 ,表明後續字節也是改數字的一部分 value >>>= 7; } } }
該方法對 int 類型的值進行 VarInt32 編碼,能夠驗證最多 5 個字節便可完成編碼。oop
理解了編碼後,解碼就沒什麼可說的了。就是從輸入字節流中,讀取一個字節判斷最高位,將真實數據位拼接成最終的數字便可。Hadoop RPC 中使用了 Protoc Buffer 做爲數據序列化框架。其中,Hadoop 針對 writeDelimitedTo 方法實現了對 VarInt32 的解碼。源碼以下:學習
/** * Read a variable length integer in the same format that ProtoBufs encodes. * @param in the input stream to read from * @return the integer * @throws IOException if it is malformed or EOF. */ public static int readRawVarint32(DataInput in) throws IOException { byte tmp = in.readByte(); if (tmp >= 0) {// tmp >= 0 表明最高位是 0 ,不然 tmp < 0 表明最高位是 1 ,須要繼續往下讀 return tmp; } int result = tmp & 0x7f; if ((tmp = in.readByte()) >= 0) { result |= tmp << 7; } else { result |= (tmp & 0x7f) << 7; if ((tmp = in.readByte()) >= 0) { result |= tmp << 14; } else { result |= (tmp & 0x7f) << 14; if ((tmp = in.readByte()) >= 0) { result |= tmp << 21; } else { result |= (tmp & 0x7f) << 21; result |= (tmp = in.readByte()) << 28; if (tmp < 0) {//咱們說 VarInt32 最多 5 個字節表示,當程序執行到這裏,tmp < 0,說明,編碼格式有問題// Discard upper 32 bits. for (int i = 0; i < 5; i++) { if (in.readByte() >= 0) { return result; } } throw new IOException("Malformed varint"); } } } } return result; }
在 Hadoop 源碼中並無使用循環去解碼,而是使用多個 if 條件判斷,根據 tmp 的正負號來判斷最高位是不是 1。若是讀取的該數字用了 5 個字節編碼,當讀到了第 5 個字節,理論上 tmp 應該大於 0 。可是若是 tmp 小於 0 ,說明編碼格式有問題。在 Hadoop 源碼中程序會繼續往下讀,最多再向下讀 5 個字節且丟掉最高位仍然 < 0 的字節。若是在該過程某個字節最高位爲 0 ,便中止讀取直接返回。這個處理邏輯在其餘框架源碼中也有出現。優化
看完 Hadoop 的源碼,咱們在看看 Protoc Buffer 本身提供的解析源碼:編碼
/** * Like {@link #readRawVarint32(InputStream)}, but expects that the caller * has already read one byte. This allows the caller to determine if EOF * has been reached before attempting to read. */ public static int readRawVarint32( final int firstByte, final InputStream input) throws IOException { if ((firstByte & 0x80) == 0) { return firstByte; } int result = firstByte & 0x7f; int offset = 7; for (; offset < 32; offset += 7) { final int b = input.read(); if (b == -1) { throw InvalidProtocolBufferException.truncatedMessage(); } result |= (b & 0x7f) << offset; if ((b & 0x80) == 0) { return result; } } // Keep reading up to 64 bits. for (; offset < 64; offset += 7) { final int b = input.read(); if (b == -1) { throw InvalidProtocolBufferException.truncatedMessage(); } if ((b & 0x80) == 0) { return result; } } throw InvalidProtocolBufferException.malformedVarint(); }
能夠看到 Protoc Buffer 本身提供的解碼方式與 Hadoop 是同樣的,包括遇到錯誤的編碼時候的異常處理方式也是同樣的。spa
本篇文章主要介紹了 VarInt32 編解碼,VarInt32 表示一個整型數字最少用 1 個字節, 最多用 5 個字節。因此在傳輸數字大部分都比較小的場景下適合使用。固然,咱們也能夠用 VarInt64 來表示長整型的數字。 在介紹 VarInt32 的同時咱們也看到了 ProtoBuf 和 Hadoop 這樣的框架在傳輸數據的優化上不放過任何一個細節,值得咱們學習。code
公衆號「渡碼」orm