背景
个别玩家反馈,某个活动打开时,消费列表显示错误,经前端日志排查发现是因为该玩家的消费信息解析失败了
我们的 protobuf 的简易配置
syntax = "proto3";
message Pay {
'''
string role_id = 2;
'''
int32 tc=22;
'''
}
message MsgPayInfo {
repeated Pay pay_list = 1;
int32 flag=2;
}
经过就线上数据的排除测试,发现错误数据
- 当tc的值是 127的浮点数时会报错,protobuf解析失败,例如:
lua脚本层发送数据
{pay_list = {{tc=127.05}}}
protobuf编译后
uint8_t* = [10,4,176,1,127]
开始猜想
- tc是 int32 类型,赋值时使用浮点数会出错
- 尝试发送几个数据 可以发送浮点数,并且不会报错
lua脚本层发送数据
{pay_list = {{tc=128.05}}}
protobuf编译后
uint8_t* = [10,4,176,1,128,1]
lua脚本层发送数据
{pay_list = {{tc=126.05}}}
protobuf编译后
uint8_t* = [10,3,176,1,126]
lua脚本层发送数据
{pay_list = {{tc=127}}}
protobuf编译后
uint8_t* = [10,3,176,1,127]
观察几个数据我们得出一些结论:
- 当发送tc<=127时,第二个数字是3
- [10,3,176,1,126]
- [10,3,176,1,127]
- 当发送tc>=128时,第二个数字是4
- [10,4,176,1,128,1]
-
与之对应的是发送tc=128时,uint8数组长度多了1个数据
- 猜想第二个数字的3、4可能代表着后续数据长度
后续观察出错的数据
lua脚本层发送数据
{pay_list = {{tc=127.05}}}
protobuf编译后
uint8_t* = [10,4,176,1,127]
- 这个数据的第二个数字是4,而他的后续数据长度只有3
- !!!可能是因为这样所以数据长度对不上,导致protobuf解析消息失败了!!!
查看官网的protobuf协议文档验证我们的猜想
官网文档的白话理解以报错数据为例子
lua脚本层发送数据
{pay_list = {{tc=127.05}}}
protobuf编译后
uint8_t* = [10,4,176,1,127]
- 第一个数据 10, 我们可以根据公式 (field_number << 3) | wire_type 将解析出字段编号,和后续读取数据的类型
// 10的二进制对应00001010
10 = 0b00001010
可以解析出
field_number = 0b00001 = 1
wire_type = 0b010 = 2
wire_type是什么意思呢?继续看文档
ID | Name | Used For |
---|---|---|
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 |
- 我们回到我们上面错误的例子
message MsgPayInfo {
repeated Pay pay_list = 1;
int32 flag=2;
}
// 10的二进制对应00001010
10 = 0b00001010
可以解析出
field_number = 0b00001 = 1 代表proto中编号为1的字段,既 pay_list
wire_type = 0b010 = 2
- wire_type是2代表的,接下来是一个长度值
!!!我们的猜想是对的,第二位代表着数据长度值,而我们收到的会报错的数据,长度值不对!!!
我们继续看为什么当tc=128.05的时候可以发送成功
lua脚本层发送数据
{pay_list = {{tc=128.05}}}
protobuf编译后
uint8_t* = [10,4,176,1,128,1]
- 可以看出,第二位是4,而后续确实有4个数据,所以能解析成功
那么[176,1,128,1]又是如何解析出
message Pay {
'''
string role_id = 2;
'''
int32 tc=22;
'''
}
176 = 0b10110000
field_number = 0b10110 = 22 代表proto中编号为22的字段,既 tc !!!这是错的,凑巧对上了,后续会说明!!!
wire_type = 0b000 = 0
- wire_type是0代表的,接下来是一个VARINT
开始有了新疑问?只有5位代表编号,也就31,线上使用的字段不止31
message Pay {
'''
string role_id = 2;
'''
int32 tc=22;
'''
int32 tc1=1;
int32 tc15=15;
int32 tc16=16;
int32 tc30=30;
int32 tc31=31;
int32 tc32=32;
}
尝试发送几个数据
lua脚本层发送数据
{pay_list = {{tc1=1}}}
protobuf编译后
uint8_t* = [10,2,8,1]
lua脚本层发送数据
{pay_list = {{tc15=15}}}
protobuf编译后
uint8_t* = [10,2,120,15]
lua脚本层发送数据
{pay_list = {{tc16=16}}}
protobuf编译后
uint8_t* = [10,3,128,1,16]
lua脚本层发送数据
{pay_list = {{tc30=30}}}
protobuf编译后
uint8_t* = [10,3,240,1,30]
lua脚本层发送数据
{pay_list = {{tc31=31}}}
protobuf编译后
uint8_t* = [10,3,248,1,31]
lua脚本层发送数据
{pay_list = {{tc32=32}}}
protobuf编译后
uint8_t* = [10,3,128,2,32]
!!!发现几个结论!!!
- 当字段编号小于<=15时候 数据[120,15]长度是2
- 当字段编号在 16-31的时候,数据[248,1,31]长度是3
- 当字段编号在 大于31的时候, 数据[128,2,32]第二位会发生变化
我们文档查看发生了什么
- https://developers.google.com/protocol-buffers/docs/encoding#varints
- 官网说了,varints白话理解
- 一个8位整数
- 最高位1代表还有后续数据,最高位0代表数据结束了
- 低7位用来保存数据
- 如果有多个数据,要用小端序合并起来
回到我们的例子[176,1,128,1]
lua脚本层发送数据
{pay_list = {{tc=128.05}}}
protobuf编译后
uint8_t* = [10,4,176,1,128,1]
[176,1]用来解析字段长度
- 数字:176
- field_number = 0b10110
- 根据varints白话第一点,还有后续数据
- 保留其余部分 0b0110
- field_number = 0b10110
- 数字:1
- 因为176中数据未取完,继续读取
- 1 = 0b00000001
- 根据 varints 白话理解 第三点 以小端序合并数据
- 00000001 0110 = 000000010110 = 22
- 得到字段编号为22
[128,1] 是我们的数据 128
- 数字:128
- 0b128 = 10000000
- 根据varints白话第一点,还有后续数据
- 保留其余部分 0b0000000
- 数字:1
- 因为128中数据未取完,继续读取
- 1 = 0b00000001
- 根据 varints 白话理解 第三点 以小端序合并数据
- 00000001 0000000 = 000000010000000 = 128
- 得到数据128
解决
- 将数据 * 100 取整后在发送给前端
额外的收获
- 当我们字段编号>=16的时候,会导致数据多使用一个 uint8的长度