WebSocket 详解
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,实现了浏览器与服务器之间的实时双向数据传输。
一、WebSocket 基础概念
1. 为什么需要 WebSocket?
传统 HTTP 的问题:
单向通信(只能客户端发起请求)
实时性差(需要轮询)
头部信息冗余(每次请求都包含完整的 HTTP 头部)
WebSocket 的优势:
真正的双向通信
低延迟(建立连接后持续通信)
减少带宽占用(连接建立后头部信息很少)
2. WebSocket 协议特点
协议标识:
ws://(非加密)或wss://(加密,类似 HTTPS)握手过程: 基于 HTTP 协议升级
持久连接: 一旦建立,保持连接状态
跨域支持: 内置支持跨域通信
二、WebSocket 客户端 API
1. 基本使用
// 创建 WebSocket 连接
const socket = new WebSocket('ws://localhost:8080');// 连接建立时触发
socket.onopen = function(event) {console.log('连接已建立');// 发送消息socket.send('Hello Server!');
};// 接收消息时触发
socket.onmessage = function(event) {console.log('收到消息:', event.data);const message = JSON.parse(event.data);handleMessage(message);
};// 连接关闭时触发
socket.onclose = function(event) {console.log('连接关闭:', event.code, event.reason);if (event.wasClean) {console.log('连接正常关闭');} else {console.log('连接异常断开');}
};// 发生错误时触发
socket.onerror = function(error) {console.error('WebSocket 错误:', error);
};2. 完整的 WebSocket 客户端类
class WebSocketClient {constructor(url, options = {}) {this.url = url;this.socket = null;this.reconnectAttempts = 0;this.maxReconnectAttempts = options.maxReconnectAttempts || 5;this.reconnectInterval = options.reconnectInterval || 3000;this.messageHandlers = new Map();this.isConnected = false;this.setupEventHandlers();this.connect();}connect() {try {this.socket = new WebSocket(this.url);this.setupSocketEvents();} catch (error) {console.error('创建 WebSocket 连接失败:', error);this.handleReconnection();}}setupSocketEvents() {this.socket.onopen = (event) => {console.log('WebSocket 连接已建立');this.isConnected = true;this.reconnectAttempts = 0;this.emit('connected', event);};this.socket.onmessage = (event) => {try {const data = JSON.parse(event.data);this.handleMessage(data);} catch (error) {console.error('消息解析错误:', error);}};this.socket.onclose = (event) => {console.log('WebSocket 连接关闭');this.isConnected = false;this.emit('disconnected', event);this.handleReconnection();};this.socket.onerror = (error) => {console.error('WebSocket 错误:', error);this.emit('error', error);};}handleMessage(data) {const { type, payload } = data;// 调用特定类型的处理器if (this.messageHandlers.has(type)) {this.messageHandlers.get(type).forEach(handler => {handler(payload);});}// 调用全局处理器this.emit('message', data);}send(type, payload) {if (this.isConnected && this.socket) {const message = JSON.stringify({ type, payload, timestamp: Date.now() });this.socket.send(message);} else {console.warn('WebSocket 未连接,消息发送失败');}}on(event, handler) {if (!this.messageHandlers.has(event)) {this.messageHandlers.set(event, []);}this.messageHandlers.get(event).push(handler);}emit(event, data) {if (this.messageHandlers.has(event)) {this.messageHandlers.get(event).forEach(handler => {handler(data);});}}handleReconnection() {if (this.reconnectAttempts < this.maxReconnectAttempts) {this.reconnectAttempts++;console.log(`尝试重新连接... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);setTimeout(() => {this.connect();}, this.reconnectInterval);} else {console.error('达到最大重连次数,停止重连');this.emit('reconnect_failed');}}close() {if (this.socket) {this.socket.close(1000, '正常关闭');}}
}3. 在 React 中使用 WebSocket
import React, { useState, useEffect, useRef } from 'react';function ChatApp() {const [messages, setMessages] = useState([]);const [isConnected, setIsConnected] = useState(false);const socketRef = useRef();useEffect(() => {// 创建 WebSocket 连接socketRef.current = new WebSocketClient('ws://localhost:8080/chat');// 注册消息处理器socketRef.current.on('connected', () => {setIsConnected(true);});socketRef.current.on('disconnected', () => {setIsConnected(false);});socketRef.current.on('chat_message', (message) => {setMessages(prev => [...prev, message]);});socketRef.current.on('user_joined', (user) => {setMessages(prev => [...prev, {type: 'system',content: `${user.username} 加入了聊天室`}]);});// 清理函数return () => {if (socketRef.current) {socketRef.current.close();}};}, []);const sendMessage = (content) => {if (socketRef.current) {socketRef.current.send('chat_message', {content,userId: 'current-user-id',timestamp: Date.now()});}};return (<div className="chat-app"><div className={`status ${isConnected ? 'connected' : 'disconnected'}`}>{isConnected ? '已连接' : '连接中...'}</div><MessageList messages={messages} /><MessageInput onSendMessage={sendMessage} /></div>);
}三、WebSocket 服务器端实现
1. Node.js + ws 库实现
const WebSocket = require('ws');
const http = require('http');
const url = require('url');// 创建 HTTP 服务器
const server = http.createServer((req, res) => {res.writeHead(200, { 'Content-Type': 'text/plain' });res.end('WebSocket Server');
});// 创建 WebSocket 服务器
const wss = new WebSocket.Server({ server,// 验证连接verifyClient: (info, callback) => {const { query } = url.parse(info.req.url, true);// 简单的 token 验证if (query.token && isValidToken(query.token)) {callback(true);} else {callback(false, 401, 'Unauthorized');}}
});// 存储连接的客户端
const clients = new Map();function isValidToken(token) {// 实现 token 验证逻辑return token === 'valid-token';
}wss.on('connection', (ws, req) => {const { query } = url.parse(req.url, true);const userId = query.userId;console.log(`客户端连接: ${userId}`);// 存储客户端连接clients.set(userId, ws);// 广播用户上线通知broadcast({type: 'user_online',userId: userId,timestamp: Date.now()}, userId);// 处理消息ws.on('message', (data) => {try {const message = JSON.parse(data);handleMessage(ws, message, userId);} catch (error) {console.error('消息解析错误:', error);sendError(ws, '消息格式错误');}});// 处理连接关闭ws.on('close', (code, reason) => {console.log(`客户端断开: ${userId}, 代码: ${code}, 原因: ${reason}`);clients.delete(userId);// 广播用户离线通知broadcast({type: 'user_offline',userId: userId,timestamp: Date.now()});});// 处理错误ws.on('error', (error) => {console.error(`客户端错误 (${userId}):`, error);});// 发送欢迎消息ws.send(JSON.stringify({type: 'welcome',message: '连接成功',userId: userId,timestamp: Date.now()}));
});function handleMessage(ws, message, userId) {const { type, payload } = message;switch (type) {case 'chat_message':// 广播聊天消息broadcast({type: 'chat_message',payload: {...payload,userId: userId,messageId: generateMessageId(),timestamp: Date.now()}});break;case 'private_message':// 私聊消息const targetUserId = payload.targetUserId;const targetClient = clients.get(targetUserId);if (targetClient && targetClient.readyState === WebSocket.OPEN) {targetClient.send(JSON.stringify({type: 'private_message',payload: {...payload,fromUserId: userId,timestamp: Date.now()}}));}break;case 'typing':// 广播输入状态broadcast({type: 'user_typing',userId: userId,isTyping: payload.isTyping}, userId);break;default:sendError(ws, `未知的消息类型: ${type}`);}
}function broadcast(message, excludeUserId = null) {const data = JSON.stringify(message);clients.forEach((client, userId) => {if (userId !== excludeUserId && client.readyState === WebSocket.OPEN) {client.send(data);}});
}function sendError(ws, errorMessage) {ws.send(JSON.stringify({type: 'error',message: errorMessage,timestamp: Date.now()}));
}function generateMessageId() {return Date.now().toString(36) + Math.random().toString(36).substr(2);
}// 启动服务器
server.listen(8080, () => {console.log('WebSocket 服务器运行在端口 8080');
});2. 心跳检测实现
// 心跳检测机制
function setupHeartbeat(ws) {let isAlive = true;let heartbeatInterval;const heartbeat = () => {if (!isAlive) {return ws.terminate();}isAlive = false;ws.ping();};ws.on('pong', () => {isAlive = true;});ws.on('close', () => {clearInterval(heartbeatInterval);});// 每 30 秒发送一次心跳检测heartbeatInterval = setInterval(heartbeat, 30000);// 初始心跳检测heartbeat();
}四、WebSocket 协议详解
1. 握手过程
客户端请求:
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=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 ... |
+---------------------------------------------------------------+五、WebSocket 与 Socket.IO 对比
特性 | WebSocket | Socket.IO |
|---|---|---|
协议支持 | 纯 WebSocket 协议 | WebSocket + HTTP 长轮询降级 |
浏览器兼容 | 现代浏览器 | 所有浏览器(包括旧版) |
自动重连 | 需要手动实现 | 内置自动重连机制 |
房间支持 | 需要手动实现 | 内置房间和命名空间 |
二进制数据 | 原生支持 | 原生支持 |
体积 | 轻量(浏览器内置) | 较大(需要引入库) |
六、生产环境最佳实践
1. 安全性考虑
// 1. 使用 WSS
const socket = new WebSocket('wss://yourdomain.com/ws');// 2. 验证 Origin
wss.on('headers', (headers, req) => {const origin = req.headers.origin;if (!isAllowedOrigin(origin)) {throw new Error('Origin not allowed');}
});// 3. 限制消息大小
ws.on('message', (data) => {if (data.length > MAX_MESSAGE_SIZE) {ws.close(1009, 'Message too large');return;}// 处理消息...
});2. 性能优化
// 1. 消息压缩
function compressMessage(message) {// 实现消息压缩逻辑return JSON.stringify(message);
}// 2. 批量发送
function sendBatch(messages) {const batch = {type: 'batch',messages: messages,timestamp: Date.now()};socket.send(compressMessage(batch));
}// 3. 连接池管理
class ConnectionPool {constructor(maxConnections = 100) {this.maxConnections = maxConnections;this.connections = new Set();}addConnection(ws) {if (this.connections.size >= this.maxConnections) {// 关闭最旧的连接const oldest = this.getOldestConnection();oldest.close(1000, 'Connection pool full');}this.connections.add(ws);}
}3. 监控和日志
// 监控连接状态
function setupMonitoring(wss) {let connections = 0;let messagesReceived = 0;wss.on('connection', (ws) => {connections++;console.log(`活跃连接数: ${connections}`);ws.on('close', () => {connections--;console.log(`活跃连接数: ${connections}`);});ws.on('message', () => {messagesReceived++;});});// 定期报告指标setInterval(() => {console.log(`统计 - 连接数: ${connections}, 消息数: ${messagesReceived}`);}, 60000);
}七、常见应用场景
实时聊天应用
在线游戏
股票行情推送
协同编辑工具
实时通知系统
物联网设备监控
WebSocket 是现代 Web 应用中实现实时通信的核心技术,正确使用它可以构建出高性能的实时应用程序。
