WebSocket 介绍
什么是 WebSocket?
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,允许客户端和服务器之间进行实时、双向的数据传输。
核心特点
- 持久连接:建立连接后保持打开状态,直到主动关闭
- 双向通信:客户端和服务器都可以主动发送消息
- 低延迟:无需每次通信都建立新连接
- 协议升级:基于 HTTP 协议,通过
Upgrade头升级为 WebSocket 协议
工作原理
-
握手阶段:客户端发送 HTTP 请求,请求升级为 WebSocket 协议
GET /chat HTTP/1.1 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Version: 13 -
服务端响应:服务端确认升级,返回 101 状态码
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= -
数据传输:升级成功后,双方通过 WebSocket 帧进行数据传输
WebSocket 的优势
1. 实时性强
- 消息可以立即推送,无需客户端轮询
- 延迟低,适合实时应用场景
2. 性能优势
- 减少 HTTP 请求:一次握手,多次通信
- 降低服务器压力:不需要频繁建立和关闭连接
- 减少带宽消耗:WebSocket 帧头只有 2-14 字节,比 HTTP 请求头小得多
3. 双向通信
- 客户端和服务器都可以主动发送消息
- 不需要客户端主动请求就能接收服务器推送
4. 协议开销小
- WebSocket 帧格式简单,开销远小于 HTTP 请求/响应
5. 支持二进制和文本数据
- 可以传输文本、JSON、二进制数据等多种格式
WebSocket 的劣势
1. 浏览器兼容性
- 需要浏览器支持(IE 10+)
- 移动端浏览器支持良好,但需要考虑降级方案
2. 连接管理复杂
- 需要处理连接断开、重连、心跳检测等
- 网络不稳定时连接可能频繁断开
3. 服务器资源占用
- 每个连接都需要保持,大量并发连接会占用服务器资源
- 需要合理设计连接池和资源管理
4. 代理和防火墙问题
- 某些代理服务器可能不支持 WebSocket
- 需要处理代理穿透问题
5. 调试相对困难
- 不像 HTTP 请求那样容易在浏览器 Network 面板查看
- 需要专门的工具或浏览器扩展来调试
6. 不支持 HTTP 缓存
- 无法利用浏览器和 CDN 的缓存机制
与其他方案的对比
1. HTTP 短轮询(Short Polling)
原理:客户端定时向服务器发送请求
setInterval(() => {
fetch('/api/data').then(res => res.json());
}, 1000);
对比:
- ❌ 实时性差,有延迟
- ❌ 服务器压力大,频繁请求
- ❌ 浪费带宽,很多请求是无用的
- ✅ 实现简单,兼容性好
2. HTTP 长轮询(Long Polling)
原理:客户端发送请求,服务器保持连接直到有数据或超时
function longPoll() {
fetch('/api/data')
.then(res => res.json())
.then(data => {
// 处理数据
longPoll(); // 立即发起下一次请求
});
}
对比:
- ✅ 实时性较好
- ⚠️ 服务器需要保持连接,资源占用中等
- ⚠️ 实现相对复杂
- ❌ 仍然需要频繁建立连接
3. Server-Sent Events (SSE)
原理:服务器向客户端推送数据,单向通信
const eventSource = new EventSource('/api/events');
eventSource.onmessage = (event) => {
console.log(event.data);
};
对比:
- ✅ 实现简单,基于 HTTP
- ✅ 自动重连
- ✅ 浏览器原生支持
- ❌ 只能服务器向客户端推送(单向)
- ❌ IE 不支持
4. WebSocket
对比:
- ✅ 双向通信
- ✅ 实时性最好
- ✅ 性能最优
- ✅ 支持二进制数据
- ⚠️ 实现相对复杂
- ⚠️ 需要处理连接管理
对比总结表
| 方案 | 实时性 | 双向通信 | 服务器压力 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 短轮询 | ⭐ | ✅ | ❌ 高 | ⭐ 简单 | 更新频率低 |
| 长轮询 | ⭐⭐ | ✅ | ⚠️ 中 | ⭐⭐ | 实时性要求不高 |
| SSE | ⭐⭐⭐ | ❌ 单向 | ⚠️ 中 | ⭐⭐ | 服务器推送 |
| WebSocket | ⭐⭐⭐ | ✅ | ⚠️ 中 | ⭐⭐⭐ | 实时双向通信 |
什么情况选择使用 WebSocket?
✅ 适合使用的场景
-
实时聊天应用
- 即时通讯、在线客服
- 需要双向实时通信
-
实时数据展示
- 股票行情、实时监控
- 数据大屏、实时统计
-
在线游戏
- 多人游戏、实时对战
- 需要低延迟的双向通信
-
协作工具
- 在线文档编辑、代码协作
- 多人实时编辑
-
实时通知
- 系统通知、消息推送
- 订单状态更新
-
IoT 设备控制
- 智能家居控制
- 设备状态监控
❌ 不适合使用的场景
-
简单的数据获取
- 只需要获取一次数据,使用 HTTP 即可
-
单向推送
- 只需要服务器推送,考虑使用 SSE
-
更新频率很低
- 几分钟或更长时间更新一次,使用轮询即可
-
需要利用 HTTP 缓存
- 需要 CDN 缓存、浏览器缓存,使用 HTTP
-
对兼容性要求极高
- 需要支持老旧浏览器,考虑降级方案
WebSocket 基本使用
客户端 API
// 1. 创建连接
const ws = new WebSocket('ws://localhost:8080');
// 2. 连接成功
ws.onopen = function(event) {
console.log('连接成功');
ws.send('Hello Server');
};
// 3. 接收消息
ws.onmessage = function(event) {
console.log('收到消息:', event.data);
// 可以接收文本或二进制数据
if (event.data instanceof ArrayBuffer) {
// 处理二进制数据
}
};
// 4. 连接关闭
ws.onclose = function(event) {
console.log('连接关闭', event.code, event.reason);
};
// 5. 连接错误
ws.onerror = function(error) {
console.error('连接错误:', error);
};
// 6. 发送消息
ws.send('文本消息');
ws.send(JSON.stringify({ type: 'message', content: 'Hello' }));
ws.send(new ArrayBuffer(8)); // 二进制数据
// 7. 关闭连接
ws.close();
// 8. 连接状态
// ws.readyState:
// CONNECTING (0) - 正在连接
// OPEN (1) - 已连接
// CLOSING (2) - 正在关闭
// CLOSED (3) - 已关闭
服务端实现(Node.js)
import http from 'http';
import { WebSocketServer } from 'ws';
const server = http.createServer();
const wss = new WebSocketServer({ server });
wss.on('connection', (ws, req) => {
// 新连接
ws.on('message', (data) => {
// 接收消息
ws.send(`Echo: ${data}`);
});
ws.on('close', () => {
// 连接关闭
});
});
server.listen(8080);
实际开发中的注意事项
1. 心跳检测(Heartbeat)
防止连接被代理服务器或防火墙关闭:
// 客户端
const heartbeat = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000);
ws.onclose = () => {
clearInterval(heartbeat);
};
2. 自动重连
let ws;
let reconnectTimer;
function connect() {
ws = new WebSocket('ws://localhost:8080');
ws.onclose = () => {
reconnectTimer = setTimeout(connect, 3000);
};
ws.onerror = () => {
ws.close();
};
}
connect();
3. 消息队列
连接断开时缓存消息,连接恢复后发送:
const messageQueue = [];
function sendMessage(msg) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(msg);
} else {
messageQueue.push(msg);
}
}
ws.onopen = () => {
while (messageQueue.length > 0) {
ws.send(messageQueue.shift());
}
};
4. 错误处理
ws.onerror = (error) => {
console.error('WebSocket error:', error);
// 记录错误日志
// 通知用户
// 尝试重连
};
5. 安全性
- 使用
wss://(WebSocket Secure)加密传输 - 验证来源(Origin)
- 使用 Token 进行身份验证
- 限制连接数和消息频率
相关技术介绍
Socket.io
Socket.io 是一个基于 WebSocket 的实时通信库,提供了更高级的抽象和额外的功能。
核心特点
-
自动降级
- 优先使用 WebSocket
- 如果不支持,自动降级到长轮询、短轮询等
- 保证在各种环境下都能工作
-
自动重连
- 内置自动重连机制
- 连接断开后自动尝试重连
-
房间(Rooms)和命名空间(Namespaces)
- 可以将客户端分组到不同的房间
- 支持命名空间,实现多应用隔离
房间(Rooms):将客户端分组,可以向特定房间广播消息
命名空间(Namespaces):类似于 URL 路径,用于隔离不同的应用或功能模块
-
事件系统
- 基于事件的 API,类似 Node.js 的 EventEmitter
- 可以自定义事件名称
-
广播功能
- 支持向所有客户端或特定房间广播消息
基本使用
// 服务端
const io = require('socket.io')(server);
io.on('connection', (socket) => {
// 加入房间
socket.join('room1');
// 监听自定义事件
socket.on('chat message', (msg) => {
// 广播到所有客户端
io.emit('chat message', msg);
// 广播到特定房间
io.to('room1').emit('chat message', msg);
});
socket.on('disconnect', () => {
console.log('用户断开连接');
});
});
// 客户端
const socket = io('http://localhost:3000');
socket.on('connect', () => {
console.log('已连接');
});
socket.emit('chat message', 'Hello');
socket.on('chat message', (msg) => {
console.log('收到消息:', msg);
});
房间(Rooms)详解
什么是房间?
房间是 Socket.io 中的一个核心概念,用于将客户端分组。客户端可以加入(join)或离开(leave)房间,服务器可以向特定房间的所有客户端广播消息。
使用场景:
- 聊天室:不同聊天室的用户互不干扰
- 在线游戏:不同游戏房间的玩家
- 协作编辑:不同文档的协作者
- 实时通知:向特定用户组推送通知
示例代码:
// 服务端
io.on('connection', (socket) => {
// 用户加入房间
socket.on('join room', (roomId) => {
socket.join(roomId);
// 通知房间内其他用户
socket.to(roomId).emit('user joined', {
userId: socket.id,
message: '新用户加入房间'
});
});
// 向房间发送消息
socket.on('room message', (data) => {
// 只向指定房间广播,不包括发送者
socket.to(data.roomId).emit('room message', {
userId: socket.id,
message: data.message
});
// 或者包括发送者
io.to(data.roomId).emit('room message', {
userId: socket.id,
message: data.message
});
});
// 离开房间
socket.on('leave room', (roomId) => {
socket.leave(roomId);
});
// 断开连接时自动离开所有房间
socket.on('disconnect', () => {
// Socket.io 会自动处理
});
});
// 客户端
socket.emit('join room', 'room123');
socket.on('room message', (data) => {
console.log(`${data.userId}: ${data.message}`);
});
房间操作:
socket.join(room)- 加入房间socket.leave(room)- 离开房间io.to(room).emit()- 向房间广播(不包括发送者)io.in(room).emit()- 向房间广播(包括发送者)socket.to(room).emit()- 向房间广播(不包括发送者)
实际应用场景:
// 场景1:聊天室应用
socket.on('join chatroom', (chatroomId) => {
socket.join(`chatroom:${chatroomId}`);
// 只向这个聊天室的用户发送消息
});
// 场景2:在线游戏
socket.on('join game', (gameId) => {
socket.join(`game:${gameId}`);
// 游戏状态只推送给这个游戏的玩家
});
// 场景3:用户通知
socket.on('user online', (userId) => {
socket.join(`user:${userId}`);
// 可以向特定用户推送通知
io.to(`user:${userId}`).emit('notification', { ... });
});
命名空间(Namespaces)详解
什么是命名空间?
命名空间类似于 URL 路径,用于将 Socket.io 应用划分为不同的逻辑单元。每个命名空间都有自己的连接、事件和房间。
默认命名空间: /(根命名空间)
使用场景:
- 多应用隔离:同一个服务器运行多个应用
- 功能模块分离:不同功能使用不同的命名空间
- 权限控制:不同命名空间可以有不同的认证机制
示例代码:
// 创建不同的命名空间
const adminNamespace = io.of('/admin');
const userNamespace = io.of('/user');
const chatNamespace = io.of('/chat');
// 管理后台命名空间
adminNamespace.on('connection', (socket) => {
// 需要管理员权限
socket.on('admin action', (data) => {
// 处理管理员操作
adminNamespace.emit('admin update', data);
});
});
// 用户命名空间
userNamespace.on('connection', (socket) => {
socket.on('user message', (data) => {
// 处理用户消息
userNamespace.emit('user update', data);
});
});
// 聊天命名空间
chatNamespace.on('connection', (socket) => {
socket.on('chat message', (data) => {
chatNamespace.emit('chat message', data);
});
});
客户端连接:
// 连接到默认命名空间
const socket = io('http://localhost:3000');
// 连接到指定命名空间
const adminSocket = io('http://localhost:3000/admin');
const userSocket = io('http://localhost:3000/user');
const chatSocket = io('http://localhost:3000/chat');
命名空间的特点:
- 每个命名空间是独立的,互不干扰
- 每个命名空间有自己的房间系统
- 可以有不同的中间件和认证机制
- 可以有不同的连接处理逻辑
实际应用场景:
// 场景1:多租户应用
const tenant1 = io.of('/tenant1');
const tenant2 = io.of('/tenant2');
// 不同租户的数据完全隔离
// 场景2:功能模块分离
const notificationNs = io.of('/notifications');
const chatNs = io.of('/chat');
const gameNs = io.of('/game');
// 不同功能模块使用不同命名空间
// 场景3:权限控制
io.of('/admin').use((socket, next) => {
// 中间件:验证管理员权限
if (isAdmin(socket.handshake.auth.token)) {
next();
} else {
next(new Error('Unauthorized'));
}
});
房间 vs 命名空间:
| 特性 | 房间(Rooms) | 命名空间(Namespaces) |
|---|---|---|
| 作用范围 | 单个命名空间内 | 整个应用级别 |
| 隔离程度 | 消息隔离 | 完全隔离(连接、事件、房间) |
| 使用场景 | 分组客户端 | 分离应用/功能模块 |
| 客户端连接 | 自动加入 | 需要指定命名空间 URL |
| 性能影响 | 小 | 中等(每个命名空间独立管理) |
组合使用示例:
// 在命名空间中使用房间
const chatNs = io.of('/chat');
chatNs.on('connection', (socket) => {
// 用户加入聊天室(房间)
socket.on('join chatroom', (chatroomId) => {
socket.join(`chatroom:${chatroomId}`);
});
// 向特定聊天室发送消息
socket.on('send message', (data) => {
// 在命名空间内的特定房间广播
chatNs.to(`chatroom:${data.chatroomId}`).emit('new message', {
userId: socket.id,
message: data.message
});
});
});
Socket.io vs 原生 WebSocket
| 特性 | Socket.io | 原生 WebSocket |
|---|---|---|
| 兼容性 | ✅ 自动降级,兼容性好 | ⚠️ 需要浏览器支持 |
| 自动重连 | ✅ 内置 | ❌ 需要自己实现 |
| 房间/命名空间 | ✅ 支持 | ❌ 不支持 |
| 事件系统 | ✅ 基于事件 | ⚠️ 需要自己封装 |
| 二进制数据 | ✅ 支持 | ✅ 支持 |
| 性能 | ⚠️ 有额外开销 | ✅ 性能更好 |
| 包大小 | ⚠️ 较大(~100KB) | ✅ 原生 API |
适用场景
- ✅ 需要兼容老旧浏览器
- ✅ 需要房间、命名空间等高级功能
- ✅ 需要快速开发,不想处理底层细节
- ✅ 需要自动重连、心跳等开箱即用的功能
RabbitMQ
RabbitMQ 是一个开源的消息队列中间件,实现了 AMQP(Advanced Message Queuing Protocol)协议。
核心概念
-
消息队列(Message Queue)
- 存储消息的缓冲区
- 生产者发送消息到队列,消费者从队列接收消息
-
生产者(Producer)
- 发送消息的应用
-
消费者(Consumer)
- 接收和处理消息的应用
-
交换机(Exchange)
- 接收生产者发送的消息,根据路由规则将消息路由到队列
-
路由键(Routing Key)
- 用于决定消息如何路由到队列
核心特点
-
可靠性
- 消息持久化
- 支持消息确认机制
- 支持事务
-
灵活的路由
- Direct(直接路由)
- Topic(主题路由)
- Fanout(广播)
- Headers(头部路由)
-
高可用性
- 支持集群
- 支持镜像队列
-
多协议支持
- AMQP、MQTT、STOMP 等
在 WebSocket 中的应用
场景:多服务器 WebSocket 负载均衡
客户端1 ──┐
├──> 服务器1 (WebSocket)
客户端2 ──┘ │
├──> RabbitMQ ──> 服务器2 (WebSocket) ──> 客户端3
客户端4 ──┐ │
├──> 服务器3 (WebSocket)
客户端5 ──┘
实现原理:
- 多个 WebSocket 服务器实例
- 客户端连接到不同的服务器
- 服务器1收到消息后,通过 RabbitMQ 发布消息
- 其他服务器订阅 RabbitMQ,收到消息后推送给各自的客户端
- 实现跨服务器的消息广播
基本使用示例
// 发布消息(服务器1)
const amqp = require('amqplib');
async function publishMessage() {
const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();
const queue = 'websocket_messages';
await channel.assertQueue(queue, { durable: true });
// 发布消息
channel.sendToQueue(queue, Buffer.from(JSON.stringify({
type: 'chat',
message: 'Hello',
userId: '123'
})));
console.log('消息已发送');
}
// 消费消息(服务器2)
async function consumeMessages() {
const connection = await amqp.connect('amqp://localhost');
const channel = await connection.createChannel();
const queue = 'websocket_messages';
await channel.assertQueue(queue, { durable: true });
channel.consume(queue, (msg) => {
if (msg) {
const content = JSON.parse(msg.content.toString());
// 推送给 WebSocket 客户端
broadcastToClients(content);
channel.ack(msg); // 确认消息已处理
}
});
}
RabbitMQ vs 其他消息队列
| 特性 | RabbitMQ | Redis Pub/Sub | Kafka |
|---|---|---|---|
| 协议 | AMQP | Redis 协议 | Kafka 协议 |
| 消息持久化 | ✅ 支持 | ⚠️ 不支持 | ✅ 支持 |
| 消息确认 | ✅ 支持 | ❌ 不支持 | ✅ 支持 |
| 性能 | ⚠️ 中等 | ✅ 高 | ✅ 非常高 |
| 适用场景 | 通用消息队列 | 简单发布订阅 | 大数据流处理 |
| 学习曲线 | ⭐⭐ | ⭐ | ⭐⭐⭐ |
Socket.io + RabbitMQ 组合使用
Socket.io 提供了 Redis adapter,但也可以使用 RabbitMQ:
// 使用 Socket.io Redis Adapter
const io = require('socket.io')(server);
const redisAdapter = require('socket.io-redis');
io.adapter(redisAdapter({ host: 'localhost', port: 6379 }));
// 或者使用自定义的 RabbitMQ adapter
// 实现跨服务器的消息同步
优势:
- 多服务器实例之间可以共享消息
- 客户端连接到任意服务器都能收到消息
- 实现水平扩展
常见面试问题
Q1: WebSocket 和 HTTP 的区别?
A:
- HTTP 是无状态的请求-响应协议,每次请求都需要建立连接
- WebSocket 是持久连接,建立后可以双向通信
- HTTP 是单向的(客户端请求,服务器响应)
- WebSocket 支持双向通信
Q2: WebSocket 如何实现心跳检测?
A: 客户端定时发送 ping 消息,服务器回复 pong。如果超时未收到回复,认为连接断开,进行重连。
Q3: WebSocket 连接断开如何处理?
A:
- 监听
onclose事件 - 实现指数退避重连机制
- 使用消息队列缓存未发送的消息
- 通知用户连接状态
Q4: 如何保证 WebSocket 消息的顺序性?
A:
- 在消息中添加序列号
- 客户端维护接收缓冲区
- 按序列号顺序处理消息
- 处理乱序和丢包情况
Q5: WebSocket 支持哪些数据格式?
A:
- 文本字符串
- JSON 字符串
- ArrayBuffer(二进制)
- Blob(二进制)
Q6: 如何实现 WebSocket 的负载均衡?
A:
-
Sticky Session(会话粘性)
- 通过负载均衡器(如 Nginx)将同一客户端的请求路由到同一服务器
- 优点:实现简单
- 缺点:服务器宕机会导致连接丢失,无法真正实现水平扩展
-
使用 Redis 共享连接状态
- 将连接信息存储在 Redis 中
- 服务器之间通过 Redis 发布订阅机制同步消息
- Socket.io 的 Redis adapter 就是这种实现
-
使用消息队列(RabbitMQ/Kafka)
- 服务器收到消息后发布到消息队列
- 其他服务器订阅消息队列,收到消息后推送给各自的客户端
- 优点:解耦、可靠、支持复杂路由
-
专门的 WebSocket 网关
- 使用 API Gateway(如 Kong、Zuul)的 WebSocket 支持
- 或使用专门的 WebSocket 代理(如 Socket.io 的 Redis adapter)
推荐方案: 对于生产环境,推荐使用 Socket.io + Redis adapter 或自定义的 RabbitMQ 方案,既能实现负载均衡,又能保证消息的可靠传递。