介绍:超越基础 WebSocket
对 WebSocket 的基础理解可以让你创建简单的聊天或通知功能。然而,要构建生产级的实时应用——如多人在线游戏、金融终端、协同编辑系统——需要掌握更高级的技术。在本文中,我们将超越"Hello, World"的范畴,深入探讨如何在 Node.js 上构建可靠、可扩展且安全的 WebSocket 应用。
我们将涵盖四个关键领域:使用 Redis 进行扩展、状态与房间管理、安全与授权,以及多处理器集群化。所有代码示例均使用 JavaScript 编写,并采用 ws 库和 Node.js。
1. 使用 Pub/Sub(Redis)进行扩展
WebSocket 服务器的一个主要限制是它无法与其他实例"通信"。如果你在负载均衡器后运行两台服务器,连接到服务器 A 的客户端将无法收到通过服务器 B 发送的消息。解决方案是使用基于 Redis 的 Pub/Sub(发布-订阅)模式。
Redis 充当中央代理:每个服务器订阅一个频道并向其发布消息。这样,任何服务器发送的消息都会立即传播到所有其他服务器。
const WebSocket = require('ws');
const redis = require('redis');
const wss = new WebSocket.Server({ port: 8080 });
const publisher = redis.createClient();
const subscriber = redis.createClient();
// 订阅 'broadcast' 频道
subscriber.subscribe('broadcast');
subscriber.on('message', (channel, message) => {
// 向所有连接的客户端广播消息
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
wss.on('connection', (ws) => {
ws.on('message', (data) => {
// 将消息发布到 Redis
publisher.publish('broadcast', data.toString());
});
});
这种方法允许你几乎无限地水平扩展应用。当添加新服务器时,它只需创建自己的 Redis 订阅即可。
2. 房间与状态管理
在大多数实时应用中,需要将客户端分组到"房间"(如聊天室、游戏大厅)并管理其状态。基础的 wss.clients 不提供此功能。让我们实现一个简单但有效的房间系统。
我们将创建一个 rooms 对象,其中键是房间名称,值是 WebSocket 客户端的 Set。并添加加入、离开以及房间内广播的方法。
const rooms = new Map();
function joinRoom(ws, roomName) {
if (!rooms.has(roomName)) {
rooms.set(roomName, new Set());
}
rooms.get(roomName).add(ws);
// 将房间信息保存在 ws 对象上以便快速访问
ws.room = roomName;
}
function leaveRoom(ws) {
if (ws.room && rooms.has(ws.room)) {
rooms.get(ws.room).delete(ws);
if (rooms.get(ws.room).size === 0) {
rooms.delete(ws.room);
}
delete ws.room;
}
}
function broadcastToRoom(roomName, message, exclude = null) {
if (!rooms.has(roomName)) return;
rooms.get(roomName).forEach(client => {
if (client !== exclude && client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', (data) => {
const msg = JSON.parse(data);
switch (msg.type) {
case 'join':
joinRoom(ws, msg.room);
break;
case 'message':
broadcastToRoom(ws.room, msg.text, ws);
break;
}
});
ws.on('close', () => leaveRoom(ws));
});
对于状态管理(例如玩家位置、光标位置),可以将数据存储在 Map 中