三种方式
| 方式 | 机制 | 缺点 |
|---|---|---|
| 短轮询 | 定时发请求问有没有新消息 | 空跑请求多、延迟高 |
| 长轮询 | 请求挂起直到有数据才返回 | 仍每次重建、服务端要hold连接 |
| WebSocket | 一次握手后全双工长连 | 连接需维护、有状态 |
怎么选
- 低频、可接受延迟:短轮询最简单;
- 准实时、单向推送:长轮询够用,且对代理友好;
- 高频双向、聊天 / 行情:WebSocket 最合适。
WebSocket 要注意
- 心跳:定时发 ping,否则中间代理可能以空闲断开;
- 断线重连:网络抖动后要指数退避重连,避免打爆服务端;
- 横向扩展:多实例时要靠消息总线把消息广播到持有该连接的节点。
实战案例:三个“连上了却不稳”
- 几分钟后集体掉线:Nginx 默认
proxy_read_timeout为 60 秒,空闲长连被断开。修复:调大超时,并让客户端定时心跳。 - 重连风暴打垮服务:网络恢复后所有客户端同时重连。修复:指数退避加随机抖动,服务端限制单 IP 建连速率。
- 多实例消息发丢:用户连在 A 实例,消息却由 B 实例产生,B 上并没有那条连接。修复:用 Redis Pub/Sub 或消息总线广播,或按用户做一致性路由。
常见问题(FAQ)
WebSocket 能带鉴权头吗?浏览器原生 WebSocket 不支持自定义请求头,常见做法是在握手 URL 上带一次性短期令牌,或用 Cookie 校验。断线期间的消息会补发吗?不会,长连不保证投递;需要消息序号加历史拉取来补齐。能用 SSE 代替吗?只要服务端单向推送,SSE 更简单,自带重连且走普通 HTTP。WebSocket 一定比轮询省资源吗?只有在连接稳定、消息频繁时才省;低频场景维持长连反而更贵。
长连接的容量估算
WebSocket 的瓶颈往往不在带宽,而在连接数与内存:
- 每连接内存:单连接常占用几十到上百 KB(含缓冲区与运行时开销),一进程数万连接需要按此估算内存上限;
- 文件描述符:每个连接占一个 fd,需要同步调高进程与系统的
ulimit,否则会在达到上限时报错; - 心跳放大:若每条连接每 30 秒发一次心跳,10 万连接意味着每秒数千条心跳消息,需要评估消息处理与广播开销;
- 有状态带来的运维成本:滚动发布时必须优雅摘流并让客户端重连,否则会引发重连风暴;扩缩容要考虑连接迁移;
- 下行广播:一次广播给所有连接会瞬间放大出网流量,热门场景应按订阅分组只推给相关连接。
因此常见做法是:只有在消息频率高、双向交互明确的场景才用长连接;低频通知优先用 SSE 或推送服务,把连接维护成本交给更擅长的系统。
客户端侧的实现要点
- 重连要带状态恢复:重连成功只是第一步,还需要按消息序号或时间戳补齐断线期间缺失的内容,否则用户会看到不连续的数据。
- 区分“未连接”与“无数据”:界面应能提示当前连接状态,避免把网络中断显示成“暂无数据”,让用户做出错误判断。
- 页面切换与息屏要处理:移动端切到后台后连接可能被系统回收,回到前台应主动检查并重建连接,而不是依赖底层自动恢复。
- 限制消息体积:单条消息过大时会阻塞其它消息的处理,应把大对象改为引用加按需拉取,或分片传输。
- 发送要排队与限流:客户端在弱网下可能堆积发送请求,应有发送队列与上限,避免恢复连接后瞬间灌入大量消息。
长连接的复杂性大部分在客户端的状态管理上,把连接状态、数据一致性与用户体验三者明确区分开,实现会清晰很多。