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, 虚心求教,有错误还请指正轻拍,谢谢

版权声明:本文出自志健的原创文章,未经博主允许不得转载

发布了23 篇原创文章 · 获赞 9 · 访问量 3万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览