半年前我接手了一个带实时推送需求的后台项目,第一反应就是:这不就是套WebSocket嘛,把原来的定时轮询改成一条长连接推送不就行了。结果真的把WebSocket接上线之后,接下来的两周我基本都在跟“连接突然断开”“浏览器页面崩溃”“线上告警半夜响”较劲。最后把我从坑里捞出来的,不是某个高大上的框架,而是一个专门负责扛住长连接接入层的小网关,我习惯把它叫做“脉脉”。这里先说明一下,它不是职场社交平台那个脉脉,是我们项目里给消息网关起的内部代号,干的是消息伴侣的活。
如果你正准备接触WebSocket,或者已经写过几条收发消息的Demo但还没上过生产环境,这篇文章值得看完。我会讲清楚WebSocket为什么能解决实时通信问题、为什么我不建议一上来就自建长连接服务、前端接入最常见的几个坑、一条报错信息的完整排查流程,以及Charles抓包、心跳保活、断线重连这些平时容易被忽略的稳定性细节。这里面的每一段,基本都来自我真实踩过的坑和复盘过的线上事故。
1. WebSocket解决的远不止是“省几个HTTP请求”
1.1 为什么轮询总是最省事的方案,却不是最省钱的方案
最早这个后台项目的需求其实很朴素:订单状态变了,希望正在看详情页的用户能比较快地看到新状态。第一版实现用的是定时轮询,前端每5秒请求一次接口,把最新的订单状态拉回来。从开发角度讲,这确实是最省事的方案,后端就是一个普通HTTP接口,前端一个setInterval完事,没有任何额外学习成本。
但上线之后问题马上暴露出来了。第一是延迟,5秒的轮询间隔意味着用户最坏情况下要等接近5秒才能看到状态变化,做客服工单的时候用户会反复追问“为什么还没刷新”。第二是浪费,我们把间隔缩短到2秒之后,后端接口的QPS直接翻了快三倍,高峰期一台实例的CPU从20%冲到60%多,而真正有数据变化的请求占比可能连10%都不到。换句话说,大量请求都是在空转。
这就涉及一个本质问题:HTTP协议是“请求-响应”模型,如果服务端没有新数据,客户端也必须定期问一次才知道“没有新数据”。这就像你每隔几秒就去信箱看一次有没有信,明明大部分时候信箱都是空的,但你不敢不去看,因为寄信人不会主动通知你。
1.2 HTTP、长轮询、SSE与WebSocket:四条实时路线的对比
当时我梳理了一下,能实现“服务端主动推送”的方案大概有四条路,它们的差异非常值得入门时搞清楚。
| 方案 | 连接模型 | 服务端能否主动推送 | 实时性 | 主要成本 |
|---|---|---|---|---|
| HTTP轮询 | 每次请求独立建立/关闭连接 | 不能 | 取决于轮询间隔 | 请求量大,大量空转 |
| 长轮询 | 请求挂起,有数据或超时才返回 | 半主动,返回后再重新建立连接 | 较好 | 连接占用高,实现复杂 |
| SSE | 一条HTTP长连接,服务端单向推送 | 能,单向 | 好 | 只支持服务端到客户端,双向场景不行 |
| WebSocket | HTTP升级为TCP长连接,全双工 | 能,双向 | 好 | 需要自己维护连接、心跳、重连 |
长轮询当时也考虑过,原理是客户端发一个请求过去,服务端先不给响应,等有新数据或者超过比如30秒再返回,客户端收到后立刻发起下一个请求。看起来比普通轮询省一点请求量,但服务端要维护大量挂起的请求,一旦实例重启或发生超时,这些请求全部要重新建立,处理起来并不轻松。而且它本质上仍然是单向的,做聊天、做实时状态上报这种需要客户端也频繁上行的场景会很别扭。
SSE则适合服务端单向广播的场景,比如股票行情、日志流,服务端往客户端推很舒服。但我们的需求里还有用户点击按钮后的即时交互,需要把操作也通过同一个通道传到服务端。这时候WebSocket的优势就很明显:它能把一条TCP连接升级成双向通道,客户端和服务端随时都能往对端发数据。
1.3 WebSocket协议里的最小常识:握手、数据帧和心跳
入门WebSocket一定要先理解它的握手过程。WebSocket并不是凭空冒出来的协议,它的连接通常是这样建立的:客户端先发一个普通HTTP请求,请求头里带上Upgrade: websocket,服务端同意后返回HTTP 101状态码,然后这条TCP连接就从HTTP协议切换成了WebSocket协议。
code复制GET /ws HTTP/1.1
Host: api.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xxxx
Sec-We
