Express+Typescript服务端如何全局维护socket.io实例与socket连接映射
首先修正你现有代码的一个逻辑顺序问题:当前io.use中间件注册放在了io.on('connection')监听之后,会导致新连接不会触发该中间件,无法正确给socket挂载username属性,需要把io.use代码移到io.on('connection')前面。
方案1:抽离单例管理模块(TS类型友好,最推荐)
这种方式通过单独的模块维护socket实例和用户映射,全局唯一,类型可控,是TS项目的首选方案。
步骤1:新建单例管理文件
创建/src/utils/socketManager.ts文件,代码如下:
import { Server } from "socket.io"; // 定义类型 type SocketIdMap = Record<string, string>; let ioInstance: Server | null = null; const socketIdMap: SocketIdMap = {}; // 保存io实例 export const setIOInstance = (io: Server) => { ioInstance = io; }; // 获取io实例 export const getIOInstance = () => { if (!ioInstance) throw new Error("Socket.io 实例尚未初始化"); return ioInstance; }; // 更新用户-socketId映射 export const setSocketId = (username: string, socketId: string) => { socketIdMap[username] = socketId; }; // 移除用户映射(断开连接时调用) export const removeSocketId = (username: string) => { delete socketIdMap[username]; }; // 获取用户对应的socket实例 export const getUserSocket = (username: string) => { const io = getIOInstance(); const socketId = socketIdMap[username]; if (!socketId) return null; return io.sockets.sockets.get(socketId); };
步骤2:修改入口文件逻辑
调整/src/index.ts代码,初始化时关联单例:
import { setIOInstance, setSocketId, removeSocketId } from "./utils/socketManager"; const initializeExpress = (): void => { const app = express(); let http = require("http").Server(app); let io = require("socket.io")(http, { cors: {} }); // 保存io实例到单例 setIOInstance(io); // 把中间件移到connection监听前面 io.use((socket, next) => { socket.username = socket.handshake.auth.username; socket.organization = socket.handshake.auth.organization; next(); }); io.on("connection", function(socket: any) { console.log("a user connected. socket.id: ", socket.id, "socket.username: ", socket.username); setSocketId(socket.username, socket.id); // 监听断开连接,清除映射避免内存泄漏 socket.on("disconnect", () => { removeSocketId(socket.username); }); }); // 剩余原有代码保持不变... }; initializeExpress();
步骤3:其他业务文件调用示例
比如在路由文件中直接导入方法使用即可:
import { getUserSocket } from "../utils/socketManager"; // 业务接口逻辑 app.post("/send-notice", (req, res) => { const { targetUsername, content } = req.body; const targetSocket = getUserSocket(targetUsername); if (targetSocket) { targetSocket.emit("notice", content); res.success("通知发送成功"); } else { res.fail("用户不在线"); } });
方案2:挂载到Express app.locals(轻量快捷)
如果不想额外创建模块,也可以直接把io和映射挂载到Express的app全局属性上,所有能拿到req对象的地方都可以访问:
步骤1:修改入口文件
const initializeExpress = (): void => { const app = express(); const socket_ids = {}; let http = require("http").Server(app); let io = require("socket.io")(http, { cors: {} }); // 挂载到app.locals全局属性 app.locals.io = io; app.locals.socketIds = socket_ids; // 中间件、连接监听等原有逻辑调整顺序后保持不变... };
步骤2:业务路由调用
app.post("/send-notice", (req, res) => { const { targetUsername, content } = req.body; const io = req.app.locals.io; const socketId = req.app.locals.socketIds[targetUsername]; const targetSocket = io.sockets.sockets.get(socketId); if (targetSocket) { targetSocket.emit("notice", content); res.success("通知发送成功"); } else { res.fail("用户不在线"); } });
注意事项
- 一定要在socket断开连接时删除对应的映射条目,避免内存泄漏和无效数据
- 上述方案仅适用于单实例部署的场景,如果是多实例部署需要搭配socket.io的redis适配器实现跨实例消息推送
- TS项目中可以给socket的自定义属性(username、organization)补充类型声明,避免使用any类型
内容的提问来源于stack exchange,提问作者Kid_Learning_C
相关产品推荐
相关产品推荐

