直接用net.Conn写JT808网关等于裸奔,因TCP字节流无法保证报文边界,需自行实现帧提取(0x7E定界)、转义还原(0x7D处理)、长度校验(bodyLength)、XOR校验、版本路由(MsgBodyAttr bit15)及分包重组(SplitFlag+缓存)。

直接用 net.Conn 写 JT808 网关,等于在裸奔——它连“这是一条完整报文”都判断不了,更别提转义还原、校验、分包、版本路由这些刚需。
为什么 conn.Read() 拿到的永远不是一条 JT808 报文
JT808 是二进制帧协议,靠 0x7E 起止,中间还可能有 0x7D 转义(比如 0x7E 本身要变成 0x7D 0x02)。TCP 是字节流,conn.Read(buf) 返回的可能是:
- 半个报文(比如只读到前半截
0x7E...,后半截还在内核缓冲区) - 两个报文粘在一起(
0x7E...0x7E...0x7E连续出现) - 一个报文被拆成两段(比如
0x7E在第一包末尾,...在第二包开头) - 甚至夹杂非法字节(设备异常、网络干扰)
没状态机识别 0x7E 边界、没逻辑处理 0x7D 转义、不按消息头里的 bodyLength 截取,就敢把 buf 当作一帧去解,大概率 panic 或解出乱码。
bufio.Reader + 状态机才是 net 包能走通的最小路径
你必须自己补四块:帧提取、长度校验、转义还原、XOR 校验。用 bufio.Reader 替代裸 conn.Read(),配合一个三态机(idle/in_frame/escaped)是可行底线:
- 遇到
0x7E:若当前是idle,切到in_frame并清空 buffer;若已是in_frame,说明帧结束,立即触发解析 - 遇到
0x7D:切到escaped状态,下个字节异或0x20后追加,然后回到in_frame - 其他字节:直接追加到当前 buffer(仅限
in_frame状态) - 解析前先检查 buffer 长度 ≥ 消息头固定长度(12 字节),再读
bodyLength字段,确认总长是否匹配;不匹配就继续读,不丢不 panic
示例关键逻辑片段:
case 0x7E:
if state == idle {
state = in_frame
buf.Reset()
} else if state == in_frame {
// 完整帧 ready,校验、解包、投递
if isValidFrame(buf.Bytes()) {
handleJT808(buf.Bytes())
}
buf.Reset()
state = idle
}
校验和版本路由不能跳过,否则 2011/2013/2019 报文全乱套
JT808 不同版本的消息体结构差异极大:0x0200 在 2011 版没有加密标志位,在 2019 版第 3–4 字节的 MsgBodyAttr 里 bit15 才标识是否为 2019 版。不解析这个字段,就无法决定该用 protocol.T808_0x0200_2013 还是 T808_0x0200_2019 去解包。
- XOR 校验必须在帧提取完成后、解包前执行:对
[msgHeader[0] ... body[last]]区间逐字节异或,结果应为0x00,否则直接丢弃 - 版本位提取必须从消息头第 3–4 字节(即
buf.Bytes()[2:4])开始,用binary.BigEndian.Uint16()解出MsgBodyAttr,再查 bit15 - 忽略版本差异硬解,轻则字段错位(把车牌号当报警标志),重则 panic(访问越界)
分包补传逻辑藏在 SplitFlag 和 TotalPkg/PkgNo 里,不是可选项
大消息(如音视频、固件)会被终端按 SplitFlag=1 分成多帧发送,每帧带 TotalPkg(总包数)和 PkgNo(当前序号)。服务端必须缓存 session 上下文,等齐所有子包再合并解体。
- 收到首包(
PkgNo == 1)时,以phone + msgId + TotalPkg为 key 创建临时缓存,存入[][]byte切片 - 后续包按
PkgNo插入对应位置,缺包不阻塞,但需设超时(如 30 秒)自动清理 - 最后一包(
PkgNo == TotalPkg)到达后,拼接所有子包,再做 XOR 校验和版本解析 - 不用 session 缓存而直接转发分片,MQTT 下行端根本无法还原原始数据
真正难的从来不是“连上”,而是让每一帧字节都按协议本意被识别、还原、验证、路由——这些事 net.Conn 一句都不会帮你做。


















