1. 摘要(TL;DR)
fastbee MQTT Broker 在回复 QoS1 发布(PUBLISH)的 PUBACK 报文时,错误地把"入站 PUBLISH 的 QoS"写进了 PUBACK 固定头的 QoS 字段。按 MQTT 规范,PUBACK 固定头的 QoS 位为 Reserved,必须置 0 ;错误写入 QoS1 会使 PUBACK 首字节从合法的 0x40 变成非法的 0x42。
后果仅在 "设备以 QoS1 发布 + 客户端严格校验保留位" 的组合下显现:
- 设备使用的 libmosquitto 2.0.14 客户端会判定该 PUBACK 为畸形报文(
MOSQ_ERR_MALFORMED_PACKET, rc=21)并主动断开连接; - 设备反复掉线 → 周期状态/属性上报(property/post)无法正常送达 → "运行状态"不显示设备上报的值。
fastbee 开源原版(社区版)长期未发现此问题,是因为其典型工况为 QoS0 或宽松客户端,不会触发。本平台(派生版)的部署工况(设备 = QoS1 + libmosquitto)恰好命中,因此这是一个对本平台真实有害、必须修复的 BUG。
修复方法(一行):将 PUBACK 固定头 QoS 固定为 MqttQoS.AT_MOST_ONCE(即 0)。
2. 背景与问题现象
问题现象:设备周期性出现"运行状态"无法显示上报值的情况------表现为设备被 MQTT Broker 反复踢下线。
经抓包与客户端日志定位,根因在 fastbee MQTT Broker 侧回发的 PUBACK 报文 :其固定头 QoS 字段错误复用了入站 PUBLISH 的 QoS,违反 MQTT 规范,被严格客户端(libmosquitto 2.0.14)判定为畸形报文(rc=21)并主动断开连接,导致周期状态上报(property/post)无法送达。
本报告即对这一 PUBACK 固定头 QoS 缺陷 的根因分析、设备侧证据与修复方案。
3. 涉及组件与代码位置
| 角色 | 仓库 / 路径 | 关键文件 |
|---|---|---|
| fastbee MQTT Broker(缺陷方,本平台派生版) | fastbee/springboot/fastbee-server/mqtt-broker/ |
src/main/java/com/fastbee/mqtt/handler/MqttPublish.java |
| 设备客户端(libmosquitto 2.0.14,QoS1 发布) | ---(见第 7 节通用代码) | 通用示例(第 7 节) |
| fastbee 开源原版(社区版,同源、未修复参照) | fastbee 开源原版 master |
springboot/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/handler/MqttPublish.java |
4. MQTT 协议规范依据
依据 MQTT 3.1.1 / 5.0 规范:
- 固定头(Fixed Header)第 1 字节布局 :
- bit 7-4:控制报文类型(Control Packet Type)
- bit 3:DUP
- bit 2-1:QoS(对特定报文为 Reserved)
- bit 0:RETAIN
- PUBACK 报文类型 = 4。
- §3.4.1(MQTT 3.1.1) :PUBACK 固定头中 QoS 位为 Reserved,必须置
0,0。PUBACK 永远是 QoS0 的控制报文。 - 同样的规则适用于 PUBREC / PUBREL / PUBCOMP(QoS2 握手链路),其固定头 QoS 位也必须为 0。
推论:无论入站 PUBLISH 的 QoS 是 0 还是 1,回发的 PUBACK 固定头 QoS 位必须为 0。
字节级验证
- 合法 PUBACK(QoS=0):
(4 << 4) | (0 << 1) | 0 = 0x40 - 缺陷 PUBACK(误用 QoS1):
(4 << 4) | (1 << 1) | 0 = 0x42← 保留位非 0,违规 - Netty
MqttEncoder在编码MqttFixedHeader时,会忠实地把qosLevel()写入 bit 2-1,因此mqttQoS = AT_LEAST_ONCE即产生0x42字节流。
5. 缺陷分析(fastbee Broker 侧,本平台派生版)
5.1 缺陷代码
文件:fastbee/springboot/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/handler/MqttPublish.java
java
// 约 line 197
MqttQoS mqttQoS = message.fixedHeader().qosLevel(); // 取入站 PUBLISH 的 QoS
int packetId = message.variableHeader().packetId();
MqttFixedHeader header;
switch (mqttQoS.value()) {
/*0,1消息等级,直接回复*/
case 0:
case 1:
// ❌ 缺陷:把发布报文的 QoS 复用到 PUBACK 固定头
header = new MqttFixedHeader(MqttMessageType.PUBACK, false, mqttQoS, false, 0);
break;
case 2:
// 处理Qos2的消息确认
...
header = new MqttFixedHeader(MqttMessageType.PUBREC, false, MqttQoS.AT_MOST_ONCE, false, 0);
break;
...
}
问题本质 :mqttQoS 来自入站 PUBLISH。当设备以 QoS1 发布时,mqttQoS == AT_LEAST_ONCE,于是 PUBACK 固定头 QoS 位被写成 1,产生畸形报文 0x42。注意同方法内 PUBREC(QoS2 分支)反而已正确写成 MqttQoS.AT_MOST_ONCE------说明开发者在 QoS2 路径上已经意识到"确认类报文固定头 QoS 必须为 0",只是 PUBACK 分支漏改。
5.2 修复代码
java
case 0:
case 1:
// ✅ 修复:PUBACK 固定头 QoS 字段必须置 0(AT_MOST_ONCE)。
// 若沿用发布报文的 QoS,Netty 会把 QoS 编码进固定头低 4 位,
// 导致首字节为 0x42 而非 0x40;客户端 libmosquitto 会判定为
// MOSQ_ERR_MALFORMED_PACKET(rc=21) 而断开连接。
header = new MqttFixedHeader(MqttMessageType.PUBACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
break;
该 PUBACK QoS 修复已随提交 2d7e31e 合入 main。
6. 触发条件分析(为什么开源原版长期没发现)
该缺陷是条件性的,只在特定组合下造成可观测故障:
| 条件 | 是否触发缺陷 | 说明 |
|---|---|---|
| 设备以 QoS0 发布 | ❌ 不触发 | QoS0 不需要 PUBACK;mqttQoS = AT_MOST_ONCE,恰好生成合法 0x40 |
| 设备以 QoS1 发布 + 宽松客户端(Eclipse Paho、多数嵌入式 SDK) | ⚠️ 产生畸形包但不报错 | 客户端解析 PUBACK 时不校验保留 QoS 位,只认报文类型=4,连接照常 |
| 设备以 QoS1 发布 + 严格客户端(libmosquitto 等,校验保留位) | ✅ 真实故障 | 收包校验失败 → rc=21 → 客户端断开 → 状态不上报 |
由此解释:
- 开源原版未察觉:其典型接入示例/设备默认 QoS0,或用不校验保留位的客户端,缺陷始终"隐形"。
- 本平台(派生版)命中 :设备(见第 7 节)恰恰是 QoS1 + libmosquitto 组合,缺陷被稳定触发。
结论:该缺陷在规范层面是确定的"非合规";在影响层面是"条件性有害"。对本平台而言属于真实 BUG,而非过度防御。
7. 设备客户端分析(通用示例)
以下为通用示例代码(用于说明客户端侧机制),不引用任何具体工程文件或代码仓库。
7.1 使用严格客户端 libmosquitto 2.0.14
设备客户端基于 libmosquitto 2.0.14 实现 MQTT 传输,该版本默认对入向报文的保留位/格式做校验:
c
#include <mosquitto.h>
/* 基于 libmosquitto 2.0.14 的 MQTT 传输客户端 */
struct mosquitto *mqtt_new_client(const char *id) {
return mosquitto_new(id, /*clean_session=*/true, /*userdata=*/NULL);
}
7.2 上报均以 QoS1 发布
客户端对事件、周期状态/属性、命令回执等所有上行消息一律使用 qos=1,因此必然触发 Broker 回 PUBACK:
c
/* 事件上报 */
mosquitto_publish(mosq, NULL, "x/event/post", ev_len, ev_payload, 1, false);
/* 周期状态/属性上报(心跳,约每 30s) */
mosquitto_publish(mosq, NULL, "x/property/post", st_len, st_payload, 1, false);
/* 命令回执 */
mosquitto_publish(mosq, NULL, reply_topic, rp_len, rp_payload, 1, false);
三处发布点清一色 qos=1,即客户端必然向 Broker 发送 QoS1 的 PUBLISH,必然收到 PUBACK。
7.3 客户端观测到 rc=21 畸形入向包
客户端挂载日志回调以定位畸形入向包,实践中确曾捕获 received a malformed packet 这类 ERR 级日志(对应 MOSQ_ERR_MALFORMED_PACKET / rc=21):
c
/* 开启 libmosquitto 内部日志:连断、收发、尤其是"received a malformed packet"
* 等 ERR 级信息都经此回调输出,用于定位 rc=21 畸形入向包的来源。 */
mosquitto_log_callback_set(mosq, on_log);
这直接证明客户端侧历史上确实收到过畸形 PUBACK 并被断线,正是 Broker 侧 PUBACK QoS 缺陷的下游症状。
8. 根因链路(完整因果链)
设备(QoS1 发布 property/post)
│ PUBLISH (QoS=1)
▼
fastbee Broker 收到 QoS1 PUBLISH
│ 构造 PUBACK 时复用 mqttQoS=AT_LEAST_ONCE
▼
PUBACK 首字节 = 0x42(保留 QoS 位 = 1,违反 MQTT 规范)
│ 经网络发回设备
▼
设备 libmosquitto 2.0.14 收到 PUBACK
│ 校验固定头保留位 ≠ 0 → MOSQ_ERR_MALFORMED_PACKET (rc=21)
▼
libmosquitto 主动断开 MQTT 连接
│
▼
设备反复掉线 → 周期状态上报(30s)持续失败 → 云端收不到上报
│
▼
前端"运行状态"无法显示设备上报的值 ★ 用户原始问题
修复 MqttPublish.java 使 PUBACK 首字节回到 0x40 后,libmosquitto 收包合法,连接稳定,状态上报正常送达。
9. 上游开源原版对比
通过 fastbee 开源原版(master)对应文件
springboot/fastbee-server/mqtt-broker/src/main/java/com/fastbee/mqtt/handler/MqttPublish.java 核查,其 callBack 方法代码片段如下:
java
MqttQoS mqttQoS = message.fixedHeader().qosLevel();
int packetId = message.variableHeader().packetId();
MqttFixedHeader header;
switch (mqttQoS.value()) {
/*0,1消息等级,直接回复*/
case 0:
case 1:
header = new MqttFixedHeader(MqttMessageType.PUBACK, false, mqttQoS, false, 0); // ← 同样缺陷
break;
case 2:
...
header = new MqttFixedHeader(MqttMessageType.PUBREC, false, MqttQoS.AT_MOST_ONCE, false, 0);
break;
default:
header = null;
}
结论 :fastbee 开源原版(社区版)仍未修复 此缺陷,代码与本平台修复前完全一致(同包名的 PUBREC 分支也已正确用 AT_MOST_ONCE)。
10. 验证结果
10.1 代码修复验证
- 提交
2d7e31e已将MqttPublish.java的 PUBACK 固定头 QoS 改为MqttQoS.AT_MOST_ONCE,与PUBREC分支保持一致。
10.2 端到端服务健康检查
通过 bash scripts/fastbee.sh status 与 HTTP 探活:
- 容器 mysql / redis:
running✅ - 后端 8080 / MQTT 1883 / 8083:监听中 ✅
- 前端 8081:监听中 ✅
- HTTP
/prod-api/captchaImage:200✅
注:状态脚本对 jar 显示"已停止"为 PID 文件跟踪的假阴性(进程由更早的启动遗留、未写入当前
scripts/fastbee-admin.pid),实际服务端口监听且 HTTP 均正常,功能无碍。
11. 第三方平台互通性影响与 QoS 建议
若设备将来需接入第三方(使用 fastbee 开源原版、未修复)平台,该缺陷将再次导致设备掉线。处置建议(按优先级):
-
首选:在 Broker 侧修复(根治)
第三方平台的 PUBACK QoS 缺陷修正同样只需一行(
mqttQoS→MqttQoS.AT_MOST_ONCE)。若该平台是可控 fork 或可提 PR 的开源项目,应反哺修复,双方均受益。 -
次选(无法改第三方时):将 QoS 做成可配置,而非全局降级
在设备端客户端中增加
publish_qos配置项:- 连 本平台(已修复) →
qos=1(保留可靠投递) - 连 第三方开源原版(未修复) →
qos=0(QoS0 不触发 PUBACK,规避缺陷)
这样不牺牲自有平台的可靠性,又能保证跨平台互通。
- 连 本平台(已修复) →
-
兜底(必须全局单一值且需兼容未修复开源原版):选 QoS0
设备已有"约每 30s 周期重报状态"的心跳机制(
publish_status_report),单条 QoS0 丢失最多 30s 内自愈,对"状态展示"类可容忍最终一致场景影响很小。
注意:该缺陷只影响上行 PUBLISH 的 PUBACK 。下行(Broker→设备 命令)由设备侧 libmosquitto 自己正确回 PUBACK,不受对方 Broker 缺陷影响;因此降级 QoS 仅牺牲上行可靠性。
12. 结论与后续行动
- 定性 :本平台(fastbee 派生版)
MqttPublish.java的 PUBACK 固定头 QoS 复用发布 QoS 是确定的 MQTT 规范违规(Reserved 位非 0)。对本平台(QoS1 + libmosquitto)构成真实掉线 BUG,是设备掉线及状态不上报的根因。 - 已修复 :提交
2d7e31e将 PUBACK 固定头 QoS 固定为MqttQoS.AT_MOST_ONCE,与PUBREC分支保持一致;端到端验证通过。 - 上游状态:fastbee 开源原版(社区版)仍含相同缺陷。
- 后续建议 :
- 若设备需跨平台接入第三方开源原版,按第 11 节将
publish_qos配置化。
- 若设备需跨平台接入第三方开源原版,按第 11 节将
附录 A:关键代码引用速查
| 仓库 | 文件 | 关键行 | 内容 |
|---|---|---|---|
| 本平台(fastbee 派生版) | springboot/fastbee-server/mqtt-broker/.../handler/MqttPublish.java |
~197 | mqttQoS = message.fixedHeader().qosLevel(); |
| 本平台 | 同上 | case 1(~208) | 修复后 new MqttFixedHeader(..., MqttQoS.AT_MOST_ONCE, ...) |
| 开源原版(社区版) | springboot/fastbee-server/mqtt-broker/.../handler/MqttPublish.java |
case 1 | 仍用 mqttQoS(未修复) |
附录 B:复现 / 验证命令
bash
# 1) 启动平台(脚本)
cd <平台根目录> && bash scripts/fastbee.sh start
# 2) 状态与探活
bash scripts/fastbee.sh status
curl -s -o /dev/null -w "captchaImage=%{http_code}\n" -m 5 http://127.0.0.1:8080/prod-api/captchaImage
# 3) 抓包验证 PUBACK 首字节(修复后应为 0x40)
# tcpdump -i any -A -n 'tcp port 1883' # 观察设备 QoS1 发布后的 PUBACK 字节
附录 C:协议字节速算
PUBACK type=4 → 0x40 基准
+ QoS1(误用) → 0x40 | 0x02 = 0x42 (非法,触发 rc=21)
+ QoS0(正确) → 0x40 | 0x00 = 0x40 (合法)