欢迎关注微信公众号:FSA全栈行动 👋
一、核心矛盾:如何实现"服务器主动推数据"
其实 HTTP 的核心逻辑一直很单纯:客户端提问,服务端回答。浏览器发个请求,服务器回个响应,这对话就结束了。
但这里有个很有意思的问题:如果服务器手里有新消息,但客户端压根没问过,该怎么办?
为了解决这个"服务器主动权"的问题,业界演化出了几种方案:Polling(轮询)、Long Polling(长轮询)、SSE(Server-Sent Events)和 WebSocket。很多人一听说要搞实时功能,第一反应就是上 WebSocket,觉得这才是"实时通信"的代名词。但我折腾了这么久发现,在很多场景下,直接上 WebSocket 其实是过度设计了。
与其直接背诵这些概念,不如从 TCP 层的逻辑开始,一层层往上剥,你会发现 SSE 其实是顺理成章的必然选择。
二、从轮询到 SSE 的演进之路
在聊方案之前,我得先理清一个逻辑。HTTP 本身不是独立的传输层,它其实是盖在 TCP 之上的。TCP 就像一根管道,而 HTTP 的请求和响应,只是在这根管道里传递的消息。
TCP 建立连接需要经过"三次握手",一旦管道打通,对话就开始了。在 HTTP/1.1 时代,我们有了 Keep-Alive,意思是这根管道可以用好几次,不需要每次请求都重新握手。这里有个关键点:HTTP 响应结束了,并不代表 TCP 连接必须断开。
基于这个逻辑,我们来看看各种方案是怎么演进的。
1、Polling:最原始的轮询
最笨的办法就是"反复确认"。每隔几秒钟,客户端就发个请求问一句:"有新消息吗?"
JavaScript
setInterval(async () => {
const res = await fetch('/orders/1001/status');
const data = await res.json();
if (data.status !== lastKnownStatus) {
lastKnownStatus = data.status;
render(data.status);
}
}, 5000);
这种方式虽然好实现,逻辑也简单,但代价非常大。无论有没有新消息,每 5 秒钟就要跑一次完整的 HTTP 交换,这意味着服务器要处理大量的无效请求、查询数据库、序列化数据。如果你的用户有几千个,服务器的压力会非常大,而且你还得在"更新及时性"和"服务器负载"之间做痛苦的权衡。
2、Long Polling:稍微聪明一点的折腾
Long Polling 稍微高级一点。它不再是"问了没消息就立刻回",而是服务器先吊着这个请求不放,等到真有消息了再返回。客户端收到响应后,立刻再开一个新请求接着等。
JavaScript
async function longPoll() {
try {
const res = await fetch('/orders/1001/wait-for-update');
const data = await res.json();
render(data.status);
} catch (err) {
// 连接断了或超时了,很正常
} finally {
longPoll(); // 赶紧开下一个
}
}
longPoll();
这确实减少了无效请求,但它依然没能解决本质问题:请求结束了,你还得重新开。在两个请求的间隙,数据是有可能"漏掉"的(虽然概率很小),而且这种"频繁开启/关闭"的模式对服务器资源管理依然不够优雅。
3、SSE:把连接"挂"住
SSE 的思路非常直接:既然 HTTP 响应可以不结束,那我们干脆就让它一直连着,服务器想发消息就直接往里写。
SSE 本质上就是 HTTP 响应流(Streaming)。它不需要新协议,只需要一个 Content-Type: text/event-stream 的请求头。客户端通过 EventSource API 就能轻松玩转。
下面是一个最简单的 Express 实现示例:
JavaScript
import express from 'express';
const app = express();
app.get('/events', (req, res) => {
// 必须设置这些头,告诉浏览器这是个流
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.write(`event: connected`);
res.write(`data: {"message": "Connected"}`);
req.on('close', () => {
console.log('客户端断开连接');
});
});
app.listen(3000);
客户端也非常爽:
JavaScript
const source = new EventSource('/events');
source.addEventListener('connected', (event) => {
console.log(JSON.parse(event.data));
});
这里的灵魂在于 res.write() 而不是 res.end()。res.write() 会把数据推入现有的连接,而不会关闭它。
三、落地实战:连接管理与分布式架构
如果你只是写个 Demo,上面的代码足够了。但如果你要上线,你会面临两个非常现实的难题:如何管理这些连接?如果你的服务水平扩展了,消息怎么发给对应的用户?
1、连接管理与分布式分发
在生产环境下,你不能把 res 对象随便扔在外面。我通常会封装一个 ConnectionManager 类,用 Map 来维护用户和连接的关系。
JavaScript
class ConnectionManager {
constructor() {
this.connections = new Map();
}
add(userId, res) {
if (!this.connections.has(userId)) {
this.connections.set(userId, new Set());
}
this.connections.get(userId).add(res);
}
remove(userId, res) {
const userConnections = this.connections.get(userId);
if (!userConnections) return;
userConnections.delete(res);
if (userConnections.size === 0) {
this.connections.delete(userId);
}
}
sendToUser(userId, eventName, payload) {
const userConnections = this.connections.get(userId);
if (!userConnections) return;
for (const res of userConnections) {
res.write(`event: ${eventName}`);
res.write(`data: ${JSON.stringify(payload)}`);
}
}
}
export const connectionManager = new ConnectionManager();
注意: 一个用户可能有多个连接(比如同时开着网页和手机 App),所以我用 Set 来存。
当你开始做微服务架构时,问题就来了。Order Service 里的订单状态变了,它怎么通知到连接在 SSE Service 上的用户呢?这时候就需要一个消息中间件(如 Redis 的 Pub/Sub 或 Kafka)来做桥梁。
SSE Service 的角色其实就是一个"适配器":它订阅中间件的消息,然后通过 connectionManager 把消息推给对应的 res 对象。
2、SSE vs WebSocket:到底该选谁?
这是最关键的问题。很多人觉得"实时"="WebSocket",但其实这很武断。
我总结了一个简单的决策逻辑:
| 维度 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向(服务器 -> 客户端) | 双向(全双工) |
| 协议复杂度 | 极低(就是普通的 HTTP) | 较高(需要协议升级 Upgrade) |
| 基础设施兼容性 | 极好(防火墙、代理、负载均衡无压力) | 一般(有时需要特殊配置才能跑通代理) |
| 自动重连 | 原生自带 | 需要自己实现重连逻辑 |
| 认证方式 | 可以直接用 Cookie / Header |
通常需要手动处理握手时的 Token |
我的经验是:
-
如果你的业务是单向更新 (比如:订单状态流转、AI 回复内容的逐字输出、实时仪表盘、系统日志),闭眼选
SSE。它更轻量,更稳定,而且你不需要为了一个单向的需求去折腾复杂的WebSocket协议升级和重连逻辑。 -
只有当你真正需要频繁、双向交互 (比如:多人协作文档、高频实时竞技游戏、即时通讯聊天)时,再去考虑
WebSocket。
四、总结
从底层的 TCP 逻辑来看,SSE 并不是什么黑科技,它只是在充分利用 HTTP "连接不关闭"的特性。
在处理高并发和分布式场景时,大家要记住:本地内存存的是实实在在的 res 连接,而消息中间件传递的是业务事件。 这种"解耦"的思想,才是让你的系统能从 Demo 走向生产环境的关键。
如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有Android技术, 还有iOS, Python等文章, 可能有你想要了解的技能知识点哦~