说说对 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 - 连接已关闭或无法建立
// 8. 完整封装类
class WebSocketClient {
constructor(url, options = {}) {
this.url = url;
this.reconnectInterval = options.reconnectInterval || 3000;
this.maxReconnectAttempts = options.maxReconnectAttempts || 5;
this.reconnectAttempts = 0;
this.listeners = new Map();
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket 连接成功');
this.reconnectAttempts = 0;
this.emit('open');
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.emit('message', data);
// 按类型分发
if (data.type) {
this.emit(data.type, data);
}
} catch (e) {
this.emit('message', event.data);
}
};
this.ws.onclose = (event) => {
this.emit('close', event);
this.attemptReconnect();
};
this.ws.onerror = (error) => {
this.emit('error', error);
};
}
attemptReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
setTimeout(() => this.connect(), this.reconnectInterval);
}
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(typeof data === 'string' ? data : JSON.stringify(data));
} else {
console.warn('WebSocket 未连接');
}
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
}
emit(event, data) {
if (this.listeners.has(event)) {
this.listeners.get(event).forEach(callback => callback(data));
}
}
close() {
this.ws.close();
}
}
// 使用示例
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);
});
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. 安全性
// WebSocket 安全最佳实践
// 1. 使用 WSS(WebSocket Secure)
// ws:// → wss://
// 2. 身份验证
wss.on('connection', async (ws, req) => {
// 从 URL 参数或 Cookie 获取 Token
const url = new URL(req.url, 'http://localhost');
const token = url.searchParams.get('token');
try {
const user = await verifyToken(token);
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 握手
- 基于 HTTP Upgrade 机制
- 关键头部:Upgrade、Connection、Sec-WebSocket-Key、Sec-WebSocket-Accept
- 101 状态码表示协议切换成功
-
WebSocket 与 HTTP 区别
- HTTP 是半双工,WebSocket 是全双工
- HTTP 是无状态、短连接,WebSocket 是有状态、长连接
- WebSocket 头部更小,实时性更好
-
心跳机制
- 原因:检测连接是否存活,防止 NAT 超时
- 实现:Ping/Pong 帧或应用层心跳
- 频率:通常 30-60 秒
-
应用场景
- 实时聊天
- 在线游戏
- 股票行情
- 协同编辑
- 实时通知
-
面试高频问题
- WebSocket 如何兼容 HTTP 代理和防火墙?(使用 80/443 端口,Upgrade 头部)
- 如何处理 WebSocket 断线重连?
- WebSocket 如何进行身份认证?(URL Token、Cookie、握手后发送认证消息)
- 如何实现 WebSocket 集群?(Redis Pub/Sub、消息队列)
- WebSocket 与 SSE 的区别?(双向 vs 单向)