创见博客
普通 HTTP、SSE 与 WebSocket:通信模型、代码与选型
七崽爱吃小饼干2026/08/04阅读 0

先看三个几乎同时发生的动作:

text
普通 HTTP:客户端发起请求 → 服务端返回结果 → 本轮通信结束
SSE:      客户端发起订阅 → 服务端不断推送事件 → 客户端关闭连接
WebSocket:双方完成握手 → 客户端和服务端都能随时发送消息

它们都能把服务端的数据送到浏览器,但解决的不是同一个问题。

普通 HTTP 适合“我问一次,你答一次”;SSE 适合“我订阅,你持续告诉我”;WebSocket 适合“连接建立后,我们随时互相说话”。如果只记住“短连接、长连接”这几个词,很容易在真正做技术选型时选错。

下面通过一个可以实际操作的 Demo,拆开看三者的通信过程。

HTTP、SSE 与 WebSocket 通信方式演示

先纠正一个常见误区:HTTP 不等于每次都重新建立 TCP

很多对比文章会把普通 HTTP 直接写成“短连接”,把 SSE 和 WebSocket 写成“长连接”。这个说法便于入门,但并不准确。

现代 HTTP 通常会复用底层连接。HTTP/1.1 默认支持 Keep-Alive,HTTP/2 还可以在同一条连接上并发传输多个请求。因此,完成一次 fetch 并不意味着浏览器一定马上断开 TCP,也不意味着下一次请求一定要重新进行三次握手。

真正应该比较的是应用层的通信模型:

  • 普通 HTTP 中,一个请求对应一个响应。服务端不能脱离请求凭空返回下一条业务消息。
  • SSE 中,一个 HTTP 响应可以一直不结束,服务端在响应体中持续写入事件。
  • WebSocket 握手完成后,连接进入独立的消息协议,双方都能主动发送消息。

所以,“一次请求一次响应”“服务端单向事件流”“全双工消息通道”,比“短连接和长连接”更接近三者的本质。

普通 HTTP:客户端问一次,服务端答一次

点击 Demo 中的“发送请求”,浏览器会发出一个普通 POST 请求:

ts
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);

服务端读取参数、执行业务逻辑,然后返回结果:

ts
export async function POST(request: Request) {
  const body = await request.json();

  return Response.json({
    message: `服务端处理完成:${body.message}`,
    time: new Date().toISOString(),
  });
}

这次响应完成后,这一轮应用层通信就结束了。即使底层连接被浏览器保留并复用,服务端也不能在 10 秒后通过这个已经结束的响应再追加一条“用户状态发生变化”。

如果页面需要不断获得最新数据,最直观的办法是轮询:

ts
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,不断向响应体写入事件:

text
id: 1
event: update
data: {"message":"服务端推送事件 #1"}

id: 2
event: update
data: {"message":"服务端推送事件 #2"}

每个事件由一个空行分隔。浏览器收到一段就处理一段,不需要等整个响应结束。

前端可以使用原生 EventSource:

ts
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();

服务端的核心则是创建一个持续输出的流:

ts
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 对话中,用户只提交一次问题,之后主要是服务端持续返回生成结果:

text
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 消息通道。

浏览器端代码很直接:

ts
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 请求,也可以随时把其他用户的操作广播过来。

聊天室就是典型例子:

text
客户端 → 服务端:我发送了一条消息
服务端 → 客户端:张三发送了一条消息
客户端 → 服务端:我正在输入
服务端 → 客户端:李四已读

多人文档协作、在线游戏、实时音视频信令也有类似特点:上下行消息都很频繁,而且任何一方都可能成为下一条消息的发起者。此时如果把客户端上行拆成 HTTP、服务端下行拆成 SSE,虽然也能实现,但协议和状态管理会变得别扭,WebSocket 更自然。

WebSocket 支持文本和二进制帧,消息头开销也比较小。不过,得到全双工能力的同时,也要承担更多工程工作:

  • 实现心跳,及时发现半开连接。
  • 实现断线重连和退避策略。
  • 设计消息 ID、确认机制与重复消息处理。
  • 在多实例部署时处理连接归属和跨节点广播。
  • 配置网关的 Upgrade、空闲超时和最大连接数。
  • 发布或扩容时妥善迁移、关闭已有连接。

浏览器原生 WebSocket 不会像 EventSource 一样自动重连。连接断开后是否补发消息、从哪里恢复,都需要业务协议自己定义。

把三者放在一张表里

对比项普通 HTTPSSEWebSocket
通信模型一次请求,一次响应一个请求,持续响应持续的消息通道
数据方向客户端发起,服务端响应服务端单向推送客户端与服务端双向发送
基础协议HTTPHTTPWebSocket(先通过 HTTP 握手)
浏览器 APIfetchEventSourceWebSocket
数据类型文本、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 解决的,就不维护长连接;只需要服务端推送的,就不急着上全双工。

协议越贴近真实的业务数据流,实现通常越简单,系统也越稳定。

评论
0/100