Protoc Buffer 優化傳輸大小的一個細節

Protoc Buffer 是咱們比較經常使用的序列化框架,Protocol Buffer 序列化後的佔空間小,傳輸高效,能夠在不一樣編程語言以及平臺之間傳輸。今天這篇文章主要介紹 Protocol Buffer 使用 VarInt32 減小序列化後的數據大小。編程

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

VarInt32 解碼

 理解了編碼後,解碼就沒什麼可說的了。就是從輸入字節流中,讀取一個字節判斷最高位,將真實數據位拼接成最終的數字便可。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

相關文章
相關標籤/搜索