轮询、SSE 与 WebSocket
轮询、SSE 与 WebSocket
页面第一次加载数据时,普通 HTTP 请求就够了。麻烦的是后续变化:任务完成了、订单状态更新了、聊天消息到了,页面怎么知道?
轮询、SSE 和 WebSocket 经常被放在一起比较,但它们并不是简单的低配、中配和高配。区别主要在两件事:谁主动发起通信,以及连接建立后谁能发送数据。
| 方式 | 谁主动 | 数据方向 | 连接方式 |
|---|---|---|---|
| 短轮询 | 客户端 | 服务端响应客户端 | 定期发起普通 HTTP 请求 |
| 长轮询 | 客户端 | 服务端响应客户端 | 服务端暂缓响应,完成后客户端再次请求 |
| SSE | 客户端建立连接 | 服务端到客户端 | 持续的 HTTP 响应流 |
| WebSocket | 客户端建立连接 | 双向 | 握手后保持消息通道 |
轮询并不一定落后
HTTP 本来就是请求和响应的模型。短轮询只是在固定时间后重新查询一次,比如每五秒读取任务状态。
它的好处是省事。原来的接口、鉴权、日志和缓存都能继续用,也不用维护额外连接。状态很少变化,或者晚几秒显示没有关系时,轮询通常已经足够。
问题在于间隔不好定。太慢会让页面显得迟钝,太快又会产生很多没有新内容的请求。实现时也不适合无条件使用固定定时器;如果前一个请求还没结束,后一个请求又发出去,很容易出现请求重叠。更合适的做法是等本次请求结束,再安排下一次,并在页面进入后台、网络失败或遇到限流时降低频率。
ETag、If-None-Match 或资源版本可以减少重复传输,但请求本身的鉴权和服务端处理仍然存在。它们是在优化轮询,不是消除轮询成本。
长轮询仍然是轮询
长轮询也由客户端发起请求,只是服务端在没有新数据时先不返回。等到数据发生变化或等待超时,服务端才结束这次响应,客户端收到后马上发起下一次请求。
这样做比短轮询少了很多空响应,事件出现后也能较快到达客户端。不过每次响应结束,客户端仍然要重新请求,所以它没有改变 HTTP 请求和响应的基本关系。
断线时有一个容易忽略的边界:服务端可能已经取到新事件,但响应没有到达客户端。下一次请求最好带上游标或最后事件 ID,让服务端知道从哪里继续,而不是每次只返回“现在最新是什么”。
长轮询适合需要较低延迟、又暂时不能使用流式连接的环境。如果浏览器和部署链路已经能稳定支持 SSE,而且数据只需要从服务端流向页面,继续维护一轮又一轮请求的意义就不大了。
SSE 适合单向更新
SSE 通过 EventSource 接收服务端事件。客户端先发起请求,服务端返回 text/event-stream,随后在同一个响应中不断写入数据。
这个模型很适合构建进度、通知、日志和文本生成结果。页面如果偶尔需要提交操作,照常调用 HTTP 接口即可,不必因为少量上行请求换成 WebSocket。
SSE 还有一套简单的重连约定。事件可以带 id,浏览器重连时会通过 Last-Event-ID 告诉服务端最后收到了哪一条。不过这不代表消息会自动补回来。服务端还要保存一段事件历史,并支持从指定 ID 继续读取。HTML 标准 定义的是协商方式,事件怎么保存仍然属于应用设计。
它也有一些实际限制。SSE 只能由服务端向客户端发送文本事件;原生 EventSource 不能像 fetch 一样自由设置请求头。部署时还要确认反向代理不会缓冲响应,也不会太早清理空闲连接,否则服务端已经写出数据,浏览器却迟迟收不到。
WebSocket 只是提供双向通道
WebSocket 完成握手后,客户端和服务端都可以随时发送消息。聊天、多人协作、在线对战这类高频双向交互,更适合这个模型。
但连接能双向传输,不等于业务消息已经可靠。消息类型、版本、事件 ID、错误格式和确认方式都要由应用自己定义。连接断开后,重新鉴权、恢复订阅和补拉缺失消息也不会自动发生。
服务端还要多处理一层连接问题,包括心跳、空闲超时、连接数、负载均衡和节点间广播。滚动发布时,旧节点上的连接怎么关闭,客户端什么时候重连,也需要提前考虑。只是接收偶尔变化的任务进度时,这些成本通常不划算。
断线后从哪里继续
不管选择哪一种方式,连接正常时往往都不难。更值得先想清楚的是断线以后怎么办。
轮询可以记录资源版本或更新时间,避免重复处理同一状态。长轮询和 SSE 可以携带最后事件 ID。WebSocket 重连后则要重新订阅,并通过游标或普通 HTTP 接口补齐断线期间的数据。
需要严格顺序的事件还应该由服务端分配序号,不能拿客户端的接收时间排序。网络重试可能带来重复消息,因此消费端也要能识别同一个事件,而不是假设每条消息只会到一次。
选择时先看方向
如果页面只是偶尔查询状态,从短轮询开始最省事。请求量和延迟真的出现问题后,再考虑换方案。
数据主要从服务端流向浏览器时,SSE 通常比 WebSocket 更贴合需求。只有双方都要频繁主动发消息,WebSocket 的双向通道才值得承担额外的连接管理成本。长轮询则更像兼容方案,适合夹在普通轮询和流式连接之间。