物联网设备为什么不用 JSON?本文详解本项目迭代了 3 版的 24 字节定长帧协议:字段如何布局、字节序如何处理、校验和怎么算,以及协议演进过程中的经验教训。
一、为什么用二进制协议(核心要点)
| 维度 | JSON 方案 | 二进制方案(本项目) |
|---|---|---|
| 传输体积 | 每个字段名+括号+逗号冗余 | 定长 24 字节,压缩 ~60% |
| 解析成本 | 字符串解析 + 类型转换 | 按偏移量位运算直读 |
| 弱网适配 | 4G 流量按 KB 计费 | 适合 NB-IoT/4G 低频上报 |
| 可读性 | 人类可读 | 需 hex 工具,调试困难 |
| 协议演进 | 天然兼容加字段 | 需版本控制 + 两端同步升级 |
结论话术:物联网设备内存小、带宽贵、嵌入式开发简单化诉求强,二进制定长帧是行业主流做法(类比 OCPP、国标 GB/T 27930 均含二进制扩展帧)。
二、24 字节帧结构(v3.0,大端序)★必须背
| 偏移 | 长度 | 字段 | Java 类型 | 说明 |
|---|---|---|---|---|
| 0 | 1 | startTag | byte | 起始符,固定 0x76 |
| 1 | 2 | chargeStationId | short | 充电桩 ID |
| 3 | 8 | chargeRecordId | long | 充电记录 ID(雪花算法) |
| 11 | 4 | chargingTime | int | 已充电时长(秒) |
| 15 | 2 | chargedEnergy | short | 已充电量(×100 存储) |
| 17 | 2 | currentPower | short | 当前功率 |
| 19 | 2 | currentCost | short | 当前费用 |
| 21 | 1 | chargingProgress | byte | 充电进度(%) |
| 22 | 1 | chargingState | byte | 充电状态 |
| 23 | 1 | checksum | byte | 校验和(累加和取低 8 位) |
协议演进史(v1.0 → v3.0):
- v1.0:18 字节(初始版本,ChargeStatData 原始结构)
- v2.0:chargeRecordId 从 4 字节扩展为 8 字节,22 字节
- v3.0:chargingTime 从 2 字节扩展为 4 字节 int → 24 字节定长帧(当前版本)
类注释中保留了 v1.0/v2.0/v3.0 演进说明,可以主动讲这段演进,展现"协议是迭代出来的"。
三、核心代码(charge-protocol 模块)
charge-protocol/src/main/java/com/yeseesion/SmartChargeStation/protocol/
├── mqtt/message/ChargeStatData.java (228 行) ★真·二进制编解码
├── mqtt/message/ChargeCmdPayload.java ( 38 行) 仅字段,无编解码
└── protobuf/ChargingCmdProtobuf.java (579 行) protoc 生成,已废弃
ChargeStatData 关键方法
| 方法 | 作用 |
|---|---|
toBytes() |
对象 → 24 字节数组(序列化) |
fromBytes(byte[]) |
字节数组 → 对象(反序列化) |
fromHexString(String) |
MQTT 收到的 hex 字符串 → 对象 |
| 校验和计算 | 前 23 字节累加取低 8 位,与第 24 字节比对 |
位运算要点(动手实现时重点掌握):
java
// short → 2 字节(大端序)
bytes[i] = (byte) (value >> 8);
bytes[i+1] = (byte) (value & 0xFF);
// 2 字节 → short
short v = (short) ((bytes[i] & 0xFF) << 8 | (bytes[i+1] & 0xFF));
⚠️ 协议设计说明:上下行两套方案(值得关注)
- 下行指令为什么不用真·二进制? 这是有意的设计取舍 :下行指令由小程序发起 ,若用二进制帧,前端开发者无法直观看到发出的请求参数(如目标桩 ID、充电时长);改用「对象 JSON 序列化 → hex」后,抓包/日志里能直接看清字段内容,调试体验大幅提升 。代价是报文冗余、字段名(
start_tag等 snake_case)也进了 payload------但下行指令频率极低(用户手动点击),对带宽不敏感,用可读性换传输效率在这个方向是划算的。 - chargingTime 截断不一致 :协议是 4 字节 int,但下游某处用
& 0xFFFF截断成 2 字节 ------ 充电超过 65535 秒(约 18.2 小时)会溢出(这是真实 bug)。 - Protobuf 已退役 :
ChargingCmdProtobuf.java是 protoc 生成的死代码,前端链路已改 JSON 文本帧。
四、常见问题与解答
- 为什么起始符用 0x76 而不是 0x00?(避免与空字节混淆,便于抓包识别帧起点)
- 校验和用累加和,能发现哪些错误?(单比特翻转、数据错位能检测;不能防篡改,需要 HMAC/CRC16)
- 定长帧 vs 变长帧怎么选?(定长:解析简单、无需长度字段,适合固定采集项;变长:灵活但需 length 字段 + 粘包处理)
- 协议升级如何兼容老设备?(帧头加 version 字段,服务端按版本分发解析------当前未实现)
- chargedEnergy 为什么 ×100 存储?(浮点数不适合传输比较,转整型避免精度丢失------但要约定小数位)
小结
协议的 24 字节布局、大端序编解码与校验和逻辑已经讲完。这套协议是迭代了 3 版才稳定的,核心经验就一句:版本号从第一天就要留 。下一篇 【从零搭建物联网智能充电桩系统】3、MQTT 客户端 讲解设备数据接入云端的消息处理链路。