问题: - 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>
260 lines
6.2 KiB
Markdown
260 lines
6.2 KiB
Markdown
# 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: 消息验证失败
|
||
|
||
检查以下验证失败的情况:
|
||
|
||
1. **Protobuf 解码失败**
|
||
```
|
||
parseServiceEnvelope() 返回错误
|
||
→ MQTTPP 返回 valid=false
|
||
```
|
||
|
||
2. **解密失败**
|
||
```
|
||
describePacket() 无法解密
|
||
→ type="encrypted_packet" 且 AllowEncryptedForwarding=false
|
||
→ MQTTPP 返回 valid=false
|
||
```
|
||
|
||
3. **屏蔽规则命中**
|
||
```
|
||
blockingViolationForRecord() 返回非 nil
|
||
→ OnPublish 返回 ErrRejectPacket
|
||
```
|
||
|
||
#### 原因 B: 设备期待应用层响应
|
||
|
||
某些 MQTT 客户端实现可能:
|
||
- 虽然使用 QoS0(不需要 PUBACK)
|
||
- 但仍然期待某种应用层响应或订阅回显
|
||
- 没有收到预期响应时触发重试逻辑
|
||
|
||
#### 原因 C: 设备端 Bug
|
||
|
||
设备固件可能有 bug:
|
||
- 错误地认为 QoS0 需要应用层确认
|
||
- 超时机制设置不当
|
||
- 重试逻辑实现错误
|
||
|
||
## 诊断步骤
|
||
|
||
### 步骤 1: 查看服务器日志
|
||
|
||
检查消息是否被拒绝:
|
||
|
||
```bash
|
||
# 启用控制台日志
|
||
./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: 抓包分析
|
||
|
||
```bash
|
||
# 抓取 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: 检查数据库
|
||
|
||
```sql
|
||
-- 查看被丢弃的消息
|
||
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: 测试不同的消息
|
||
|
||
```bash
|
||
# 发送一个有效的测试消息
|
||
mosquitto_pub -h localhost -p 1883 -t "msh/CN/2/e/LongFast/!12345678" -m "test" -q 0
|
||
|
||
# 观察是否也会重发
|
||
```
|
||
|
||
## 解决方案
|
||
|
||
### 方案 1: 修复消息验证问题
|
||
|
||
如果是消息验证失败导致:
|
||
|
||
**检查 PSK 配置:**
|
||
```bash
|
||
# 确保服务器配置了正确的 PSK
|
||
./meshtastic_mqtt_server --psk="your_base64_psk"
|
||
```
|
||
|
||
**检查屏蔽规则:**
|
||
```sql
|
||
-- 查看当前的屏蔽规则
|
||
SELECT * FROM blocking_rules WHERE enabled = 1;
|
||
|
||
-- 临时禁用所有规则测试
|
||
UPDATE blocking_rules SET enabled = 0;
|
||
```
|
||
|
||
### 方案 2: 允许加密消息转发
|
||
|
||
如果消息是加密的且无法解密:
|
||
|
||
```yaml
|
||
# config.yaml
|
||
meshtastic:
|
||
allow_encrypted_forwarding: true
|
||
```
|
||
|
||
这样即使无法解密,消息也会被转发而不是拒绝。
|
||
|
||
### 方案 3: 返回明确的错误响应(不推荐)
|
||
|
||
理论上可以在消息被拒绝时返回 MQTT 错误码,但:
|
||
- ❌ QoS 0 协议规定不应该有 PUBACK
|
||
- ❌ 违反 MQTT 规范
|
||
- ❌ 可能导致客户端行为异常
|
||
|
||
### 方案 4: 设备端修复
|
||
|
||
如果是设备固件 bug:
|
||
- 更新设备固件到最新版本
|
||
- 检查设备日志,确认重发原因
|
||
- 联系设备厂商报告 bug
|
||
|
||
## 监控和调试
|
||
|
||
### 添加详细日志
|
||
|
||
修改 `main.go` 的 `OnPublish` 方法:
|
||
|
||
```go
|
||
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
|
||
}
|
||
|
||
// ... 其他逻辑
|
||
}
|
||
```
|
||
|
||
### 监控重发率
|
||
|
||
```sql
|
||
-- 创建视图统计每个客户端的重发率
|
||
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 层的延迟问题,但如果消息本身被服务器拒绝(验证失败、解密失败、屏蔽规则等),设备仍然会重发。
|
||
|
||
**下一步行动:**
|
||
|
||
1. ✅ 启用详细日志,查看是否有消息被拒绝
|
||
2. ✅ 检查 `discarded_packets` 表,确认拒绝原因
|
||
3. ✅ 抓包分析,确认是 TCP 重传还是应用层重发
|
||
4. ✅ 根据诊断结果选择对应的解决方案
|
||
|
||
**如果所有消息都被正常处理(没有被拒绝),但仍然重发,那么问题在设备端固件。**
|