做这件事之前,我也以为把消息推送从 HTTP 轮询换成 WebSocket,只是“换一个接口、改一下前端定时器”的事。真正动手之后才发现,协议一变,连接模型、服务端线程模型、网关配置、运维监控、甚至消息丢失时的排查思路全都要跟着重来一遍。这篇文章就围绕我实际改造一个 B 端项目消息推送模块的过程,把 HTTP 和 WebSocket 两种模式下那些“看不到但真实存在”的变化讲清楚,重点是换了之后到底改了什么、为什么改、以及哪些坑是上线后才冒出来的。
1. 改造前,HTTP轮询是怎么把消息“拖”成秒级延迟的
1.1 定时轮询:请求次数和实时性成了死对头
先说最传统的方案。客户端写一个 setInterval,每 3 秒或 5 秒向后端拉一次未读消息、工单状态、告警通知。这个方案最大的问题不在功能,而在资源消耗和实时性的矛盾。
假设一个企业后台有 5000 个在线用户,推送间隔定 5 秒,那么后端每秒钟至少要承受 1000 个 /message/poll 请求。这些请求绝大多数都不会产生真正的推送数据,但每一次都要走完一遍完整的 HTTP 流程:DNS 解析(如果没做长连接复用)、TCP 握手、TLS 握手、HTTP 请求/响应头、服务端查库、序列化 JSON、然后客户端拿到一个空数组。这里面的 CPU、内存、带宽、数据库压力,有一大半是“白干的”。
更难受的是实时性。如果后端某个告警在两次轮询之间到达,客户端最多要等 5 秒才能看到。把轮询间隔从 5 秒改成 1 秒,实时性变好了,但后端请求量直接变成每秒 5000 次,数据库连接池先扛不住。这就是 HTTP 轮询的死结:想快就得增加请求频率,增加请求频率就得烧服务器资源。
1.2 长轮询与 SSE:中间态的妥协
后来有团队改用长轮询(Long Polling),思想是客户端发一个请求,后端没有新消息就挂着不响应,等有消息了再返回,客户端收到后立刻发起下一个请求。这样真正做到了“消息一产生就返回”,实时性比定时轮询好很多。
长轮询的代价是服务端需要维护大量挂起的请求连接。每个挂起连接都会占用一个连接槽位,如果项目用的是 Tomcat 等传统 Servlet 容器,线程池很容易被打满。另一个问题是请求重放:客户端因为网络抖动重发请求时,后端可能同时存在两个相同业务的挂起请求,消息可能被推两次。
SSE(Server-Sent Events)比长轮询更“Web”一点,它是 HTTP 协议上实现的服务端单向推送,客户端用 EventSource 订阅,服务端可以持续吐出数据。SSE 实现简单、自动重连、还有 Last-Event-ID 做断点续传,但它有一个天然的约束:只能服务端往客户端推,客户端想发消息得另走 HTTP 接口。而且 SSE 在 HTTP/1.1 下同样占用一个连接,连接数上来了,代理和浏览器连接上限的限制也一样绕不过去。
1.3 我决定换掉它的真实触发点
我们项目当时用的是“短轮询 + 少量长轮询”的混合方式。真正让我下决心换 WebSocket 的,不是某一次性能压测,而是线上一次告警:某个时段大量用户同时在线,轮询请求量把网关和数据库连接池打到临界值,消息队列里的工单状态更新已经做完了,用户端却迟迟刷不出来。
当时我打开服务端日志一看,一个推送接口 1 秒内被调了几千次,但绝大多数响应都是空数组。这种感觉就像你为了等一封邮件,每 5 秒去小区门口看一次信箱,而 WebSocket 能做到的是:门口装一个门铃,信到了门铃才响。门铃方案需要拉一根专用线(长连接),但一定比所有人每 5 秒跑一趟门口要省力气。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. WebSocket不是“HTTP的升级版”,而是另一套连接模型
2.1 从三次握手到一次升级:连接的生命周期完全变了
很多人误以为 WebSocket 是 HTTP 加了一个新方法,其实不是。WebSocket 的连接建立还是借用了 HTTP 的握手过程:客户端发一个包含 Upgrade: websocket 头的请求,服务端返回 101 Switching Protocols,然后这条 TCP 连接就从 HTTP 协议切换成了 WebSocket 帧协议。
这个“切换”非常关键。握手完成后,客户端和服务端之间不再有“请求-响应”的语义,而是建立了一条全双工的长连接。客户端可以随时发帧给服务端,服务端也可以随时发帧给客户端,双方都不需要再重复握手、重复带一堆 Cookie、重复协商 TLS。对业务层来说,连接的生命周期从“一次一断”变成了“建立后长期持有”,而我们后面所有的改造点,都是围绕这个生命周期展开的。
连接模型的不同,直接影响了服务端的设计思维。HTTP 时代,服务器是无状态的,每个请求进来,处理完就丢,水平扩展非常容易。WebSocket 时代,服务端必须记住“这个连接属于哪个用户”,连接是有状态的,断线、迁移、重连都需要额外处理。这也是为什么很多项目把 HTTP 换成 WebSocket 之后,第一个炸的问题就是实例重启导致所有客户端集体掉线。
2.2 服务端主动推送:实时性不再依赖客户端“来问”
WebSocket 最直接的变化是消息的到达方式从“拉”变成“推”。以前后端产生一条工单状态流转消息,得等客户端下一次轮询来拿;现在后端业务代码里调用一下 session.getBasicRemote().sendText(payload),客户端只要连接还在,
