You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.02 14:48:01