GRPC学习之路(5)——protobuf解码过程解析

本文深入探讨了Protobuf如何从字节流中解析数据并生成Java对象的过程,详细介绍了解析核心代码,包括整型和字符串类型的读取逻辑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

接着上一篇文章的例子,本篇主要研究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, 虚心求教,有错误还请指正轻拍,谢谢

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

### 如何在 Kotlin 中使用 Protocol Buffers Protocol Buffers (简称 Protobuf) 是一种语言无关、平台无关的可扩展机制,用于序列化结构化数据[^2]。为了在 Kotlin 中使用 Protobuf,开发者通常遵循一系列流程来定义消息格式并生成相应的代码。 #### 定义 `.proto` 文件 首先,在项目中创建一个新的文件,其扩展名为`.proto`。此文件用来描述想要交换的数据结构。例如: ```protobuf syntax = "proto3"; option java_package = "com.example.protobuf"; option java_outer_classname = "PersonProto"; message Person { string name = 1; int32 id = 2; string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { string number = 1; PhoneType type = 2; } repeated PhoneNumber phones = 4; } ``` 上述代码片段展示了如何定义一个简单的 `Person` 消息类型及其嵌套的消息和枚举类型[^3]。 #### 编译 .proto 文件到 Kotlin 类 安装好必要的工具链之后——通常是通过构建工具如 Gradle 或 Maven 来配置插件——就可以利用命令行工具或 IDE 插件编译这些 `.proto` 文件了。这一步会自动生成对应的 Kotlin 类以便于后续操作。 对于基于 Gradle 的项目来说,可以在 build.gradle.kts 添加如下依赖项以支持 ProtobufgRPC: ```kotlin plugins { kotlin("jvm") version "..." id("com.google.protobuf") version "..." } dependencies { implementation("io.grpc:grpc-kotlin-stub:...") implementation("com.google.protobuf:protobuf-java-util:...") } protobuf { protoc { artifact = "com.google.protobuf:protoc:..." } plugins { id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:..." } id("grpckt") { artifact = "io.grpc:protoc-gen-grpc-kotlin:...:jdk7@jar" } } generateProtoTasks { all().forEach { task -> task.plugins { id("grpc") id("grpckt") } } } } ``` 这段脚本设置了 Protobuf 插件以及 GRPC 支持,并指定了所需的版本号。当执行同步操作时,Gradle 将自动下载所需资源并将它们应用到工程当中去[^4]。 #### 使用生成的 Kotlin API 进行编码与解码 一旦完成了以上准备工作,则可以直接调用由 `.proto` 文件转换而来的 Kotlin 接口来进行对象实例化、属性设置等常规动作;同时也能够方便地完成对二进制流形式的数据包进行打包发送或是接收解析等工作。 比如读取来自网络响应中的 Protobuf 格式的二进制数据并将其反序列化为 Kotlin 对象的过程可能像这样实现: ```kotlin val personBytes: ByteArray = ... // 假设这是从服务器接收到的内容 try { val person = Person.parseFrom(personBytes) } catch (e: InvalidProtocolBufferException) { e.printStackTrace() } ``` 同样地,如果需要向远程服务端提交信息的话也可以按照相似的方式处理: ```kotlin // 创建新的人实体 val newPersonBuilder = Person.newBuilder() newPersonBuilder.setName("John Doe").setId(1).setEmail("john@example.com") // 序列化为人字节数组准备传输 val bytesToSend = newPersonBuilder.build().toByteArray() // 发送bytesToSend... ``` 综上所述,借助官方提供的库函数,即使是在跨不同编程环境之间也能轻松达成高效稳定的数据交互目标[^5]。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值