WebSocket 长连接不稳?一次完整的排查与修复之旅

一、现象

我的在线工具站(onltool.site)通过 WebSocket + 本地 Agent 实现了浏览器直连内网数据库。上线后频繁遇到以下问题:

现象 频率
用着用着突然显示"Agent 已断开连接" 每天数次
Agent 闪退,窗口瞬间消失 每次启动都有
点击断开 → 重新连接后,运行按钮永久卡在"加载中" 每次必现
网页提示"Invalid sessionId",Agent 已经不见了 偶发

二、逐层排查

架构是三层 WebSocket 链路:

复制代码
浏览器 ──WS──► Nginx ──WS──► Spring Boot ──WS──► 本地 Agent ──TCP──► MySQL/Redis

逐层检查保活和容错机制:

保活机制 容错机制
浏览器 WS
Nginx proxy_read_timeout=3600s ✓ -
Spring Boot 未配置
Agent WS ping_interval=30 ✓ 断开即退出
Agent → DB connect_timeout=5 ✓ 无 ping 检测

五个检查项,四个缺位。


三、问题根因

3.1 浏览器缺少心跳 → 被网络设备切断

复制代码
浏览器 ──静默 30s+──► 防火墙/NAT ──"这个连接死了"──► 发送 RST

TCP 连接如果长时间没有数据流动,中间的网络设备(防火墙、NAT 网关、运营商设备)会在 60-120 秒后主动切断。浏览器不主动发任何数据帧,导致频繁被断。

3.2 无重连机制 → 断了就死

onclose 触发后直接置 connected=false,不尝试恢复。用户只能手动重新连接。

3.3 loading 状态未清理 → 按钮永久卡死

复制代码
执行 SQL → loading=true → WebSocket 断开 → onclose 没重置 loading
→ 用户重连 → loading 仍然是 true → 运行按钮永远转圈

这是最隐蔽的 bug:disconnect()onclose 都漏掉了 loading=false

3.4 Agent 闪退 → 缺少 signal 模块

重写 Agent 主循环时用了 signal.signal(signal.SIGINT, ...) 但忘了 import signal,启动即抛 NameError 崩溃。sys.stdout 没 flush,终端窗口瞬间消失看不到报错。

3.5 Agent 断后 re-connect 生成新 sessionId → 浏览器连不上

Agent 断开后服务器立即删除 sessionId。Agent 重连获得新 ID,但浏览器还握着旧 ID,连上去提示 "Invalid sessionId"。


四、解决方案

4.1 浏览器心跳 + 自动重连

javascript 复制代码
// 每 15s 发一次心跳,防止中间设备切断
startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
            this.ws.send(JSON.stringify({ type: 'ping' }))
        }
    }, 15000)  // 30s → 15s,容错窗口更大
}

// 断开自动重连,指数退避
scheduleReconnect() {
    const delay = Math.min(1000 * Math.pow(2, this.reconnectDelay), 30000)
    this.reconnectDelay++
    this.reconnectTimer = setTimeout(() => {
        this.connectViaAgent()  // 带着同样的 sessionId 重连
    }, delay)
}

选择 15s 而不是 30s:即使丢一次心跳,实际间隔最大 30s,远低于防火墙 60s 超时线。

4.2 服务端 echo pong

java 复制代码
// BrowserWebSocketHandler.java
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
    String payload = message.getPayload();
    sessionManager.relayToAgent(sessionId, payload);  // 照常 relay

    if (payload.contains("\"ping\"")) {
        send(session, "{\"type\":\"pong\"}");  // 同时回 pong,双向保活
    }
}

relay 到 Agent 保持 Agent WS 活跃;回 pong 证明服务器健在。两层保护。

4.3 重置 loading 状态

javascript 复制代码
// disconnect() 中补充
this.connecting = false
this.loading = false

// ws.onclose 中补充
this.loading = false

断开时无论处于什么状态,全部清零。

4.4 Agent 保持 sessionId + 自动重连

python 复制代码
# 首次连接:服务器分配 sessionId
session_id = msg["sessionId"]

# 断开后自动重连,URL 带上旧 sessionId
def build_url():
    url = server_url
    if session_id:
        url += "?sessionId=" + session_id
    return url

# 重连循环,指数退避
while should_run:
    ws = WebSocketApp(build_url(), ...)
    ws.run_forever(ping_interval=0)
    delay = min(delay * 2, 30)
    time.sleep(delay)

4.5 服务器保留 sessionId,支持 Agent 断线重连

java 复制代码
// SessionManager.java
// Agent 断开:不删 pair,清空 agentSession,通知浏览器等待
public void removeAgent(WebSocketSession agentSession) {
    pair.agentSession = null;  // 保留 pair,不删除!
    send(pair.browserSession, "{\"type\":\"agentDisconnected\"}");
}

// Agent 重连:复用原 pair
public boolean reconnectAgent(String sessionId, WebSocketSession agentSession) {
    pair.agentSession = agentSession;
    send(pair.browserSession, "{\"type\":\"agentReconnected\"}");
}

4.6 Agent 查询前 ping 数据库

python 复制代码
def query_mysql(ws, msg):
    try:
        db.ping(reconnect=True)  # 先 ping,死了自动重连
    except Exception:
        return send_error(ws, "MySQL connection lost, please reconnect")
    # ... 正常查询

def query_redis(ws, msg):
    try:
        db.ping()  # 先 ping,死了报错
    except Exception:
        return send_error(ws, "Redis connection lost, please reconnect")
    # ... 正常查询

4.7 全局异常截获,避免闪退

python 复制代码
if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        print(f"\nFATAL: {e}", flush=True)
        print("\nPress Enter to exit...", end="", flush=True)
        input()

五、最终架构

复制代码
浏览器 ──ping/15s──► 服务器 ──relay ping──► Agent
   ▲                    │                       │
   └── pong ────────────┘                       │
                                                │
                                          db.ping()  ──► MySQL/Redis
                                          查询前检测
                                                
断开恢复:
Agent 断 → 服务器保留 sessionId → Agent 带旧 ID 重连 → 服务器复用 pair
浏览器断 → 15s 内未 detect → auto-reconnect → 重发 connect

六、总结

WebSocket 长连接在生产环境中有三个必须处理的问题:

  1. 保活:浏览器和 Agent 两端都必须有心跳,间隔不超过防火墙超时的一半
  2. 重连:断连是常态,自动恢复才是关键,前端退避重连 + 后端复用 sessionId
  3. 状态清理:任何断开路径都必须重置 loading/pending 等临时状态,否则 UI 永久卡死

这些问题在开发环境难以复现,因为 localhost 没有防火墙、NAT、运营商设备这些中间节点。部署到公网后立刻暴露。


项目在线地址:https://onltool.site

相关推荐
发量惊人的中年网工1 小时前
AI服务器托管怎么选机房?GPU集群对机柜、电力和网络的硬要求
服务器·网络·人工智能
玖玥拾1 小时前
Unity 3D 笔记(十五)Unity/C# Socket 网络笔记4
服务器·网络·unity·c#
JackieDYH1 小时前
WebSocket-不同平台封装-Hooks和使用案例
网络·websocket·网络协议
andxe12 小时前
安科士 AndXe 技术博客:400G QSFP112 SR4 光模块|AI 算力与超算短距互联最优方案
网络·人工智能·光模块·光通信
shiyi.十一13 小时前
第2章:应用层 — 知识要点与架构
网络·计算机网络·架构
DFT计算杂谈14 小时前
无 Root 权限在 Tesla K80 零门槛部署 DeepSeek 大模型
linux·服务器·网络·数据库·机器学习
水境传感 李兆栋14 小时前
GNSS 位移监测站 :毫米级感知,筑牢安全监测防线
网络
中微极客16 小时前
2026主流AI Agent框架技术选型与性能对比
运维·网络·人工智能
猿的天空20 小时前
机器人双手迎来全栈训练系统:灵初智能EgoSteer让灵巧手无所不能
网络·人工智能·计算机·ai·程序员·机器人·编程