19 KiB
19 KiB
WebSocket增强服务技术设计文档
概述
本文档详细解释了 src/services/ws.ts
的技术设计思路、架构选择和实现细节。这个文件实现了一个增强的WebSocket服务,在保持原有接口不变的前提下,添加了心跳检测、自动重连、错误处理等企业级功能。
设计目标
主要目标
- 零侵入性:业务代码无需修改,完全透明的功能增强
- 企业级稳定性:心跳检测、自动重连、错误恢复
- 可配置性:全局配置,易于调整和优化
- 类型安全:完整的TypeScript类型支持
- 内存安全:正确的资源管理,防止内存泄漏
兼容性目标
- 保持原有
create(path): Promise<WebSocket>
接口不变 - 返回标准WebSocket实例,支持所有原生API
- 业务代码中的
ws.onmessage
,ws.close()
等调用完全兼容
架构设计
整体架构图
┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ 业务代码 │ │ EnhancedWebSocket │ │ 原生WebSocket │
│ │ │ (包装器) │ │ │
│ ws.onmessage = ... │───▶│ 事件拦截和过滤 │───▶│ 实际网络连接 │
│ ws.send(data) │ │ 心跳检测逻辑 │ │ │
│ ws.close() │ │ 重连管理 │ │ │
└─────────────────────┘ └──────────────────────┘ └─────────────────────┘
│
▼
┌──────────────────────┐
│ WS_CONFIG │
│ (全局配置) │
│ - 心跳间隔 │
│ - 重连策略 │
│ - 超时设置 │
└──────────────────────┘
设计模式选择
1. 包装器模式 (Wrapper Pattern)
class EnhancedWebSocket {
private ws: WebSocket; // 包装原生WebSocket
}
为什么选择包装器而不是继承?
-
继承的问题:
// 继承方式的问题 class EnhancedWebSocket extends WebSocket { constructor(url: string) { super(url); // 连接立即开始,无法在事件处理器设置前进行拦截 } }
-
包装器的优势:
// 包装器方式的优势 class EnhancedWebSocket { constructor(path: string, baseUrl: string) { this.ws = new WebSocket(baseUrl + path); // 控制创建时机 this.setupHandlers(); // 立即设置我们的处理器 } }
2. 代理模式 (Proxy Pattern)
通过getter/setter拦截用户对事件处理器的设置:
get onmessage(): ((event: MessageEvent) => void) | null {
return this.userOnMessage;
}
set onmessage(handler: ((event: MessageEvent) => void) | null) {
this.userOnMessage = handler; // 保存用户的处理器
// 我们的处理器已经在构造时设置,会调用用户的处理器
}
核心技术实现
1. Class 设计选择
为什么使用 Class?
class EnhancedWebSocket {
// 私有状态管理
private ws: WebSocket;
private path: string;
private heartbeatTimer?: NodeJS.Timeout;
// ...
}
选择Class的原因:
- 状态封装:WebSocket连接需要管理多个状态(连接、定时器、配置等)
- 方法绑定:事件处理器需要访问实例状态,Class提供了自然的this绑定
- 生命周期管理:连接的创建、维护、销毁有清晰的生命周期
- 类型安全:TypeScript对Class有更好的类型推导和检查
与函数式方案的对比:
// 函数式方案的问题
function createEnhancedWS(path: string) {
let heartbeatTimer: NodeJS.Timeout;
let reconnectTimer: NodeJS.Timeout;
// 需要大量闭包来管理状态,复杂度高
}
// Class方案的优势
class EnhancedWebSocket {
private heartbeatTimer?: NodeJS.Timeout; // 清晰的状态管理
private reconnectTimer?: NodeJS.Timeout;
// 方法可以直接访问状态
}
2. Private 成员设计
为什么大量使用 private?
class EnhancedWebSocket {
private ws: WebSocket; // 内部WebSocket实例
private path: string; // 连接路径
private heartbeatTimer?: NodeJS.Timeout; // 心跳定时器
private reconnectTimer?: NodeJS.Timeout; // 重连定时器
private reconnectAttempts: number = 0; // 重连次数
private isManualClose: boolean = false; // 手动关闭标志
private isHeartbeatTimeout: boolean = false; // 心跳超时标志
}
Private的重要性:
- 封装原则:防止外部直接访问和修改内部状态
- API稳定性:内部实现可以随时重构,不影响公共接口
- 状态一致性:防止外部代码破坏内部状态的一致性
- 错误预防:避免用户误用内部API导致的bug
示例对比:
// 如果没有private,用户可能这样做
const ws = new EnhancedWebSocket('/test');
ws.heartbeatTimer = undefined; // 💥 破坏了心跳检测
ws.reconnectAttempts = -1; // 💥 破坏了重连逻辑
// 有了private,这些操作被编译器阻止
// ✅ 确保了内部状态的安全性
3. Constructor 设计
构造函数的关键作用
constructor(path: string, baseUrl: string) {
this.path = path;
this.baseUrl = baseUrl;
this.ws = new WebSocket(baseUrl + path); // 创建实际连接
this.setupHandlers(); // 立即设置事件处理器
}
设计要点:
- 立即执行:构造时立即创建连接和设置处理器
- 状态初始化:确保所有私有状态都有正确的初始值
- 参数验证:(可以添加)对输入参数进行验证
- 最小权限:只接收必要的参数,其他配置使用全局配置
为什么不延迟创建连接?
// ❌ 错误方案:延迟创建
constructor(path: string, baseUrl: string) {
this.path = path;
this.baseUrl = baseUrl;
// 不创建连接,等用户调用connect()
}
// ✅ 正确方案:立即创建
constructor(path: string, baseUrl: string) {
// 立即创建,因为原有接口期望构造后就有连接
this.ws = new WebSocket(baseUrl + path);
this.setupHandlers();
}
4. Getter/Setter 设计
透明的属性代理
// 只读属性的getter
get readyState(): number {
return this.ws.readyState; // 直接代理到内部WebSocket
}
get url(): string {
return this.ws.url;
}
// 可写属性的getter/setter
get binaryType(): BinaryType {
return this.ws.binaryType;
}
set binaryType(value: BinaryType) {
this.ws.binaryType = value;
}
为什么需要这些getter/setter?
- API兼容性:用户期望能够访问标准WebSocket的所有属性
- 透明代理:用户感觉在使用标准WebSocket,实际上是我们的增强版本
- 状态同步:确保外部看到的状态与内部WebSocket状态一致
事件处理器的特殊getter/setter
// 事件处理器的拦截
get onmessage(): ((event: MessageEvent) => void) | null {
return this.userOnMessage; // 返回用户设置的处理器
}
set onmessage(handler: ((event: MessageEvent) => void) | null) {
this.userOnMessage = handler; // 保存用户的处理器
// 我们的内部处理器会调用用户的处理器
}
关键设计思路:
- 双层处理:我们的处理器 + 用户的处理器
- 透明性:用户感觉直接在设置WebSocket的事件处理器
- 控制权:我们先处理(如过滤心跳),再传递给用户
5. 事件处理架构
事件流设计
WebSocket原生事件 → 我们的处理器 → 过滤/处理 → 用户的处理器
具体实现
private setupHandlers(): void {
// 1. 设置我们的处理器
this.ws.onmessage = (event) => {
const messageData = event.data;
// 2. 我们先处理(心跳检测)
let isHeartbeatResponse = false;
if (typeof messageData === 'string' && messageData === WS_CONFIG.heartbeatResponseType) {
isHeartbeatResponse = true;
}
if (isHeartbeatResponse) {
this.clearHeartbeatTimeout(); // 清除心跳超时
return; // 不传递给用户
}
// 3. 传递给用户的处理器
if (this.userOnMessage) {
this.userOnMessage(event);
}
};
}
设计优势:
- 消息过滤:自动过滤心跳消息,用户无感知
- 状态管理:自动处理连接状态变化
- 错误恢复:自动处理连接错误和重连
6. 定时器管理
定时器生命周期管理
class EnhancedWebSocket {
private heartbeatTimer?: NodeJS.Timeout; // 心跳发送定时器
private heartbeatTimeoutTimer?: NodeJS.Timeout; // 心跳响应超时定时器
private reconnectTimer?: NodeJS.Timeout; // 重连定时器
}
为什么需要三个定时器?
- heartbeatTimer:定期发送心跳包
- heartbeatTimeoutTimer:检测心跳响应超时
- reconnectTimer:延迟重连
定时器清理策略
// 停止心跳检测
private stopHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = undefined; // 重置为undefined
}
this.clearHeartbeatTimeout(); // 同时清理超时检测
}
// 清除心跳响应超时检测
private clearHeartbeatTimeout(): void {
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = undefined; // 重置为undefined
}
}
内存安全保证:
- 及时清理:每次停止时都清理定时器
- 状态重置:清理后设置为undefined
- 多重清理:在多个关键点都进行清理(连接关闭、手动关闭等)
7. 状态标志设计
关键状态标志
private isManualClose: boolean = false; // 是否手动关闭
private isHeartbeatTimeout: boolean = false; // 是否心跳超时
private reconnectAttempts: number = 0; // 重连次数
为什么需要这些标志?
- 区分关闭原因:手动关闭 vs 异常断开 vs 心跳超时
- 重连决策:根据不同原因决定是否重连
- 状态跟踪:跟踪重连进度和次数
状态转换逻辑
// 心跳超时时
private startHeartbeatTimeout(): void {
this.heartbeatTimeoutTimer = setTimeout(() => {
this.isHeartbeatTimeout = true; // 设置心跳超时标志
this.ws.close(1000, 'Heartbeat timeout');
}, WS_CONFIG.heartbeatTimeout);
}
// 连接关闭时的决策
this.ws.onclose = (event) => {
// 如果不是手动关闭,或者是心跳超时导致的关闭,则重连
if (!this.isManualClose || this.isHeartbeatTimeout) {
this.scheduleReconnect();
}
this.isHeartbeatTimeout = false; // 重置标志
};
8. addEventListener/removeEventListener 实现
为什么需要这些方法?
addEventListener<K extends keyof WebSocketEventMap>(
type: K,
listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void {
this.ws.addEventListener(type, listener, options);
}
removeEventListener<K extends keyof WebSocketEventMap>(
type: K,
listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any,
options?: boolean | EventListenerOptions
): void {
this.ws.removeEventListener(type, listener, options);
}
重要性:
- 完整的API兼容性:某些业务代码可能使用addEventListener而不是onXXX
- 事件管理:支持多个监听器
- 标准兼容:遵循WebSocket标准API
类型安全:
- 使用泛型
<K extends keyof WebSocketEventMap>
确保事件类型正确 - listener参数的类型根据事件类型自动推导
9. 心跳检测机制
心跳超时检测逻辑
// 发送心跳时,只在没有超时检测时才设置新的
this.heartbeatTimer = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(WS_CONFIG.heartbeatMessage);
if (!this.heartbeatTimeoutTimer) {
// 关键:避免重复设置
this.startHeartbeatTimeout();
}
}
}, WS_CONFIG.heartbeatInterval);
设计要点:
- 避免重复设置:只有在没有超时检测时才设置新的
- 超时逻辑:设定时间内没收到响应就断开连接
- 状态同步:收到响应时清除超时检测
心跳响应处理
// 检查是否为心跳响应(支持字符串和JSON格式)
let isHeartbeatResponse = false;
// 1. 检查简单字符串格式
if (typeof messageData === 'string' && messageData === WS_CONFIG.heartbeatResponseType) {
isHeartbeatResponse = true;
}
// 2. 检查JSON格式
if (!isHeartbeatResponse && typeof messageData === 'string') {
try {
const data = JSON.parse(messageData);
if (data.type === WS_CONFIG.heartbeatResponseType) {
isHeartbeatResponse = true;
}
} catch (e) {
// JSON解析失败,不是JSON格式的心跳响应
}
}
兼容性设计:支持两种心跳响应格式,适应不同的服务器实现。
10. 重连机制
指数退避算法
private scheduleReconnect(): void {
if (this.isManualClose || this.reconnectAttempts >= WS_CONFIG.maxReconnectAttempts) {
return;
}
this.reconnectAttempts++;
// 指数退避重连策略
const delay = Math.min(
WS_CONFIG.reconnectBaseDelay * Math.pow(2, this.reconnectAttempts - 1),
WS_CONFIG.maxReconnectDelay
);
this.reconnectTimer = setTimeout(() => {
this.reconnect();
}, delay);
}
算法解释:
- 第1次重连:1000ms 后
- 第2次重连:2000ms 后
- 第3次重连:4000ms 后
- 第4次重连:8000ms 后
- 第5次重连:16000ms 后(受maxReconnectDelay限制,实际为30000ms)
设计考虑:
- 指数退避:避免对服务器造成压力
- 最大延迟限制:防止延迟过长
- 次数限制:避免无限重连
- 服务器友好:给服务器恢复时间
11. 类型安全设计
严格的类型定义
// 事件处理器类型
private userOnMessage: ((event: MessageEvent) => void) | null = null;
private userOnClose: ((event: CloseEvent) => void) | null = null;
private userOnError: ((event: Event) => void) | null = null;
private userOnOpen: ((event: Event) => void) | null = null;
类型安全的好处:
- 编译时检查:在编译时捕获类型错误
- IDE支持:更好的自动补全和错误提示
- 重构安全:类型系统确保重构的正确性
泛型的使用
addEventListener<K extends keyof WebSocketEventMap>(
type: K,
listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void
泛型的价值:
K extends keyof WebSocketEventMap
:确保事件类型只能是WebSocket支持的类型ev: WebSocketEventMap[K]
:根据事件类型自动推导事件对象类型
12. 资源管理
完整的清理机制
close(code?: number, reason?: string): void {
console.log(`手动关闭WebSocket: ${this.path}`);
this.isManualClose = true;
this.isHeartbeatTimeout = false; // 重置心跳超时标志
this.stopHeartbeat(); // 清理心跳定时器
this.clearReconnectTimer(); // 清理重连定时器
this.ws.close(code, reason); // 关闭实际连接
}
资源清理的重要性:
- 内存泄漏预防:确保所有定时器都被清理
- 状态一致性:重置所有状态标志
- 优雅关闭:按正确顺序清理资源
配置设计
全局配置对象
const WS_CONFIG = {
heartbeatInterval: 3000, // 心跳间隔
heartbeatTimeout: 5000, // 心跳响应超时时间
maxReconnectAttempts: 5, // 最大重连次数
reconnectBaseDelay: 1000, // 重连基础延迟
maxReconnectDelay: 30000, // 最大重连延迟
heartbeatMessage: 'ping', // 心跳消息
heartbeatResponseType: 'pong', // 心跳响应类型
};
配置设计原则:
- 集中管理:所有配置在一个地方,易于维护
- 合理默认值:开箱即用的配置
- 易于调整:生产环境可以快速调整参数
- 文档化:每个配置都有清晰的注释
接口兼容性
原有接口保持不变
// 原有接口
function create(path: string): Promise<WebSocket> {
const baseUrl = import.meta.env.ENV_WEBSOCKET_BASE ?? '';
const ws = new EnhancedWebSocket(path, baseUrl) as any;
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
ws.close();
reject(new Error('WebSocket connection timeout'));
}, 10000);
ws.addEventListener('open', () => {
clearTimeout(timeout);
resolve(ws); // 返回增强的WebSocket,但类型为WebSocket
});
ws.addEventListener('error', (e: any) => {
clearTimeout(timeout);
reject(e);
});
});
}
兼容性保证:
- 相同的函数签名:
create(path: string): Promise<WebSocket>
- 相同的返回类型:返回Promise
- 相同的使用方式:业务代码无需任何修改
总结
技术选择总结
技术选择 | 原因 | 替代方案 | 为什么不选择替代方案 |
---|---|---|---|
Class | 状态封装、方法绑定、生命周期管理 | 函数+闭包 | 复杂度高,类型支持差 |
包装器模式 | 控制创建时机、事件拦截 | 继承 | 无法在事件设置前拦截 |
Private成员 | 封装、API稳定性、状态保护 | Public成员 | 容易被误用,状态不安全 |
Getter/Setter | 透明代理、API兼容性 | 直接方法 | 不符合WebSocket API习惯 |
多定时器 | 职责分离、精确控制 | 单定时器 | 逻辑混乱,难以维护 |
状态标志 | 精确控制重连逻辑 | 仅依赖状态码 | WebSocket状态码限制多 |
架构优势
- 零侵入性:业务代码完全无需修改
- 高可靠性:多重保障确保连接稳定
- 高可维护性:清晰的架构和完整的类型支持
- 高性能:最小的性能开销
- 高扩展性:易于添加新功能
最佳实践体现
- 单一职责原则:每个方法只负责一个功能
- 开闭原则:对扩展开放,对修改封闭
- 依赖倒置原则:依赖抽象(接口)而非具体实现
- 接口隔离原则:用户只看到需要的接口
- 里氏替换原则:增强版本完全可以替换原版本
这个实现展示了如何在保持向后兼容的同时,提供企业级的功能增强,是一个很好的渐进式增强的例子。