WebSocket

WebSocket 面试笔记

一、WebSocket 基础概念

1.1 什么是 WebSocket?

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,允许服务端主动向客户端推送数据。

核心特点:

  • 全双工通信:客户端和服务端可以同时发送和接收数据
  • 持久连接:建立连接后保持长连接状态
  • 低延迟:无需频繁建立连接,实时性强
  • 较少的控制开销:相比 HTTP 轮询,数据包头部更小

1.2 WebSocket 的应用场景

  • 实时聊天应用
  • 在线协作工具(如文档编辑)
  • 实时数据推送(股票行情、体育比分)
  • 多人在线游戏
  • 实时监控系统
  • 视频弹幕

二、HTTP vs WebSocket

2.1 协议对比

特性 HTTP WebSocket
通信方式 半双工(请求-响应) 全双工
连接方式 短连接(HTTP/1.1 可keep-alive) 长连接
服务端推送 不支持(需轮询/长轮询) 原生支持
协议标识 http:// 或 https:// ws:// 或 wss://
数据格式 文本(带完整HTTP头) 二进制帧(头部小)
开销 每次请求都有完整HTTP头 首次握手后头部很小
状态 无状态 有状态(连接保持)

2.2 HTTP 实现实时通信的方式

短轮询(Polling):

1
2
3
4
5
6
// 客户端每隔一段时间发送请求
setInterval(() => {
fetch('/api/messages')
.then(res => res.json())
.then(data => updateUI(data));
}, 3000);

缺点:大量无效请求,服务器压力大,实时性差

长轮询(Long Polling):

1
2
3
4
5
6
7
8
function longPoll() {
fetch('/api/messages')
.then(res => res.json())
.then(data => {
updateUI(data);
longPoll(); // 收到响应后立即发起下一次请求
});
}

缺点:仍需频繁建立连接,服务器需要保持大量挂起的请求

SSE(Server-Sent Events):

1
2
3
4
const eventSource = new EventSource('/api/stream');
eventSource.onmessage = (event) => {
console.log(event.data);
};

缺点:只支持服务端到客户端的单向通信

2.3 为什么选择 WebSocket?

当需要:

  • 高频率的双向数据交互
  • 低延迟要求
  • 服务端主动推送
  • 减少网络开销

三、WebSocket 连接建立过程

3.1 握手过程

WebSocket 通过 HTTP 协议进行握手升级:

客户端请求:

1
2
3
4
5
6
7
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com

服务端响应:

1
2
3
4
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

关键字段说明:

  • Upgrade: websocket:请求升级协议
  • Connection: Upgrade:表示要升级连接
  • Sec-WebSocket-Key:客户端生成的随机字符串(Base64编码)
  • Sec-WebSocket-Accept:服务端根据Key计算的值,用于验证
  • 101 Switching Protocols:协议切换成功

3.2 前端基础使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 创建 WebSocket 连接
const ws = new WebSocket('ws://localhost:8080');

// 连接成功
ws.onopen = (event) => {
console.log('连接已建立');
ws.send('Hello Server!');
};

// 接收消息
ws.onmessage = (event) => {
console.log('收到消息:', event.data);
};

// 连接关闭
ws.onclose = (event) => {
console.log('连接已关闭', event.code, event.reason);
};

// 连接错误
ws.onerror = (error) => {
console.error('WebSocket错误:', error);
};

// 发送消息
ws.send('Hello');
ws.send(JSON.stringify({ type: 'message', content: 'Hello' }));

// 关闭连接
ws.close(1000, 'Normal closure');

3.3 连接状态

1
2
3
4
5
// WebSocket.readyState 的值
ws.CONNECTING // 0 - 正在连接
ws.OPEN // 1 - 已连接
ws.CLOSING // 2 - 正在关闭
ws.CLOSED // 3 - 已关闭

四、心跳检测机制

4.1 为什么需要心跳检测?

  • 检测连接是否存活(网络异常、服务器崩溃等)
  • 防止连接被中间代理或防火墙断开(长时间无数据传输)
  • 及时发现”假死”连接

4.2 心跳检测实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.heartbeatInterval = 30000; // 30秒
this.heartbeatTimer = null;
this.serverHeartbeatTimer = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
}

connect() {
this.ws = new WebSocket(this.url);

this.ws.onopen = () => {
console.log('连接成功');
this.reconnectAttempts = 0;
this.startHeartbeat();
};

this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);

// 收到服务端心跳响应
if (data.type === 'pong') {
console.log('收到心跳响应');
this.resetServerHeartbeat();
return;
}

// 处理业务消息
this.handleMessage(data);
};

this.ws.onclose = () => {
console.log('连接关闭');
this.stopHeartbeat();
this.reconnect();
};

this.ws.onerror = (error) => {
console.error('连接错误:', error);
};
}

// 启动心跳
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
console.log('发送心跳');
}
}, this.heartbeatInterval);

// 启动服务端心跳超时检测
this.resetServerHeartbeat();
}

// 重置服务端心跳超时计时器
resetServerHeartbeat() {
clearTimeout(this.serverHeartbeatTimer);
this.serverHeartbeatTimer = setTimeout(() => {
console.log('服务端心跳超时,关闭连接');
this.ws.close();
}, this.heartbeatInterval + 5000); // 比心跳间隔多5秒
}

// 停止心跳
stopHeartbeat() {
clearInterval(this.heartbeatTimer);
clearTimeout(this.serverHeartbeatTimer);
}

// 发送消息
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
} else {
console.error('连接未建立');
}
}

// 处理业务消息
handleMessage(data) {
console.log('收到消息:', data);
}

// 断线重连(下一节详细说明)
reconnect() {
// 实现见下节
}
}

4.3 心跳策略优化

客户端心跳 vs 服务端心跳:

  • 客户端主动发送心跳:适合客户端需要确认连接状态
  • 服务端主动发送心跳:适合服务端需要清理无效连接
  • 双向心跳:最可靠,但开销稍大

心跳间隔设置:

  • 太短:增加网络开销
  • 太长:无法及时发现断线
  • 建议:30-60秒,根据业务场景调整

优化技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. 有业务消息时不发心跳
let lastMessageTime = Date.now();

ws.onmessage = (event) => {
lastMessageTime = Date.now();
// 处理消息
};

setInterval(() => {
if (Date.now() - lastMessageTime > 30000) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);

// 2. 页面可见性优化
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// 页面隐藏时降低心跳频率或停止
clearInterval(heartbeatTimer);
} else {
// 页面显示时恢复心跳
startHeartbeat();
}
});

五、断线重连机制

5.1 重连策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class WebSocketClient {
constructor(url, options = {}) {
this.url = url;
this.ws = null;

// 重连配置
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
this.reconnectInterval = options.reconnectInterval || 1000;
this.maxReconnectInterval = options.maxReconnectInterval || 30000;
this.reconnectDecay = options.reconnectDecay || 1.5; // 指数退避

this.isManualClose = false; // 是否手动关闭
this.messageQueue = []; // 消息队列
}

connect() {
this.isManualClose = false;
this.ws = new WebSocket(this.url);

this.ws.onopen = () => {
console.log('连接成功');
this.reconnectAttempts = 0;
this.reconnectInterval = 1000; // 重置重连间隔
this.flushMessageQueue(); // 发送队列中的消息
this.startHeartbeat();
};

this.ws.onclose = (event) => {
console.log('连接关闭', event.code, event.reason);
this.stopHeartbeat();

// 非手动关闭且未超过最大重连次数
if (!this.isManualClose && this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnect();
}
};

this.ws.onerror = (error) => {
console.error('连接错误:', error);
};
}

reconnect() {
this.reconnectAttempts++;
console.log(`第 ${this.reconnectAttempts} 次重连...`);

setTimeout(() => {
this.connect();
}, this.getReconnectInterval());
}

// 计算重连间隔(指数退避算法)
getReconnectInterval() {
const interval = this.reconnectInterval * Math.pow(this.reconnectDecay, this.reconnectAttempts - 1);
return Math.min(interval, this.maxReconnectInterval);
}

// 手动关闭连接
close() {
this.isManualClose = true;
this.stopHeartbeat();
this.ws?.close();
}

// 发送消息(带队列)
send(data) {
const message = JSON.stringify(data);

if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(message);
} else {
// 连接未建立,加入队列
this.messageQueue.push(message);
console.log('消息已加入队列');
}
}

// 发送队列中的消息
flushMessageQueue() {
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
this.ws.send(message);
}
}
}

5.2 重连策略对比

固定间隔重连:

1
2
// 每次都等待固定时间(如5秒)
setTimeout(() => this.connect(), 5000);

优点:简单
缺点:服务器压力大,可能造成雪崩

指数退避重连:

1
2
// 1秒 -> 1.5秒 -> 2.25秒 -> 3.375秒 ...
const interval = baseInterval * Math.pow(decay, attempts);

优点:减轻服务器压力,更合理
缺点:需要设置最大间隔

随机延迟重连:

1
2
// 在基础间隔上加随机值,避免大量客户端同时重连
const interval = baseInterval + Math.random() * 1000;

5.3 重连优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class WebSocketClient {
// ... 前面的代码

// 网络状态监听
setupNetworkListener() {
window.addEventListener('online', () => {
console.log('网络已恢复');
if (this.ws.readyState !== WebSocket.OPEN) {
this.reconnectAttempts = 0; // 重置重连次数
this.connect();
}
});

window.addEventListener('offline', () => {
console.log('网络已断开');
this.stopHeartbeat();
});
}

// 页面可见性监听
setupVisibilityListener() {
document.addEventListener('visibilitychange', () => {
if (!document.hidden && this.ws.readyState !== WebSocket.OPEN) {
console.log('页面重新可见,检查连接');
this.connect();
}
});
}

// 连接状态检查
checkConnection() {
if (this.ws.readyState !== WebSocket.OPEN) {
console.log('连接异常,尝试重连');
this.reconnect();
}
}
}

六、完整封装示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
class WebSocketManager {
constructor(url, options = {}) {
this.url = url;
this.ws = null;

// 配置项
this.options = {
heartbeatInterval: 30000,
heartbeatTimeout: 35000,
reconnectInterval: 1000,
maxReconnectInterval: 30000,
maxReconnectAttempts: 5,
reconnectDecay: 1.5,
...options
};

// 状态
this.reconnectAttempts = 0;
this.isManualClose = false;
this.messageQueue = [];

// 定时器
this.heartbeatTimer = null;
this.serverHeartbeatTimer = null;

// 事件监听器
this.listeners = {
open: [],
message: [],
close: [],
error: [],
reconnect: []
};

this.init();
}

init() {
this.setupNetworkListener();
this.setupVisibilityListener();
}

connect() {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
return;
}

this.isManualClose = false;
this.ws = new WebSocket(this.url);

this.ws.onopen = (event) => {
console.log('WebSocket 连接成功');
this.reconnectAttempts = 0;
this.reconnectInterval = this.options.reconnectInterval;
this.startHeartbeat();
this.flushMessageQueue();
this.emit('open', event);
};

this.ws.onmessage = (event) => {
let data;
try {
data = JSON.parse(event.data);
} catch (e) {
data = event.data;
}

// 处理心跳响应
if (data.type === 'pong') {
this.resetServerHeartbeat();
return;
}

this.emit('message', data);
};

this.ws.onclose = (event) => {
console.log('WebSocket 连接关闭', event.code, event.reason);
this.stopHeartbeat();
this.emit('close', event);

if (!this.isManualClose && this.reconnectAttempts < this.options.maxReconnectAttempts) {
this.reconnect();
}
};

this.ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
this.emit('error', error);
};
}

reconnect() {
this.reconnectAttempts++;
const interval = this.getReconnectInterval();

console.log(`第 ${this.reconnectAttempts} 次重连,${interval}ms 后执行`);
this.emit('reconnect', { attempts: this.reconnectAttempts, interval });

setTimeout(() => {
this.connect();
}, interval);
}

getReconnectInterval() {
const interval = this.options.reconnectInterval *
Math.pow(this.options.reconnectDecay, this.reconnectAttempts - 1);
return Math.min(interval, this.options.maxReconnectInterval);
}

startHeartbeat() {
this.stopHeartbeat();

this.heartbeatTimer = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping', timestamp: Date.now() }));
}
}, this.options.heartbeatInterval);

this.resetServerHeartbeat();
}

resetServerHeartbeat() {
clearTimeout(this.serverHeartbeatTimer);
this.serverHeartbeatTimer = setTimeout(() => {
console.log('服务端心跳超时');
this.ws.close();
}, this.options.heartbeatTimeout);
}

stopHeartbeat() {
clearInterval(this.heartbeatTimer);
clearTimeout(this.serverHeartbeatTimer);
}

send(data) {
const message = typeof data === 'string' ? data : JSON.stringify(data);

if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(message);
} else {
this.messageQueue.push(message);
console.warn('连接未就绪,消息已加入队列');
}
}

flushMessageQueue() {
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
this.ws.send(message);
}
}

close() {
this.isManualClose = true;
this.stopHeartbeat();
this.ws?.close();
}

// 事件监听
on(event, callback) {
if (this.listeners[event]) {
this.listeners[event].push(callback);
}
}

off(event, callback) {
if (this.listeners[event]) {
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
}
}

emit(event, data) {
if (this.listeners[event]) {
this.listeners[event].forEach(callback => callback(data));
}
}

setupNetworkListener() {
window.addEventListener('online', () => {
console.log('网络已恢复');
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
this.reconnectAttempts = 0;
this.connect();
}
});

window.addEventListener('offline', () => {
console.log('网络已断开');
this.stopHeartbeat();
});
}

setupVisibilityListener() {
document.addEventListener('visibilitychange', () => {
if (!document.hidden && (!this.ws || this.ws.readyState !== WebSocket.OPEN)) {
this.connect();
}
});
}

getState() {
const states = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
return this.ws ? states[this.ws.readyState] : 'CLOSED';
}
}

// 使用示例
const wsManager = new WebSocketManager('ws://localhost:8080', {
heartbeatInterval: 30000,
maxReconnectAttempts: 10
});

wsManager.on('open', () => {
console.log('连接已建立');
});

wsManager.on('message', (data) => {
console.log('收到消息:', data);
});

wsManager.on('reconnect', ({ attempts, interval }) => {
console.log(`正在重连: 第${attempts}次`);
});

wsManager.connect();
wsManager.send({ type: 'chat', message: 'Hello' });

七、高级面试题

7.1 WebSocket 的安全性

问题:如何保证 WebSocket 的安全性?

  1. 使用 WSS(WebSocket Secure)
1
const ws = new WebSocket('wss://example.com');
  1. Token 认证
1
2
3
4
5
6
7
// 方式1:URL参数
const ws = new WebSocket(`wss://example.com?token=${token}`);

// 方式2:首次连接后发送认证消息
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'auth', token }));
};
  1. Origin 验证(服务端)
1
2
3
4
// 服务端检查请求来源
if (request.headers.origin !== 'https://trusted-domain.com') {
reject();
}
  1. 消息加密
1
2
3
// 敏感数据加密后传输
const encrypted = encrypt(data);
ws.send(encrypted);
  1. 防止 XSS 和 CSRF
  • 对接收的数据进行验证和转义
  • 使用 CSP(Content Security Policy)

7.2 WebSocket 的性能优化

问题:如何优化 WebSocket 性能?

  1. 消息压缩
1
2
3
4
// 使用 permessage-deflate 扩展
const ws = new WebSocket('ws://example.com', {
perMessageDeflate: true
});
  1. 二进制数据传输
1
2
3
4
// 使用 ArrayBuffer 或 Blob
ws.binaryType = 'arraybuffer';
const buffer = new ArrayBuffer(8);
ws.send(buffer);
  1. 消息批量发送
1
2
3
4
// 合并多个小消息
const batch = [];
batch.push(msg1, msg2, msg3);
ws.send(JSON.stringify(batch));
  1. 消息节流
1
2
3
4
5
6
7
let pending = [];
const throttle = setInterval(() => {
if (pending.length > 0) {
ws.send(JSON.stringify(pending));
pending = [];
}
}, 100);
  1. 连接池管理
1
2
3
4
5
6
7
8
9
10
11
12
13
// 多个 WebSocket 连接复用
class WebSocketPool {
constructor(urls) {
this.connections = urls.map(url => new WebSocket(url));
this.currentIndex = 0;
}

send(data) {
const ws = this.connections[this.currentIndex];
ws.send(data);
this.currentIndex = (this.currentIndex + 1) % this.connections.length;
}
}

7.3 WebSocket 的兼容性处理

问题:如何处理不支持 WebSocket 的浏览器?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createConnection(url) {
if ('WebSocket' in window) {
return new WebSocket(url);
} else if ('MozWebSocket' in window) {
return new MozWebSocket(url);
} else {
// 降级方案:使用长轮询
return new LongPollingClient(url);
}
}

// 使用 Socket.IO 等库自动降级
import io from 'socket.io-client';
const socket = io('http://localhost:3000');

7.4 大量消息处理

问题:如何处理高频消息导致的性能问题?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 1. 使用 requestAnimationFrame 批量更新 UI
let messageBuffer = [];

ws.onmessage = (event) => {
messageBuffer.push(JSON.parse(event.data));
};

function updateUI() {
if (messageBuffer.length > 0) {
const messages = messageBuffer.splice(0, messageBuffer.length);
renderMessages(messages);
}
requestAnimationFrame(updateUI);
}
requestAnimationFrame(updateUI);

// 2. 虚拟滚动
import VirtualList from 'virtual-list';
const list = new VirtualList({
container: document.getElementById('messages'),
items: messages,
renderItem: (item) => `<div>${item.content}</div>`
});

// 3. Web Worker 处理消息
// main.js
const worker = new Worker('message-worker.js');
ws.onmessage = (event) => {
worker.postMessage(event.data);
};
worker.onmessage = (event) => {
updateUI(event.data);
};

// message-worker.js
self.onmessage = (event) => {
const processed = processMessage(event.data);
self.postMessage(processed);
};

7.5 断线重连的边界情况

问题:重连时如何处理消息丢失?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class ReliableWebSocket {
constructor(url) {
this.url = url;
this.messageId = 0;
this.pendingMessages = new Map(); // 未确认的消息
this.receivedMessages = new Set(); // 已接收的消息ID(去重)
}

send(data) {
const id = ++this.messageId;
const message = { id, data, timestamp: Date.now() };

this.pendingMessages.set(id, message);
this.ws.send(JSON.stringify(message));

// 设置超时重发
setTimeout(() => {
if (this.pendingMessages.has(id)) {
this.resend(id);
}
}, 5000);
}

onMessage(event) {
const message = JSON.parse(event.data);

// 消息确认
if (message.type === 'ack') {
this.pendingMessages.delete(message.id);
return;
}

// 去重
if (this.receivedMessages.has(message.id)) {
return;
}
this.receivedMessages.add(message.id);

// 发送确认
this.ws.send(JSON.stringify({ type: 'ack', id: message.id }));

// 处理消息
this.handleMessage(message);
}

resend(id) {
const message = this.pendingMessages.get(id);
if (message && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
}
}

onReconnect() {
// 重连后重发未确认的消息
this.pendingMessages.forEach((message) => {
this.ws.send(JSON.stringify(message));
});
}
}

7.6 多标签页同步

问题:如何在多个标签页间同步 WebSocket 状态?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 使用 BroadcastChannel
const channel = new BroadcastChannel('websocket-sync');

// 主标签页
let isLeader = false;

channel.onmessage = (event) => {
if (event.data.type === 'leader-check') {
if (isLeader) {
channel.postMessage({ type: 'leader-exists' });
}
} else if (event.data.type === 'message') {
handleMessage(event.data.payload);
}
};

// 检查是否需要成为主标签页
channel.postMessage({ type: 'leader-check' });
setTimeout(() => {
if (!isLeader) {
isLeader = true;
connectWebSocket();
}
}, 100);

ws.onmessage = (event) => {
// 广播给其他标签页
channel.postMessage({ type: 'message', payload: event.data });
};

// 使用 SharedWorker(更优方案)
const worker = new SharedWorker('websocket-worker.js');
worker.port.start();

worker.port.onmessage = (event) => {
console.log('收到消息:', event.data);
};

worker.port.postMessage({ type: 'send', data: 'Hello' });

7.7 WebSocket vs Server-Sent Events

问题:什么时候选择 SSE 而不是 WebSocket?

特性 WebSocket SSE
通信方向 双向 单向(服务端→客户端)
协议 独立协议 基于 HTTP
数据格式 文本/二进制 仅文本
浏览器支持 好(IE不支持)
自动重连 需手动实现 原生支持
复杂度 较高 较低

选择 SSE 的场景:

  • 只需服务端推送(如通知、实时更新)
  • 需要自动重连
  • 希望利用 HTTP/2 多路复用

八、面试口述版

8.1 WebSocket 基础

面试官:介绍一下 WebSocket

WebSocket 是 HTML5 提供的一种在单个 TCP 连接上进行全双工通信的协议。它最大的特点是服务器可以主动向客户端推送消息,客户端也可以主动向服务器发送消息,实现真正的双向平等对话。

它解决了 HTTP 协议的一些局限性。HTTP 是半双工的,只能由客户端发起请求,服务器被动响应。如果要实现服务器推送,传统方案只能用轮询或长轮询,这会造成大量无效请求和资源浪费。

WebSocket 的连接建立过程是先通过 HTTP 发起握手请求,请求头里会带上 Upgrade: websocket 和 Connection: Upgrade,告诉服务器要升级协议。服务器同意后返回 101 状态码,之后就切换到 WebSocket 协议进行通信了。

8.2 HTTP vs WebSocket

面试官:WebSocket 和 HTTP 有什么区别?

主要有几个方面:

首先是通信方式,HTTP 是请求-响应模式,只能客户端主动发起;WebSocket 是全双工的,双方都可以随时发送消息。

其次是连接方式,HTTP 是短连接,每次请求都要建立连接(虽然 HTTP/1.1 有 keep-alive,但本质还是请求-响应);WebSocket 是长连接,建立后会一直保持。

再就是开销,HTTP 每次请求都要带完整的请求头,包括 Cookie、User-Agent 等,数据量大;WebSocket 握手后的数据帧头部很小,只有几个字节。

最后是实时性,HTTP 要实现实时推送只能轮询,延迟高;WebSocket 可以做到毫秒级的实时通信。

所以 WebSocket 特别适合需要高频双向通信的场景,比如在线聊天、实时协作、游戏、股票行情等。

8.3 心跳检测

面试官:为什么需要心跳检测?怎么实现?

心跳检测主要有三个目的:

第一是检测连接是否还活着。网络可能出现异常,或者服务器崩溃了,但客户端不知道,还以为连接正常。通过定期发送心跳包,如果长时间没收到响应,就知道连接断了。

第二是防止连接被中间设备断开。很多代理服务器或防火墙会关闭长时间没有数据传输的连接。定期发送心跳可以保持连接活跃。

第三是及时发现”假死”连接。有时候连接看起来是正常的,但实际上已经无法通信了,心跳可以发现这种情况。

实现上,我一般是客户端定时发送 ping 消息,服务端收到后回复 pong。比如每 30 秒发一次心跳,如果 35 秒内没收到 pong,就认为连接断了,主动关闭并重连。

代码实现就是用 setInterval 定时发送,用 setTimeout 检测超时。收到 pong 后要重置超时计时器。

还有个优化点是,如果有业务消息在传输,就不需要发心跳了,因为有数据传输就说明连接是正常的。

8.4 断线重连

面试官:断线重连怎么实现?有什么注意点?

断线重连的核心是在 onclose 事件里判断是否需要重连,如果不是手动关闭的,就尝试重新建立连接。

但不能无限重连,要设置最大重连次数,比如 5 次或 10 次。超过了就放弃,避免无意义的请求。

重连间隔也很重要,我一般用指数退避算法。第一次重连等 1 秒,第二次 1.5 秒,第三次 2.25 秒,以此类推,但要设置一个上限,比如最多 30 秒。这样可以避免大量客户端同时断线后同时重连,造成服务器压力过大。

还可以加一些随机延迟,进一步分散重连时间。

另外要注意的是,重连成功后,之前没发送成功的消息要重新发送。所以我会维护一个消息队列,连接断开时把消息放进去,重连成功后再发出去。

还有一些优化,比如监听网络状态变化,网络恢复时立即重连;监听页面可见性,页面重新可见时检查连接状态。

最重要的是要区分手动关闭和异常断开,手动关闭就不要重连了。

8.5 安全性

面试官:WebSocket 有哪些安全问题?怎么防范?

主要有几个方面:

第一是传输安全,要用 WSS 而不是 WS,就像 HTTPS 和 HTTP 的区别,WSS 是加密的。

第二是身份认证,建立连接时要验证用户身份。可以在 URL 里带 token,或者连接建立后第一条消息发送 token。服务端验证通过才允许后续通信。

第三是来源验证,服务端要检查 Origin 请求头,只允许信任的域名连接,防止 CSRF 攻击。

第四是消息验证,对接收到的消息要做校验和过滤,防止 XSS 攻击。不能直接把收到的内容插入 DOM,要做转义处理。

第五是限流和防护,要限制连接数、消息频率、消息大小,防止 DDoS 攻击。

还有就是敏感数据要加密,不要明文传输。

8.6 性能优化

面试官:WebSocket 有哪些性能优化手段?

首先是消息层面的优化。可以启用消息压缩,减少传输数据量。对于二进制数据,用 ArrayBuffer 而不是字符串,效率更高。多个小消息可以合并成一个批量发送,减少网络开销。

然后是高频消息的处理。如果消息很频繁,不要每条消息都立即更新 UI,可以用 requestAnimationFrame 批量更新,或者做节流处理。对于大量数据的展示,用虚拟滚动,只渲染可见区域。

如果消息处理比较复杂,可以放到 Web Worker 里,避免阻塞主线程。

连接层面,可以考虑连接池,建立多个连接分散负载。但要注意不要建太多,浏览器和服务器都有限制。

还有就是心跳间隔要合理设置,太频繁会增加开销,太长又无法及时发现断线。一般 30-60 秒比较合适。

8.7 实际项目经验

面试官:在实际项目中使用 WebSocket 遇到过什么问题?

我之前做过一个在线协作文档的项目,用 WebSocket 实现多人实时编辑。

遇到的一个问题是消息顺序。多个用户同时编辑,消息到达顺序可能不一致,导致内容冲突。我们用了操作转换(OT)算法来解决,每个操作都有版本号和位置信息,可以正确合并。

另一个问题是断线重连后的状态同步。用户断线期间可能错过了很多消息,重连后要拉取这段时间的所有变更。我们在服务端维护了一个变更日志,客户端重连时带上最后一次的版本号,服务端返回之后的所有变更。

还有就是大文件传输的问题。WebSocket 不适合传大文件,我们改成先上传到 OSS,然后通过 WebSocket 发送文件 URL。

性能方面,一开始每个字符变化都发消息,太频繁了。后来改成防抖处理,用户停止输入 300ms 后才发送,大大减少了消息量。

多标签页同步也是个问题,我们用 SharedWorker 实现了一个标签页建立连接,其他标签页共享这个连接,避免重复连接。

8.8 与其他方案对比

面试官:什么时候用 WebSocket,什么时候用其他方案?

这要看具体场景:

如果只是服务端偶尔推送通知,不需要客户端频繁发消息,用 SSE(Server-Sent Events)就够了,它更简单,而且自带重连。

如果是低频的请求-响应,比如提交表单、查询数据,用普通 HTTP 就行,不需要 WebSocket。

如果需要高频双向通信,比如聊天、游戏、实时协作,WebSocket 是最佳选择。

如果需要兼容老浏览器,可以用 Socket.IO 这样的库,它会自动降级到长轮询。

对于一些特殊场景,比如视频流传输,WebRTC 可能更合适。

总的来说,要根据实时性要求、通信频率、数据量、兼容性等因素综合考虑。

九、常见面试题补充

9.1 WebSocket 的状态码

问题:WebSocket 关闭时的状态码有哪些?

常见状态码:

  • 1000:正常关闭
  • 1001:端点离开(如页面关闭)
  • 1002:协议错误
  • 1003:不支持的数据类型
  • 1006:异常关闭(没有发送关闭帧)
  • 1007:数据格式错误
  • 1008:违反策略
  • 1009:消息过大
  • 1010:客户端期望协商扩展
  • 1011:服务器遇到意外情况
1
2
3
4
5
6
7
8
ws.onclose = (event) => {
console.log('关闭码:', event.code);
console.log('关闭原因:', event.reason);
console.log('是否干净关闭:', event.wasClean);
};

// 主动关闭时指定状态码
ws.close(1000, 'Normal closure');

9.2 WebSocket 的子协议

问题:什么是 WebSocket 子协议?

子协议是在 WebSocket 之上定义的应用层协议,用于约定消息格式和通信规则。

1
2
3
4
5
6
// 客户端指定子协议
const ws = new WebSocket('ws://example.com', ['soap', 'wamp']);

// 服务端选择一个子协议
// 客户端可以通过 ws.protocol 查看
console.log('使用的子协议:', ws.protocol);

常见子协议:

  • STOMP:消息队列协议
  • WAMP:Web Application Messaging Protocol
  • MQTT:物联网消息协议

9.3 WebSocket 的扩展

问题:WebSocket 支持哪些扩展?

主要扩展:

  • permessage-deflate:消息压缩
  • permessage-bzip2:使用 bzip2 压缩
  • multiplexing:多路复用
1
2
3
4
5
6
// 握手时协商扩展
// 客户端请求
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

// 服务端响应
Sec-WebSocket-Extensions: permessage-deflate

9.4 WebSocket 的限制

问题:WebSocket 有哪些限制?

  1. 浏览器限制
  • 同一域名下的并发连接数有限制(通常 6-10 个)
  • 移动端后台运行时可能被系统挂起
  1. 消息大小限制
  • 浏览器通常限制单条消息大小(几 MB 到几十 MB)
  • 服务端也会设置限制
  1. 网络限制
  • 某些企业网络或防火墙可能阻止 WebSocket
  • 需要降级方案
  1. 代理问题
  • 某些 HTTP 代理不支持 WebSocket 升级
  • 需要配置代理支持
1
2
3
4
5
6
// 检测 WebSocket 支持
if ('WebSocket' in window) {
console.log('支持 WebSocket');
} else {
console.log('不支持 WebSocket,使用降级方案');
}

9.5 WebSocket 与 HTTP/2

问题:HTTP/2 出现后,WebSocket 还有必要吗?

HTTP/2 虽然支持服务器推送(Server Push),但它和 WebSocket 的推送是不同的:

HTTP/2 Server Push:

  • 服务器主动推送资源(如 CSS、JS)
  • 客户端必须先发起请求
  • 推送的是完整的 HTTP 响应
  • 适合推送静态资源

WebSocket:

  • 真正的双向通信
  • 任何时候都可以发送消息
  • 推送的是应用数据
  • 适合实时交互

所以两者是互补的,不是替代关系。HTTP/2 适合优化页面加载,WebSocket 适合实时通信。

9.6 WebSocket 的调试

问题:如何调试 WebSocket?

  1. Chrome DevTools
  • Network 面板 -> WS 标签
  • 可以查看握手请求、消息收发、连接状态
  • 可以查看消息内容和时间戳
  1. 使用工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 添加日志
const originalSend = WebSocket.prototype.send;
WebSocket.prototype.send = function(data) {
console.log('发送:', data);
return originalSend.call(this, data);
};

// 使用代理
class WebSocketProxy {
constructor(url) {
this.ws = new WebSocket(url);
this.ws.onmessage = (event) => {
console.log('接收:', event.data);
this.onmessage?.(event);
};
}

send(data) {
console.log('发送:', data);
this.ws.send(data);
}
}
  1. 第三方工具
  • Postman:支持 WebSocket 测试
  • wscat:命令行工具
  • WebSocket King:Chrome 插件

9.7 WebSocket 的测试

问题:如何测试 WebSocket 功能?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 单元测试(使用 Mock)
import { WebSocket } from 'mock-socket';

describe('WebSocket', () => {
let ws;

beforeEach(() => {
ws = new WebSocket('ws://localhost:8080');
});

it('should connect', (done) => {
ws.onopen = () => {
expect(ws.readyState).toBe(WebSocket.OPEN);
done();
};
});

it('should send message', (done) => {
ws.onopen = () => {
ws.send('test');
};

ws.onmessage = (event) => {
expect(event.data).toBe('test');
done();
};
});
});

// 集成测试
describe('WebSocket Integration', () => {
it('should handle reconnection', async () => {
const ws = new WebSocketManager('ws://localhost:8080');
ws.connect();

// 模拟断线
ws.ws.close();

// 等待重连
await new Promise(resolve => {
ws.on('open', resolve);
});

expect(ws.getState()).toBe('OPEN');
});
});

9.8 WebSocket 的监控

问题:生产环境如何监控 WebSocket?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class MonitoredWebSocket {
constructor(url) {
this.url = url;
this.metrics = {
connectTime: 0,
messagesSent: 0,
messagesReceived: 0,
reconnectCount: 0,
errors: 0,
avgLatency: 0
};
}

connect() {
const startTime = Date.now();
this.ws = new WebSocket(this.url);

this.ws.onopen = () => {
this.metrics.connectTime = Date.now() - startTime;
this.reportMetrics('connect', this.metrics.connectTime);
};

this.ws.onmessage = (event) => {
this.metrics.messagesReceived++;

// 计算延迟
const data = JSON.parse(event.data);
if (data.timestamp) {
const latency = Date.now() - data.timestamp;
this.updateLatency(latency);
}
};

this.ws.onerror = () => {
this.metrics.errors++;
this.reportMetrics('error', this.metrics.errors);
};
}

send(data) {
this.metrics.messagesSent++;
this.ws.send(JSON.stringify({
...data,
timestamp: Date.now()
}));
}

updateLatency(latency) {
const { avgLatency, messagesReceived } = this.metrics;
this.metrics.avgLatency = (avgLatency * (messagesReceived - 1) + latency) / messagesReceived;
}

reportMetrics(event, value) {
// 上报到监控系统
fetch('/api/metrics', {
method: 'POST',
body: JSON.stringify({
event,
value,
timestamp: Date.now(),
metrics: this.metrics
})
});
}
}

监控指标:

  • 连接成功率
  • 平均连接时间
  • 消息发送/接收量
  • 平均延迟
  • 重连次数
  • 错误率
  • 在线用户数

十、总结

10.1 核心知识点

  1. WebSocket 是全双工通信协议,通过 HTTP 握手升级
  2. 相比 HTTP 轮询,WebSocket 实时性更好、开销更小
  3. 心跳检测用于保持连接活跃和检测连接状态
  4. 断线重连需要指数退避算法和消息队列
  5. 安全性要考虑 WSS、认证、来源验证、消息过滤
  6. 性能优化包括消息压缩、批量发送、节流、虚拟滚动

10.2 最佳实践

  1. 始终使用 WSS 而不是 WS
  2. 实现完善的心跳和重连机制
  3. 合理设置心跳间隔(30-60秒)
  4. 使用指数退避算法重连
  5. 维护消息队列处理断线期间的消息
  6. 监听网络状态和页面可见性
  7. 对高频消息做节流处理
  8. 添加完善的错误处理和日志
  9. 生产环境要有监控和告警
  10. 提供降级方案(如长轮询)

10.3 面试准备建议

  1. 理解 WebSocket 的工作原理和握手过程
  2. 能手写心跳检测和断线重连的代码
  3. 了解与 HTTP、SSE 的区别和适用场景
  4. 掌握常见的性能优化和安全防护手段
  5. 准备实际项目中的使用经验和遇到的问题
  6. 了解浏览器兼容性和降级方案
  7. 熟悉调试和测试方法

10.4 进阶学习方向

  1. WebRTC:点对点实时通信
  2. Socket.IO:跨平台实时通信库
  3. MQTT:物联网消息协议
  4. GraphQL Subscriptions:基于 WebSocket 的订阅
  5. 操作转换(OT)和 CRDT:协同编辑算法
  6. WebSocket 集群和负载均衡
  7. WebSocket 网关和代理
  8. 消息队列集成(Redis、RabbitMQ)

参考资源