gRPC源码分析(四):剖析Proto序列化

简介: 首先,针对读源码是先看源代码还是测试代码,因人而异。个人建议在对源码毫无头绪时,先从测试入手,了解大致功能;如果有一定基础,那么也可以直接入手源代码。我认为优秀的Go源码可读性是非常高的,所以一般情况下,我都直接从源文件入手,遇到问题才会去对应的测试里阅读。

在前面的分析中,我们已经知道了使用proto序列化的代码在encoding目录中,路径中只有三个文件,其中2个还是测试文件,看起来这次的工作量并不大。

首先,针对读源码是先看源代码还是测试代码,因人而异。个人建议在对源码毫无头绪时,先从测试入手,了解大致功能;如果有一定基础,那么也可以直接入手源代码。我认为优秀的Go源码可读性是非常高的,所以一般情况下,我都直接从源文件入手,遇到问题才会去对应的测试里阅读。

Marshal

Marshal的代码不多,关键在于传入参数的类型,有2个分支路线:

  1. proto.Marshaler类型,实现了Marshal() ([]byte, error)方法
  2. proto.Message类型,实现了Reset()String() stringProtoMessage()三个方法

我们回头看看proto生成的go文件,发现对应的是第二个接口。那我们接着看:

  1. 调用了protoBufferPool,是一个sync.Pool,是为了加速proto对象的分配
  2. 内部采用的是 marshalAppend,字面来看就是 序列化并追加,对应了 wire-format这个概念,并不需要将整个结构加载完毕、再进行序列化
  3. 接下来调用的是protoV2.MarshalOptions,需要关注的是protoV2是另一个package,protoV2 "google.golang.org/protobuf/proto"
  4. 在正式marshal前,调用m.ProtoReflect()方法,根据名字可以猜测是对Message做反射,详细内容不妨后面再看
  5. 最后就是正式的marshal了,分两个分支:out, err = methods.Marshal(in)out.Buf, err = o.marshalMessageSlow(b, m)。后者是慢速的,一般情况下是不会用到,我们重点关注前者,这时就需要回头看4中的实现了
  6. 逐个往前搜索,接口protoreflect.Message => 接口Message =>函数MessageV2 => 函数ProtoMessageV2Of => 函数legacyWrapMessage => 函数MessageOf => 类型messageReflectWrapper,终于,在这里找到了目标函数 ProtoMethods
  7. 因为我们取的是methods,所以很快将代码定位到 makeCoderMethods => marshal => marshalAppendPointer ,最后找到一行核心代码 b, err = f.funcs.marshal(b, fptr, f, opts)
  8. 那这个marshal什么时候被赋值的呢?在步骤7中,我们查看了methods被赋值的地方,其实旁边就有一个函数 makeReflectFuncs ,最后定位到了 /google.golang.org/protobuf/internal/impl/codec_gen.go 文件中。每种变量的序列化,都是按照特定规则来执行的。

实战

那么 protobuf 实际是如何对每种类型进行Encoding的呢?有兴趣的朋友可以点击这个链接,阅读原文。这里,我直接拿出一个实例进行讲解。

定义proto

message People {
    bool male = 1;
    int32 age = 2;
    string address = 3;
}

生成对应文件后,编写测试用例

func main() {
   
    people := &pbmsg.People{
   
        Male:    true,
        Age:     80,
        Address: "China Town",
    }
    b, _ := proto.Marshal(people)
    fmt.Printf("%b\n", b)
}

运行生成结果

[1000 1 10000 1010000 11010 1010 1000011 1101000 1101001 1101110 1100001 100000 1010100 1101111 1110111 1101110]

分析第一个字段Bool

首先,Male是一个bool字段,序号为1。

根据Google上的文档,bool是Varint,所以计算

(field_number << 3) | wire_type = (1<<3)|0 = 8,对应第一个字节: 1000

然后,它的值true对应第二个字节1

分析第二个字段Int

同样的,(field_number << 3) | wire_type = (2<<3)|0 = 16,对应第三个字节10000

值80对应1010000

分析第三个字段String

因为string是不定长的,所以需要一个额外的长度字段

(field_number << 3) | wire_type = (3<<3)|2=26,对应11010

接下来是长度字段,我们有10个英文单词,所以长度为10,对应 1010

然后就是10个Byte表示"China Town”了

结语

本次的分析到这里就暂时告一段落了,阅读protobuf的相关代码还是非常耗时耗力的。其实这块最主要的复杂度在于为了兼容新老版本,采用了大量的Interface实现。Interface带有面向对象特色,在重构代码时很有意义,不过也给阅读代码时,查找方法对应实现时带来了复杂度。

目录
相关文章
|
2月前
|
JSON fastjson Java
niubility!即使JavaBean没有默认无参构造器,fastjson也可以反序列化。- - - - 阿里Fastjson反序列化源码分析
本文详细分析了 Fastjson 反序列化对象的源码(版本 fastjson-1.2.60),揭示了即使 JavaBean 沲有默认无参构造器,Fastjson 仍能正常反序列化的技术内幕。文章通过案例展示了 Fastjson 在不同构造器情况下的行为,并深入探讨了 `ParserConfig#getDeserializer` 方法的核心逻辑。此外,还介绍了 ASM 字节码技术的应用及其在反序列化过程中的角色。
65 10
|
XML Java 程序员
zookeeper源码分析--序列化篇
zookeeper源码分析--序列化篇
172 0
|
存储
【Zookeeper】源码分析之序列化
在完成了前面的理论学习后,现在可以从源码角度来解析Zookeeper的细节,首先笔者想从序列化入手,因为在网络通信、数据存储中都用到了序列化,下面开始分析。
109 0
【Zookeeper】源码分析之序列化
|
Java Android开发
【Android Protobuf 序列化】Protobuf 使用 ( Protobuf 源码分析 | 创建 Protobuf 对象 )
【Android Protobuf 序列化】Protobuf 使用 ( Protobuf 源码分析 | 创建 Protobuf 对象 )
194 0
|
存储 算法 关系型数据库
MySQL · 源码分析 · Tokudb序列化和反序列化过程
序列化和写盘 Tokudb数据节点写盘主要是由后台线程异步完成的: checkpoint线程:把cachetable(innodb术语buffer pool)中所有脏页写回 evictor线程:释放内存,如果victim节点是dirty的,需要先将数据写回。
3178 0
|
分布式计算 Java Hadoop
|
3月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
9天前
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第22天】在Java的世界里,对象序列化和反序列化是数据持久化和网络传输的关键技术。本文将带你了解如何在Java中实现对象的序列化与反序列化,并探讨其背后的原理。通过实际代码示例,我们将一步步展示如何将复杂数据结构转换为字节流,以及如何将这些字节流还原为Java对象。文章还将讨论在使用序列化时应注意的安全性问题,以确保你的应用程序既高效又安全。