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

使用Express与Socket.IO持久化SessionID实现多人协同绘图

问题:多人绘图网站的SessionID复用问题

我正在开发一个支持多人同时绘图的网站,核心需求是:用户首次连接时分配唯一ID,刷新页面或关闭窗口重新连接后,仍能复用同一个ID。目前卡在Cookie和SessionID的存储逻辑上。

额外问题:app.get("/")接口无法正常工作,必须改成/canvas这类其他端点才能运行。之前不用Session、仅依赖socket.id的版本是正常的。我试过把app.get里的逻辑移到io.on("connect")里,虽然能运行,但刷新页面会生成新的SessionID。查了很多资料,大部分Session示例都是登录注册场景,找不到无登录情况下复用SessionID的可行方案,卡壳了。


当前主服务器代码

const express = require("express");
const { Server } = require('socket.io');
const cookieParser = require('cookie-parser');

const { sessionMiddleware, wrap } = require('../server/sessionStore');

const { createServer } = require("node:http");
const { join } = require("node:path");

const app = express();
const server = createServer(app);
const io = new Server(server, {
  cors: {
    origin: 'http://localhost:3000',
    credentials: true,
  }
});

const db = require("../database/db");

app.use(cookieParser());
app.use(sessionMiddleware);
app.use(express.static("public"));

app.get("/canvas", (req, res) => {
  if (req.session) {
    req.session.visited
    console.log(req.session);
    console.log(req.session.id +' connected');
  } else {
    res.send("Invalid session");
  }
  res.sendFile(join(__dirname, "../public/index.html"));
});

const clients = {};

io.use(wrap(sessionMiddleware));
io.on("connection", (socket) => {
  const sessionId = socket.request.session.id;

  clients[sessionId] = '#000000';

  db.getAllDrawingData((drawingData) => {
    socket.emit("loadDrawingData", drawingData);
  });

  // start drawing event
  socket.on('startDrawing', ({ x, y }) => {
    const data = { type: 'start', x, y, color: clients[sessionId] };
    db.insertDrawingData(sessionId, data); socket.broadcast.emit('startDrawing', { x, y, color: clients[sessionId] });
  });

  // drawing event
  socket.on('draw', ({ x, y }) => {
    const data = { type: 'draw', x, y, color: clients[sessionId] };
    db.insertDrawingData(sessionId, data); socket.broadcast.emit('draw', { x, y, color: clients[sessionId] });
  });

  // stop drawing event
  socket.on('stopDrawing', () => {
    const data = { type: 'stop' };
    db.insertDrawingData(sessionId, data);
    socket.broadcast.emit('stopDrawing');
  });

  // change stroke color event
  socket.on('changeStrokeColor', (color) => {
    clients[sessionId] = color;
    socket.broadcast.emit('changeStrokeColor', { socketId: sessionId, color });
  });

  socket.on('eraseDrawings', () => {
    db.eraseDrawings(sessionId);
    socket.broadcast.emit('eraseDrawings');
  });

  // disconnect event
  socket.on('disconnect', () => {
    console.log('Disconnected');
    delete clients[sessionId];
  });
});

// Handle server shutdown gracefully
process.on('SIGINT', () => {
  console.log('Received SIGINT signal, shutting down...');
  db.close((err) => {
    if (err) {
      console.error('Error closing SQLite database:', err);
    } else {
      console.log('SQLite database connection closed successfully.');
    }
    process.exit(0);
  });
});

process.on('SIGTERM', () => {
  console.log('Received SIGTERM signal, shutting down...');
  db.close((err) => {
    if (err) {
      console.error('Error closing SQLite database:', err);
    } else {
      console.log('SQLite database connection closed successfully.');
    }
    process.exit(0);
  });
});

server.listen(3000, () => {
  console.log("server running at http://127.0.0.1:3000");
});

SessionStore配置代码

const session = require("express-session");
const Redis = require("ioredis");
const RedisStore = require("connect-redis").default;
const { v4: uuidv4 } = require("uuid");
const config = require("./config");

const redisClient = new Redis({
  host: config.redis.host,
  port: config.redis.port,
});
redisClient.on("error", function (err) {
  console.log("Could not establish a connection to Redis" + err);
});
redisClient.on("connect", function (err) {
  console.log("Connected to Redis");
});

const sessionMiddleware = session({
  secret: "secret",
  store: new RedisStore({ client: redisClient }),
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === "production",
    sameSite: true,
    maxAge: 24 * 60 * 60 * 1000, // 30 days
  },
  genid: (req) => uuidv4() + "-" + Date.now(),
});

const wrap = (expressMiddleware) => (socket, next) =>
  expressMiddleware(socket.request, {}, next);

module.exports = { sessionMiddleware, wrap };

问题分析与修复方案

1. SessionID刷新失效的核心原因

你设置了saveUninitialized: false,但无登录场景下,初始请求的session没有被修改,导致Session不会被存入Redis,也不会在客户端设置Cookie。刷新页面时,服务器找不到对应的Session,就会生成新的ID。

2. 修复步骤

(1)确保Session被初始化存储

两种可选方案:

  • 把saveUninitialized: false改成true,允许未修改的Session被存储;
  • 或者在首次请求时主动修改session的某个属性,触发保存逻辑:
// 在app.get("/canvas")中修改
if (req.session) {
  req.session.visited = (req.session.visited || 0) + 1; // 主动修改session属性,触发存储
  console.log(req.session);
  console.log(req.session.id +' connected');
}

这样服务器会把Session存入Redis,客户端也会收到SessionID的Cookie,刷新时就能复用。

(2)修复app.get("/")无法工作的问题

app.use(express.static("public"))会优先匹配静态文件,如果public目录下有index.html,访问/时会直接返回该文件,不会走到你的路由。解决方法:

  • 把app.get("/")放在app.use(express.static("public"))之前,让路由优先匹配:
app.get("/", (req, res) => {
  if (req.session) {
    req.session.visited = (req.session.visited || 0) + 1;
    console.log(req.session.id +' connected');
  }
  res.sendFile(join(__dirname, "../public/index.html"));
});

app.use(express.static("public"));
  • 或者删除public目录下的index.html,让路由处理返回文件。

(3)确保Socket连接携带Cookie

前端初始化Socket时必须开启withCredentials,否则不会携带Session Cookie:

// 前端Socket初始化示例
const socket = io('http://localhost:3000', {
  withCredentials: true
});

(4)优化SessionID生成逻辑

express-session默认会生成唯一ID,不需要额外拼接时间戳,简化genid配置:

genid: (req) => uuidv4(),

3. 测试验证

修改后,首次访问页面时服务器会设置Session Cookie;刷新页面或关闭窗口重新打开,服务器能通过Cookie找到对应的SessionID,不会生成新ID;Socket连接时也能拿到同一个SessionID,实现用户身份的持久化。


内容的提问来源于stack exchange,提问作者Isaac Del Castillo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 01:52:15