问题: - TCP_NODELAY 修复了 TCP ACK 延迟,但设备仍然重发 - 需要确定是消息被服务器拒绝,还是设备端 bug 改进内容: - OnPublish 中添加详细的拒绝日志输出 - 当消息验证失败时,输出 client_id、topic、qos、错误原因 - 当消息被屏蔽时,输出屏蔽类型和原因 - 帮助快速诊断 QoS0 重发的根本原因 新增文档: - doc/QOS0_RETRANSMIT_ANALYSIS.md - 深度分析重发问题的各种原因 - doc/DIAGNOSTIC_GUIDE.md - 完整的诊断指南和解决方案 使用方法: ./meshtastic_mqtt_server --console-log-mqtt=true 观察日志中是否有: [mqtt] PUBLISH rejected: ... - 消息被拒绝(服务器问题) [mqtt] PUBLISH blocked: ... - 消息被屏蔽(配置问题) 如果没有 rejected/blocked 日志但仍重发,则是设备端 bug。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
6.2 KiB
6.2 KiB
QoS0 消息重发问题深度分析
问题现状
即使启用了 TCP_NODELAY,设备发送 QoS0 消息后仍然重发 3 次。
根本原因分析
1. TCP_NODELAY 修复了什么?
✅ TCP_NODELAY 确实解决了 TCP ACK 延迟问题:
- Nagle 算法延迟从 40-200ms 降低到 ~0.05ms
- TCP 层的确认现在是即时的
❌ 但这不能解决消息被拒绝的问题。
2. 消息被拒绝的流程
当设备发送的消息不符合服务器要求时:
设备 → MQTT PUBLISH (QoS0)
↓
服务器 TCP 层收到 → 发送 TCP ACK ✅
↓
MQTT 层处理 → OnPublish hook
↓
MQTTPP 验证失败 → valid=false
↓
返回 packets.ErrRejectPacket
↓
mochi-mqtt 处理: return nil (不发送任何 MQTT 响应)
↓
设备收到 TCP ACK ✅ 但没有收到 MQTT 层响应
↓
设备认为消息可能丢失 → 重发 ❌
3. 为什么会重发?
可能的原因:
原因 A: 消息验证失败
检查以下验证失败的情况:
-
Protobuf 解码失败
parseServiceEnvelope() 返回错误 → MQTTPP 返回 valid=false -
解密失败
describePacket() 无法解密 → type="encrypted_packet" 且 AllowEncryptedForwarding=false → MQTTPP 返回 valid=false -
屏蔽规则命中
blockingViolationForRecord() 返回非 nil → OnPublish 返回 ErrRejectPacket
原因 B: 设备期待应用层响应
某些 MQTT 客户端实现可能:
- 虽然使用 QoS0(不需要 PUBACK)
- 但仍然期待某种应用层响应或订阅回显
- 没有收到预期响应时触发重试逻辑
原因 C: 设备端 Bug
设备固件可能有 bug:
- 错误地认为 QoS0 需要应用层确认
- 超时机制设置不当
- 重试逻辑实现错误
诊断步骤
步骤 1: 查看服务器日志
检查消息是否被拒绝:
# 启用控制台日志
./meshtastic_mqtt_server --console-log-mqtt=true --console-log-meshtastic=true
# 查找被拒绝的消息
grep -E "error|dropped|rejected" logs.txt
关键日志标识:
protobuf decode failed- protobuf 解析失败cannot be decrypted- 解密失败blocked node/forbidden word- 屏蔽规则命中
步骤 2: 抓包分析
# 抓取 MQTT 流量
tcpdump -i any -nn port 1883 -w mqtt.pcap
# 用 Wireshark 分析:
# 1. 查看是否有 TCP 重传 (Retransmission)
# 2. 查看 MQTT PUBLISH 是否有对应的响应
# 3. 检查时序图,看设备重发的时间间隔
期待的正常流程 (QoS0):
Client → Server: MQTT PUBLISH (QoS0)
Server → Client: TCP ACK
(没有 MQTT 层的 PUBACK,因为是 QoS0)
如果消息被拒绝:
Client → Server: MQTT PUBLISH (QoS0)
Server → Client: TCP ACK
(服务器静默丢弃,没有任何 MQTT 响应)
Client → Server: MQTT PUBLISH (QoS0) [重发]
Server → Client: TCP ACK
...
步骤 3: 检查数据库
-- 查看被丢弃的消息
SELECT * FROM discarded_packets
ORDER BY created_at DESC
LIMIT 20;
-- 统计丢弃原因
SELECT
json_extract(record, '$.error') as error_type,
COUNT(*) as count
FROM discarded_packets
GROUP BY error_type;
步骤 4: 测试不同的消息
# 发送一个有效的测试消息
mosquitto_pub -h localhost -p 1883 -t "msh/CN/2/e/LongFast/!12345678" -m "test" -q 0
# 观察是否也会重发
解决方案
方案 1: 修复消息验证问题
如果是消息验证失败导致:
检查 PSK 配置:
# 确保服务器配置了正确的 PSK
./meshtastic_mqtt_server --psk="your_base64_psk"
检查屏蔽规则:
-- 查看当前的屏蔽规则
SELECT * FROM blocking_rules WHERE enabled = 1;
-- 临时禁用所有规则测试
UPDATE blocking_rules SET enabled = 0;
方案 2: 允许加密消息转发
如果消息是加密的且无法解密:
# config.yaml
meshtastic:
allow_encrypted_forwarding: true
这样即使无法解密,消息也会被转发而不是拒绝。
方案 3: 返回明确的错误响应(不推荐)
理论上可以在消息被拒绝时返回 MQTT 错误码,但:
- ❌ QoS 0 协议规定不应该有 PUBACK
- ❌ 违反 MQTT 规范
- ❌ 可能导致客户端行为异常
方案 4: 设备端修复
如果是设备固件 bug:
- 更新设备固件到最新版本
- 检查设备日志,确认重发原因
- 联系设备厂商报告 bug
监控和调试
添加详细日志
修改 main.go 的 OnPublish 方法:
func (h *meshtasticFilterHook) OnPublish(cl *mqtt.Client, pk packets.Packet) (packets.Packet, error) {
valid, _, record := mqtpp.MQTTPP(pk.TopicName, pk.Payload, h.key, mqtpp.Options{
AllowEncryptedForwarding: h.settings.AllowEncryptedForwarding(),
PKIKeyResolver: h.pkiResolver,
})
info := mqttClientInfoFromClient(cl)
if !valid {
// 添加详细日志
printJSON(map[string]any{
"event": "publish_rejected",
"reason": "validation_failed",
"client_id": info.ClientID,
"topic": pk.TopicName,
"payload_len": len(pk.Payload),
"error": record["error"],
})
h.rejectPublish(cl, pk, record)
return pk, packets.ErrRejectPacket
}
// ... 其他逻辑
}
监控重发率
-- 创建视图统计每个客户端的重发率
CREATE VIEW client_retransmit_stats AS
SELECT
client_id,
COUNT(*) as total_attempts,
COUNT(DISTINCT packet_id) as unique_packets,
(COUNT(*) - COUNT(DISTINCT packet_id)) * 100.0 / COUNT(*) as retransmit_rate
FROM packets
GROUP BY client_id
HAVING retransmit_rate > 10;
结论
TCP_NODELAY 修复了 TCP 层的延迟问题,但如果消息本身被服务器拒绝(验证失败、解密失败、屏蔽规则等),设备仍然会重发。
下一步行动:
- ✅ 启用详细日志,查看是否有消息被拒绝
- ✅ 检查
discarded_packets表,确认拒绝原因 - ✅ 抓包分析,确认是 TCP 重传还是应用层重发
- ✅ 根据诊断结果选择对应的解决方案
如果所有消息都被正常处理(没有被拒绝),但仍然重发,那么问题在设备端固件。