为什么需要封装工具类
直接在组件中使用 new WebSocket() 存在几个问题:心跳检测、断线重连、消息格式化等逻辑散落在业务代码中,难以复用和维护。将这些通用逻辑封装为一个独立的 WebSocketClient 类,可以在不同项目中直接复用,也为后续的单例模式和 SharedWorker 共享打下基础。
工具类的核心架构
// src/utils/websocket-client.ts
interface WebSocketClientOptions {
url: string
pingInterval?: number // ping 发送间隔,默认 30000ms
pongTimeout?: number // pong 响应超时,默认 5000ms
autoReconnect?: boolean // 是否自动重连,默认 true
reconnectInterval?: number // 重连间隔,默认 3000ms
retryStrategy?: (times: number) => number // 自定义重连延迟策略
maxRetries?: number // 最大重试次数
onOpen?: (client: any) => void
onMessage?: (event: string, data: any) => void
onError?: (error: any) => void
onClose?: () => void
}
export class WebSocketClient {
private client: any
private options: WebSocketClientOptions
private pingTimeout: any
private pongTimeout: any
private reconnecting: boolean = false
private retryCount: number = 0
private closeState: boolean = false
private dataArr: any[] = []
constructor(options: WebSocketClientOptions) {
this.options = options
this.connect()
}
private connect() {
this.client = new WebSocket(this.options.url)
this.setupEvents()
}
private setupEvents() {
this.client.onopen = () => {
this.reconnecting = false
this.retryCount = 0
this.sendPing()
if (this.options.onOpen) {
this.options.onOpen(this.client)
}
}
this.client.onmessage = (event: MessageEvent) => {
const data = JSON.parse(event.data)
if (data.event === 'pong') {
this.handlePong()
} else {
this.processData(data.event, data.data)
}
}
this.client.onerror = (error: any) => {
if (this.options.onError) {
this.options.onError(error)
}
}
this.client.onclose = () => {
if (this.options.onClose) {
this.options.onClose()
}
this.reconnect()
}
}
// ... 其他方法
}
typescript
心跳检测的完整实现
心跳检测的目的是让客户端和服务端互认"对方还活着"。实现原理:
- 连接成功后,客户端启动
pingInterval定时器(默认 30 秒),定期发送 ping 消息 - 每次发送 ping 后,启动
pongTimeout定时器(默认 5 秒),等待服务端回复 pong - 收到 pong 后,清除超时定时器,重置 ping 定时器等待下一轮
- 如果 pong 超时未到,认为连接异常,关闭当前连接触发重连
private sendPing() {
// 先清除旧的定时器
clearTimeout(this.pingTimeout)
// 发送 ping
this.send({ event: 'ping' })
// 设置 pong 超时检测
this.pongTimeout = setTimeout(() => {
if (this.client) {
// 超时未收到 pong,关闭连接触发重连
this.client.close()
}
}, this.options.pongTimeout || 5000)
}
private handlePong() {
// 收到 pong,清除超时定时器
clearTimeout(this.pongTimeout)
// 设置下一次 ping 的定时器
this.pingTimeout = setTimeout(() => {
this.sendPing()
}, this.options.pingInterval || 30000)
}
private send(data: any) {
if (typeof data === 'string') {
this.client.send(data)
} else {
this.client.send(JSON.stringify(data))
}
}
typescript
ping 和 pong 形成一个小循环:发送 ping -> 等待 pong -> 间隔 -> 发送 ping -> ...。两个定时器各有分工:pingTimeout 控制发送间隔,pongTimeout 检测连接健康度。
服务端心跳配合
服务端需要在收到 ping 时立即回复 pong,并设置超时清理不健康的连接:
// NestJS Gateway 中
private clientsMap = new Map()
private controlsMap = new Map()
@SubscribeMessage('message')
handleMessage(client: any, payload: any) {
const data = JSON.parse(payload)
if (data.event === 'ping') {
client.send(JSON.stringify({ event: 'pong' }))
// 清除旧的超时定时器
const clientId = client.clientId
const oldControl = this.controlsMap.get(clientId)
if (oldControl) {
clearTimeout(oldControl)
}
// 设置新的超时(60 秒内没收到下次 ping 就断开)
const control = setTimeout(() => {
client.close()
}, 60000)
this.controlsMap.set(clientId, control)
}
}
typescript
服务端通过 clientsMap 和 controlsMap 管理所有连接的心跳状态。每个客户端在连接时被分配一个唯一的 clientId(通过 UUID),ping 消息到来时重置超时定时器,超时后主动关闭连接释放 TCP 资源。
为什么心跳检测不能省略
WebSocket 长连接并不总是可靠的。网络波动、NAT 超时、代理服务器中间断开等情况都可能导致连接"假死"——客户端和服务端都以为连接还在,但实际上数据已经无法传输。心跳检测是发现这种假死状态的唯一可靠手段。每一条 WebSocket 连接都会占用一个 TCP 端口(IO 资源)和服务器内存中的实例对象,如果不清理不健康的连接,随着时间推移会积累大量无效连接,最终拖垮服务器。
↑