一、为什么我们需要WebSocket?
想象一下这样的场景:你在一个网页聊天室里,每次发送或接收消息,都需要整个页面重新加载 ,就像每次对话都要刷新一次屏幕。这就是传统的HTTP通信方式------它就像写信,每次交流都需要完整的"信封"(请求头)和"回信"(响应)。
但实时应用需要的是即时对话 ,这正是WebSocket诞生的原因。它让浏览器和服务器之间建立起一条持久化的双向通道,双方可以随时主动发送消息,就像打电话一样自然流畅。
二、WebSocket的核心:一次握手,长久通话
1. WebSocket握手:从HTTP升级而来
WebSocket的建立始于一次特殊的HTTP"升级"请求:
客户端请求:
GET /chat HTTP/1.1
Host: 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=
这个握手过程的关键点:
-
客户端通过
Upgrade: websocket头部表明想要升级协议 -
Sec-WebSocket-Key是一个随机生成的base64编码字符串 -
服务器验证后返回101状态码和计算出的
Sec-WebSocket-Accept -
从此之后,连接保持在TCP层,不再遵循HTTP协议
2. 建立连接后的数据传输
握手成功后,数据通过WebSocket帧传输。每个帧包含:
-
操作码:标识这是文本(0x1)、二进制数据(0x2)、关闭连接(0x8)等
-
负载长度:数据部分的长度
-
掩码键(仅客户端到服务器):安全措施,防止缓存污染攻击
-
实际数据:要传输的内容
这种设计极其高效------一个简单的消息可能只有6-10字节的开销,而HTTP请求至少需要几百字节的头部。
三、WebSocket与HTTP/轮询的对比
| 特性 | HTTP短轮询 | HTTP长轮询 | WebSocket |
|---|---|---|---|
| 通信方向 | 单向(客户端轮询) | 准双向(服务器可延迟响应) | 真正双向 |
| 头部开销 | 每次请求都有完整HTTP头部 | 每次请求都有完整HTTP头部 | 握手后几乎无额外头部 |
| 延迟 | 高(取决于轮询间隔) | 中等(服务器可即时响应) | 极低(实时) |
| 服务器压力 | 高(频繁建立连接) | 中高(连接保持较久) | 低(一个连接持久化) |
| 适用场景 | 更新频率低的应用 | 中等实时性要求 | 高实时性应用 |
一个直观的例子:
假设有一个实时股价显示应用:
-
HTTP轮询:每5秒问一次"股价变了吗?" → 获取最新价格
-
WebSocket:建立连接后,服务器主动推送:"股价变了,现在是XX元"
四、动手实践:构建一个简单聊天室
1. 客户端实现
html
<!DOCTYPE html>
<html>
<head>
<title>WebSocket聊天室</title>
<style>
#messages { border: 1px solid #ccc; height: 300px; overflow-y: scroll; padding: 10px; }
.message { margin: 5px 0; padding: 8px; background: #f0f0f0; border-radius: 4px; }
.self { background: #d0e8ff; text-align: right; }
</style>
</head>
<body>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="输入消息...">
<button onclick="sendMessage()">发送</button>
<button onclick="connectWebSocket()">连接</button>
<button onclick="disconnectWebSocket()">断开</button>
<script>
let socket = null;
const messageContainer = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
function connectWebSocket() {
// 创建WebSocket连接,使用wss://(安全)或ws://(非安全)
socket = new WebSocket('ws://localhost:8080/chat');
// 连接建立时的回调
socket.onopen = function(event) {
addMessage('系统', '连接已建立!', 'system');
console.log('WebSocket连接已打开');
};
// 接收消息时的回调
socket.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
addMessage(data.user, data.message, 'remote');
} catch(e) {
addMessage('系统', event.data, 'system');
}
};
// 连接关闭时的回调
socket.onclose = function(event) {
const reason = event.reason || '未知原因';
addMessage('系统', `连接关闭:${reason}`, 'system');
console.log('WebSocket连接关闭', event);
};
// 发生错误时的回调
socket.onerror = function(error) {
addMessage('系统', '连接发生错误', 'system');
console.error('WebSocket错误:', error);
};
}
function sendMessage() {
if (!socket || socket.readyState !== WebSocket.OPEN) {
alert('请先建立连接!');
return;
}
const message = messageInput.value.trim();
if (!message) return;
// 发送消息到服务器
const messageData = {
user: '用户' + Math.floor(Math.random() * 1000),
message: message,
timestamp: new Date().toISOString()
};
socket.send(JSON.stringify(messageData));
// 在本地显示自己发送的消息
addMessage('我', message, 'self');
messageInput.value = '';
}
function disconnectWebSocket() {
if (socket) {
// 1000表示正常关闭
socket.close(1000, '用户主动断开连接');
}
}
function addMessage(user, message, type) {
const messageElement = document.createElement('div');
messageElement.className = `message ${type}`;
messageElement.innerHTML = `<strong>${user}:</strong> ${message}`;
messageContainer.appendChild(messageElement);
messageContainer.scrollTop = messageContainer.scrollHeight;
}
// 页面加载时自动连接(可选)
window.onload = connectWebSocket;
// 输入框按Enter发送消息
messageInput.addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
sendMessage();
}
});
</script>
</body>
</html>
2. 服务器端实现(Node.js + ws库)
javascript
// 安装ws库:npm install ws
const WebSocket = require('ws');
const http = require('http');
// 创建HTTP服务器(可选,用于提供客户端页面)
const server = http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<h1>WebSocket聊天服务器</h1>
<p>服务器正在运行,客户端可以连接到 ws://localhost:8080/chat</p>
`);
}
});
// 创建WebSocket服务器,附加到HTTP服务器上
const wss = new WebSocket.Server({
server,
path: '/chat' // 指定WebSocket端点
});
// 存储所有连接的客户端
const clients = new Set();
// 连接统计
let connectionCount = 0;
wss.on('connection', (ws, request) => {
connectionCount++;
const clientId = connectionCount;
clients.add(ws);
console.log(`客户端 ${clientId} 已连接,IP: ${request.socket.remoteAddress}`);
// 发送欢迎消息给新客户端
ws.send(JSON.stringify({
type: 'system',
user: '服务器',
message: `欢迎来到聊天室!你是第${clientId}位用户`,
timestamp: new Date().toISOString()
}));
// 广播新用户加入的消息给所有客户端(除自己)
broadcast({
type: 'notification',
user: '系统',
message: `用户${clientId}加入了聊天室`,
timestamp: new Date().toISOString()
}, ws);
// 处理客户端发来的消息
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
console.log(`收到来自客户端 ${clientId} 的消息:`, data);
// 添加客户端ID和时间戳
const enrichedData = {
...data,
clientId,
timestamp: new Date().toISOString(),
type: 'message'
};
// 广播消息给所有客户端
broadcast(enrichedData);
} catch (error) {
console.error('消息解析错误:', error);
ws.send(JSON.stringify({
type: 'error',
message: '消息格式错误,请发送有效的JSON'
}));
}
});
// 处理连接关闭
ws.on('close', (code, reason) => {
clients.delete(ws);
console.log(`客户端 ${clientId} 断开连接,代码: ${code}, 原因: ${reason || '无'}`);
// 广播用户离开的消息
broadcast({
type: 'notification',
user: '系统',
message: `用户${clientId}离开了聊天室`,
timestamp: new Date().toISOString()
});
});
// 处理错误
ws.on('error', (error) => {
console.error(`客户端 ${clientId} 发生错误:`, error);
});
});
// 广播消息给所有连接的客户端
function broadcast(data, excludeWs = null) {
const message = JSON.stringify(data);
clients.forEach(client => {
// 排除特定客户端(例如不给自己发通知)
if (client === excludeWs) return;
// 检查客户端连接状态
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
// 启动服务器
const PORT = 8080;
server.listen(PORT, () => {
console.log(`服务器已启动,HTTP端口: ${PORT}`);
console.log(`WebSocket端点: ws://localhost:${PORT}/chat`);
});
// 添加心跳机制,防止连接超时
setInterval(() => {
const pingMessage = JSON.stringify({
type: 'ping',
timestamp: Date.now()
});
clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(pingMessage);
}
});
console.log(`发送心跳,当前连接数: ${clients.size}`);
}, 30000); // 每30秒发送一次心跳
五、WebSocket在实际项目中的最佳实践
1. 连接管理与重连机制
在实际生产环境中,网络是不稳定的。一个健壮的WebSocket客户端需要:
javascript
class RobustWebSocket {
constructor(url, options = {}) {
this.url = url;
this.reconnectDelay = options.reconnectDelay || 1000;
this.maxReconnectDelay = options.maxReconnectDelay || 30000;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('WebSocket连接成功');
this.reconnectAttempts = 0; // 重置重连计数
this.onOpen && this.onOpen();
};
this.ws.onclose = (event) => {
console.log(`连接关闭,代码: ${event.code}, 原因: ${event.reason}`);
// 如果不是正常关闭,尝试重连
if (event.code !== 1000) {
this.scheduleReconnect();
}
this.onClose && this.onClose(event);
};
this.ws.onerror = (error) => {
console.error('WebSocket错误:', error);
this.onError && this.onError(error);
};
this.ws.onmessage = (event) => {
this.onMessage && this.onMessage(event);
};
}
scheduleReconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('达到最大重连次数,停止重连');
return;
}
this.reconnectAttempts++;
// 指数退避算法:延迟时间逐渐增加
const delay = Math.min(
this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1),
this.maxReconnectDelay
);
console.log(`${delay}ms后尝试第${this.reconnectAttempts}次重连...`);
setTimeout(() => {
if (this.ws.readyState === WebSocket.CLOSED) {
this.connect();
}
}, delay);
}
send(data) {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(data);
return true;
} else {
console.warn('无法发送消息,WebSocket未连接');
return false;
}
}
close() {
this.ws.close(1000, '用户主动关闭');
}
}
2. 消息协议设计
对于复杂应用,需要设计自己的消息协议:
javascript
// 消息类型枚举
const MessageType = {
AUTH: 1, // 认证
CHAT: 2, // 聊天消息
NOTIFICATION: 3,// 通知
HEARTBEAT: 4, // 心跳
ERROR: 5 // 错误
};
// 消息编码器
class MessageEncoder {
static encode(type, payload) {
return JSON.stringify({
t: type, // 消息类型
p: payload, // 消息内容
ts: Date.now() // 时间戳
});
}
static decode(message) {
try {
const data = JSON.parse(message);
return {
type: data.t,
payload: data.p,
timestamp: data.ts
};
} catch (error) {
return {
type: MessageType.ERROR,
payload: { error: '消息格式无效' },
timestamp: Date.now()
};
}
}
}
// 使用示例
const chatMessage = MessageEncoder.encode(MessageType.CHAT, {
userId: '123',
content: '你好!',
roomId: 'general'
});
// 服务器端处理
function handleMessage(rawMessage) {
const message = MessageEncoder.decode(rawMessage);
switch(message.type) {
case MessageType.CHAT:
handleChatMessage(message.payload);
break;
case MessageType.HEARTBEAT:
handleHeartbeat(message.payload);
break;
// ... 其他类型处理
}
}
六、WebSocket的常见应用场景
1. 即时通讯应用
-
聊天应用(如微信网页版、Slack)
-
客服系统
-
实时协作工具(如Google Docs的实时编辑)
2. 实时数据推送
-
股票行情系统
-
实时监控仪表盘
-
体育比赛实时比分
-
新闻资讯推送
3. 在线游戏
-
多人在线游戏
-
实时对战游戏
-
游戏状态同步
4. 物联网(IoT)
-
设备状态实时监控
-
远程控制指令下发
-
传感器数据实时收集
七、安全考虑
-
使用WSS(WebSocket Secure):就像HTTPS对HTTP的保护,WSS通过TLS加密数据
-
身份验证:在握手阶段通过URL参数、Cookie或令牌进行身份验证
-
输入验证:始终验证和清理接收到的数据
-
速率限制:防止客户端发送过多消息
-
Origin检查:验证WebSocket连接的来源
javascript
// 在服务器端进行Origin验证
const wss = new WebSocket.Server({
server,
verifyClient: (info, callback) => {
// 只允许来自特定域名的连接
const allowedOrigins = ['https://example.com', 'https://app.example.com'];
const origin = info.origin || info.req.headers.origin;
if (allowedOrigins.includes(origin)) {
callback(true);
} else {
callback(false, 403, 'Forbidden: Origin not allowed');
}
}
});
八、浏览器兼容性与备选方案
现代浏览器都支持WebSocket,但对于不支持的环境,需要有备选方案:
javascript
// 检测浏览器是否支持WebSocket
if ('WebSocket' in window) {
// 使用原生WebSocket
const ws = new WebSocket('wss://example.com/socket');
} else {
// 降级方案:使用长轮询或Server-Sent Events
console.warn('浏览器不支持WebSocket,使用长轮询作为备选');
setupLongPolling();
}
// 使用Socket.io等库可以自动处理降级
// Socket.io会先尝试WebSocket,如果不支持则自动降级到轮询