WebSocket 完全指南:从入门到通关
·
目录
- 1. 什么是 WebSocket
- 2. 为什么需要 WebSocket
- 3. WebSocket 核心概念
- 4. 工作原理详解
- 5. API 详解与使用
- 6. 心跳机制与重连策略
- 7. 面试高频题
- 8. 源码实现示例
1. 什么是 WebSocket
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它于 2011 年被 IETF 定为标准 RFC 6455,并被 W3C 标准化为浏览器 API。
核心特征
- 全双工通信:客户端和服务器可以同时发送消息
- 持久连接:一旦建立,连接保持打开状态
- 低延迟:无需像 HTTP 那样每次请求都建立新连接
- 轻量级:数据帧头部开销小(2-14 字节)
与 HTTP 的关系
WebSocket 使用 HTTP/1.1 的 101 Switching Protocols 状态码完成协议升级,之后完全独立于 HTTP 协议运行。
2. 为什么需要 WebSocket
传统 HTTP 的局限性
| 特性 | HTTP | WebSocket |
|---|---|---|
| 通信方向 | 单向(客户端发起) | 双向 |
| 连接方式 | 短连接 | 长连接 |
| 实时性 | 差(需轮询) | 好(推送) |
| 头部开销 | 大(~800 字节) | 小(2-14 字节) |
| 服务端推送 | 不支持 | 原生支持 |
适用场景
- 💬 即时通讯:微信、Slack、Discord
- 🎮 在线游戏:实时对战、多人互动
- 📊 实时数据:股票行情、体育比分、仪表盘
- 🤝 协同编辑:Google Docs、Figma
- 📡 位置服务:外卖配送、共享单车
3. WebSocket 核心概念
3.1 URL 格式
ws://example.com/socket // 非加密
wss://example.com/socket // 加密(TLS)
3.2 协议帧结构
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 ... |
+---------------------------------------------------------------+
3.3 重要字段说明
| 字段 | 大小 | 说明 |
|---|---|---|
| FIN | 1 bit | 表示是否为消息的最后一帧 |
| opcode | 4 bits | 操作码:0x1=文本,0x2=二进制,0x8=关闭,0x9=Ping,0xA=Pong |
| MASK | 1 bit | 是否使用掩码(客户端→服务端必须为 1) |
| Payload len | 7 bits | 载荷长度:0-125 直接表示,126 时后续 2 字节,127 时后续 8 字节 |
3.4 操作码(Opcode)
| 值 | 含义 |
|---|---|
| 0x0 | 延续帧 |
| 0x1 | 文本帧 |
| 0x2 | 二进制帧 |
| 0x8 | 连接关闭 |
| 0x9 | Ping |
| 0xA | Pong |
4. 工作原理详解
4.1 握手过程
Websocket握手其实就是借HTTP的壳来完成协议切换,整个过程就是一个来回。
客户端先发一个HTTP Get 请求,路径就是WebSocket的地址,关键是要带上请求头。

4.2 握手关键 Headers
客户端请求头:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket // 必须
Connection: Upgrade // 必须
Sec-WebSocket-Key: dGhlIHNhbXBsZQ== // Base64 编码的 16 字节随机值
Sec-WebSocket-Version: 13 // 必须为 13
Sec-WebSocket-Protocol: chat, super // 可选,子协议
Sec-WebSocket-Extensions: permessage-deflate // 可选,扩展
Origin: http://example.com // 浏览器自动添加
服务端响应头:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat // 从客户端列表中选择
4.3 Sec-WebSocket-Accept 计算
const crypto = require('crypto');
function computeAccept(key) {
const magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const sha1 = crypto.createHash('sha1');
sha1.update(key + magic);
return sha1.digest('base64');
}
4.4 数据帧格式详解
客户端发送数据(必须掩码):
// 发送 "Hello"
// 帧头:0x81(FIN=1, opcode=0x1)
// 长度:0x85(MASK=1, payload=5)
// Masking-key: 4 字节随机值
// 载荷:Hello 异或 Masking-key
服务端发送数据(无需掩码):
// 帧头:0x81(FIN=1, opcode=0x1)
// 长度:0x05(MASK=0, payload=5)
// 载荷:Hello
5. API 详解与使用
5.1 浏览器端 API
// 创建连接
const ws = new WebSocket('wss://example.com/chat');
// 连接事件
ws.onopen = function(event) {
console.log('连接已建立');
ws.send('Hello Server!');
};
// 接收消息
ws.onmessage = function(event) {
console.log('收到消息:', event.data);
// event.data 类型可能是: string, Blob, ArrayBuffer
};
// 错误处理
ws.onerror = function(error) {
console.error('WebSocket 错误:', error);
};
// 连接关闭
ws.onclose = function(event) {
console.log('连接关闭:', event.code, event.reason);
// event.code: 1000(正常), 1001(离开), 1006(异常)
};
// 发送数据
ws.send('Hello World'); // 文本
ws.send(JSON.stringify({type: 'ping'})); // JSON
ws.send(new Blob(['binary data'])); // 二进制
ws.send(new ArrayBuffer(16)); // ArrayBuffer
// 关闭连接
ws.close(1000, 'Goodbye');
5.2 服务端(Node.js + ws 库)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// 连接管理
wss.on('connection', function connection(ws, req) {
console.log('客户端连接:', req.socket.remoteAddress);
// 发送欢迎消息
ws.send('Welcome to WebSocket server!');
// 接收消息
ws.on('message', function incoming(data) {
console.log('收到:', data.toString());
// 广播给所有客户端
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
});
// 错误处理
ws.on('error', function(error) {
console.error('连接错误:', error);
});
// 关闭处理
ws.on('close', function(code, reason) {
console.log('连接关闭:', code, reason.toString());
});
// 发送 Ping 保持连接
const pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
}, 30000);
ws.on('close', () => clearInterval(pingInterval));
});
console.log('WebSocket 服务器启动在 ws://localhost:8080');
5.3 ReadyState 状态码
| 常量 | 值 | 描述 |
|---|---|---|
| CONNECTING | 0 | 正在连接 |
| OPEN | 1 | 已连接,可通信 |
| CLOSING | 2 | 正在关闭 |
| CLOSED | 3 | 已关闭 |
6. 心跳机制与重连策略
6.1 为什么需要心跳
- 检测连接存活:网络中断时无法立即发现
- 防止超时断开:NAT/防火墙会清理空闲连接
- 保持连接活跃:避免被中间设备断开
6.2 心跳实现方案
方案一:Ping/Pong 帧(推荐)
// 客户端
let isAlive = true;
ws.on('pong', () => {
isAlive = true;
});
// 服务端定时检测
const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, 30000);
方案二:应用层心跳
// 客户端发送心跳
class HeartbeatClient {
constructor(ws, interval = 30000) {
this.ws = ws;
this.interval = interval;
this.pingTimer = null;
this.pongTimer = null;
this.start();
}
start() {
this.pingTimer = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
// 设置超时检测
this.pongTimer = setTimeout(() => {
this.ws.close();
}, 5000);
}
}, this.interval);
}
handlePong() {
clearTimeout(this.pongTimer);
}
stop() {
clearInterval(this.pingTimer);
clearTimeout(this.pongTimer);
}
}
6.3 重连策略
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.options = {
maxRetries: 5,
retryDelay: 1000,
maxRetryDelay: 30000,
backoffMultiplier: 1.5,
...options
};
this.retryCount = 0;
this.ws = null;
this.isManualClose = false;
this.reconnectTimer = null;
this.connect();
}
connect() {
try {
this.ws = new WebSocket(this.url);
this.setupEventListeners();
} catch (error) {
this.scheduleReconnect();
}
}
setupEventListeners() {
this.ws.onopen = () => {
console.log('WebSocket 连接成功');
this.retryCount = 0;
this.isManualClose = false;
};
this.ws.onclose = (event) => {
console.log('WebSocket 关闭:', event.code);
// 1000: 正常关闭,不重连
// 1001: 主动离开,不重连
if (this.isManualClose || event.code === 1000 || event.code === 1001) {
return;
}
this.scheduleReconnect();
};
this.ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
this.ws.onmessage = (event) => {
// 透传消息
this.onmessage && this.onmessage(event);
};
}
scheduleReconnect() {
if (this.retryCount >= this.options.maxRetries) {
console.log('达到最大重连次数,停止重连');
return;
}
const delay = Math.min(
this.options.retryDelay * Math.pow(this.options.backoffMultiplier, this.retryCount),
this.options.maxRetryDelay
);
this.retryCount++;
console.log(`${delay}ms 后进行第 ${this.retryCount} 次重连`);
this.reconnectTimer = setTimeout(() => {
this.connect();
}, delay);
}
send(data) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
} else {
console.warn('WebSocket 未连接,数据丢弃');
}
}
close(code = 1000, reason = '') {
this.isManualClose = true;
clearTimeout(this.reconnectTimer);
if (this.ws) {
this.ws.close(code, reason);
}
}
}
7. 面试高频题
Q1: WebSocket 和 HTTP 的区别是什么?
| 维度 | HTTP | WebSocket |
|---|---|---|
| 协议 | 无状态应用层协议 | 有状态应用层协议 |
| 连接 | 短连接(每次请求独立) | 长连接(持久) |
| 方向 | 单向(客户端→服务端) | 双向 |
| 实时性 | 需要轮询 | 服务端可主动推送 |
| 头部大小 | ~800 字节 | 2-14 字节 |
| 二进制支持 | 需编码(Base64) | 原生支持 |
| 适用范围 | 请求-响应模型 | 实时通信场景 |
Q2: WebSocket 握手过程是怎样的?
- 客户端发送 HTTP GET 请求,包含
Upgrade: websocket头 - 服务端验证请求,返回 101 状态码
- 连接升级为 WebSocket 协议
- 后续通信使用 WebSocket 帧格式
关键验证:服务端根据 Sec-WebSocket-Key 计算 Sec-WebSocket-Accept
Q3: WebSocket 如何保证安全性?
- WSS(WebSocket Secure):使用 TLS/SSL 加密
- Origin 验证:服务端验证
Origin头防止 CSRF - 子协议协商:
Sec-WebSocket-Protocol限制协议 - 关闭码验证:检查
close帧的合法状态码
Q4: WebSocket 的二进制数据传输方式?
// 方式1:ArrayBuffer
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setInt32(0, 12345);
ws.send(buffer);
// 方式2:Blob
const blob = new Blob(['binary data']);
ws.send(blob);
// 方式3:TypedArray
const uint8 = new Uint8Array([1, 2, 3, 4]);
ws.send(uint8);
Q5: WebSocket 如何处理粘包问题?
WebSocket 协议本身基于帧(Frame),不存在传统 TCP 的粘包问题。但应用层消息可能分片:
let buffer = '';
ws.onmessage = (event) => {
buffer += event.data;
try {
const message = JSON.parse(buffer);
buffer = ''; // 清空缓冲
handleMessage(message);
} catch (e) {
// 消息不完整,继续等待
}
};
Q6: 如何实现 WebSocket 的鉴权?
// 方案1:URL 参数
const ws = new WebSocket(`wss://example.com?token=${token}`);
// 方案2:Cookie(浏览器自动携带)
// 需要同域,且 Cookie 设置了 SameSite
// 方案3:协议头(需自定义实现)
// 握手时传递自定义头(部分浏览器支持有限)
// 方案4:握手后鉴权
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'auth', token: 'xxx' }));
};
Q7: WebSocket 的注意事项和限制?
- 代理支持:某些 HTTP 代理不支持 WebSocket
- 负载均衡:需要 sticky session 或独立服务
- 连接数限制:浏览器限制(通常 256 个)
- 内存管理:长连接需注意内存泄漏
- 自动重连:网络变化时需手动实现
Q8: WebSocket 的替代方案有哪些?
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| SSE | 简单、自动重连 | 单向 | 服务器推送(通知、股票) |
| Long Polling | 兼容性好 | 延迟高、开销大 | 旧浏览器兼容 |
| WebRTC | P2P、低延迟 | 复杂 | 音视频通话、P2P |
| MQTT | 轻量、QoS | 需 broker | IoT、移动设备 |
Q9: WebSocket 的关闭码含义?
| 码 | 含义 | 描述 |
|---|---|---|
| 1000 | 正常关闭 | 正常结束会话 |
| 1001 | 离开 | 端点离开(如页面跳转) |
| 1002 | 协议错误 | 协议违规 |
| 1003 | 数据类型错误 | 收到不支持的数据 |
| 1005 | 无状态码 | 预期接收但未收到 |
| 1006 | 异常关闭 | 非正常断开(无 close 帧) |
| 1007 | 数据不一致 | 帧数据不符合类型 |
| 1008 | 策略违规 | 违反服务器策略 |
| 1009 | 消息太大 | 无法处理 |
| 1011 | 服务端错误 | 内部错误 |
8. 源码实现示例
8.1 简易 WebSocket 客户端
class SimpleWebSocket {
constructor(url) {
this.url = url;
this.socket = null;
this.callbacks = {
open: [],
message: [],
close: [],
error: []
};
this.connect();
}
connect() {
if (this.socket) return;
this.socket = new WebSocket(this.url);
this.socket.onopen = (event) => {
this.emit('open', event);
};
this.socket.onmessage = (event) => {
this.emit('message', event);
};
this.socket.onclose = (event) => {
this.emit('close', event);
this.socket = null;
};
this.socket.onerror = (error) => {
this.emit('error', error);
};
}
on(event, callback) {
if (this.callbacks[event]) {
this.callbacks[event].push(callback);
}
return this;
}
off(event, callback) {
if (this.callbacks[event]) {
this.callbacks[event] = this.callbacks[event].filter(cb => cb !== callback);
}
return this;
}
emit(event, data) {
if (this.callbacks[event]) {
this.callbacks[event].forEach(callback => callback(data));
}
}
send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(data);
} else {
throw new Error('WebSocket is not connected');
}
}
close(code = 1000, reason = '') {
if (this.socket) {
this.socket.close(code, reason);
}
}
get readyState() {
return this.socket ? this.socket.readyState : WebSocket.CLOSED;
}
}
8.2 简易 WebSocket 服务端(原生实现)
const http = require('http');
const crypto = require('crypto');
class WebSocketServer {
constructor(options) {
this.server = http.createServer();
this.clients = new Set();
this.handleUpgrade();
this.server.listen(options.port || 8080);
}
handleUpgrade() {
this.server.on('upgrade', (req, socket, head) => {
// 验证 WebSocket 握手
if (!this.isValidWebSocketRequest(req)) {
socket.destroy();
return;
}
// 计算 Accept
const acceptKey = this.computeAccept(req.headers['sec-websocket-key']);
// 发送握手响应
const response = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${acceptKey}`,
'\r\n'
].join('\r\n');
socket.write(response);
// 保存客户端
this.clients.add(socket);
// 设置事件
socket.on('data', (data) => {
this.handleData(socket, data);
});
socket.on('close', () => {
this.clients.delete(socket);
this.emit('close', socket);
});
socket.on('error', (error) => {
console.error('Socket error:', error);
});
this.emit('connection', socket);
});
}
isValidWebSocketRequest(req) {
return req.headers.upgrade === 'websocket' &&
req.headers.connection === 'Upgrade' &&
req.headers['sec-websocket-version'] === '13';
}
computeAccept(key) {
const magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const hash = crypto.createHash('sha1');
hash.update(key + magic);
return hash.digest('base64');
}
handleData(socket, data) {
// 简化解析,仅处理文本帧
if (data.length < 2) return;
const opcode = data[0] & 0x0F;
const isMasked = (data[1] & 0x80) !== 0;
let payloadLen = data[1] & 0x7F;
let offset = 2;
// 扩展长度
if (payloadLen === 126) {
payloadLen = data.readUInt16BE(offset);
offset += 2;
} else if (payloadLen === 127) {
payloadLen = Number(data.readBigUInt64BE(offset));
offset += 8;
}
let payload = data.slice(offset + (isMasked ? 4 : 0));
// 解码掩码
if (isMasked) {
const mask = data.slice(offset, offset + 4);
for (let i = 0; i < payload.length; i++) {
payload[i] = payload[i] ^ mask[i % 4];
}
}
// 处理不同帧类型
if (opcode === 0x1) { // 文本帧
const message = payload.toString('utf8');
this.emit('message', socket, message);
} else if (opcode === 0x8) { // 关闭帧
socket.end();
} else if (opcode === 0x9) { // Ping
this.sendFrame(socket, 0xA, payload); // Pong
}
}
sendFrame(socket, opcode, payload) {
let header = [0x80 | opcode];
const len = payload.length;
if (len < 126) {
header.push(len);
} else if (len < 65536) {
header.push(126);
header = header.concat([(len >> 8) & 0xFF, len & 0xFF]);
} else {
header.push(127);
const buf = Buffer.alloc(8);
buf.writeBigUInt64BE(BigInt(len));
header = header.concat([...buf]);
}
socket.write(Buffer.from([...header, ...payload]));
}
broadcast(message) {
const data = Buffer.from(message);
this.clients.forEach(client => {
if (client.writable) {
this.sendFrame(client, 0x1, data);
}
});
}
on(event, callback) {
this[`_${event}Listeners`] = this[`_${event}Listeners`] || [];
this[`_${event}Listeners`].push(callback);
return this;
}
emit(event, ...args) {
const listeners = this[`_${event}Listeners`] || [];
listeners.forEach(cb => cb(...args));
}
}
module.exports = WebSocketServer;
9. 实践总结
✅
- 使用 WSS 加密连接
- 实现心跳机制检测连接状态
- 实现自动重连逻辑(指数退避)
- 处理二进制数据时使用 ArrayBuffer
- 设置合理的 超时时间
- 监控连接数和内存使用
- 使用 子协议 进行版本管理
- 生产环境使用成熟的库(如
ws、Socket.IO)
❌
- 不要在连接建立前发送数据
- 不要依赖
readyState做状态同步 - 不要在
onmessage中执行阻塞操作 - 不要忽略
onerror事件 - 不要在生产环境使用未经测试的简易实现
- 不要忘记处理连接关闭清理资源
10. 参考资源
写在最后:WebSocket 作为实时通信的基石,理解其原理和最佳实践对现代 Web 开发至关重要。希望这篇文章能帮助你构建高质量的应用,并在面试中游刃有余。如有疑问或补充,欢迎交流讨论!
更多推荐




所有评论(0)