fix(remotedevice): 心跳 pong 忘了 Flush —— 修「设备通道每 60 秒掉线重连」

真因(实测定位):服务端 writePong 只调 writeFrameHeader,**不 Flush**。
pong 只有两个字节,且设备空闲时没有任何别的写会顺带把 bufio 缓冲刷出去 ——
于是 pong 永远留在服务端缓冲里。

链路:客户端每 30s 发一个 ping(pingLoop)→ 服务端算出 pong 却没发出 →
客户端的读循环设的是「2 倍 ping 间隔」读超时(默认 60s)→ 每 60 秒准点
i/o timeout → 桥断开 → 3s 后重连 → 服务端 markOffline 注销 outputch,
重连后再注册。

生产日志就是这个指纹(online :20 → offline 下一分钟 :20 → 重连 :23,
连续数小时无一次例外);面板上表现为设备通道/工具凭空消失又出现,
/devices 列表跟着闪。

改法:writePong 复用 writeFrame(它 Flush)。另把客户端读循环退出时的
静默 return 改成带错误与 opcode 的日志 —— 此前断线真因在设备侧完全不可见,
只能靠对端日志倒推,正是这次排查一开始卡住的地方。

回归用例 TestWSPingGetsPongWhileIdle:只发一个 ping,随后什么都不发,
要求 2s 内必须收到 pong。**反向验证过**:把修复改回 writeFrameHeader,
用例即以 `read tcp ...: i/o timeout` 失败(与生产症状一致)。
This commit is contained in:
JianFeeeee
2026-09-14 11:15:21 +08:00
parent 8756f8d77f
commit 75f377fd4d
3 changed files with 66 additions and 2 deletions

View File

@ -411,7 +411,12 @@ func (b *Bridge) readLoop() {
_ = ws.writePong()
continue
}
// 超时或其他错误,退出
// 超时或其他错误,退出
//
// **必须记日志**:此前这里静默 return设备断线的真因读超时 / 对端
// 关闭 / 帧错)在设备侧完全不可见,只能靠对端日志倒推。
// 2 倍 ping 间隔内的读超时通常是“心跳没人回”——查服务端 writePong 是否真发出。
log.Printf("[devicebridge] read loop exit (opcode=%#x, close=%v): %v", opcode, isClose, err)
return
}
if isClose {