说说对 WebSocket 的理解?应用场景
面试官通过此问题主要考察:
说说对 WebSocket 的理解?应用场景
问题解析(面试官考察点)
面试官通过此问题主要考察:
- 理解 WebSocket 的基本概念和特点
- 掌握 WebSocket 握手过程和协议细节
- 了解 WebSocket 与 HTTP 轮询的区别
- 理解 WebSocket 的应用场景和最佳实践
- 了解 WebSocket 连接管理和安全性
核心概念(基础知识点)
什么是 WebSocket
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它使得客户端和服务器之间可以建立持久连接,双方都可以随时发送数据,实现了真正的实时双向通信。
WebSocket 的核心特点:
| 特点 | 说明 |
|---|---|
| 全双工通信 | 客户端和服务器可以同时发送和接收数据 |
| 持久连接 | 一次握手后保持连接,无需重复建立 |
| 低延迟 | 避免了 HTTP 轮询的开销,实时性高 |
| 轻量级头部 | 数据帧头部很小(2-14 字节),节省带宽 |
| 基于 TCP | 使用 TCP 传输,可靠有序 |
WebSocket vs HTTP
HTTP 轮询(Polling):
┌─────────┐ 请求 ┌─────────┐
│ 客户端 │─────────────►│ 服务器 │
│ │◄─────────────│ │
│ │ 响应(无新数据) │
│ │ │ │
│ │ 请求 │ │
│ │─────────────►│ │
│ │◄─────────────│ │
│ │ 响应(无新数据) │
│ │ │ │
│ │ 请求 │ │
│ │─────────────►│ │
│ │◄─────────────│ │
│ │ 响应(有新数据) │
└─────────┘ └─────────┘
问题:大量无效请求,浪费带宽和服务器资源
WebSocket:
┌─────────┐ 握手请求 ┌─────────┐
│ 客户端 │────────────►│ 服务器 │
│ │◄────────────│ │
│ │ 握手响应 │ │
│◄═══════►│◄══════════►│◄═══════►│
│ 双向 │ 持久连接 │ 双向 │
│ 通信 │ │ 通信 │
│ │ 任意时刻 │ │
│ │ 互相推送 │ │
└─────────┘ └─────────┘
优势:一次连接,持续通信,实时高效
WebSocket 协议栈
┌─────────────────────────────────────────────────────────────┐
│ 应用层:WebSocket 协议(RFC 6455) │
│ - 数据帧格式定义 │
│ - 握手协议(基于 HTTP Upgrade) │
│ - 控制帧(Ping/Pong/Close) │
├─────────────────────────────────────────────────────────────┤
│ 传输层:TCP │
│ - 可靠传输 │
│ - 端口 80/443(与 HTTP 相同,便于穿透防火墙) │
├─────────────────────────────────────────────────────────────┤
│ 安全层:TLS/SSL(wss://) │
│ - WebSocket over TLS │
│ - 端口 443 │
└─────────────────────────────────────────────────────────────┘
URI 格式:
- ws://example.com/socket (WebSocket)
- wss://example.com/socket (WebSocket Secure)
详细解答(代码示例)
WebSocket 握手过程
// 1. 客户端握手请求(HTTP Upgrade)
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Extensions: permessage-deflate
// 关键头部说明:
// Upgrade: websocket - 表示要升级到 WebSocket 协议
// Connection: Upgrade - 表示连接要升级
// Sec-WebSocket-Key: Base64 编码的 16 字节随机数
// Sec-WebSocket-Version: WebSocket 协议版本(13 是当前标准)
// 2. 服务器握手响应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
// 关键头部说明:
// 101 状态码:协议切换成功
// Sec-WebSocket-Accept: 对 Sec-WebSocket-Key 的响应
// 计算方式:Base64(SHA1(Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
// Sec-WebSocket-Accept 计算验证
const crypto = require('crypto');
function calculateAcceptKey(key) {
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
return crypto
.createHash('sha1')
.update(key + GUID)
.digest('base64');
}
// 示例
const clientKey = 'dGhlIHNhbXBsZSBub25jZQ==';
const serverAccept = calculateAcceptKey(clientKey);
console.log(serverAccept); // s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
WebSocket 客户端实现
// 浏览器原生 WebSocket API
// 1. 创建连接
const ws = new WebSocket('wss://example.com/socket');
// 2. 连接建立回调
ws.onopen = function(event) {
console.log('连接已建立');
// 发送消息
ws.send('Hello Server!');
// 发送 JSON 数据
ws.send(JSON.stringify({
type: 'join',
room: 'room-123',
user: 'Alice'
}));
// 发送二进制数据
const blob = new Blob(['binary data'], { type: 'text/plain' });
ws.send(blob);
// 发送 ArrayBuffer
const buffer = new ArrayBuffer(8);
ws.send(buffer);
};
// 3. 接收消息回调
ws.onmessage = function(event) {
console.log('收到消息:', event.data);
// 根据数据类型处理
if (typeof event.data === 'string') {
// 文本消息
const message = JSON.parse(event.data);
handleMessage(message);
} else if (event.data instanceof Blob) {
// 二进制消息
handleBinaryData(event.data);
}
};
// 4. 错误处理
ws.onerror = function(error) {
console.error('WebSocket 错误:', error);
};
// 5. 连接关闭回调
ws.onclose = function(event) {
console.log('连接已关闭');
console.log('关闭代码:', event.code);
console.log('关闭原因:', event.reason);
console.log('是否干净关闭:', event.wasClean);
// 可选:自动重连
if (!event.wasClean) {
setTimeout(reconnect, 3000);
}
};
// 6. 关闭连接
ws.close(1000, '正常关闭');
// 7. 连接状态
console.log('连接状态:', ws.readyState);
// 0: CONNECTING - 连接正在建立中
// 1: OPEN - 连接已建立,可以通信
// 2: CLOSING - 连接正在关闭
// 3: CLOSED - 连接已关闭或无法建立
下面的封装管理一个连接及其重连定时器。close() 表示调用方结束生命周期;连接失败才触发有限重试。示例仅发送文本或可 JSON 序列化的数据,发送失败由调用方处理。
class WebSocketClient {
constructor(url, options = {}) {
this.url = url;
this.reconnectInterval = options.reconnectInterval ?? 3000;
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
this.maxBufferedBytes = options.maxBufferedBytes ?? 1024 * 1024;
this.reconnectAttempts = 0;
this.reconnectTimer = null;
this.stopped = false;
this.listeners = new Map();
this.connect();
}
connect() {
if (this.stopped || this.reconnectTimer !== null) return;
if (this.ws && this.ws.readyState !== WebSocket.CLOSED) return;
const ws = new WebSocket(this.url);
this.ws = ws;
const isCurrent = () => !this.stopped && this.ws === ws;
ws.onopen = () => {
if (!isCurrent()) return;
this.reconnectAttempts = 0;
this.emit('open');
};
ws.onmessage = (event) => {
if (!isCurrent()) return;
let data = event.data;
try {
data = JSON.parse(event.data);
} catch { /* 非 JSON 消息保留原始数据。 */ }
this.emit('message', data);
if (isCurrent() && data && typeof data.type === 'string') {
this.emit(data.type, data);
}
};
ws.onclose = (event) => {
if (!isCurrent()) return;
if (event.code !== 1000 && event.code !== 1008) this.attemptReconnect();
this.emit('close', event);
};
ws.onerror = (error) => {
if (isCurrent()) this.emit('error', error);
};
}
attemptReconnect() {
if (this.stopped || this.reconnectTimer !== null) return;
if (this.reconnectAttempts >= this.maxReconnectAttempts) return;
const delay = Math.min(30_000, this.reconnectInterval * 2 ** this.reconnectAttempts++);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connect();
}, delay);
}
send(data) {
if (this.stopped || this.ws.readyState !== WebSocket.OPEN) return false;
const text = typeof data === 'string' ? data : JSON.stringify(data);
if (typeof text !== 'string') throw new TypeError('Expected text or JSON data');
const bytes = new TextEncoder().encode(text).byteLength;
if (this.ws.bufferedAmount + bytes > this.maxBufferedBytes) return false;
this.ws.send(text);
return true;
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event).add(callback);
return () => this.listeners.get(event)?.delete(callback);
}
emit(event, data) {
if (this.listeners.has(event)) {
this.listeners.get(event).forEach(callback => callback(data));
}
}
close() {
if (this.stopped) return;
this.stopped = true;
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
this.listeners.clear();
if (this.ws.readyState < WebSocket.CLOSING) this.ws.close(1000, 'Client disposed');
}
}
const client = new WebSocketClient('wss://example.com/socket');
client.on('open', () => {
client.send({ type: 'join', room: 'lobby' });
});
client.on('chat', (data) => {
console.log('收到聊天消息:', data);
});
client.on('user_joined', (data) => {
console.log('用户加入:', data.user);
});
send() 返回 true 只表示写入浏览器发送缓冲区,不代表服务端已经处理。重连后的应用队列需要明确条数、字节数、过期时间和丢弃策略;业务使用 messageId、确认游标和服务端幂等记录,仅恢复未确认且允许重试的消息。支付、创建订单等动作不能因为断线直接重发。
生命周期可以按下面的顺序检查:
| 事件 | 需要的处理 |
|---|---|
| 临时断线 | 只保留一个重连定时器,指数退避;多客户端场景再加随机抖动 |
| 组件卸载或主动结束 | 停止重连、清理监听和心跳、关闭连接 |
| 旧连接迟到的事件 | 比较连接对象,不能更新新连接的 UI 或再次重连 |
| 认证失效或策略拒绝 | 停止自动重试,重新认证或向业务返回失败 |
| 心跳长期没有应答 | 根据 deadline 判定连接失效,不能只依赖 readyState |
浏览器 WebSocket API 不暴露发送协议 Ping 帧的方法。服务端 ws.ping() 与应用层 { type: 'ping' } 是两种机制;应用心跳要与服务端约定 pong,发送前检查 OPEN,停止时清除定时器。后台节流、网络切换也会延迟心跳,应结合实际超时预算,不能把每次延迟都当服务端故障。重连成功后的计数重置只限制连续建连失败;需要限制频繁闪断时,还应增加整体重连截止时间或稳定连接窗口。
WebSocket 服务器实现(Node.js)
// 使用 ws 库
const WebSocket = require('ws');
const http = require('http');
// 1. 创建 HTTP 服务器
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('WebSocket Server\n');
});
// 2. 创建 WebSocket 服务器
const wss = new WebSocket.Server({ server });
// 3. 连接管理
const clients = new Map();
wss.on('connection', (ws, req) => {
const clientId = generateClientId();
const clientInfo = {
id: clientId,
ws: ws,
rooms: new Set(),
userData: {}
};
clients.set(clientId, clientInfo);
console.log(`客户端 ${clientId} 已连接`);
// 发送欢迎消息
ws.send(JSON.stringify({
type: 'connected',
clientId: clientId,
message: '欢迎连接到 WebSocket 服务器'
}));
// 接收消息
ws.on('message', (data) => {
try {
const message = JSON.parse(data);
handleMessage(clientId, message);
} catch (e) {
console.error('消息解析失败:', e);
ws.send(JSON.stringify({
type: 'error',
message: '无效的消息格式'
}));
}
});
// 连接关闭
ws.on('close', (code, reason) => {
console.log(`客户端 ${clientId} 已断开: ${code} ${reason}`);
// 从所有房间移除
const client = clients.get(clientId);
client.rooms.forEach(room => {
leaveRoom(clientId, room);
});
clients.delete(clientId);
});
// 错误处理
ws.on('error', (error) => {
console.error(`客户端 ${clientId} 错误:`, error);
});
// Ping/Pong 保活
ws.isAlive = true;
ws.on('pong', () => {
ws.isAlive = true;
});
});
// 4. 消息处理
function handleMessage(clientId, message) {
const client = clients.get(clientId);
switch (message.type) {
case 'join':
joinRoom(clientId, message.room);
break;
case 'leave':
leaveRoom(clientId, message.room);
break;
case 'chat':
broadcastToRoom(message.room, {
type: 'chat',
from: clientId,
content: message.content,
timestamp: Date.now()
}, clientId);
break;
case 'private':
sendToClient(message.to, {
type: 'private',
from: clientId,
content: message.content,
timestamp: Date.now()
});
break;
default:
client.ws.send(JSON.stringify({
type: 'error',
message: '未知的消息类型'
}));
}
}
// 5. 房间管理
const rooms = new Map();
function joinRoom(clientId, roomName) {
const client = clients.get(clientId);
if (!rooms.has(roomName)) {
rooms.set(roomName, new Set());
}
rooms.get(roomName).add(clientId);
client.rooms.add(roomName);
// 通知房间内其他用户
broadcastToRoom(roomName, {
type: 'user_joined',
room: roomName,
user: clientId
}, clientId);
console.log(`客户端 ${clientId} 加入房间 ${roomName}`);
}
function leaveRoom(clientId, roomName) {
const client = clients.get(clientId);
if (rooms.has(roomName)) {
rooms.get(roomName).delete(clientId);
// 清理空房间
if (rooms.get(roomName).size === 0) {
rooms.delete(roomName);
}
}
client.rooms.delete(roomName);
broadcastToRoom(roomName, {
type: 'user_left',
room: roomName,
user: clientId
});
console.log(`客户端 ${clientId} 离开房间 ${roomName}`);
}
// 6. 广播功能
function broadcastToRoom(roomName, message, excludeClientId = null) {
if (!rooms.has(roomName)) return;
const messageStr = JSON.stringify(message);
rooms.get(roomName).forEach(clientId => {
if (clientId !== excludeClientId) {
const client = clients.get(clientId);
if (client && client.ws.readyState === WebSocket.OPEN) {
client.ws.send(messageStr);
}
}
});
}
function sendToClient(clientId, message) {
const client = clients.get(clientId);
if (client && client.ws.readyState === WebSocket.OPEN) {
client.ws.send(JSON.stringify(message));
}
}
function broadcastAll(message, excludeClientId = null) {
const messageStr = JSON.stringify(message);
clients.forEach((client, clientId) => {
if (clientId !== excludeClientId && client.ws.readyState === WebSocket.OPEN) {
client.ws.send(messageStr);
}
});
}
// 7. 心跳检测
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, 30000);
wss.on('close', () => {
clearInterval(interval);
});
// 8. 启动服务器
server.listen(8080, () => {
console.log('WebSocket 服务器启动在端口 8080');
});
// 辅助函数
function generateClientId() {
return Math.random().toString(36).substring(2, 15);
}
深入理解(原理剖析)
WebSocket 数据帧格式
WebSocket 数据帧结构:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - -+
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
字段说明:
- FIN (1 bit): 是否为最后一个分片
- RSV1-3 (3 bits): 保留位,用于扩展
- Opcode (4 bits): 帧类型
- 0x0: 连续帧
- 0x1: 文本帧
- 0x2: 二进制帧
- 0x8: 关闭连接
- 0x9: Ping
- 0xA: Pong
- MASK (1 bit): 是否使用掩码(客户端必须置 1)
- Payload length (7 bits): 负载长度
- 0-125: 实际长度
- 126: 后续 2 字节表示长度
- 127: 后续 8 字节表示长度
- Masking-key (32 bits): 掩码密钥(仅客户端发送时存在)
- Payload data: 实际数据(客户端发送时会被掩码)
WebSocket 与 HTTP 长轮询对比
┌─────────────────────────────────────────────────────────────┐
│ 实时通信方案对比 │
├─────────────────────────────────────────────────────────────┤
│ 1. 短轮询(Polling) │
│ - 客户端定时发送请求 │
│ - 服务器立即响应(有数据或无数据) │
│ - 延迟:取决于轮询间隔 │
│ - 开销:大量 HTTP 请求,浪费带宽 │
├─────────────────────────────────────────────────────────────┤
│ 2. 长轮询(Long Polling) │
│ - 客户端发送请求,服务器保持连接直到有数据 │
│ - 有数据时立即响应,客户端立即发起新请求 │
│ - 延迟:较低 │
│ - 开销:连接保持时间长,但仍需频繁重建连接 │
├─────────────────────────────────────────────────────────────┤
│ 3. SSE(Server-Sent Events) │
│ - 服务器向客户端单向推送 │
│ - 基于 HTTP,自动重连 │
│ - 适用:股票行情、新闻推送等单向场景 │
│ - 限制:只能服务器向客户端推送 │
├─────────────────────────────────────────────────────────────┤
│ 4. WebSocket │
│ - 全双工双向通信 │
│ - 持久连接,低延迟 │
│ - 支持二进制 │
│ - 适用:聊天、游戏、协同编辑等双向场景 │
└─────────────────────────────────────────────────────────────┘
性能对比:
┌──────────────┬──────────┬──────────┬──────────┬──────────┐
│ 方案 │ 延迟 │ 实时性 │ 开销 │ 复杂度 │
├──────────────┼──────────┼──────────┼──────────┼──────────┤
│ 短轮询 │ 高 │ 差 │ 高 │ 低 │
│ 长轮询 │ 中 │ 中 │ 中 │ 中 │
│ SSE │ 低 │ 好 │ 低 │ 低 │
│ WebSocket │ 极低 │ 极好 │ 极低 │ 高 │
└──────────────┴──────────┴──────────┴──────────┴──────────┘
WebSocket 扩展
WebSocket 扩展(Extensions):
1. permessage-deflate
- 消息压缩扩展
- 减少传输数据量
- 握手时协商:Sec-WebSocket-Extensions: permessage-deflate
2. 多路复用扩展(未广泛支持)
- 在单个 WebSocket 连接上复用多个逻辑通道
3. 自定义扩展
- 可以定义应用层协议扩展
// 启用压缩的 WebSocket 服务器
const WebSocket = require('ws');
const wss = new WebSocket.Server({
port: 8080,
perMessageDeflate: {
zlibDeflateOptions: {
chunkSize: 1024,
memLevel: 7,
level: 3
},
zlibInflateOptions: {
chunkSize: 10 * 1024
},
clientNoContextTakeover: true,
serverNoContextTakeover: true,
serverMaxWindowBits: 10,
concurrencyLimit: 10
}
});
最佳实践
1. 连接管理
// 连接池管理
class WebSocketPool {
constructor(maxConnections = 100) {
this.pools = new Map(); // room -> Set<ws>
this.maxConnections = maxConnections;
}
addToRoom(room, ws) {
if (!this.pools.has(room)) {
this.pools.set(room, new Set());
}
const roomConnections = this.pools.get(room);
// 限制房间人数
if (roomConnections.size >= this.maxConnections) {
ws.close(1008, 'Room is full');
return false;
}
roomConnections.add(ws);
ws.room = room;
return true;
}
removeFromRoom(ws) {
if (ws.room && this.pools.has(ws.room)) {
this.pools.get(ws.room).delete(ws);
}
}
broadcastToRoom(room, message, excludeWs = null) {
if (!this.pools.has(room)) return;
const messageStr = JSON.stringify(message);
this.pools.get(room).forEach(ws => {
if (ws !== excludeWs && ws.readyState === WebSocket.OPEN) {
ws.send(messageStr);
}
});
}
}
2. 消息协议设计
// 结构化消息协议
const MessageTypes = {
// 系统消息
SYSTEM: 'system',
ERROR: 'error',
PING: 'ping',
PONG: 'pong',
// 用户消息
AUTH: 'auth',
JOIN: 'join',
LEAVE: 'leave',
// 业务消息
CHAT: 'chat',
TYPING: 'typing',
READ: 'read',
// 信令消息(WebRTC)
OFFER: 'offer',
ANSWER: 'answer',
ICE_CANDIDATE: 'ice_candidate'
};
// 消息格式
class Message {
constructor(type, payload, options = {}) {
this.id = generateMessageId();
this.type = type;
this.payload = payload;
this.timestamp = Date.now();
this.from = options.from;
this.to = options.to; // 私聊目标
this.room = options.room;
}
toJSON() {
return {
id: this.id,
type: this.type,
payload: this.payload,
timestamp: this.timestamp,
from: this.from,
to: this.to,
room: this.room
};
}
}
// 使用示例
const message = new Message(MessageTypes.CHAT, {
text: 'Hello!',
attachments: []
}, { from: 'user-123', room: 'room-456' });
ws.send(JSON.stringify(message));
3. 安全性
浏览器构造器不能任意设置 Authorization 请求头。可由同站受控 Cookie 完成握手认证并校验 Origin,或先通过 HTTPS 获取短期、单次使用的连接票据。长期 token 放进 URL 可能进入代理和服务日志;下例 URL 参数必须是短期票据,服务端要脱敏并在兑换后作废。若握手后再发送认证消息,认证完成前不得订阅房间或处理业务事件。
// WebSocket 安全最佳实践
// 1. 使用 WSS(WebSocket Secure)
// ws:// → wss://
// 2. 身份验证
wss.on('connection', async (ws, req) => {
// 兑换短期连接票据;不能在日志中记录完整 URL。
const url = new URL(req.url, 'http://localhost');
const ticket = url.searchParams.get('ticket');
try {
const user = await consumeConnectionTicket(ticket);
ws.userId = user.id;
ws.authorized = true;
} catch (e) {
ws.close(1008, 'Authentication failed');
return;
}
});
// 3. 速率限制
const rateLimiter = new Map();
function checkRateLimit(clientId, limit = 100, windowMs = 60000) {
const now = Date.now();
const clientData = rateLimiter.get(clientId) || { count: 0, resetTime: now + windowMs };
if (now > clientData.resetTime) {
clientData.count = 0;
clientData.resetTime = now + windowMs;
}
clientData.count++;
rateLimiter.set(clientId, clientData);
return clientData.count <= limit;
}
// 4. 消息大小限制
wss.on('connection', (ws) => {
ws.on('message', (data) => {
if (data.length > 1024 * 1024) { // 1MB 限制
ws.close(1009, 'Message too large');
return;
}
});
});
// 5. 输入验证
const Joi = require('joi');
const messageSchema = Joi.object({
type: Joi.string().valid(...Object.values(MessageTypes)).required(),
payload: Joi.object().required(),
room: Joi.string().alphanum().max(50)
});
function validateMessage(data) {
return messageSchema.validate(data);
}
4. 水平扩展
单服务器架构:
┌─────────┐ ┌─────────┐
│ 客户端 │◄───────►│ WS 服务器│
└─────────┘ └─────────┘
多服务器架构(使用 Redis 发布订阅):
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 客户端 A │◄───────►│ WS 服务器 1 │◄───────►│ │
└─────────┘ └─────────┘ │ │
│ Redis │
┌─────────┐ ┌─────────┐ │ Pub/Sub│
│ 客户端 B │◄───────►│ WS 服务器 2 │◄───────►│ │
└─────────┘ └─────────┘ └─────────┘
实现代码:
// 使用 Redis 实现多服务器消息广播
const Redis = require('ioredis');
const redisPub = new Redis();
const redisSub = new Redis();
// 订阅频道
redisSub.subscribe('websocket:broadcast');
redisSub.on('message', (channel, message) => {
const data = JSON.parse(message);
// 广播给本服务器上的所有客户端
wss.clients.forEach((ws) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data));
}
});
});
// 发送消息时同时发布到 Redis
function broadcastAll(message) {
// 本地广播
wss.clients.forEach((ws) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
}
});
// 发布到其他服务器
redisPub.publish('websocket:broadcast', JSON.stringify(message));
}
面试要点
-
WebSocket 握手(知识点:WebSocket 握手过程)
- 基于 HTTP Upgrade 机制
- 关键头部:Upgrade、Connection、Sec-WebSocket-Key、Sec-WebSocket-Accept
- 101 状态码表示协议切换成功
-
WebSocket 与 HTTP 区别(知识点:WebSocket vs HTTP)
- WebSocket 提供双向消息通道;HTTP 请求/响应同样可以流式传输
- HTTP/1.1 可复用持久连接,HTTP/2 和 HTTP/3 可多路复用,不能概括为短连接或半双工
- 是否选 WebSocket 取决于双向交互、代理支持、消息恢复和实际开销
-
心跳机制(知识点:连接管理)
- 原因:检测连接是否存活,防止 NAT 超时
- 实现:Ping/Pong 帧或应用层心跳
- 频率:通常 30-60 秒
-
应用场景(知识点:什么是 WebSocket)
- 实时聊天
- 在线游戏
- 股票行情
- 协同编辑
- 实时通知
-
面试高频问题
- WebSocket 如何兼容 HTTP 代理和防火墙?(使用 80/443 端口,Upgrade 头部)(知识点:WebSocket 握手过程)
- 如何处理 WebSocket 断线重连?(知识点:连接管理)
- WebSocket 如何进行身份认证?(URL Token、Cookie、握手后发送认证消息)(知识点:安全性)
- 如何实现 WebSocket 集群?(Redis Pub/Sub、消息队列)(知识点:水平扩展)
- WebSocket 与 SSE 的区别?(双向 vs 单向)(知识点:WebSocket 与 HTTP 长轮询对比)