web-map/docs/WebSocket增强服务技术设计文档.md

647 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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