首页 > 应用开发 > Go语言 > JT808转义处理的一个致命Bug,让我排查了三天
JT808协议里有一条规则,写在标准文档最不起眼的位置:
消息体中如果出现0x7e,需要转义为0x7d 0x02;出现0x7d,转义为0x7d 0x01。
看起来很简单。我第一次实现的时候,也是这么想的。
Bug是怎么出现的
去年做一个运输车辆平台,JT808解析跑了一段时间都正常。直到接了某厂商的一批终端,问题来了:大约5%的位置上报消息解析失败,校验码不匹配。
抓包看原始数据,帧头和帧尾的0x7e都在,校验码也对得上。但解析出来的消息体内容是乱的。
我把转义还原的代码翻来覆去看了几遍,逻辑没问题:遍历字节,遇到0x7d就看下一个字节,如果是0x01还原成0x7d,0x02还原成0x7e。
代码长这样:
func Unescape(data []byte) []byte { var out []byte for i := 0; i < len(data); i++ { if data[i] == 0x7d && i+1 < len(data) { if data[i+1] == 0x01 { out = append(out, 0x7d) i++ continue } if data[i+1] == 0x02 { out = append(out, 0x7e) i++ continue } } out = append(out, data[i]) } return out } 逻辑上,这没问题。但为什么解析失败?
三天排查过程
第一天,我怀疑是终端发送数据有误。联系厂商,厂商说设备没问题,是平台解析错误。扯皮了半天,没结果。
第二天,我把失败的那几条原始数据单独拿出来,手动做转义还原,发现还原出来的数据长度不对。原始消息体属性里的长度是42字节,但还原后只有40字节。
少了2个字节。
哪里少的?我逐字节对比,发现有两个相邻的0x7d 0x01,被还原成了一个0x7d,但后一个0x01被吃掉了。
等等,不对。0x7d 0x01应该还原成0x7d,后面的0x01是独立的字节,不应该被吃掉。
我重新看代码,发现问题了:
if data[i+1] == 0x01 { out = append(out, 0x7d) i++ // 这里跳过了下一个字节 continue } 当遇到0x7d 0x01时,还原成0x7d,然后i++跳过下一个字节。但如果下一个字节本身也是数据的一部分,就被跳过了。
举个例子:原始数据里有 0x7d 0x01 0x7d 0x01。按照标准,第一个0x7d 0x01还原成0x7d,第二个0x7d 0x01也还原成0x7d。但我的代码在处理第一个0x7d 0x01后,i++跳到了第二个0x7d 0x01的位置,继续循环时i又加1,直接跳过了第二个0x7d。
结果就是:还原后的数据少了后面的0x7d。
正确的实现
修复方式很简单:在跳过下一个字节之前,先记录当前字节,然后i只加1(因为for循环本身会加1)。
func Unescape(data []byte) []byte { var out []byte for i := 0; i < len(data); i++ { if data[i] == 0x7d && i+1 < len(data) { if data[i+1] == 0x01 { out = append(out, 0x7d) i++ continue } if data[i+1] == 0x02 { out = append(out, 0x7e) i++ continue } } out = append(out, data[i]) } return out } 看起来和之前一样?其实关键在for循环的条件。Go的for循环每次迭代后自动i++。当我们在循环体内执行i++时,实际上跳过了两个字节。
正确的做法是:如果当前字节是0x7d,并且下一个字节是0x01或0x02,那么消费两个字节,还原成一个字节。但i应该只加1,而不是加2。
我之前的代码在i++之后,for循环又执行一次i++,导致多跳了一个字节。
修复后的代码应该是: func Unescape(data []byte) []byte { var out []byte i := 0 for i < len(data) { if data[i] == 0x7d && i+1 < len(data) { if data[i+1] == 0x01 { out = append(out, 0x7d) i += 2 continue } if data[i+1] == 0x02 { out = append(out, 0x7e) i += 2 continue } } out = append(out, data[i]) i++ } return out } 用i += 2明确表示消费了两个字节,就不会出现多跳的问题。
这个Bug给我的启示
转义逻辑看起来简单,但在边界条件下容易出错。特别是当转义字符连续出现、或者转义字符出现在数据末尾时,必须仔细处理。
JTE在pkg/jt808的转义实现中,专门写了单元测试覆盖这些边界场景。如果你也在写JT808解析,建议至少测试以下几种情况:
连续多个0x7d 0x01
0x7d 0x01紧跟着0x7d 0x02
数据以0x7d结尾
数据以0x7d 0x01结尾
一个小小的转义Bug,让我排查了三天。你猜我第三天晚上在想什么?不是愤怒,是庆幸——庆幸问题出在开发环境,不是生产环境。如果是生产环境,5%的位置数据丢失,客户早就炸了。
项目仓库:
Gitee:suoten/jt-engine GitHub:suoten/JTEngine
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70050062/viewspace-3183395/ ,如需转载,请注明出处,否则将追究法律责任。