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

Socket.IO新用户加入房间时为单用户生成2个socket ID问题排查

React协同应用Socket.IO单用户生成2个Socket ID问题排查

问题现象

开发基于React的实时协同应用时出现异常:新用户加入房间时,Socket.IO会为单个用户生成2个独立的socket ID,代码参照官方文档编写,暂未定位问题根因,相关代码片段如下。


服务端代码(server.js)

const cors = require('cors');
const axios = require('axios');
const {Server} = require('socket.io');
const http = require('http');
// *注意:贴出的原始代码漏了express引入,运行会直接报错,记得补上
const express = require('express');
const ACTIONS = require('../src/Actions');

const app = express();  // Create an instance of express
const server = http.createServer(app)  // Create an instance of http server
const io = new Server(server); // Create an instance of socket.io server


// Storing a client list
const clients = new Map();


// Switching on the server socket to listen for connections
io.on('connection', (socket) => {

   const clientSocketId = socket.id;   
   
   console.log(clientSocketId+' connected');


   socket.on(ACTIONS.JOIN,({roomId,username})=>{
       console.log(roomId,username)
         clients.set(socket.id,{
               roomId,
               username,
               socketId: socket.id,
         })
        socket.join(roomId);
       const clientlist = Array.from(clients.values())
       clientlist.forEach(client=>{
         io.to(client.socketId).emit(ACTIONS.JOINED,{
             clientlist,
               username,
               socketId: socket.id,
         })
       })
   })

   // The server is listening to two events Code Change and Code Sync
   // Code Change is emitted when the user changes the code
   // Code Sync is called when the user joins the room to sync the previously typed code

   socket.on(ACTIONS.CODE_CHANGE, ({ roomId, code }) => {
    socket.in(roomId).emit(ACTIONS.CODE_CHANGE, { code });
  });

socket.on(ACTIONS.SYNC_CODE, ({ socketId, code }) => {
    io.to(socketId).emit(ACTIONS.CODE_CHANGE, { code });
  });


   // Disconnecting the current socket
    socket.on('disconnecting',()=>{
        console.log(clientSocketId+' disconnected')
        // Getting the list of all the present rooms
        const rooms = Object.keys(socket.rooms);
        rooms.forEach(roomId=>{  
          socket.in(roomId).emit(ACTIONS.DISCONNECTED,{
            socketId: socket.id,
            username: clients.get(socket.id).username,
          })
        })
        clients.delete(socket.id);
        socket.leave();
    })
   
})

const PORT = process.env.SERVER_PORT || 5000;

server.listen(PORT,()=>{console.log('Listening on  '+PORT)});

客户端Socket初始化代码

export const initSocket = async () => {
   const options =  {
        transports: ['websocket'],
        reconnection: true,
        reconnectionAttempts: 'Infinity',
        forceNew: true,
        reconnectionDelay: 1000,
        reconnectionDelayMax: 5000,
        timeout: 10000,
        autoConnect: true,
        secure: true,
   }
    const socket = io(process.env.REACT_APP_SERVER_URL,options)
    return socket
}

Dashboard.js中useEffect初始化逻辑

React.useEffect(()=>{
    // As the user joins the room we initialize the client socket which connects to the server
    const init = async () => {

      socketRef.current = await initSocket(); 

      // Handling connection errors
      socketRef.current.on('connect_error',(err)=>handleError(err))
      socketRef.current.on('connect_failed',(err)=>handleError(err))

      const handleError = (err)=>{
        console.log(err)
        toast({
          title: 'Error connecting to the server',
          status: 'error',
          duration: 9000,
          isClosable: true,
        })
        reactNavigater('/')

      }

      socketRef.current.emit(ACTIONS.JOIN,{
        roomId: roomId,
        username: location.state?.username,
      });

      // Listening for joined event when a even user joins
      socketRef.current.on(ACTIONS.JOINED,({clientlist,username,socketId})=>{
        if(username !== location.state?.username){
          toast({
            title: `${username} has joined the room`,
            status: 'success',
            duration: 9000,
            isClosable: true,
          })
        }
        setClientlist(clientlist)
        socketRef.current.emit(ACTIONS.SYNC_CODE, {
          socketId: socketRef.current.id,
          code: codeRef.current,
      });
      })

      // Listening for disconnected event when a even user disconnects
      socketRef.current.on(ACTIONS.DISCONNECTED,({socketId,username})=>{
          toast({
            title: `${username} has disconnected`,
            status: 'warning',
            duration: 9000,
            isClosable: true,
          })
        // Filter the clientlist to remove the disconnected client
        setClientlist(Clientlist.filter(client=>client.socketId !== socketId))
      }
      )

    }
    init()

    // Here we have multiple listeners, so we have to remove them when the component unmounts
    return ()=>{
      if(socketRef.current){
      socketRef.current.disconnect()
      socketRef.current.off(ACTIONS.JOINED)
      socketRef.current.off(ACTIONS.DISCONNECTED)
      }
    }

  },[])

排查思路与修复方案

按优先级从高到低排查:

  • 优先排查React 18严格模式导致的重复连接
    这是90%同类问题的根因。React 18+开发环境下,严格模式会对组件执行「挂载→卸载→重新挂载」的重复渲染流程,虽然写了effect清理逻辑,但第一次初始化的socket断开请求存在网络延迟,服务端还没收到断开事件,第二次effect已经创建了新的socket连接,就会出现单用户两个socket ID的情况。
    验证方式:临时移除src/index.js中根组件的<React.StrictMode>包裹,重启开发服务测试,如果单用户只生成1个socket ID即可确认。
    修复不需要关闭严格模式,给socket初始化加单例缓存即可,保证全局只存在一个socket实例:
    let socketInstance = null;
    export const initSocket = async () => {
      if(socketInstance?.connected) return socketInstance;
      const options =  {
           transports: ['websocket'],
           reconnection: true,
           reconnectionAttempts: 'Infinity',
           forceNew: false, // 关闭强制新建连接,配合单例使用
           reconnectionDelay: 1000,
           reconnectionDelayMax: 5000,
           timeout: 10000,
           autoConnect: true,
           secure: true,
      }
       socketInstance = io(process.env.REACT_APP_SERVER_URL,options)
       return socketInstance
    }
    
  • 修复服务端断开逻辑的空指针问题
    现有disconnecting事件中直接取clients.get(socket.id).username,如果socket建立连接后还没触发JOIN事件就断开,这里会拿到undefined,直接访问属性会抛错,导致后续clients.delete(socket.id)不执行,残留无效socket记录。修复后代码:
    socket.on('disconnecting',()=>{
        console.log(clientSocketId+' disconnected')
        const rooms = Object.keys(socket.rooms);
        const clientInfo = clients.get(socket.id)
        rooms.forEach(roomId=>{  
          if(clientInfo){
            socket.in(roomId).emit(ACTIONS.DISCONNECTED,{
              socketId: socket.id,
              username: clientInfo.username,
            })
          }
        })
        clients.delete(socket.id);
        socket.leave();
    })
    
  • 调整客户端事件绑定与JOIN触发时机
    现有代码中handleError定义在事件绑定语句之后,虽然JS函数声明存在提升,但容易出现引用失效导致连接错误后无限重试、生成新连接。另外不要在socket初始化完成后立刻发JOIN事件,要等连接真正建立后再发,避免连接未就绪触发底层重连:
    1. 把handleError定义移到事件绑定之前
    2. 把JOIN事件的触发放到socket的connect事件回调中:
    socketRef.current.on('connect', () => {
      socketRef.current.emit(ACTIONS.JOIN,{
        roomId: roomId,
        username: location.state?.username,
      });
    })
    
  • 补全CORS配置
    现有服务端只引入了cors中间件但没有挂载,也没有给Socket.IO配置跨域允许规则,会导致部分浏览器环境下连接被拦截,触发自动重连生成新ID。补上对应配置:
    // 挂载express cors中间件
    app.use(cors());
    // Socket.IO 配置跨域
    const io = new Server(server, {
      cors: {
        origin: process.env.CLIENT_URL || "http://localhost:3000",
        credentials: true
      }
    });
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 05:18:53