为什么这个2分钟后就不能完成交卷了?记一次bug修复,复习记录一下WebSocket。
一、为什么需要 WebSocket?
在 WebSocket 出现之前,面对实现实时通信的业务场景,比较常采用的方式是长、短轮询,在特定时间间隔(比如每秒)由浏览器发出请求,服务器返回最新的数据。但这种轮询方式的缺陷也相对比较明显。
- HTTP 请求一般包含的
头部信息比较多,其中有效的数据可能只占很小的一部分,导致带宽浪费; - 服务器被动接收浏览器的请求然后响应,数据没有更新时仍然要接收并处理请求,导致
服务器 CPU 占用; - 短轮询频繁轮询对服务器压力较大,即使使用长轮询方案,客户端较多时仍会对客户端造成不小压力;
而 WebSocket 的出现就很好的解决上述问题:
- WebSocket 的头部信息少,通常只有 2Bytes 左右,能节省带宽;
- WebSocket 支持服务端主动推送消息,更好地支持实时通信;
二、WebSocket 是什么?
WebSocket API 是一种先进的技术,可在用户浏览器和服务器之间开启双向交互式通信会话。利用该 API,可以向服务器发送信息,并接收事件驱动的响应,而无需轮询服务器以获得回复。
WebSocket 作为一种网络传输协议,可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。
WebSocket 属性
| 名称 | 描述 |
|---|---|
| binaryType | 使用二进制的数据类型连接 |
| bufferedAmount(只读) | 未发送至服务器的字节数 |
| extensions(只读) | 服务器选择的扩展 |
| onclose | 用于指定连接关闭后的回调函数 |
| onerror | 用于指定连接失败后的回调函数 |
| onmessage | 用于指定当从服务器接受到信息时的回调函数 |
| onopen | 用于指定连接成功后的回调函数 |
| protocol(只读) | 用于返回服务器端选中的子协议的名字 |
| readyState(只读) | 返回当前 WebSocket 的连接状态,共有 4 种状态: 0 - 表示连接尚未建立。1 - 表示连接已建立,可以进行通信。2 - 表示连接正在进行关闭。3 - 表示连接已经关闭或者连接不能打开。 |
WebSocket 方法
(已创建Socket对象)
| 方法 | 描述 |
|---|---|
| Socket.send() | 使用连接发送数据 |
| Socket.close() | 关闭连接 |
三、 WebSocket 优缺点
优点
- 实时性: WebSocket 提供了双向通信,服务器可以主动向客户端推送数据,实现实时性非常高,适用于实时聊天、在线协作等应用。
- 减少网络延迟: 与轮询和长轮询相比,WebSocket 可以显著减少网络延迟,因为不需要在每个请求之间建立和关闭连接。
- 较小的数据传输开销: WebSocket 的数据帧相比于 HTTP 请求报文较小,减少了在每个请求中传输的开销,特别适用于需要频繁通信的应用。
- 较低的服务器资源占用: 由于 WebSocket 的长连接特性,服务器可以处理更多的并发连接,相较于短连接有更低的资源占用。
- 跨域通信: 与一些其他跨域通信方法相比,WebSocket 更容易实现跨域通信。
缺点
- 连接状态保持: 长时间保持连接可能会导致服务器和客户端都需要维护连接状态,可能增加一些负担。
- 不适用于所有场景: 对于一些请求-响应模式较为简单的场景,WebSocket 的实时特性可能并不是必要的,使用 HTTP 请求可能更为合适。
- 复杂性: 与传统的 HTTP 请求相比,WebSocket 的实现和管理可能稍显复杂,尤其是在处理连接状态、异常等方面。
四、WebSocket 适用场景
- 实时聊天应用: WebSocket 是实现实时聊天室、即时通讯应用的理想选择,因为它能够提供低延迟和高实时性。
- 在线协作和协同编辑: 对于需要多用户协同工作的应用,如协同编辑文档或绘图,WebSocket 的实时性使得用户能够看到其他用户的操作。
- 实时数据展示: 对于需要实时展示数据变化的应用,例如股票行情、实时监控系统等,WebSocket 提供了一种高效的通信方式。
- 在线游戏: 在线游戏通常需要快速、实时的通信,WebSocket 能够提供低延迟和高并发的通信能力。
- 推送服务: 用于实现消息推送服务,向客户端主动推送更新或通知。
五、WebSocket 个人实践经验,仅供参考
ts
export interface WebSocketConfig {
/** WebSocket服务器地址 */
url: string;
/** 心跳间隔时间(毫秒),默认30秒 */
heartbeatInterval?: number;
/** 心跳超时时间(毫秒),默认10秒 */
heartbeatTimeout?: number;
/** 重连间隔时间(毫秒),默认5秒 */
reconnectInterval?: number;
/** 最大重连次数,默认5次,-1表示无限重连 */
maxReconnectAttempts?: number;
/** 心跳消息内容,默认为'ping' */
heartbeatMessage?: string;
/** 是否启用自动重连,默认true */
autoReconnect?: boolean;
/** 是否启用心跳检测,默认true */
enableHeartbeat?: boolean;
/** WebSocket协议 */
protocols?: string | string[];
}
export interface WebSocketEvents {
/** 连接打开 */
onOpen?: (event: Event) => void;
/** 接收到消息 */
onMessage?: (data: any, event: MessageEvent) => void;
/** 连接关闭 */
onClose?: (event: CloseEvent) => void;
/** 连接错误 */
onError?: (event: Event) => void;
/** 心跳超时 */
onHeartbeatTimeout?: () => void;
/** 重连开始 */
onReconnect?: (attempt: number) => void;
/** 重连失败 */
onReconnectFailed?: () => void;
}
export enum WebSocketState {
CONNECTING = 0,
OPEN = 1,
CLOSING = 2,
CLOSED = 3,
}
export class WebSocketManager {
private ws: WebSocket | null = null;
private config: Required<WebSocketConfig>;
private events: WebSocketEvents;
private heartbeatTimer: number | null = null;
private heartbeatTimeoutTimer: number | null = null;
private reconnectTimer: number | null = null;
private reconnectAttempts = 0;
private isManualClose = false;
private lastHeartbeatTime = 0;
constructor(config: WebSocketConfig, events: WebSocketEvents = {}) {
this.config = {
heartbeatInterval: 30000,
heartbeatTimeout: 10000,
reconnectInterval: 5000,
maxReconnectAttempts: 5,
heartbeatMessage: "ping",
autoReconnect: true,
enableHeartbeat: true,
protocols: [],
...config,
};
this.events = events;
}
/**
* 连接WebSocket
*/
public connect(): void {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
return;
}
try {
this.ws = new WebSocket(this.config.url, this.config.protocols);
this.setupEventListeners();
} catch (error) {
console.error("WebSocket连接失败:", error);
this.events.onError?.(error as Event);
}
}
/**
* 断开WebSocket连接
*/
public disconnect(): void {
this.isManualClose = true;
this.clearTimers();
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
/**
* 发送消息
*/
public send(data: string | ArrayBuffer | Blob): boolean {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return false;
}
try {
this.ws.send(data);
return true;
} catch (error) {
console.error("发送消息失败:", error);
return false;
}
}
/**
* 获取连接状态
*/
public getState(): WebSocketState {
return this.ws?.readyState ?? WebSocketState.CLOSED;
}
/**
* 是否已连接
*/
public isConnected(): boolean {
return this.ws?.readyState === WebSocket.OPEN;
}
/**
* 获取重连次数
*/
public getReconnectAttempts(): number {
return this.reconnectAttempts;
}
/**
* 设置事件监听器
*/
private setupEventListeners(): void {
if (!this.ws) return;
this.ws.onopen = (event) => {
this.isManualClose = false;
this.reconnectAttempts = 0;
this.lastHeartbeatTime = Date.now();
if (this.config.enableHeartbeat) {
this.startHeartbeat();
}
this.events.onOpen?.(event);
};
this.ws.onmessage = (event) => {
try {
// 检查是否是心跳响应
if (this.isHeartbeatResponse(event.data)) {
this.lastHeartbeatTime = Date.now();
this.clearHeartbeatTimeout();
return;
}
// 解析消息数据
let data: any;
try {
data = JSON.parse(event.data);
} catch {
data = event.data;
}
this.events.onMessage?.(data, event);
} catch (error) {
console.error("处理消息失败:", error);
}
};
this.ws.onclose = (event) => {
this.clearTimers();
this.events.onClose?.(event);
// 如果不是手动关闭且启用自动重连,则尝试重连
if (!this.isManualClose && this.config.autoReconnect) {
this.attemptReconnect();
}
};
this.ws.onerror = (event) => {
this.events.onError?.(event);
};
}
/**
* 开始心跳检测
*/
private startHeartbeat(): void {
this.clearHeartbeat();
this.heartbeatTimer = window.setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.send(this.config.heartbeatMessage);
this.startHeartbeatTimeout();
}
}, this.config.heartbeatInterval);
}
/**
* 开始心跳超时检测
*/
private startHeartbeatTimeout(): void {
this.clearHeartbeatTimeout();
this.heartbeatTimeoutTimer = window.setTimeout(() => {
this.events.onHeartbeatTimeout?.();
if (this.ws) {
this.ws.close();
}
}, this.config.heartbeatTimeout);
}
/**
* 清除心跳定时器
*/
private clearHeartbeat(): void {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}
/**
* 清除心跳超时定时器
*/
private clearHeartbeatTimeout(): void {
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
}
/**
* 清除所有定时器
*/
private clearTimers(): void {
this.clearHeartbeat();
this.clearHeartbeatTimeout();
this.clearReconnectTimer();
}
/**
* 清除重连定时器
*/
private clearReconnectTimer(): void {
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
/**
* 尝试重连
*/
private attemptReconnect(): void {
if (
this.config.maxReconnectAttempts !== -1 &&
this.reconnectAttempts >= this.config.maxReconnectAttempts
) {
this.events.onReconnectFailed?.();
return;
}
this.reconnectAttempts++;
this.events.onReconnect?.(this.reconnectAttempts);
this.reconnectTimer = window.setTimeout(() => {
this.connect();
}, this.config.reconnectInterval);
}
/**
* 检查是否是心跳响应
*/
private isHeartbeatResponse(data: string): boolean {
return data === "pong" || data === this.config.heartbeatMessage;
}
}
/**
* 创建WebSocket实例的工厂函数
*/
export function createWebSocket(
config: WebSocketConfig,
events?: WebSocketEvents
): WebSocketManager {
return new WebSocketManager(config, events);
}
/**
* 默认配置
*/
export const defaultWebSocketConfig: Partial<WebSocketConfig> = {
heartbeatInterval: 30000,
heartbeatTimeout: 10000,
reconnectInterval: 5000,
maxReconnectAttempts: 5,
heartbeatMessage: "ping",
autoReconnect: true,
enableHeartbeat: true,
};