如何在Express Node.js服务器中使用ws库将WebSocket会话存储到Redis?
将WebSocket会话从内存迁移到Redis的实现方案
嘿,我来帮你搞定把WebSocket会话从内存迁到Redis的事儿!首先得明确一点:咱们不能直接把WebSocket实例存进Redis(因为它是内存里的对象,没法序列化),但可以把会话的元数据(比如连接标识、关联用户、心跳状态这些)存在Redis里,同时保留ws库自带的clients集合来管理活跃连接。下面一步步来:
第一步:安装依赖
先装Redis客户端和生成唯一ID的工具(用ioredis是因为它比原生客户端更顺手,功能也全):
npm install ioredis uuid
第二步:配置Redis客户端
单独抽一个Redis模块,方便整个项目复用:
// redis-client.js import Redis from 'ioredis'; // 替换成你的Redis配置 const redis = new Redis({ host: 'localhost', port: 6379, password: 'your-redis-password', // 没密码就删掉这行 db: 0, }); // 监听Redis连接错误,方便排查问题 redis.on('error', (err) => { console.error('Redis连接出问题啦:', err); }); export default redis;
第三步:修改WebSocket核心代码
接下来改造你的ws代码,主要做这几件事:生成唯一连接ID、连接时存元数据到Redis、心跳时更新状态、关闭时清理Redis数据,还有定期清理死连接。直接上代码:
import WebSocket from 'ws'; import redis from './redis-client.js'; import { v4 as uuidv4 } from 'uuid'; const websocketServer = new WebSocket.Server({ noServer: true }); // 心跳处理:更新内存和Redis里的存活状态 function heartbeat() { this.isAlive = true; // 更新Redis里的心跳状态,同时刷新过期时间(防止异常断开后残留数据) redis.hset(`ws:connection:${this.connectionId}`, 'isAlive', 'true'); redis.expire(`ws:connection:${this.connectionId}`, 60); } websocketServer.on('connection', async (ws, request, client) => { // 给每个连接生成唯一ID,或者用express-session的sessionID(如果你的项目用了会话) const connectionId = uuidv4(); ws.connectionId = connectionId; ws.isAlive = true; // 把连接元数据存到Redis:这里假设client里有用户标识,你可以按需调整字段 const userId = client?.userId || 'unknown-user'; await redis.hset(`ws:connection:${connectionId}`, { userId, isAlive: 'true', connectedAt: Date.now().toString(), }); // 把用户ID和连接ID关联起来,方便后续通过用户ID找到所有连接 await redis.sadd(`ws:user:${userId}`, connectionId); // 绑定心跳、消息、关闭事件 ws.on('pong', heartbeat); ws.on('message', async (msg) => { console.info(`收到来自用户${client}的消息:${msg}`); ws.send(true); // 如果你需要给其他用户发消息,可以这么做 // const targetUserId = 'some-target-user-id'; // const targetConnIds = await redis.smembers(`ws:user:${targetUserId}`); // websocketServer.clients.forEach(conn => { // if (targetConnIds.includes(conn.connectionId) && conn.readyState === WebSocket.OPEN) { // conn.send(msg); // } // }); }); ws.on('close', async () => { // 连接关闭时清理Redis里的相关数据 await redis.del(`ws:connection:${connectionId}`); await redis.srem(`ws:user:${userId}`, connectionId); }); }); // 定期检测死连接,和心跳机制配合 setInterval(async () => { websocketServer.clients.forEach(async (ws) => { if (!ws.isAlive) { // 先清理Redis数据,再终止连接 const userId = await redis.hget(`ws:connection:${ws.connectionId}`, 'userId'); await redis.del(`ws:connection:${ws.connectionId}`); if (userId) await redis.srem(`ws:user:${userId}`, ws.connectionId); return ws.terminate(); } ws.isAlive = false; ws.ping(); }); }, 30000); // 每30秒检测一次 export default websocketServer;
关键细节提醒
- 为啥不存WS实例到Redis?:WebSocket实例是内存里的TCP连接对象,没法序列化存储,所以咱们只存元数据,实际连接还是靠ws的
clients集合管理。 - 多实例部署怎么办?:如果你的服务是多机器/多实例跑的,单靠
clients集合没法跨实例发消息,这时候得加Redis发布订阅:每个WS实例订阅一个频道,发消息时先把内容发布到Redis,所有实例收到后,检查自己的clients里有没有目标连接,有的话就发送。 - Redis键的规范:用
ws:前缀统一管理WebSocket相关的键,方便后续批量清理或者监控。给连接键加过期时间是个好习惯,就算连接异常断开,Redis也会自动删掉无效数据。 - 错误处理:记得给所有Redis异步操作加
try/catch,别让Redis的小问题搞挂整个WS服务哦!
内容的提问来源于stack exchange,提问作者PirateApp
相关产品推荐
相关产品推荐

