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

如何让新用户加入已有主机Session,而非创建新会话?

问题描述

我正在开发一款应用,需求是第一个访问URL的用户作为主机,获取专属hostId并进入对应会话;后续访问同一URL的用户需加入该主机的会话。目前视觉上功能正常,用户能共享信息,但存在两个核心问题:

  • 新用户会同时创建新会话
  • 重复加入主机会话

我推测原因是:新用户首次渲染时host状态为null,触发了socket.emit("host:joined"),同时StartPage组件中的socket.emit("user:joined")也被触发。尝试过加载spinner、重构前后端代码,都没解决后台创建新会话和重复加入的问题。


前端App组件代码

const socket = socketio.connect(import.meta.env.VITE_APP_SOCKET_URL)

function App() {
  const [host, setHost] = useState(null)
  const [loading, setLoading] = useState(true)
  const [showModal, setShowModal] = useState(false)
  const [queueList, setQueueList] = useState([])

  useEffect(() => {
      if (!host) {
        socket.emit("host:joined")
      }

      console.log("socket.id", socket.id)
      socket.on("host:joined", (hostId) => {
        setHost(hostId)
        setLoading(false)
      })
      socket.on("user:joined", (id) => {
        console.log("user joined session:", id)
      })
      socket.on("playlist", (data) => {
        setQueueList(data)
      })

      if (host === undefined) {
        socket.emit("disconnect")
      }

      console.log("HOST", host)
      return () => {
        console.log("Cleaning up")
        socket.off("host:joined")
        socket.off("disconnect")
        socket.off("playlist")
      }

  }, [host, queueList])

  
  return (
      <div className="App">
        {loading && <LoadingSpinner />}

        <Routes>
          <Route
            path="/:id"
            element={
              <StartPage
                socket={socket}
                host={host}
                queueList={queueList}
                setShowModal={setShowModal}
                showModal={showModal}
              />
            }
          />
          {host && <Route path="/" element={<Navigate to={`/${host}`} />} />}
        </Routes>
      </div>
  )
}

export default App

前端StartPage组件代码

const StartPage = ({ socket, host, queueList, setShowModal, showModal }) => {
  const { id } = useParams()
  useEffect(() => {
    if (id !== socket.id) {
      socket.emit("user:joined", id)
    }
  }, [id])
  return (
    <>
    </>
  )
}

export default StartPage

后端Socket控制器代码

let io = null

let sessions = []

// Create a session when host enters the site
const handleConnect = function (id) {
  console.log("ID", id)
  if (!id) {
    console.log("Host joined with id:", this.id)
    let session = {
      id: this.id,
      playlist: [],
      users: [],
    }

    if (sessions.find((session) => session.id === this.id)) {
      return
    } else {
      sessions.push(session)
      this.join(session)
    }
    io.to(session).emit("host:joined", this.id)
  } else {
    console.log("User joined with id:", this.id)

    const session = sessions.find((session) => session.id === id)
    console.log("session", session)
    session.users.push(this.id)
    this.join(session)
    io.to(session).emit("host:joined", id)
    console.log("sessions", sessions)
  }
}

/**
 * Export controller and attach handlers to events
 */
module.exports = function (socket, _io) {
  io = _io

  // handle host connect
  socket.on("host:joined", handleConnect)

  socket.on("user:joined", handleConnect)
}

控制台日志说明

日志显示新用户访问时,前后端同时接收到host:joined和user:joined事件,导致后端既创建了新会话,又让用户加入了已有会话,出现重复会话和重复用户的记录。


问题分析与解决方案

核心问题拆解

  1. 前端事件触发逻辑混乱:App组件的useEffect在host为null时无条件触发host:joined,无论用户访问的是根路径还是带hostId的路径;同时StartPage组件加载时直接触发user:joined,两个事件同时发送给后端。
  2. 后端逻辑复用隐患:用同一个handleConnect函数处理两种不同事件,依赖参数是否存在做判断,容易出现误判;加入会话时未检查用户是否已存在,导致重复加入。
  3. Socket房间操作错误:后端调用this.join(session)时传入的是会话对象,而非字符串类型的房间ID,Socket.io无法识别,导致会话隔离失效。

具体修复方案

前端调整

  1. 区分主机与用户的触发时机:仅当访问根路径且host未初始化时,才触发host:joined成为主机;访问带hostId的路径时,直接触发user:joined。
// App组件新增useLocation引入
import { useLocation } from 'react-router-dom';

function App() {
  const location = useLocation();
  // ...其他状态

  useEffect(() => {
    // 仅根路径+host未初始化时,尝试成为主机
    if (!host && location.pathname === '/') {
      socket.emit("host:joined");
    }

    // ...原socket监听逻辑保持不变

    return () => {
      // ...原清理逻辑保持不变
    };
  }, [host, location.pathname]); // 新增location.pathname作为依赖
  1. 避免重复发送加入事件:在StartPage组件中增加状态标记,确保每个用户仅发送一次user:joined。
const StartPage = ({ socket, host, queueList, setShowModal, showModal }) => {
  const { id } = useParams();
  const [hasJoined, setHasJoined] = useState(false);

  useEffect(() => {
    if (id !== socket.id && !hasJoined) {
      socket.emit("user:joined", id);
      setHasJoined(true);
    }
  }, [id, hasJoined]);

  return (
    <>
      {/* 组件内容 */}
    </>
  );
};

后端修复

  1. 拆分事件处理函数:将host:joined和user:joined的逻辑分开,避免参数混淆。
  2. 修复房间操作错误:使用会话ID(字符串)作为房间名调用join。
  3. 增加重复检查:创建会话时检查是否已存在,加入会话时检查用户是否已在列表中。
let io = null;
let sessions = [];

// 处理主机创建会话
const handleHostJoined = function () {
  const existingSession = sessions.find(session => session.id === this.id);
  if (existingSession) {
    io.to(this.id).emit("host:joined", this.id);
    return;
  }
  const newSession = {
    id: this.id,
    playlist: [],
    users: [],
  };
  sessions.push(newSession);
  this.join(newSession.id); // 使用会话ID作为房间名
  io.to(newSession.id).emit("host:joined", newSession.id);
  console.log("Host created session:", newSession.id);
};

// 处理用户加入会话
const handleUserJoined = function (hostId) {
  const targetSession = sessions.find(session => session.id === hostId);
  if (!targetSession) {
    console.log("Session not found:", hostId);
    return;
  }
  // 检查用户是否已加入会话
  if (targetSession.users.includes(this.id)) {
    console.log("User already in session:", this.id);
    return;
  }
  targetSession.users.push(this.id);
  this.join(targetSession.id); // 使用会话ID作为房间名
  io.to(targetSession.id).emit("user:joined", this.id);
  console.log("User joined session:", hostId, "User ID:", this.id);
};

module.exports = function (socket, _io) {
  io = _io;

  socket.on("host:joined", handleHostJoined);
  socket.on("user:joined", handleUserJoined);
};

额外优化建议

  • 前端增加路由守卫,在加载状态未完成时禁止触发任何Socket事件。
  • 后端添加会话清理机制,当主机断开连接时自动移除对应会话,防止内存泄漏。
  • 前端监听Socket断开事件,重置host等状态,避免异常状态残留。

内容的提问来源于stack exchange,提问作者b3n3.b3n3

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 21:45:46