GRPC學習之路(5)——protobuf解碼過程解析

接着上一篇文章的例子,本篇主要研究protobuf如何從字節流中解析並生java對象的。之前的文章也介紹過如何從文件中讀取出一個對象的:

Message testMessage = Message.parseFrom(new FileInputStream("testmessage.txt"));

通過閱讀parseFrom這個方法的源碼,將它的流程簡要概括如下:

  1. 從InputStream中新建CodedInputStream對象
  2. 從CodedInputStream讀取下一個tag,即field_num和wire type的組合
  3. tag告知了wire type, 也就知道後面的字節是什麼類型,不同類型有不同的讀取邏輯,同時tag還告知了field_num,因此讀取成功後賦值給對應的字段。
  4. 然後循環2-3步,直到末尾結束返回Message對象

實際源碼解析

以下列出瞭解析流程中的最核心的代碼(java):

boolean done = false;
while (!done) {
  int tag = input.readTag();
  switch (tag) {
    case 0:  // 到達末尾,跳出循環
      done = true;
      break;
    default: {
      // 如果遇到未知的字段,在java的代碼裏則存儲到unknownFields,可以供調用者使用,其它語言就不一定了,具體可以參考其它語言的實現
      if (!parseUnknownFieldProto3(
          input, unknownFields, extensionRegistry, tag)) {
        done = true;
      }
      break;
    }
    case 8: {
      // tag爲8 即爲 00001000, field_num是1, wire type是0,即代表後面的字節是整型數字a的內容
      a_ = input.readInt32(); // 讀取後面的整型內容,具體怎麼讀取的後面會介紹
      break;
    }
    case 18: { 
      // tag爲18 即爲 00010010, field_num是2, wire type是2,即代表後面的字節是字符串query的內容
      String s = input.readStringRequireUtf8();
      query_ = s;
      break;
    }
  }
}

那現在來看看讀取整型變量的主要源碼,即上面代碼中input.readInt32()的邏輯

long result = 0;
for (int shift = 0; shift < 64; shift += 7) { // 每次移動7位,爲什麼是7位,因爲一個字節是8位,去掉首bit就只有7位有效了
  final byte b = readRawByte();
  result |= (long) (b & 0x7F) << shift; // 取出字節的後7位並往左移7位,上一篇文章介紹過,整型是倒過來存儲的
  if ((b & 0x80) == 0) { // 如果當前字節的首bit是0,則意味着後面的字節不屬於這個整型的一部分了
    return result;
  }
}

看起來和上一篇文章說的邏輯一樣,接下來再繼續看看讀取字符串的源碼,即上面代碼中input.readStringRequireUtf8()

final int size = readRawVarint32(); // 後面的這一個字節內容代表這個字符串是幾位
final byte[] bytes;
final int oldPos = pos;
final int tempPos;
if (size <= (bufferSize - oldPos) && size > 0) {
  // Fast path:  We already have the bytes in a contiguous buffer, so
  //   just copy directly from it.
  bytes = buffer;
  pos = oldPos + size;
  tempPos = oldPos;
} else if (size == 0) {
  return "";
} else if (size <= bufferSize) {
  refillBuffer(size);
  bytes = buffer;
  tempPos = 0;
  pos = tempPos + size;
} else {
  // Slow path:  Build a byte array first then copy it.
  bytes = readRawBytesSlowPath(size);
  tempPos = 0;
}
// TODO(martinrb): We could save a pass by validating while decoding.
if (!Utf8.isValidUtf8(bytes, tempPos, tempPos + size)) {
  throw InvalidProtocolBufferException.invalidUtf8();
}
// 上面的一長串都是和讀取的內容會不會超過預設值的bufferSize, 默認是4096, 真正取出字符串的是下面這一句
return new String(bytes, tempPos, size, UTF_8); // String的構造函數,從bytes的tempPos開始後面的size個字節,並以UTF_8編碼

總結和思考

通過上面的說明,我們對protobuf是如何讀取字節流並解碼成數據對象的過程有了一定的瞭解,看起來對我們日常使用protobuf沒什麼好處,但瞭解其內部原理確實能幫忙解惑不少protobuf的一些功能。

舉一個簡單的例子,protobuf是支持更新.protobuf文件中的對象的結構的,而且能夠兼容使用舊文件的代碼, 怎麼做到的呢?當然前提條件是要分配一個新的field_num,  有了上面的分析不難想到,使用舊的.protobuf的代碼會忽略這個屬性, 因爲新的field_num會生成一個新的tag,在switch的case中找不多這個新的tag,自然就忽略它並把它加入到unknownFields。同樣的道理,如果你指修改了字段的類型而不修改field_num,  解碼的時候就無法正確賦值了。

 

歡迎關注我的個人的博客www.zhijianliu.cn, 虛心求教,有錯誤還請指正輕拍,謝謝

版權聲明:本文出自志健的原創文章,未經博主允許不得轉載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章