一. EventSource (Server-Sent Events)
1. 特点
- 协议:基于HTTP/HTTPS,单向通信(服务端 → 客户端)
- 数据格式:仅支持UTF-8文本,即text/event-stream
- 自动重连:内置断连重试机制,默认超时时间为45s
- 轻量级:原生浏览器API,无需额外库
2. 限制
- 单向通信:客户端无法通过EventSource向服务端发送数据,例如在流式输出过程中,客户端无法通过EventSource告诉服务端中断
- 无二进制支持:仅支持文本数据,无法传输二进制(如图片、音频)
- HTTP/1.1限制:浏览器对同一域名下的并发连接数有限制(Chrome为6个)
- 无心跳检测:依赖HTTP层断连检测,可能因代理/Nginx超时设置失效
- 不支持自定义请求头:若需要支持网关,EventSource无法支持配置请求头
3. 适用场景
- 实时通知(如股票价格、新闻推送)
- 服务端日志流式输出
- 兼容性要求高的简单实时场景(不支持WebSocket的旧环境)
4. 隐患&改进建议
- 连接稳定性问题:依赖浏览器默认重连机制,但未显式设置 Retry-After(服务端控制重试间隔)
- 数据完整性风险:直接拼接 lastChat.content + response.data.content,若消息乱序或丢失分片,会导致内容错乱
- 内存泄漏风险:未在组件卸载时清理 EventSource,可能导致重复连接
5. ⚠️开发需注意
事件类型过滤
- 方法:addEventListener 监听特定事件类型(非默认 message)
- 用途:区分不同业务事件(如 status、data)
javascript
eventSource.addEventListener('status', (e) => {
console.log('Status update:', e.data);
});
自定义重试逻辑
- 特性:通过 Retry-After 响应头控制重试间隔(需服务端支持)(不推荐)
http
HTTP/1.1 503 Service Unavailable
Retry-After: 10 # 10秒后重试
- 一旦eventsource走到onerror回调中,就需要考虑断线重连问题,默认情况是eventsource会自动重连,这并不能够达到预期效果,因为前端无法感知eventsource的非预期自动重连(相当于重新发起请求,多次请求返回的数据也会以拼接的方式展示)。所以,前端需要手动重连,走到onerror逻辑就close掉eventsource,然后自定义重试逻辑(最多重试三次为佳)。重试逻辑需要考虑从上一个message的messageId+1开始,以便内容拼接正确。
连接状态检测
- 属性:readyState(CONNECTING=0, OPEN=1, CLOSED=2)
javascript
if (eventSource.readyState === EventSource.OPEN) {
console.log('Connection active');
}
消息ID跟踪
- 特性:服务端可通过 id: 字段发送消息 ID,客户端断连后通过 Last-Event-ID 请求恢复
心跳检测
-
用途:eventsource默认超时时间45s,但是在本地和在服务器超时时间可能不一致,若超时,前端会直接close掉eventsource,但是不会导致服务端close,单向通信就是这样,所以当服务端在等待其他服务的数据时,也需要吐数据给前端,保证前端的eventsource的连接 -立即生效的情况 根据MDN文档,当调用EventSource.close()方法时,在以下情况下会立即真正关闭连接:
-
✅ 连接处于开启状态时(readyState = EventSource.OPEN = 1)
-
✅ 连接处于连接中状态时(readyState = EventSource.CONNECTING = 0)
-
调用close()后,浏览器会: (1) 立即关闭与服务器的HTTP连接 (2) 将readyState属性设置为EventSource.CLOSED(值为2) (3) 停止自动重连机制
-
不会产生任何效果的情况 (1) ❌ 连接已经关闭时(readyState = EventSource.CLOSED = 2) (2) 如果连接已经关闭,该方法不会做任何事情
二. WebSocket
1. 特点
- 协议:基于TCP的独立协议,通过HTTP握手升级
- 双向通信:客户端和服务端可同时发送/接收数据
- 低延迟:无HTTP头开销,数据帧直接传输
- 数据支持:支持文本、二进制(ArrayBuffer、Blob)
- 扩展性:支持自定义子协议和压缩扩展
2. 限制
- 连接管理复杂:需手动处理连接状态、重连逻辑
- 代理穿透问题:部分代理/防火墙不支持WebSocket协议
- 资源占用:长连接占用服务器socket资源
- 无缓存机制:不支持HTTP缓存,需自行实现离线处理
- 状态同步难:客户端/服务端状态不一致时处理复杂
3. 适用场景
- 即时聊天、在线客服系统
- 实时协作编辑(Google Docs类应用)
- 在线游戏、实时竞技
- 金融交易、股票行情实时推送
- 物联网设备控制和监控
4. 隐患&改进建议
- 频繁断连问题:移动网络切换、信号弱时连接不稳定,需实现指数退避重连
- 消息丢失风险:网络中断时未确认消息可能丢失,需要消息确认机制
- 内存泄漏:事件监听器未正确移除,WebSocket对象未释放
- 安全风险:缺少认证续期机制,长连接中token过期处理
5. 开发经验
心跳检测
🚀🚀 主要作用
-
连接存活检测:确保WebSocket连接真正可用,而不是"僵尸连接"
-
网络质量监控:通过ping-pong的往返时间评估网络延迟
-
代理超时防护:防止代理服务器/负载均衡器因长时间无数据而断开连接
🔥🔥 解决的实际问题
-
移动端网络切换:4G/WiFi切换时连接断开但客户端不知情
-
企业防火墙:长时间无数据的连接被防火墙清理
-
云服务负载均衡:ALB/ELB的idle timeout导致连接被回收
三. event-source-polyfill
1. 特点
- 兼容性扩展:为不支持EventSource的浏览器提供polyfill
- 功能增强:支持自定义请求头、CORS凭证
- API一致:与原生EventSource API完全兼容
- 渐进增强:优先使用原生实现,回退到polyfill
- 轻量级:约10KB,不显著增加包体积
2. 限制
- 性能差异:polyfill基于XMLHttpRequest,性能略低于原生
- 兼容性测试负担:需在目标浏览器中充分测试
- 功能子集:某些边缘特性可能无法完全模拟
- 维护成本:需要跟进polyfill版本更新
- 调试复杂性:polyfill实现可能影响错误堆栈
3. 适用场景
- 企业内网环境(IE11等旧版浏览器)
- 需要自定义请求头的SSE场景
- 跨域SSE请求(需要凭证)
- 渐进式Web应用的兼容性方案
- 微信小程序等环境的SSE支持
4. 隐患&改进建议
- 特性检测失效:部分环境下原生检测可能不准确,建议强制使用polyfill
- 内存泄漏:polyfill的XMLHttpRequest未正确清理
- 事件时序差异:polyfill的事件触发时机可能与原生不同
- 错误处理不一致:网络错误的处理逻辑可能不同
5. 开发经验
自定义请求头支持
javascript
import EventSource from 'event-source-polyfill';
const eventSource = new EventSource('/stream', {
headers: {
'Authorization': 'Bearer ' + token,
'X-Custom-Header': 'value'
},
withCredentials: true
});
特性检测与降级
javascript
function createEventSource(url, options = {}) {
// 检测原生支持
const hasNativeSupport = typeof window.EventSource !== 'undefined';
if (hasNativeSupport && !options.headers) {
return new window.EventSource(url);
} else {
return new EventSourcePolyfill(url, options);
}
}
错误重试增强
javascript
class RobustEventSource {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.retryCount = 0;
this.maxRetries = options.maxRetries || 5;
this.retryDelay = options.retryDelay || 1000;
this.connect();
}
connect() {
this.eventSource = new EventSourcePolyfill(this.url, this.options);
this.eventSource.onerror = (e) => {
if (this.retryCount < this.maxRetries) {
setTimeout(() => {
this.retryCount++;
this.connect();
}, this.retryDelay * Math.pow(2, this.retryCount));
}
};
}
}
四. socket.io-client
1. 特点
- 传输降级:WebSocket → 长轮询 → 短轮询的自动降级
- 房间管理:内置房间(room)和命名空间(namespace)概念
- 自动重连:智能重连策略,支持指数退避
- 事件系统:基于事件的发布订阅模式
- 跨平台:支持浏览器、Node.js、React Native等环境
- 中间件支持:支持连接中间件和认证
2. 限制
- 体积较大:压缩后约60KB,显著大于原生WebSocket
- 版本兼容:客户端与服务端版本需要兼容
- 学习曲线:需要理解Socket.IO特有概念和API
- 调试复杂:多层抽象增加问题定位难度
- 服务端绑定:必须使用Socket.IO服务端,无法与标准WebSocket服务端通信
3. 适用场景
- 大型实时应用(聊天室、协作平台)
- 需要房间管理的多用户应用
- 对网络环境要求不高的企业应用
- 快速原型开发和MVP验证
- 需要跨平台统一实时通信的项目
4. 隐患&改进建议
- 连接开销过大:初始化时间长,需要优化连接参数
- 内存占用高:事件监听器过多,需要及时清理
- 网络策略冲突:企业网络可能阻止长轮询,需要配置传输方式
- 调试信息过多:生产环境需要关闭debug模式
5. 开发经验
连接配置优化
javascript
import io from 'socket.io-client';
const socket = io('http://localhost:3000', {
// 传输方式优先级
transports: ['websocket', 'polling'],
// 连接超时
timeout: 20000,
// 重连配置
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
// 随机化重连延迟
randomizationFactor: 0.5,
// 强制使用新连接
forceNew: false
});
房间和命名空间
javascript
// 命名空间
const adminSocket = io('/admin');
const userSocket = io('/user');
// 房间操作
socket.emit('join-room', 'room1');
socket.on('room-message', (data) => {
console.log('Room message:', data);
});
// 私聊
socket.emit('private-message', {
to: 'userId',
message: 'Hello'
});
中间件和认证
javascript
// 连接中间件
socket.on('connect', () => {
socket.emit('authenticate', { token: getAuthToken() });
});
socket.on('authenticated', () => {
console.log('认证成功');
});
socket.on('unauthorized', () => {
console.log('认证失败,重新登录');
});
性能监控
javascript
// 连接质量监控
socket.on('ping', () => {
socket.emit('pong', Date.now());
});
// 延迟监控
const startTime = Date.now();
socket.emit('ping-test', startTime, (ackTime) => {
const latency = Date.now() - ackTime;
console.log('延迟:', latency + 'ms');
});
六. fetch-event-source
1. 特点
- 基于Fetch API:使用现代 fetch API 实现,支持更灵活的请求配置
- 支持POST请求:可以通过POST发送请求体,突破GET参数长度限制
- 自定义请求头:完全支持自定义请求头,便于认证和网关配置
- 更好的错误处理:提供更详细的错误信息和状态码处理
- TypeScript支持:原生TypeScript支持,提供完整的类型定义
- 取消机制:支持 AbortController 取消请求
2. 限制
- 需要额外依赖:不是浏览器原生API,需要安装第三方库
- 兼容性要求:依赖 fetch API 和 ReadableStream,需要现代浏览器
- 调试复杂度:相比原生EventSource,调试和错误追踪更复杂
- 性能开销:额外的抽象层可能带来轻微性能开销
- 社区生态:相对较新,社区资源和案例较少
3. 适用场景
- 需要POST请求发送复杂参数的SSE场景
- 需要自定义请求头进行认证的企业应用
- 需要更精细错误处理的生产环境
- 现代前端框架中的SSE集成
- 需要取消机制的交互式应用
4. 隐患&改进建议
- 内存泄漏风险:未正确调用 AbortController.abort() 可能导致连接无法释放
- 错误重试复杂:需要手动实现重连逻辑,容易出现指数爆炸或无限重试
- 流解析错误:手动解析SSE流时可能出现数据截断或格式错误
- 并发控制缺失:没有内置连接数限制,可能创建过多并发连接
5. 开发经验
基本使用
javascript
import { fetchEventSource } from '@microsoft/fetch-event-source';
await fetchEventSource('/api/stream', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + token,
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: 'complex query data',
options: { temperature: 0.7 }
}),
onmessage(event) {
console.log('收到消息:', event.data);
},
onerror(err) {
console.error('连接错误:', err);
}
});
自定义重连逻辑
javascript
class RetryableEventSource {
constructor(url, options = {}) {
this.url = url;
this.options = options;
this.retryCount = 0;
this.maxRetries = options.maxRetries || 3;
this.retryDelay = options.retryDelay || 1000;
this.abortController = new AbortController();
}
async connect() {
try {
await fetchEventSource(this.url, {
...this.options,
signal: this.abortController.signal,
onmessage: (event) => {
this.retryCount = 0; // 重置重试计数
this.options.onmessage?.(event);
},
onerror: (err) => {
if (this.retryCount < this.maxRetries) {
const delay = this.retryDelay * Math.pow(2, this.retryCount);
setTimeout(() => {
this.retryCount++;
this.connect();
}, delay);
} else {
this.options.onerror?.(err);
}
// 返回重试间隔,阻止默认重连
return this.retryDelay * Math.pow(2, this.retryCount);
}
});
} catch (error) {
if (!this.abortController.signal.aborted) {
console.error('连接失败:', error);
}
}
}
disconnect() {
this.abortController.abort();
}
}
六. 技术选择指南
1. EventSource 适用场景
- ✅ 简单的单向数据流
- 🟢 实时通知、日志输出
- 🟢 对兼容性要求不高的场景
2. fetch-event-source 适用场景
- ✅ 需要POST请求体的SSE
- ✅ 需要认证头的企业应用
- 🟢 现代框架的SSE集成
- 🟢 需要精细错误控制
3. WebSocket 适用场景
- ✅ 需要双向通信
- 🟢 即时聊天、实时游戏
- 🟢 对延迟敏感的应用
4. event-source-polyfill 适用场景
- ✅ 需要自定义请求头头的SSE
- 🟢 需要支持旧浏览器
- 🟢 企业内网环境
5. socket.io-client 适用场景
- ✅ 需要房间管理
- 🟢 大型实时应用
- 🟢 复杂的企业级项目