# WebSocket快速入门
# 1. 介绍
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,使得服务器和客户端之间的数据交换变得更加简单。允许服务端主动向客户端推送数据。
# 2. 浏览器客户端
# 2.1 基本使用
// 创建 WebSocket 连接
let socket = new WebSocket("ws://localhost:8888")
// 连接打开时触发
socket.onopen = function(){
console.log('连接已建立')
// 向服务器发送消息
socket.send('hello server')
}
// 接收到服务器消息时触发
socket.onmessage = function(event){
console.log('收到消息:', event.data)
}
// 连接关闭时触发
socket.onclose = function(){
console.log('连接已关闭')
}
// 发生错误时触发
socket.onerror = function(error){
console.log('发生错误:', error)
}
# 2.2 常用方法
// 发送消息
socket.send(data)
// 关闭连接
socket.close()
// 添加事件监听器
socket.addEventListener('message', (event) => {
console.log(event.data)
})
// 移除事件监听器
socket.removeEventListener('message', handler)
# 2.3 常用属性
// 连接状态
socket.readyState
// 0: CONNECTING - 正在连接
// 1: OPEN - 已连接
// 2: CLOSING - 正在关闭
// 3: CLOSED - 已关闭
// 缓冲区中的消息数量
socket.bufferedAmount
// 子协议
socket.protocol
# 3. Node.js 服务端
# 3.1 安装 ws
npm i ws
# "ws": "^8.16.0"
# 3.2 基本使用
const { Server } = require('ws');
// 创建 WebSocket 服务器
const wsServer = new Server({ port: 8888 })
// 监听客户端连接
wsServer.on('connection', (socket) => {
console.log('客户端已连接')
// 监听客户端消息
socket.on('message', (message) => {
console.log('收到客户端消息:', message.toString())
// 向客户端发送消息
socket.send(message)
})
// 监听连接关闭
socket.on('close', () => {
console.log('客户端断开连接')
})
// 监听错误
socket.on('error', (error) => {
console.error('发生错误:', error)
})
})
// 监听服务器错误
wsServer.on('error', (error) => {
console.error('服务器错误:', error)
})
console.log('WebSocket 服务器运行在 ws://localhost:8888')
# 3.3 高级用法
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws) {
// 向所有连接的客户端广播
wss.clients.forEach(function each(client) {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ msg: '欢迎新成员' }));
}
});
ws.on('message', function incoming(data) {
// 将消息广播给所有客户端
wss.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
});
});
# 4. 完整示例
# 4.1 服务端代码
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8888 });
wss.on('connection', (ws) => {
console.log('客户端连接成功');
ws.on('message', (message) => {
console.log(`收到消息:${message}`);
// 回复消息
ws.send(`服务器收到:${message}`);
});
ws.on('close', () => {
console.log('客户端断开连接');
});
ws.on('error', (error) => {
console.error('连接错误:', error);
});
});
console.log('WebSocket 服务器已启动,监听端口 8888');
# 4.2 客户端代码
// 浏览器端
const socket = new WebSocket('ws://localhost:8888');
socket.onopen = () => {
console.log('连接到服务器');
socket.send('你好,服务器!');
};
socket.onmessage = (event) => {
console.log('收到服务器消息:', event.data);
};
socket.onclose = () => {
console.log('与服务器断开连接');
};
socket.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
# 5. 底层原理
# 5.1 协议握手过程
WebSocket 连接建立需要通过 HTTP 协议进行握手:
客户端请求:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com
服务端响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
握手说明:
Sec-WebSocket-Key: 客户端生成的随机 base64 字符串Sec-WebSocket-Accept: 服务端将 Key + GUID 后进行 SHA-1 哈希并 base64 编码- GUID:
258EAFA5-E914-47DA-95CA-C5AB0DC85B11
# 5.2 数据帧结构
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): 是否为最后一帧
- RSV (3 bits): 保留位,通常为 0
- Opcode (4 bits): 操作码
0x0: 继续帧0x1: 文本帧0x2: 二进制帧0x8: 关闭连接0x9: Ping0xA: Pong
- MASK (1 bit): 是否掩码(客户端发送必须为 1)
- Payload length: 负载长度
# 5.3 心跳机制(Ping/Pong)
保持连接活跃的心跳实现:
服务端实现(Node.js):
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8888 });
// 定时发送 ping
setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, 30000); // 每 30 秒发送一次 ping
wss.on('connection', (ws) => {
ws.isAlive = true;
// 收到 pong 响应
ws.on('pong', () => {
ws.isAlive = true;
});
});
客户端实现:
const socket = new WebSocket('ws://localhost:8888');
// 应用层心跳
let heartbeatTimer = null;
const HEARTBEAT_INTERVAL = 30000;
function startHeartbeat() {
heartbeatTimer = setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'ping' }));
}
}, HEARTBEAT_INTERVAL);
}
socket.onopen = () => {
startHeartbeat();
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
console.log('收到 pong 响应');
}
};
socket.onclose = () => {
clearInterval(heartbeatTimer);
};
# 5.4 断线重连机制
完整的重连实现:
class WebSocketClient {
constructor(url) {
this.url = url;
this.socket = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectDelay = 1000;
this.shouldReconnect = true;
}
connect() {
this.socket = new WebSocket(this.url);
this.socket.onopen = () => {
console.log('连接成功');
this.reconnectAttempts = 0;
this.onConnected();
};
this.socket.onmessage = (event) => {
this.onMessage(event.data);
};
this.socket.onclose = (event) => {
console.log(`连接关闭:code=${event.code}, reason=${event.reason}`);
this.attemptReconnect();
};
this.socket.onerror = (error) => {
console.error('连接错误:', error);
};
}
attemptReconnect() {
if (!this.shouldReconnect || this.reconnectAttempts >= this.maxReconnectAttempts) {
console.log('放弃重连');
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(`准备重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts}), 延迟 ${delay}ms`);
setTimeout(() => {
console.log('开始重连...');
this.connect();
}, delay);
}
send(data) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(data);
} else {
console.warn('连接未打开,消息发送失败');
}
}
disconnect() {
this.shouldReconnect = false;
if (this.socket) {
this.socket.close();
}
}
// 回调方法,可被子类重写
onConnected() {}
onMessage(data) {}
}
// 使用示例
const client = new WebSocketClient('ws://localhost:8888');
client.onConnected = () => {
console.log('已连接,可以开始通信');
};
client.onMessage = (data) => {
console.log('收到消息:', data);
};
client.connect();
# 5.5 二进制数据传输
处理 ArrayBuffer 和 Blob:
// 发送二进制数据
const socket = new WebSocket('ws://localhost:8888');
socket.binaryType = 'arraybuffer'; // 或 'blob'
// 发送图片
const canvas = document.querySelector('canvas');
canvas.toBlob((blob) => {
socket.send(blob);
});
// 接收二进制数据
socket.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
console.log('收到 ArrayBuffer:', event.data);
// 转换为 Uint8Array
const uint8Array = new Uint8Array(event.data);
} else if (event.data instanceof Blob) {
console.log('收到 Blob:', event.data);
// 读取为文本
const reader = new FileReader();
reader.onload = () => console.log(reader.result);
reader.readAsText(event.data);
}
};
// 服务端接收二进制数据
wsServer.on('connection', (socket) => {
socket.on('message', (data) => {
if (data instanceof Buffer) {
console.log('收到二进制数据:', data);
// 处理图片、文件等
}
});
});
# 5.6 子协议协商
使用子协议进行更严格的接口定义:
// 客户端指定子协议
const socket = new WebSocket('ws://localhost:8888', ['protocol1', 'protocol2']);
socket.onopen = () => {
console.log('使用的协议:', socket.protocol);
};
// 服务端选择协议
const WebSocket = require('ws');
const wss = new WebSocket.Server({
port: 8888,
handleProtocols: (protocols) => {
console.log('客户端支持的协议:', protocols);
// 选择一个协议
if (protocols.has('protocol1')) {
return 'protocol1';
}
return false; // 拒绝连接
}
});
# 5.7 性能优化
消息合并(批量发送):
class BatchedWebSocket {
constructor(ws) {
this.ws = ws;
this.messageQueue = [];
this.flushTimer = null;
this.BATCH_DELAY = 10; // 10ms
}
send(message) {
this.messageQueue.push(message);
if (!this.flushTimer) {
this.flushTimer = setTimeout(() => {
this.flush();
}, this.BATCH_DELAY);
}
}
flush() {
if (this.messageQueue.length > 0) {
const batchData = JSON.stringify({
batch: true,
messages: this.messageQueue
});
this.ws.send(batchData);
this.messageQueue = [];
}
this.flushTimer = null;
}
}
消息压缩(permessage-deflate):
const WebSocket = require('ws');
const wss = new WebSocket.Server({
port: 8888,
perMessageDeflate: {
zlibDeflateOptions: {
chunkSize: 1024,
memLevel: 7,
level: 3
},
zlibInflateOptions: {
chunkSize: 10 * 1024
},
clientNoContextTakeover: true,
serverNoContextTakeover: true,
serverMaxWindowBits: 10,
threshold: 1024 // 小于 1KB 的消息不压缩
}
});
# 6. 注意事项
协议:
ws://- 非加密的 WebSocket 连接wss://- 加密的 WebSocket 连接(推荐在生产环境使用)
跨域问题:WebSocket 不受同源策略限制,但需要注意 CORS 配置
心跳检测:建议实现心跳机制以保持连接活跃
重连机制:网络不稳定时需要实现自动重连
消息格式:建议使用 JSON 格式进行数据传输
连接数限制:浏览器对同一域名有最大连接数限制(通常 6-8 个)
内存泄漏:及时清理事件监听器和定时器
安全性:验证 Origin 头,防止 CSRF 攻击
# 7. 参考
上一篇: 下一篇:
本章目录