从 TCP 底层看实时通信:为什么 SSE 往往比 WebSocket 更香?

欢迎关注微信公众号: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 上的用户呢?这时候就需要一个消息中间件(如 RedisPub/SubKafka)来做桥梁。

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等文章, 可能有你想要了解的技能知识点哦~

相关推荐
全栈弄潮儿2 小时前
一周总结:AI 能帮你节省时间的 10 类任务
aigc·openai·ai编程
酒神dnspup3 小时前
服务器端口检测怎么做?用 DNSPup 核对开放端口、监听服务与暴露风险
网络·网络协议·http·ssl证书·http状态码·服务器端口
酒神dnspup3 小时前
HTTP状态码检测怎么做?用 DNSPup 排查 4xx、5xx、重定向与网站可用性
网络·网络协议·http·http状态码
架构精进之路3 小时前
Claude Code 深度使用指南:从"会用"到"用好"的7个进阶心法
后端·openai·ai编程
9i编程4 小时前
借助 Trae Work 学透 Multi-Agent 代码:从「抄出来了但没懂」到完整调通 v1.0.8
人工智能·openai·ai编程
酒神dnspup4 小时前
SSL证书检测完整指南:用 DNSPup 排查过期、域名不匹配与 TLS 握手失败
网络·网络协议·http·ssl证书·http状态码
脉动数据行情15 小时前
Python WebSocket 国内商品期货|螺纹钢 & 铁矿石实时行情监听
开发语言·python·websocket
深念Y6 小时前
局域网 HTTP 请求转发 502 问题排查记录
网络·网络协议·http·网络安全·代理·转发·中转
一条泥憨鱼6 小时前
【从0开始学习计算机网络】| DNS 污染、劫持与 CDN:你的网页是怎么被“掉包“的
学习·计算机网络·安全·http·https·dns