grpc|protobuf编码
特别提醒,本文所涉及的源码是
go1.22.4 darwin/amd64
gRPC使用protobuf作为接口定义语言(IDL),包括定义服务的方法以及通过网络发送的消息。如下代码所示:
1 | service Greeter { |
上面的proto文件中定义了一个服务叫做Greeter,其中包含一个方法名是SayHello,请求的参数为HelloRequest类型,返回的响应为HelloReply类型。
protobuf是如何将上面的消息编码为二进制的呢?
请求参数和响应参数中都会包含若干个字段,这些字段被组成下图所示的样子:

如果是json编码的话,那其中的标签就是对应的字段名,比如上面的name和message,不过这种编码方式不高效,因此protobuf中没有采用这种,而是使用序号+线路类型(wire type)来组合表示标签。其中线路类型有如下6种:
| ID | 线路类型 | 字段类型 |
|---|---|---|
| 0 | VARINT | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
| 1 | I64 | fixed64, sfixed64, double |
| 2 | LEN | string, bytes, embedded messages, packed repeated fields |
| 3 | SGROUP | group start (deprecated) |
| 4 | EGROUP | group end (deprecated) |
| 5 | I32 | fixed32, sfixed32, float |
上述线路类型总共有6种,因此需要3个bit位来唯一标识,因此protobuf中的标签由如下组合成:
1 | Tag = (filed_num << 3) | wire_type |

1 Varint类型
varint是一种可变长度整数类型,为每个整数所分配的字节并不是确定的4字节或者8字节,而是依赖于具体的值。下面具体介绍varint类型的原理:
- 将原先的8
bit一位拆分为两种,最高位用来标识是否后面还有更多的数据,如果是则该位置为1,如果不是则该位置为0,后7位用来存储数据。
例如:对于一个整数228,它的二进制表示为11100100。首先将其每7位拆分为一组则得到1和1100100,用varint编码则会表示为00000001和11100100,如果该整数是用32位来编码的话则节省了两个字节,如果是64位编码的话,则节省了6个字节。
go标准库中的varint编码跟protobuf几乎一样,看看go是如何实现的
1 | func PutUvarint(buf []byte, x uint64) int { |
- 如果
x$\ge 0x80$,则说明数据超过7位,需要拆分,并且最高位置为1 - 右移继续判断下一个7位
该编码有一个问题,当整数非常大的时候会是一个负优化。例如对于uint64的最大值即64个1来说,用varint编码的话,最后需要10个字节来表示。因此这就引出了下一个问题,负数如果用varint编码的话就会非常浪费,因为在计算机中是用补码来表示的,-5的二进制形式是11111111111111111111111111111011,但它的绝对值其实是101而已。因此针对负数,采用了zigzag编码。
zigzag的原理也非常简单:
- 对于正数
n来说,首先将其映射成2n,然后再使用varint编码 - 对于负数
n来说,首先将其映射成2*|n|-1,然后再使用varint编码
这样负数和正数在数值上完全不会冲突,正整数和负整数交错排列,这也是为什么叫做 zigzag 编码 (锯齿形编码)的原因。
同时,负数被转换成正数之后,二进制编码也精简了许多。
例如: 对 -5 进行 zigzag 编码后,变成了9,对应于二进制为 00000000000000000000000000001001,使用 1 个字节即可表示 。
至于具体的映射实现有两种方式,一种是protobuf给出的,一种是在go源码中看到的:
1.protobuf中给出的
1 | 32位执行这个 |
2.go源码中给出的
1 | func PutVarint(buf []byte, x int64) int { |
即转换成无符号数向左移一位,如果x是负数则取反。
按理来说应该是protobuf更快一些的,因为没有条件判断语句,实际情况就不清楚了。
对于线路类型为varint的字段类型来说,sint32和sint64首先使用了zigzag进行映射,然后再使用varint编码,其它的字段类型则直接使用varint编码。
2 固定长度类型
一个用来表示 64 位的数据类型,如fixed64、sfixed64 和 double;另一个用来表示 32 位的数据类型,如 fixed32、sfixed32 和 float。
3 LEN
对于使用按长度分割的类型,即string, bytes, embedded messages, packed repeated fields。这意味着首先会有一个经过 Varint 编码的长度值,随后才是指定数量的字节数据。字符串值会使用 UTF-8字符编码格式来进行编码。