# TCP、RST、掉线感知与 WebSocket 心跳与重连实践总结
## 1) TCP 连接的本质
- TCP 在不可靠的 IP 之上提供“面向连接、可靠、有序、字节流”的抽象。
- “连接”不是物理通道,而是两端内核维护的状态机同步(序列号/确认号、窗口、重传、拥塞控制等)。
- 若双方都不发数据,线上不会自动有“保活”数据;空闲≠断开。
## 2) 服务端如何得知客户端掉线
服务端不会“自动立刻知道”,除非发生可观测事件:
- 正常关闭:对端发送 FIN → 服务端读到 EOF(read 返回 0)。
- 异常终止(RST):收到 RST 立即报错关闭(常见 `ECONNRESET` / 写侧 `EPIPE`)。
- 发送超时:服务端写数据收不到 ACK → TCP 重传并指数退避 → 达到上限后才报错(分钟级,取决于内核参数)。
- TCP Keepalive(可选):空闲一段时间后发送探测包,多次无响应才判定失败(默认常为小时级,需主动调短)。
- 应用层心跳(推荐):协议内的 Ping/Pong 或自定义心跳,若超时则判定掉线(可做到秒级)。
- 中间设备反馈:少数 NAT/防火墙会返回 RST/ICMP;更多情况是静默丢包,需要靠上面机制发现。
## 3) 什么是 TCP RST
- RST(Reset)是 TCP 头标志,用于“立即/异常终止”连接或拒绝不存在的连接。
- 区别:
- FIN:有序关闭,数据已妥投。
- RST:异常终止,未交付的数据作废,应用侧立刻报错。
- 典型触发:连接到未监听端口、强制关闭(如 `SO_LINGER=0`)、有未读数据时关闭、报文落在不存在的四元组上等。
- 前提:只有当 OS/链路仍能发包时,RST 才能送达对端。
## 4) 进程崩溃 vs 物理断网/拔网线
- 进程崩溃但 OS/网络仍在:
- OS 回收 socket 并发送 FIN 或 RST → 服务端可“很快”感知。
- 机器断电/系统崩溃/拔网线/链路被剪:
- 客户端发不出 FIN/RST → 服务端无法立刻知道,只能等:
- 自己发送数据触发超时;
- Keepalive 探测失败;
- 应用心跳超时;
- 或偶发的中间设备错误反馈。
## 5) WebSocket 已建立、心跳 30s 的感知时延
- 拔网线或网络拥堵不会被“立刻”感知。
- 若“服务端发 ping、客户端自动回 pong”:
- 最佳时延 ≈ `T_pong_timeout`(断线后恰逢一次 ping 未回)。
- 最差时延 ≈ `heartbeat_interval + T_pong_timeout`(心跳间隔 30s 时,大约 30s + 几秒)。
- 若要求连续丢 N 次才判定:最差 ≈ `N × (heartbeat_interval + T_pong_timeout)`。
- 若仅客户端发 ping,服务端设置“读空闲超时”:
- 最差时延 ≈ 读空闲超时(未配置则可能拖到 TCP keepalive 或写入才发现)。
- 无心跳/无读写超时:常常是分钟到小时级才会清理。
## 6) 网络拥堵/Android 后台受限导致 Pong 发不出 → 服务端清理后,客户端如何重连
- 触发重连的信号:
- WebSocket 回调:`onFailure` / `onClosed` / `onClosing`。
- 主动探测:客户端周期性发送 ping/小数据,写失败即重连。
- 系统事件:网络从不可用→可用(`ConnectivityManager`)立即重连;APP 回到前台立即重连。
- 应用层超时:连接/首包/读空闲超时触发重连。
- 重连节流:指数退避 + 全随机抖动(如 1s、2s、4s…上限 30–60s;`delay = random(0, backoff)`)。
- 一旦出现“积极信号”(网络可用、回前台、近期连接稳定)→ 重置退避。
- 会话恢复与一致性:
- 重新认证(必要时先刷新 token)。
- 订阅恢复(topics/rooms)。
- 游标恢复(`lastMessageId`/offset/resume-token)以续传,服务端需支持。
- 幂等/去重(上行命令加 `idempotency-key`)。
- 新连接抢占旧连接(按 userId/deviceId),避免双活。
## 7) Android 端注意事项
- Doze/后台限制会延迟定时器与网络 → 后台长连极不稳定。
- 需要长连:使用 Foreground Service(常驻通知)降低系统限制概率。
- 能不用长连:改“服务器推送唤醒再连”(如 FCM),更省电、兼容性更好。
- 监听网络变化:`ConnectivityManager.registerDefaultNetworkCallback()`,网络可用即重连、重置退避。
- 客户端库:
- OkHttp WebSocket 可设 `pingInterval`(客户端也能发 ping),但断网仍送达不了 → 仍需重连策略。
- 超时策略:
- `connectTimeout` 5–10s;
- WebSocket 通常 `readTimeout=0`,用应用层“读空闲超时”代替(例如 40–60s)。
## 8) 参数与运维建议
- WebSocket 心跳(移动端友好):
- 服务端发 ping:`interval = 20–30s`;
- `pong_timeout = 5–10s`;
- 允许丢 `1–2` 次(总检测窗口约 `25–70s`)后清理。
- NAT/负载均衡:
- 心跳间隔小于它们的空闲回收阈值(常见 30–120s)。
- TCP Keepalive(兜底用):
- 开启 `SO_KEEPALIVE` 并调短系统参数(分钟级)以清理半开连接,但不要依赖它做秒级检测。
- 观测与告警:
- 心跳丢失率、平均重连次数、401/403/429、NAT/LB 空闲回收分布、连接寿命与稳定性。
## 9) 一页清单(Checklist)
- 服务端
- [ ] 定义 ping 间隔与 pong 超时(20–30s / 5–10s),允许丢 1–2 次。
- [ ] 新连接按 userId/deviceId 抢占旧连接。
- [ ] 提供会话恢复能力(resume-token/lastId)。
- [ ] 配置/评估 NAT、LB 的空闲超时;对齐心跳频率。
- [ ] 指标与告警就绪。
- 客户端(Android)
- [ ] 连接/读空闲超时与服务端心跳对齐。
- [ ] 指数退避 + 抖动;网络/前台事件立即重试。
- [ ] Foreground Service 或“推送唤醒再连”策略。
- [ ] 认证刷新、订阅/游标恢复、上行幂等。
## 10) 常见问答(精简)
- Q:拔网线后服务端能立刻知道吗?
A:不能。需等到下一次心跳 + pong 超时,或写入超时 / Keepalive 失败,才会判定断开。
- Q:RST 是什么?
A:TCP 的“重置”标志,表示异常/立即终止。收到即报错关闭,与 FIN 的“有序关闭”不同。
- Q:只有心跳无业务,30s 间隔如何估算最坏感知时间?
A:最坏 ≈ `30s + pong_timeout`(若允许丢 N 次则乘以 N)。
- Q:Android 后台导致发不出 Pong 怎么办?
A:服务端按超时清理;客户端靠事件/超时自知断线,执行“指数退避 + 会话恢复”的重连流程;必要时改为“推送唤醒再连”。
## 11) 推荐参数示例(可按需微调)
| 场景 | 建议值 |
|---|---|
| WebSocket ping 间隔(服务端) | 20–30s |
| WebSocket pong 超时 | 5–10s |
| 允许丢失次数 | 1–2 次 |
| 客户端 connectTimeout | 5–10s |
| 客户端读空闲超时(应用层) | 40–60s |
| 重连退避 | 1s 起步,×2 指数退避,抖动,最大 30–60s |
| TCP Keepalive(兜底) | 分钟级(按运维策略调优) |
## 12) 重连逻辑(伪码)
```pseudo
onDisconnected():
schedule = initialBackoff
while not connected:
if networkAvailable() or appInForeground():
wait random(0, schedule)
token = getTokenOrRefresh()
connectWS(token)
if connected:
restoreSession() // re-auth, re-subscribe, resume from lastId
schedule = initialBackoff
else:
schedule = min(schedule * 2, maxBackoff)
else:
wait until network/app-foreground event
```
---
一句话总结:
TCP 连接是状态机抽象,断网不会“立刻告知”;WebSocket 的掉线检测取决于心跳与超时设置。要在秒级感知并稳定恢复,工程上应以“服务端发 ping + 明确 pong 超时”为主,客户端配合“事件驱动的指数退避重连与会话恢复”,并结合移动端(Android)电量/后台限制采用前台服务或“推送唤醒再连”的策略。