先看三个几乎同时发生的动作:
普通 HTTP:客户端发起请求 → 服务端返回结果 → 本轮通信结束
SSE: 客户端发起订阅 → 服务端不断推送事件 → 客户端关闭连接
WebSocket:双方完成握手 → 客户端和服务端都能随时发送消息
它们都能把服务端的数据送到浏览器,但解决的不是同一个问题。
普通 HTTP 适合“我问一次,你答一次”;SSE 适合“我订阅,你持续告诉我”;WebSocket 适合“连接建立后,我们随时互相说话”。如果只记住“短连接、长连接”这几个词,很容易在真正做技术选型时选错。
下面通过一个可以实际操作的 Demo,拆开看三者的通信过程。

先纠正一个常见误区:HTTP 不等于每次都重新建立 TCP
很多对比文章会把普通 HTTP 直接写成“短连接”,把 SSE 和 WebSocket 写成“长连接”。这个说法便于入门,但并不准确。
现代 HTTP 通常会复用底层连接。HTTP/1.1 默认支持 Keep-Alive,HTTP/2 还可以在同一条连接上并发传输多个请求。因此,完成一次 fetch 并不意味着浏览器一定马上断开 TCP,也不意味着下一次请求一定要重新进行三次握手。
真正应该比较的是应用层的通信模型:
- 普通 HTTP 中,一个请求对应一个响应。服务端不能脱离请求凭空返回下一条业务消息。
- SSE 中,一个 HTTP 响应可以一直不结束,服务端在响应体中持续写入事件。
- WebSocket 握手完成后,连接进入独立的消息协议,双方都能主动发送消息。
所以,“一次请求一次响应”“服务端单向事件流”“全双工消息通道”,比“短连接和长连接”更接近三者的本质。
普通 HTTP:客户端问一次,服务端答一次
点击 Demo 中的“发送请求”,浏览器会发出一个普通 POST 请求:
const response = await fetch('/api/demo/http', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: '查询第 42 号用户' }),
});
const data = await response.json();
console.log(data);
服务端读取参数、执行业务逻辑,然后返回结果:
export async function POST(request: Request) {
const body = await request.json();
return Response.json({
message: `服务端处理完成:${body.message}`,
time: new Date().toISOString(),
});
}
这次响应完成后,这一轮应用层通信就结束了。即使底层连接被浏览器保留并复用,服务端也不能在 10 秒后通过这个已经结束的响应再追加一条“用户状态发生变化”。
如果页面需要不断获得最新数据,最直观的办法是轮询:
setInterval(async () => {
const response = await fetch('/api/order/status');
const status = await response.json();
render(status);
}, 3000);
轮询的优点是简单、兼容性好,鉴权、缓存、网关和监控体系也都很成熟。缺点同样明显:数据没有变化时,请求仍然会照常发生;轮询间隔越长,消息延迟越明显,间隔越短,无效请求越多。
普通 HTTP 最适合这些场景:
- 查询详情、搜索列表。
- 创建、修改和删除数据。
- 表单提交、文件上传。
- 对实时性要求不高的状态刷新。
绝大多数 REST API 都应该先从普通 HTTP 开始,而不是因为页面里有“动态数据”就直接上 WebSocket。
SSE:仍然是 HTTP,但响应一直没有结束
点击“建立 SSE 连接”后,浏览器只发起一次 GET 请求。不同之处在于,服务端不会立刻结束响应,而是把响应类型设置为 text/event-stream,不断向响应体写入事件:
id: 1
event: update
data: {"message":"服务端推送事件 #1"}
id: 2
event: update
data: {"message":"服务端推送事件 #2"}
每个事件由一个空行分隔。浏览器收到一段就处理一段,不需要等整个响应结束。
前端可以使用原生 EventSource:
const source = new EventSource('/api/demo/sse');
source.addEventListener('update', (event) => {
const data = JSON.parse(event.data);
console.log('收到推送:', data);
});
source.onerror = () => {
console.log('连接中断,浏览器会尝试自动重连');
};
// 不再订阅时主动关闭
source.close();
服务端的核心则是创建一个持续输出的流:
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
setInterval(() => {
const data = JSON.stringify({ time: Date.now() });
controller.enqueue(encoder.encode(`event: update\ndata: ${data}\n\n`));
}, 1000);
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
SSE 的数据方向是服务端到客户端。客户端当然仍然可以向服务端发送数据,只不过要另外使用普通 HTTP 请求,而不是通过这条 EventSource 反向写入。
这恰好符合很多业务的真实形态。比如 AI 对话中,用户只提交一次问题,之后主要是服务端持续返回生成结果:
POST /messages 提交用户问题
GET /messages/stream 持续接收生成内容
通知、构建进度、实时日志、股票报价和后台任务状态,也通常以服务端推送为主。为了这些场景引入完整的双向协议,往往没有必要。
SSE 还有几个实用特性:
- 基于标准 HTTP,通常更容易穿过代理、网关和防火墙。
- 浏览器原生
EventSource支持断线重连。 - 服务端可以发送
id,客户端重连时通过Last-Event-ID继续接收。 - 消息格式简单,调试时可以直接在 Network 面板查看响应流。
它的限制也很明确:原生 SSE 主要传输 UTF-8 文本,不适合直接发送二进制;EventSource 的请求方法固定为 GET,自定义请求头也不如 fetch 灵活。需要 POST、Bearer Token 或更自由的流式请求时,可以使用 fetch 配合 ReadableStream,但自动重连和事件解析需要自己处理。
WebSocket:握手之后,双方都可以先开口
WebSocket 一开始也会经过 HTTP,但目的不是获取一个普通响应,而是协商升级协议。连接成功后,客户端和服务端进入 WebSocket 消息通道。
浏览器端代码很直接:
const socket = new WebSocket('wss://example.com/ws');
socket.onopen = () => {
socket.send('你好,WebSocket');
};
socket.onmessage = (event) => {
console.log('服务端消息:', event.data);
};
socket.onclose = () => {
console.log('连接已断开');
};
与 SSE 最大的不同是,socket.send() 可以直接通过同一条连接发送消息。与此同时,服务端不需要等待新的 HTTP 请求,也可以随时把其他用户的操作广播过来。
聊天室就是典型例子:
客户端 → 服务端:我发送了一条消息
服务端 → 客户端:张三发送了一条消息
客户端 → 服务端:我正在输入
服务端 → 客户端:李四已读
多人文档协作、在线游戏、实时音视频信令也有类似特点:上下行消息都很频繁,而且任何一方都可能成为下一条消息的发起者。此时如果把客户端上行拆成 HTTP、服务端下行拆成 SSE,虽然也能实现,但协议和状态管理会变得别扭,WebSocket 更自然。
WebSocket 支持文本和二进制帧,消息头开销也比较小。不过,得到全双工能力的同时,也要承担更多工程工作:
- 实现心跳,及时发现半开连接。
- 实现断线重连和退避策略。
- 设计消息 ID、确认机制与重复消息处理。
- 在多实例部署时处理连接归属和跨节点广播。
- 配置网关的 Upgrade、空闲超时和最大连接数。
- 发布或扩容时妥善迁移、关闭已有连接。
浏览器原生 WebSocket 不会像 EventSource 一样自动重连。连接断开后是否补发消息、从哪里恢复,都需要业务协议自己定义。
把三者放在一张表里
| 对比项 | 普通 HTTP | SSE | WebSocket |
|---|---|---|---|
| 通信模型 | 一次请求,一次响应 | 一个请求,持续响应 | 持续的消息通道 |
| 数据方向 | 客户端发起,服务端响应 | 服务端单向推送 | 客户端与服务端双向发送 |
| 基础协议 | HTTP | HTTP | WebSocket(先通过 HTTP 握手) |
| 浏览器 API | fetch | EventSource | WebSocket |
| 数据类型 | 文本、JSON、二进制等 | UTF-8 文本事件 | 文本与二进制 |
| 自动重连 | 通常自行实现 | EventSource 原生支持 | 自行实现 |
| 服务端状态 | 通常较少 | 维护订阅连接 | 维护双向会话连接 |
| 典型场景 | CRUD、查询、提交 | AI 流式输出、通知、日志 | 聊天、协作、游戏 |
这里还要注意一个边界:普通 HTTP 并不只能一次性返回完整 JSON。fetch 同样可以读取流式响应,SSE 本身也是 HTTP Streaming 的一种标准格式。因此,“是否流式”不是 HTTP 与 SSE、WebSocket 的绝对分界线。
真正的区别是,SSE 规定了服务端事件流的格式、重连行为和浏览器 API;WebSocket 则提供了独立的双向消息协议。
实际项目应该怎么选
可以按三个问题依次判断。
1. 服务端是否需要主动推送
如果不需要,使用普通 HTTP。不要为了“以后可能实时”提前维护一套长连接系统。
如果只是偶尔刷新,而且允许几秒延迟,普通 HTTP 加轮询通常已经足够。简单方案更容易缓存、监控、重试和扩容。
2. 客户端是否也需要高频发送
如果主要是服务端持续向客户端发送文本,优先考虑 SSE。例如 AI 输出、任务进度和通知流。
如果客户端同样需要频繁发送,而且双方都可能随时产生消息,再考虑 WebSocket。例如聊天消息、光标位置、游戏操作和协同编辑指令。
3. 业务能否承担连接状态
长连接不是“建立一次就永远不断”。移动网络切换、浏览器休眠、代理超时、服务发布和实例扩容都会让连接中断。
无论 SSE 还是 WebSocket,生产环境都需要回答这些问题:
- 连接断开后何时重连?
- 重连后如何恢复错过的数据?
- 同一条消息重复到达怎么办?
- 用户权限变化后如何终止旧连接?
- 服务端实例重启时如何优雅关闭连接?
如果业务不需要实时能力,普通 HTTP 能让这些问题直接消失。如果只需要单向推送,SSE 的标准重连机制可以减少一部分工作。只有确实需要双向实时通信时,WebSocket 带来的复杂度才值得。
最后记住三句话
普通 HTTP 是“请求一次,响应一次”。
SSE 是“订阅一次,服务端持续推送”。
WebSocket 是“建立通道,双方随时通信”。
选型时不要先问哪一种技术更高级,而要先看消息究竟从哪里产生、向哪个方向流动、频率有多高。能用普通 HTTP 解决的,就不维护长连接;只需要服务端推送的,就不急着上全双工。
协议越贴近真实的业务数据流,实现通常越简单,系统也越稳定。