WebSocketWebSocket
OHNIIWebSocket 面试笔记
一、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
| 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
| ws.CONNECTING ws.OPEN ws.CLOSING ws.CLOSED
|
四、心跳检测机制
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; 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); }
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
| let lastMessageTime = Date.now();
ws.onmessage = (event) => { lastMessageTime = Date.now(); };
setInterval(() => { if (Date.now() - lastMessageTime > 30000) { ws.send(JSON.stringify({ type: 'ping' })); } }, 30000);
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
| setTimeout(() => this.connect(), 5000);
|
优点:简单
缺点:服务器压力大,可能造成雪崩
指数退避重连:
1 2
| 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 的安全性?
- 使用 WSS(WebSocket Secure)
1
| const ws = new WebSocket('wss://example.com');
|
- Token 认证
1 2 3 4 5 6 7
| const ws = new WebSocket(`wss://example.com?token=${token}`);
ws.onopen = () => { ws.send(JSON.stringify({ type: 'auth', token })); };
|
- Origin 验证(服务端)
1 2 3 4
| if (request.headers.origin !== 'https://trusted-domain.com') { reject(); }
|
- 消息加密
1 2 3
| const encrypted = encrypt(data); ws.send(encrypted);
|
- 防止 XSS 和 CSRF
- 对接收的数据进行验证和转义
- 使用 CSP(Content Security Policy)
7.2 WebSocket 的性能优化
问题:如何优化 WebSocket 性能?
- 消息压缩
1 2 3 4
| const ws = new WebSocket('ws://example.com', { perMessageDeflate: true });
|
- 二进制数据传输
1 2 3 4
| ws.binaryType = 'arraybuffer'; const buffer = new ArrayBuffer(8); ws.send(buffer);
|
- 消息批量发送
1 2 3 4
| const batch = []; batch.push(msg1, msg2, msg3); ws.send(JSON.stringify(batch));
|
- 消息节流
1 2 3 4 5 6 7
| let pending = []; const throttle = setInterval(() => { if (pending.length > 0) { ws.send(JSON.stringify(pending)); pending = []; } }, 100);
|
- 连接池管理
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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); } }
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
| 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);
import VirtualList from 'virtual-list'; const list = new VirtualList({ container: document.getElementById('messages'), items: messages, renderItem: (item) => `<div>${item.content}</div>` });
const worker = new Worker('message-worker.js'); ws.onmessage = (event) => { worker.postMessage(event.data); }; worker.onmessage = (event) => { updateUI(event.data); };
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(); }
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
| 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 }); };
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']);
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 有哪些限制?
- 浏览器限制
- 同一域名下的并发连接数有限制(通常 6-10 个)
- 移动端后台运行时可能被系统挂起
- 消息大小限制
- 浏览器通常限制单条消息大小(几 MB 到几十 MB)
- 服务端也会设置限制
- 网络限制
- 某些企业网络或防火墙可能阻止 WebSocket
- 需要降级方案
- 代理问题
- 某些 HTTP 代理不支持 WebSocket 升级
- 需要配置代理支持
1 2 3 4 5 6
| 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?
- Chrome DevTools
- Network 面板 -> WS 标签
- 可以查看握手请求、消息收发、连接状态
- 可以查看消息内容和时间戳
- 使用工具
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); } }
|
- 第三方工具
- 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
| 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 核心知识点
- WebSocket 是全双工通信协议,通过 HTTP 握手升级
- 相比 HTTP 轮询,WebSocket 实时性更好、开销更小
- 心跳检测用于保持连接活跃和检测连接状态
- 断线重连需要指数退避算法和消息队列
- 安全性要考虑 WSS、认证、来源验证、消息过滤
- 性能优化包括消息压缩、批量发送、节流、虚拟滚动
10.2 最佳实践
- 始终使用 WSS 而不是 WS
- 实现完善的心跳和重连机制
- 合理设置心跳间隔(30-60秒)
- 使用指数退避算法重连
- 维护消息队列处理断线期间的消息
- 监听网络状态和页面可见性
- 对高频消息做节流处理
- 添加完善的错误处理和日志
- 生产环境要有监控和告警
- 提供降级方案(如长轮询)
10.3 面试准备建议
- 理解 WebSocket 的工作原理和握手过程
- 能手写心跳检测和断线重连的代码
- 了解与 HTTP、SSE 的区别和适用场景
- 掌握常见的性能优化和安全防护手段
- 准备实际项目中的使用经验和遇到的问题
- 了解浏览器兼容性和降级方案
- 熟悉调试和测试方法
10.4 进阶学习方向
- WebRTC:点对点实时通信
- Socket.IO:跨平台实时通信库
- MQTT:物联网消息协议
- GraphQL Subscriptions:基于 WebSocket 的订阅
- 操作转换(OT)和 CRDT:协同编辑算法
- WebSocket 集群和负载均衡
- WebSocket 网关和代理
- 消息队列集成(Redis、RabbitMQ)
参考资源