浏览器篇 | 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 握手建立连接:
- 客户端发送一个 HTTP GET 请求,带上
Upgrade: websocket和Connection: Upgrade头 - 服务端如果支持 WebSocket,返回
101 Switching Protocols状态码 - 此后连接升级为 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=
握手过程:
- 客户端发送普通的 HTTP GET 请求,带上
Upgrade: websocket表示请求升级协议 Sec-WebSocket-Key是客户端生成的随机 Base64 字符串- 服务端将
Sec-WebSocket-Key加上一个固定的 GUID,做 SHA-1 hash,再 Base64 编码,作为Sec-WebSocket-Accept返回 - 客户端验证
Sec-WebSocket-Accept的值,确认服务端确实支持 WebSocket - 返回
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 对比与选型
核心对比
| 特性 | SSE | WebSocket |
|---|---|---|
| 通信方向 | 单向(服务端 → 客户端) | 全双工(双向) |
| 协议 | 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 三种实时通信方案。
核心要点
- 轮询:最简单但最浪费,适合低频更新、低实时性要求的场景
- SSE:单向推送,基于 HTTP,自动重连和断线续传,适合通知推送和 AI 流式输出
- WebSocket:全双工,独立协议,支持二进制,适合聊天、游戏等双向通信场景
- 选型原则:单向推送选 SSE,双向通信选 WebSocket,低频更新用轮询
- AI 流式输出用 SSE 而不是 WebSocket,因为通信模式是”请求-流式响应”,天然适合 SSE
- WebSocket 握手通过 HTTP Upgrade 完成,之后使用独立的帧协议
- 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 类,要求:
- 连接断开后自动重连
- 重连间隔递增(指数退避):1s → 2s → 4s → 8s → …,最大 30s
- 提供
send、close方法和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标记区分”主动关闭”和”异常断开”,只有异常断开才重连- 指数退避避免在服务端宕机时大量客户端同时重连(“惊群”问题)
- 连接成功后重置重连间隔
第三题(⭐⭐⭐ 综合):设计实时通知系统
你要为一个电商平台设计实时通知系统,需求:
- 用户下单后,商家后台实时收到新订单通知
- 物流状态变更时,用户收到推送
- 系统公告推送给所有在线用户
- 需要考虑:用户刷新页面后不丢失未读通知、移动端弱网环境
请设计技术方案,包括通信方式选择、消息可靠性保障、离线处理。
点击查看答案与解析
技术方案:
通信方式: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);
});
});
离线处理:
- 服务端持久化:所有通知存入数据库,标记已读/未读状态
- 上线同步:用户上线时(SSE 连接建立时),从数据库拉取所有未读通知
- 断线续传:通过
Last-Event-ID机制,重连后自动补发遗漏的消息 - 备选方案:对于离线用户,可以通过推送服务(Firebase Cloud Messaging / Apple Push Notification)发送系统通知
弱网优化:
- SSE 自动重连 +
retry字段设置合理的重连间隔 - 消息去重:客户端根据消息 ID 去重,避免重连后重复显示
- 消息压缩:使用 gzip 压缩 SSE 响应流
- 心跳检测:定期发送空注释行(
: heartbeat\n\n)保持连接存活
架构示意:
业务系统 → 消息队列(Redis/RabbitMQ)→ SSE 服务 → 客户端
↑
数据库(持久化)
自我检测
- 能说出短轮询、长轮询、SSE、WebSocket 四种方案的区别和适用场景
- 能描述 SSE 的工作原理,包括数据格式、自动重连、断线续传
- 能使用
EventSourceAPI 和fetch + ReadableStream两种方式接收 SSE - 能描述 WebSocket 的握手过程(HTTP Upgrade → 101)
- 能解释 WebSocket 帧协议的基本结构和低开销特性
- 能实现 WebSocket 的心跳机制和自动重连
- 能根据具体场景(单向/双向、文本/二进制、实时性要求)选择合适的通信方案
- 能解释为什么 AI 流式输出用 SSE 而不是 WebSocket
购买课程解锁全部内容
大厂前端面试通关:71 篇构建完整知识体系
¥89.90