踩坑记:一知半解protobuf

本篇写个小坑,别期望太高…


在广告系统里,对延迟是毫秒必争(毕竟省下来的每一毫秒都可以用在后端优化效果),因此我们和外部媒体之间的通信往往使用 protobuf 。

相比 json、xml,protobuf 确实节省了不少编解码的时间以及网络开销,不过相应的代价是牺牲了便利性,不能用 vi 等文本编辑器查看/修改,遇到问题时排查也比较麻烦。


- 入坑 -

比如 7 月份,某媒体希望一次请求中拉到多条广告(用于信息流场景),因此在 imp 添加一个 ads_count 字段,用于标识本次请求需要的广告数量。

过程是这样,在 xxx.proto 里给 Impression 类型添加一个新字段:

package com.xxx;
message BidRequest {
  string id = 1;
  int32 ver = 2;
  message Impression {
    ...
    int32 ads_count = 9;
  }
  Impression imp = 3;
  ...
}

然后用 protoc 编译,生成新版的 xxx.pb.go 

$ protoc --go_out=. xxx.proto

看起来挺简单一个流程,结果还是出了问题:不论媒体请求中填了什么值,这边 decode 出来,imp.GetAdsCount() 得到的总是 1 。


- 排查 -

由于我方代码是自测过的,能够正常取到 ads_count 的值,因此猜测是对方请求有点啥问题。

于是将对方的请求录下来,存到文件 req.pb 中,然后用 protoc 暴力解码:

$ protoc --decode-raw req.pb
1 {
  6: 0x3938373635343332
}
2: 1
3 {
  1: 1
  2: "6f63bd4df111480"
  3: 1
}
...

可以看到,我们什么也没看懂。

不过还好我们有 xxx.proto,借助已知信息,可以更好地解码请求:

$ protoc --decode=com.xxx.BidRequest xxx.proto  < req.pb
id: "123456789"
ver: 1
imp {
  id: 1
  ...
  ads_count: 1
  10: 3
}
...

看到了点不太对的东西。


- 填坑 -

在 imp 里面,除了 ads_count 之外,还看到了个 "10: 3"。

由于 protobuf 的变量名不能是纯数字,所以这应当是某个在类型定义里没有出现的字段,decode时只能用其序号代替,由此可知,应该是双方的 proto 文件应该有些差异。

经过沟通,媒体确实在 ads_count 之前还加了另一个字段(可能是和其他合作方使用到的);双方对齐以后,问题顺利解决:

修正 ads_count 的序号:

  message Impression {
    ...
    int32 ads_count = 10;
  }

用正确的 proto 来 decode:

$ protoc --decode=com.xxx.BidRequest xxx.proto  < req.pb
id: "123456789"
ver: 1
imp {
  id: 1
  ...
  ads_count: 3
}
...

MISSION COMPLETED.


- encoding -

问题是解决了,但是只写这些就显得太应付了,就再介绍下 proto 文件是怎么编解码的吧。

官方有一篇很详细的文档介绍了编码的过程(详见文末“阅读原文”),这里摘一些重点。

以一个简单的类型为例:

message Test1 {
  optional int32 a = 1;
}

如果给 a 赋值 150 并序列化,会得到3个字节(16进制):

08 96 01

其中第一个字节(08)是一个 varint(每个字节的最高位 = 1 表示该 int 还需要拼上后续字节的低 7 bits),其内容包含了第一个元素的序号(field number)和类型(wire type)。

将 08 的二进制 "0000 1000" 拆分成三部分来解释:

  • 0

    • 表示这个 varint 到这个字节就结束了

  • 0001

    • 表示其序号是1

  • 000

    • 表示其值类型也是个 varint

注意,不管这个 varint 有多大,其末3位总是用于表示类型(wire type),可能的取值有:

  • 0: varint

  • 1: 64-bit,如 fixed64, sfixed64, double

  • 2: 指定长度类型,如 string, bytes, 内嵌类型

  • 5: 32-bit,如 fixed32, sfixed32, float

第2、3个字节(96 01)是 a 的值,其二进制表示是

1001 0110 0000 0001

第 2 字节的最高位是 1 ,我们知道这个 varint 还没结束;而第 3 字节的最高位是 0 ,这个 varint 就到此结束了。

将两个最高位去掉,拼出一个完整的二进制数:

0000001 0010110 = 150

注意:varint 按字节序是小端存储,因此第 3 个字节的 0000001 放在高位。


- signed integers -

varint 看起来是个好东西,因为实践中经常会用到一些枚举值,可能的取值范围很小,使用 varint 只需要少量的空间。

不过如果我们需要用 -1 的时候怎么办呢?不管是用反码还是补码,都需要考虑符号位的问题  —— 对于 int32/int64,负数的编码总是要占用 10 个字节。

protobuf 的解决方案是为 sint32/sint64 引入 "ZigZag encoding",简单来说就是交替使用 0,1,2,3,... 来表示 0,-1,1,-2,...,从而将较小的负数编码为较小的无符号数,再使用 varint 编码。


- 没了 -

就这样吧,更多细节(string、内嵌类型以及数组的编码),请参考官方文档(文末“阅读原文”)。

最后一个小问题,下面这个编码后的消息,表示什么意思呢?

12 03 36 36 36

推荐阅读:


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值