跳到主要内容
预计阅读 54 分钟

浏览器篇 | SSE 与 WebSocket

前言

在传统的 HTTP 请求-响应模型中,通信永远是”客户端问,服务端答”。客户端不问,服务端就没法主动推送数据。但现代 Web 应用中,有大量需要”服务端主动推送”的场景:聊天消息、实时通知、股票行情、AI 流式输出。

为了解决这个问题,出现了几种不同的技术方案:轮询、长轮询、Server-Sent Events(SSE)、WebSocket。

面试中,面试官通常会问:

  • SSE 和 WebSocket 有什么区别?分别适用什么场景?
  • WebSocket 的握手过程是怎样的?为什么需要从 HTTP 升级?
  • 为什么 ChatGPT / AI 应用用 SSE 而不是 WebSocket?
  • 什么时候用轮询就够了?

本章我们就来把这些实时通信方案从原理到选型全部讲清楚。


诊断自测

Q1:SSE 和 WebSocket 最本质的区别是什么?

点击查看答案

通信方向不同。 SSE 是单向的——只能服务端向客户端推送数据,客户端不能通过 SSE 连接向服务端发送数据。WebSocket 是全双工的——客户端和服务端可以在同一个连接上双向发送数据。

此外,SSE 基于 HTTP 协议(本质是一个持久的 HTTP 响应),而 WebSocket 是独立于 HTTP 的协议(虽然握手阶段走 HTTP)。

Q2:为什么很多 AI 应用(如 ChatGPT)使用 SSE 而不是 WebSocket?

点击查看答案

因为 AI 流式输出的通信模式天然就是单向的:用户发送一个问题(普通 HTTP POST 请求),然后服务端持续推送生成的 token 直到完成。这完全符合 SSE 的设计——客户端用普通请求发送数据,服务端通过 SSE 推送流式响应。

相比 WebSocket,SSE 更轻量:基于 HTTP、原生支持自动重连、不需要额外的握手协议、能利用现有的 HTTP 基础设施(代理、负载均衡、CDN)。对于”请求-流式响应”这种模式,SSE 比 WebSocket 更合适。

Q3:WebSocket 连接是如何建立的?

点击查看答案

WebSocket 通过一次 HTTP Upgrade 握手建立连接:

  1. 客户端发送一个 HTTP GET 请求,带上 Upgrade: websocketConnection: Upgrade
  2. 服务端如果支持 WebSocket,返回 101 Switching Protocols 状态码
  3. 此后连接升级为 WebSocket 协议,双方可以通过这个连接双向发送数据帧

握手阶段走 HTTP,握手完成后就和 HTTP 没关系了。


一、HTTP 轮询的局限

在聊 SSE 和 WebSocket 之前,先看看最朴素的”实时”方案:轮询。

1.1 短轮询(Polling)

客户端定时向服务端发请求,询问”有新数据吗?”

// 每 3 秒请求一次
setInterval(async () => {
  const res = await fetch('/api/messages');
  const data = await res.json();
  if (data.length > 0) {
    renderMessages(data);
  }
}, 3000);

问题:

  • 延迟:新消息到达后,最多要等一个轮询间隔才能被获取
  • 浪费资源:大部分请求返回的都是”没有新数据”,白白消耗带宽和服务器资源
  • 频率难取舍:轮询太频繁 → 浪费资源;太不频繁 → 实时性差

1.2 长轮询(Long Polling)

客户端发请求后,服务端不立即返回——如果没有新数据就挂住连接,等有新数据了再返回。客户端收到响应后立即发起下一次请求。

async function longPoll() {
  try {
    const res = await fetch('/api/messages?wait=true');
    const data = await res.json();
    renderMessages(data);
  } catch (e) {
    // 出错后等一会儿再重试
    await new Promise(r => setTimeout(r, 3000));
  }
  // 立即开始下一次长轮询
  longPoll();
}

longPoll();

长轮询比短轮询好一些:实时性更高(有数据就立即返回),没有新数据时也不会浪费请求。但它仍然有问题:

  • 每次返回后都要重新建立 HTTP 连接(虽然 HTTP/1.1 keep-alive 能缓解)
  • 服务端需要维护大量挂起的连接
  • 在高并发场景下服务端压力大

轮询适合什么场景? 实时性要求不高(几秒的延迟可以接受)、数据更新频率低的场景。比如检查软件更新、邮箱新邮件数量等。


二、Server-Sent Events(SSE)

2.1 SSE 是什么

SSE(Server-Sent Events)是 HTML5 定义的一种服务端推送机制。它本质上是一个持久的 HTTP 连接,服务端通过这个连接不断地向客户端推送文本数据。

核心特点:

  • 单向通信:只能服务端 → 客户端
  • 基于 HTTP:使用普通的 HTTP 连接,Content-Type 为 text/event-stream
  • 自动重连:连接断开后,浏览器会自动重新连接
  • 文本协议:传输的是 UTF-8 文本(不支持二进制)

2.2 服务端实现

SSE 的服务端实现非常简单——本质就是一个不关闭的 HTTP 响应,持续向响应流中写入数据。

// Node.js + Express 示例
app.get('/events', (req, res) => {
  // 设置 SSE 响应头
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  // 定时推送数据
  const timer = setInterval(() => {
    const data = { time: new Date().toISOString(), value: Math.random() };
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  }, 1000);

  // 客户端断开时清理
  req.on('close', () => {
    clearInterval(timer);
  });
});

SSE 的数据格式

SSE 使用简单的文本协议,每条消息由若干字段组成,以两个换行符 \n\n 分隔:

data: 这是一条消息\n\n

data: 这是另一条消息\n
data: 它可以有多行\n\n

event: notification\n
data: {"title": "新消息", "body": "你有一条未读消息"}\n\n

id: 123\n
data: 带有 ID 的消息\n\n

retry: 5000\n\n

字段说明:

字段含义
data消息数据(必须)
event自定义事件类型(默认是 message
id消息 ID,用于断线重连时的续传
retry设置重连间隔(毫秒)

2.3 客户端实现

浏览器原生提供了 EventSource API 来接收 SSE:

const source = new EventSource('/events');

// 监听默认的 message 事件
source.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('收到消息:', data);
};

// 监听自定义事件
source.addEventListener('notification', (event) => {
  const data = JSON.parse(event.data);
  showNotification(data.title, data.body);
});

// 连接建立
source.onopen = () => {
  console.log('SSE 连接已建立');
};

// 错误处理
source.onerror = (event) => {
  if (source.readyState === EventSource.CLOSED) {
    console.log('SSE 连接已关闭');
  } else {
    console.log('SSE 连接出错,浏览器将自动重连');
  }
};

// 关闭连接
source.close();

2.4 自动重连和断线续传

SSE 的一大亮点是自动重连。如果连接因为网络问题断开,浏览器会自动重新连接(默认间隔约 3 秒,可通过 retry 字段调整)。

更厉害的是,重连时浏览器会自动在请求头中带上 Last-Event-ID,值为断开前收到的最后一个 id。服务端可以据此从断点继续推送,实现断线续传

// 服务端检查 Last-Event-ID
app.get('/events', (req, res) => {
  const lastId = req.headers['last-event-id'];

  if (lastId) {
    // 从断点继续推送
    const missedEvents = getEventsSince(lastId);
    missedEvents.forEach(event => {
      res.write(`id: ${event.id}\ndata: ${JSON.stringify(event.data)}\n\n`);
    });
  }

  // 继续推送新事件...
});

2.5 SSE 在 AI 流式输出中的应用

这是 SSE 最热门的应用场景之一。ChatGPT、Claude 等 AI 服务都使用 SSE 来实现流式输出——用户提问后,AI 生成的文字一个 token 一个 token 地推送到前端,让用户看到”打字机效果”。

// 客户端:使用 fetch + ReadableStream 处理 SSE
// (EventSource 不支持 POST 请求,所以 AI 场景通常用 fetch)
async function chat(message) {
  const response = await fetch('/api/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ message })
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value);
    // 解析 SSE 数据
    const lines = chunk.split('\n');
    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = line.slice(6);
        if (data === '[DONE]') return;
        const parsed = JSON.parse(data);
        appendToOutput(parsed.content);
      }
    }
  }
}
// 服务端:流式返回 AI 生成的内容
app.post('/api/chat', async (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');

  const stream = await aiModel.generateStream(req.body.message);

  for await (const token of stream) {
    res.write(`data: ${JSON.stringify({ content: token })}\n\n`);
  }

  res.write('data: [DONE]\n\n');
  res.end();
});

三、WebSocket

3.1 WebSocket 是什么

WebSocket 是一种独立的通信协议,提供全双工(Full-Duplex)通信通道。客户端和服务端建立连接后,双方可以随时互相发送数据,不需要等对方发完才能发。

核心特点:

  • 全双工通信:客户端和服务端可以同时发送数据
  • 独立协议:不是 HTTP,使用 ws://(或 wss://)协议
  • 低延迟:连接建立后,数据帧头部只有 2-14 字节,远小于 HTTP 头
  • 支持二进制:可以传输文本和二进制数据
  • 无自动重连:需要自己实现重连逻辑

3.2 握手过程

WebSocket 连接通过一次 HTTP Upgrade 握手建立:

客户端请求:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

服务端响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

握手过程:

  1. 客户端发送普通的 HTTP GET 请求,带上 Upgrade: websocket 表示请求升级协议
  2. Sec-WebSocket-Key 是客户端生成的随机 Base64 字符串
  3. 服务端将 Sec-WebSocket-Key 加上一个固定的 GUID,做 SHA-1 hash,再 Base64 编码,作为 Sec-WebSocket-Accept 返回
  4. 客户端验证 Sec-WebSocket-Accept 的值,确认服务端确实支持 WebSocket
  5. 返回 101 Switching Protocols 后,连接升级为 WebSocket,后续通信使用 WebSocket 帧协议

3.3 WebSocket 帧协议

握手完成后,数据通过**帧(Frame)**传输。每个帧的结构:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+-------------------------------+
|     Masking-key (0 or 4 bytes)                                |
+-------------------------------+-------------------------------+
|          Payload Data                                         |
+---------------------------------------------------------------+

关键字段:

  • FIN:是否为消息的最后一帧
  • opcode:帧类型(文本 0x1、二进制 0x2、关闭 0x8、Ping 0x9、Pong 0xA)
  • MASK:客户端发送的帧必须掩码(MASK=1),服务端发送的不用
  • Payload len:数据长度

WebSocket 帧的头部只有 2-14 字节,相比 HTTP 请求的几百字节头部,开销小了一个数量级。这是 WebSocket 在高频通信场景下性能优势的来源。

3.4 客户端 API

// 创建 WebSocket 连接
const ws = new WebSocket('wss://example.com/chat');

// 连接建立
ws.onopen = () => {
  console.log('WebSocket 连接已建立');
  ws.send('Hello Server!');
};

// 接收消息
ws.onmessage = (event) => {
  console.log('收到消息:', event.data);

  // 如果是二进制数据
  if (event.data instanceof Blob) {
    // 处理 Blob
  }
};

// 连接关闭
ws.onclose = (event) => {
  console.log(`连接关闭: code=${event.code}, reason=${event.reason}`);
};

// 错误处理
ws.onerror = (error) => {
  console.error('WebSocket 错误:', error);
};

// 发送数据
ws.send('文本消息');
ws.send(new Blob(['二进制数据']));
ws.send(new ArrayBuffer(8));

// 关闭连接
ws.close(1000, '正常关闭');

3.5 心跳机制

WebSocket 连接可能因为网络问题或代理超时而断开,但双方都不知道。为了检测连接是否仍然存活,通常需要实现**心跳(Heartbeat)**机制:

class WebSocketClient {
  constructor(url) {
    this.url = url;
    this.heartbeatInterval = 30000; // 30 秒
    this.reconnectDelay = 3000;
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('连接已建立');
      this.startHeartbeat();
    };

    this.ws.onmessage = (event) => {
      if (event.data === 'pong') return; // 心跳响应
      this.handleMessage(event.data);
    };

    this.ws.onclose = () => {
      this.stopHeartbeat();
      this.reconnect();
    };
  }

  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        this.ws.send('ping');
      }
    }, this.heartbeatInterval);
  }

  stopHeartbeat() {
    clearInterval(this.heartbeatTimer);
  }

  reconnect() {
    setTimeout(() => {
      console.log('正在重连...');
      this.connect();
    }, this.reconnectDelay);
  }

  handleMessage(data) {
    // 业务逻辑
  }
}

注意:WebSocket 协议本身定义了 Ping/Pong 帧(opcode 0x9/0xA),但浏览器的 WebSocket API 不暴露发送 Ping 帧的方法。所以应用层心跳通常用普通的文本消息实现。


四、SSE vs WebSocket 对比与选型

核心对比

特性SSEWebSocket
通信方向单向(服务端 → 客户端)全双工(双向)
协议HTTP独立协议 ws:// / wss://
数据格式文本(UTF-8)文本 + 二进制
自动重连内置支持需要自己实现
断线续传内置(Last-Event-ID)需要自己实现
浏览器支持所有现代浏览器(IE 不支持)所有现代浏览器
连接数限制HTTP/1.1 下每域名 6 个连接无限制
代理/防火墙兼容好(基于 HTTP)可能被阻断
头部开销普通 HTTP 头2-14 字节帧头
复杂度中等

什么时候用 SSE

  • 服务端向客户端的单向推送:实时通知、新闻推送、股票行情
  • AI 流式输出:ChatGPT 式的逐字输出
  • 简单的事件通知:用户操作触发的异步通知
  • 需要利用现有 HTTP 基础设施:CDN、负载均衡、代理等

什么时候用 WebSocket

  • 双向通信:聊天应用(用户也需要发消息)
  • 高频双向交互:在线游戏、协同编辑
  • 二进制数据传输:音视频流、文件传输
  • 超低延迟要求:WebSocket 帧头小,适合高频小数据包

什么时候轮询就够了

  • 数据更新频率低:每分钟检查一次邮件
  • 实时性要求不高:几秒的延迟可以接受
  • 实现简单优先:快速原型开发
  • 兼容性要求高:需要支持非常古老的浏览器

决策流程图

需要实时通信?

    否 → 普通 HTTP 请求足够
    │ 是

需要双向通信?

    否 → SSE(单向推送)
    │ 是

需要传输二进制?

    否 → SSE(客户端用普通 HTTP 发送) + 或 WebSocket
    │ 是

WebSocket

五、实际应用场景深入

5.1 聊天应用

聊天应用是 WebSocket 的经典使用场景,因为需要双向通信:用户发消息 + 接收消息。

// 客户端
const ws = new WebSocket('wss://chat.example.com');

// 发送消息
function sendMessage(content) {
  ws.send(JSON.stringify({
    type: 'message',
    content,
    timestamp: Date.now()
  }));
}

// 接收消息
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  switch (msg.type) {
    case 'message':
      renderMessage(msg);
      break;
    case 'typing':
      showTypingIndicator(msg.user);
      break;
    case 'online':
      updateOnlineList(msg.users);
      break;
  }
};

5.2 实时通知

通知推送只需要服务端 → 客户端的单向通信,SSE 更合适:

// 客户端
const source = new EventSource('/notifications');

source.addEventListener('new-order', (event) => {
  const order = JSON.parse(event.data);
  showNotification(`新订单:${order.id}`);
});

source.addEventListener('system-alert', (event) => {
  const alert = JSON.parse(event.data);
  showAlert(alert.message);
});

5.3 AI 流式输出

AI 应用是 SSE 最热门的新兴场景。用户通过普通 POST 请求发送问题,服务端通过 SSE 流式返回生成内容:

// 使用 fetch 实现(比 EventSource 更灵活,支持 POST)
async function askAI(question) {
  const response = await fetch('/api/chat', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ question })
  });

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });

    // 按行解析 SSE 数据
    const lines = buffer.split('\n');
    buffer = lines.pop(); // 保留不完整的最后一行

    for (const line of lines) {
      if (line.startsWith('data: ')) {
        const data = line.slice(6);
        if (data === '[DONE]') return;

        try {
          const parsed = JSON.parse(data);
          appendToken(parsed.token); // 逐字追加到界面
        } catch (e) {
          // 不是 JSON,直接作为文本处理
          appendToken(data);
        }
      }
    }
  }
}

5.4 协同编辑

协同编辑(如 Google Docs)需要实时同步多个用户的编辑操作,WebSocket 是主流选择:

// 简化的协同编辑客户端
const ws = new WebSocket('wss://collab.example.com/doc/123');

// 本地编辑操作 → 发送给服务端
editor.on('change', (operation) => {
  ws.send(JSON.stringify({
    type: 'operation',
    op: operation
  }));
});

// 接收其他用户的编辑操作 → 应用到本地
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === 'operation') {
    editor.applyRemoteOperation(msg.op);
  }
};

六、HTTP/2 和 HTTP/3 对实时通信的影响

值得一提的是,HTTP/2 和 HTTP/3 的一些特性正在改变实时通信的格局:

HTTP/2 Server Push

HTTP/2 支持服务端推送,但它推送的是资源(CSS、JS、图片等),不是事件流。而且 Chrome 已经在 2022 年移除了对 HTTP/2 Server Push 的支持,因为它在实际中很难正确使用。所以不要把 HTTP/2 Server Push 和 SSE 混淆。

HTTP/2 对 SSE 的改善

在 HTTP/1.1 下,SSE 受限于浏览器的连接数限制(每个域名通常 6 个连接)。如果你的页面打开了 6 个 SSE 连接,就没有多余的连接来发送普通请求了。

HTTP/2 的多路复用解决了这个问题——所有请求共享同一个 TCP 连接上的多个流,SSE 连接不再占用”连接数配额”。

WebTransport

WebTransport 是一个新兴的 API,基于 HTTP/3(QUIC),提供类似 WebSocket 的双向通信,但性能更好:

  • 基于 UDP(QUIC),建连更快
  • 支持不可靠传输(适合游戏、视频等丢包可容忍的场景)
  • 支持多流
  • 原生支持 0-RTT 建连

WebTransport 还在发展中,但未来可能会在某些场景下替代 WebSocket。


常见误区

误区一:“SSE 不如 WebSocket,因为它只能单向通信”

单向不等于不好。很多场景本来就只需要单向通信(通知推送、实时行情、AI 流式输出)。在这些场景下,SSE 比 WebSocket 更简单、更轻量、更可靠(自动重连和断线续传)。选择技术方案应该看场景需求,而不是看”哪个功能更全”。

误区二:“WebSocket 一直保持连接,很浪费资源”

WebSocket 连接在空闲时只占用极少的资源(一个 TCP 连接 + 一小块内存)。相比轮询(每隔几秒一次完整的 HTTP 请求-响应),WebSocket 反而更省资源。但如果连接数太多(比如十万级),服务端确实需要注意连接管理和内存使用。

误区三:“WebSocket 不需要做安全防护”

WebSocket 同样面临安全问题:

  • 跨站 WebSocket 劫持(CSWSH):类似 CSRF,攻击者网站发起 WebSocket 连接,利用用户的 Cookie。防御:检查 Origin
  • 注入攻击:通过 WebSocket 传输的数据如果直接插入 DOM,可能导致 XSS。防御:对数据做消毒处理
  • 使用 wss://:和 HTTPS 一样,使用加密的 WebSocket 连接

误区四:“EventSource 只支持 GET 请求,不能用于 AI 场景”

虽然 EventSource API 确实只支持 GET 请求,但你可以用 fetch API 来发起 POST 请求并手动解析 SSE 格式的响应流。这正是大多数 AI 应用的做法。EventSource 只是浏览器提供的便利 API,SSE 作为一种数据格式本身不限制请求方法。


小结

本章系统对比了 HTTP 轮询、SSE 和 WebSocket 三种实时通信方案。

核心要点

  1. 轮询:最简单但最浪费,适合低频更新、低实时性要求的场景
  2. SSE:单向推送,基于 HTTP,自动重连和断线续传,适合通知推送和 AI 流式输出
  3. WebSocket:全双工,独立协议,支持二进制,适合聊天、游戏等双向通信场景
  4. 选型原则:单向推送选 SSE,双向通信选 WebSocket,低频更新用轮询
  5. AI 流式输出用 SSE 而不是 WebSocket,因为通信模式是”请求-流式响应”,天然适合 SSE
  6. WebSocket 握手通过 HTTP Upgrade 完成,之后使用独立的帧协议
  7. WebSocket 需要自己实现重连和心跳机制

本章思维导图

浏览器:SSE 与 WebSocket
  • HTTP 轮询
    • 短轮询:定时请求,浪费资源
    • 长轮询:挂住连接等新数据,服务端压力大
    • 适合低频更新场景
  • SSE(Server-Sent Events)
    • 单向:服务端 → 客户端
    • 基于 HTTP(text/event-stream)
    • 自动重连 + 断线续传(Last-Event-ID)
    • 文本协议(不支持二进制)
    • API:EventSource / fetch + ReadableStream
    • 应用:实时通知、AI 流式输出、数据推送
  • WebSocket
    • 全双工(双向通信)
    • 独立协议 ws:// / wss://
    • 握手:HTTP Upgrade → 101 Switching Protocols
    • 帧协议:头部 2-14 字节,低开销
    • 支持文本 + 二进制
    • 需要自己实现重连和心跳
    • 应用:聊天、协同编辑、在线游戏
  • 对比与选型
    • 单向推送 → SSE
    • 双向通信 → WebSocket
    • 低频更新 → 轮询
    • AI 流式输出 → SSE(不是 WebSocket)
  • 前沿
    • HTTP/2 多路复用解决 SSE 连接数限制
    • WebTransport:基于 QUIC 的新方案

练习挑战

第一题(⭐ 基础):选择通信方案

以下场景分别应该选择什么通信方案?说明理由。

A. 一个股票行情页面,需要每秒更新价格 B. 一个在线客服聊天窗口 C. 一个后台管理系统,需要每 5 分钟检查一次是否有新的待审核任务

点击查看答案与解析
  • A(股票行情)→ SSE。价格更新是单向推送(服务端 → 客户端),不需要双向通信。SSE 基于 HTTP,简单可靠,自动重连。如果需要传输大量结构化数据,也可以考虑 WebSocket,但 SSE 已经足够。

  • B(在线客服)→ WebSocket。客服聊天需要双向通信——用户发消息、客服回消息。SSE 无法满足。

  • C(定时检查)→ 短轮询。5 分钟检查一次,频率极低,不需要维持持久连接。一个简单的 setInterval + fetch 就够了,引入 SSE 或 WebSocket 反而过度工程化。

第二题(⭐⭐ 进阶):实现带重连的 WebSocket

实现一个 ReconnectingWebSocket 类,要求:

  1. 连接断开后自动重连
  2. 重连间隔递增(指数退避):1s → 2s → 4s → 8s → …,最大 30s
  3. 提供 sendclose 方法和 onmessage 回调
点击查看答案与解析
class ReconnectingWebSocket {
  constructor(url) {
    this.url = url;
    this.reconnectDelay = 1000;
    this.maxDelay = 30000;
    this.onmessage = null;
    this.manualClose = false;
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      console.log('连接成功');
      this.reconnectDelay = 1000; // 重置重连间隔
    };

    this.ws.onmessage = (event) => {
      if (this.onmessage) {
        this.onmessage(event);
      }
    };

    this.ws.onclose = () => {
      if (this.manualClose) return;

      console.log(`${this.reconnectDelay / 1000}s 后重连...`);
      setTimeout(() => {
        this.connect();
      }, this.reconnectDelay);

      // 指数退避
      this.reconnectDelay = Math.min(
        this.reconnectDelay * 2,
        this.maxDelay
      );
    };

    this.ws.onerror = () => {
      this.ws.close(); // 出错后触发 onclose → 自动重连
    };
  }

  send(data) {
    if (this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(data);
    } else {
      console.warn('WebSocket 未连接,消息未发送');
    }
  }

  close() {
    this.manualClose = true;
    this.ws.close();
  }
}

// 使用
const ws = new ReconnectingWebSocket('wss://example.com/chat');
ws.onmessage = (event) => console.log('收到:', event.data);
ws.send('Hello!');

关键点:

  • manualClose 标记区分”主动关闭”和”异常断开”,只有异常断开才重连
  • 指数退避避免在服务端宕机时大量客户端同时重连(“惊群”问题)
  • 连接成功后重置重连间隔

第三题(⭐⭐⭐ 综合):设计实时通知系统

你要为一个电商平台设计实时通知系统,需求:

  1. 用户下单后,商家后台实时收到新订单通知
  2. 物流状态变更时,用户收到推送
  3. 系统公告推送给所有在线用户
  4. 需要考虑:用户刷新页面后不丢失未读通知、移动端弱网环境

请设计技术方案,包括通信方式选择、消息可靠性保障、离线处理。

点击查看答案与解析

技术方案:

通信方式:SSE

所有三种通知都是服务端 → 客户端的单向推送,不需要双向通信。SSE 比 WebSocket 更合适:

  • 基于 HTTP,兼容性好,能穿透代理和负载均衡
  • 内置自动重连,弱网环境下更可靠
  • 通过 Last-Event-ID 支持断线续传

消息可靠性保障:

// 服务端:每条消息带 ID
app.get('/notifications', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');

  const lastId = req.headers['last-event-id'];
  const userId = req.user.id;

  // 1. 补发断线期间的消息
  if (lastId) {
    const missed = db.getNotificationsSince(userId, lastId);
    missed.forEach(n => {
      res.write(`id: ${n.id}\nevent: ${n.type}\ndata: ${JSON.stringify(n)}\n\n`);
    });
  }

  // 2. 监听新消息并推送
  const listener = (notification) => {
    res.write(
      `id: ${notification.id}\nevent: ${notification.type}\ndata: ${JSON.stringify(notification)}\n\n`
    );
  };
  messageQueue.subscribe(userId, listener);

  req.on('close', () => {
    messageQueue.unsubscribe(userId, listener);
  });
});

离线处理:

  1. 服务端持久化:所有通知存入数据库,标记已读/未读状态
  2. 上线同步:用户上线时(SSE 连接建立时),从数据库拉取所有未读通知
  3. 断线续传:通过 Last-Event-ID 机制,重连后自动补发遗漏的消息
  4. 备选方案:对于离线用户,可以通过推送服务(Firebase Cloud Messaging / Apple Push Notification)发送系统通知

弱网优化:

  1. SSE 自动重连 + retry 字段设置合理的重连间隔
  2. 消息去重:客户端根据消息 ID 去重,避免重连后重复显示
  3. 消息压缩:使用 gzip 压缩 SSE 响应流
  4. 心跳检测:定期发送空注释行(: heartbeat\n\n)保持连接存活

架构示意:

业务系统 → 消息队列(Redis/RabbitMQ)→ SSE 服务 → 客户端

                                    数据库(持久化)

自我检测

  • 能说出短轮询、长轮询、SSE、WebSocket 四种方案的区别和适用场景
  • 能描述 SSE 的工作原理,包括数据格式、自动重连、断线续传
  • 能使用 EventSource API 和 fetch + ReadableStream 两种方式接收 SSE
  • 能描述 WebSocket 的握手过程(HTTP Upgrade → 101)
  • 能解释 WebSocket 帧协议的基本结构和低开销特性
  • 能实现 WebSocket 的心跳机制和自动重连
  • 能根据具体场景(单向/双向、文本/二进制、实时性要求)选择合适的通信方案
  • 能解释为什么 AI 流式输出用 SSE 而不是 WebSocket

购买课程解锁全部内容

大厂前端面试通关:71 篇构建完整知识体系

¥89.90