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

React项目中socket.io-client重复连接socket.io服务器求助

Socket.io 重复创建连接问题排查与修复

我正在开发一款无需注册即可加入房间游玩的Web游戏应用,技术栈为React + Socket.io + MongoDB,核心页面包括HomePage、CreateRoom、JoinRoom,最终进入RoomLobby处理游戏逻辑。目前遇到的问题是:用户创建/加入房间进入RoomLobby后,后端控制台会反复打印New client connected [socket-id]日志,每次的socket ID都不相同,导致连接频繁创建又断开。

服务器配置代码

const express = require('express');
const cors = require('cors');
const http = require('http');
const socketIo = require('socket.io');
const mongoose = require('mongoose');
const routes = require('./api/routes');
const cron = require('node-cron');
const { Room, Participant } = require('./model');
require('dotenv').config();

const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
  cors: {
    origin: "http://localhost:3001", // Client URL
    methods: ["GET", "POST"]
  }
});

app.use(express.json()); // For parsing application/json

app.use(cors({
  origin: 'http://localhost:3001' // Allow only your React app to access
})); // Enable CORS for all routes

// Middleware to attach io to the req object
app.use((req, res, next) => {
  req.io = io;
  next();
});

const PORT = process.env.PORT || 3000;

const MONGODB_URI = process.env.MONGODB_URI;

mongoose.connect(MONGODB_URI)
  .then(() => console.log("MongoDB connected"))
  .catch(err => console.log(err));

io.on('connection', (socket) => {
  console.log('New client connected', socket.id); // <--- 重复打印的日志

  socket.on('joinRoom', (roomCode) => {
    socket.join(roomCode);
    console.log(`A user joined room: ${roomCode}`);
  });

  socket.on('disconnect', () => {
    console.log('Client disconnected');
  });
});

app.use('/api', routes);

server.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

客户端socket.js

import io from "socket.io-client";

const URL = process.env.NODE_ENV === 'production' ? process.env.REACT_APP_SERVER_URL : 'http://localhost:3000';

export const socket = io(URL, {
    autoConnect: false
});

RoomLobby.js组件

import React, { useEffect, useState } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import axios from 'axios';
import { socket } from '../service/socket';

import ContentCopyIcon from '@mui/icons-material/ContentCopy';

const RoomLobby = () => {
  const [roomData, setRoomData] = useState(null);
  const [iconSize, setIconSize] = useState('default');
  const [dotCount, setDotCount] = useState(1);

  const { roomId } = useParams();
  const navigate = useNavigate();
  const location = useLocation();
  const userId = location.state?.userId;
  const userName = location.state?.userName;

  useEffect(() => {
    const fetchRoomData = async () => {
      try {
        const response = await axios.get(`http://localhost:3000/api/room/${roomId}`);
        setRoomData(response.data);
      } catch (error) {
        console.error('Error fetching room data', error);
        navigate('/');
      }
    };

    fetchRoomData();
  }, [roomId, navigate]);

  useEffect(() => {
    // Establish the socket connection when the component mounts
    socket.connect();

    // Join the room once roomData is available
    if (roomData && roomData.roomCode) {
      socket.emit('joinRoom', roomData.roomCode);
    }

    return () => {
      socket.disconnect();
    };
  }); // <--- 这里没有依赖数组

  useEffect(() => {   
    // Set up event listeners
    socket.on('gameStarted', (updatedRoom) => {
      setRoomData(updatedRoom);
    });
  
    socket.on('nextTurn', (data) => {
      setRoomData(prevData => ({ ...prevData, currentTurn: data.currentTurn }));
    });
  
    // Return a cleanup function to run when the component unmounts
    return () => {
      socket.disconnect();
    };
  }, [roomData]);
  
  useEffect(() => {
    if (roomData && roomData.roomStatus === 2) {
      // Disconnect the socket when the game is over
      socket.disconnect();
    }
  }, [roomData]);

  const startGame = async () => {
    try {
      await axios.post('http://localhost:3000/api/start-game', { userId: userId, roomCode: roomData.roomCode });
      setRoomData(prevData => ({ ...prevData, roomStatus: 1 }));
    } catch (error) {
      console.error('Error starting the game', error);
    }
  };

  const playTurn = async () => {
    try {
      await axios.post('http://localhost:3000/api/play-turn', { userId: userId, roomCode: roomData.roomCode });
    } catch (error) {
      console.error('Error playing turn', error);
    }
  };

  return (
    {/* Layout */}
  );
};

export default RoomLobby;

问题根源

  1. 无依赖数组的useEffect:第二个useEffect没有指定依赖数组,这会导致每次组件渲染(包括state更新、props变化)都会执行该effect。每次执行都会调用socket.connect(),而之前的cleanup函数会执行socket.disconnect(),导致连接被频繁断开又重新创建,每次重连都会生成新的socket ID,所以后端反复打印新连接日志。
  2. 多处调用disconnect:多个useEffect都调用socket.disconnect(),比如组件卸载、roomData变化、游戏结束时,这会加剧连接的频繁断开重连。
  3. 事件监听未清理:第三个useEffect中添加的socket事件监听没有在cleanup时移除,可能导致重复监听,但不是当前连接重复创建的主因。

修复步骤

1. 给连接用的useEffect添加正确的依赖数组

将第二个useEffect的依赖数组设为[roomData],确保只在组件挂载和roomData变化时执行,避免重复触发:

useEffect(() => {
  // 只有当socket未连接时才尝试连接
  if (!socket.connected) {
    socket.connect();
  }

  // Join the room once roomData is available
  if (roomData && roomData.roomCode) {
    socket.emit('joinRoom', roomData.roomCode);
  }

  return () => {
    // 仅在组件卸载时断开连接,避免中间状态变化导致断开
    socket.disconnect();
  };
}, [roomData]); // 添加依赖数组

2. 统一管理socket连接与断开

移除其他useEffect中的socket.disconnect()调用,只保留组件卸载时的cleanup逻辑。如果游戏结束后需要断开,可以单独处理,但不要在roomData变化时每次都断开:

// 移除第三个useEffect中的socket.disconnect()
useEffect(() => {   
  // Set up event listeners
  const handleGameStarted = (updatedRoom) => {
    setRoomData(updatedRoom);
  };

  const handleNextTurn = (data) => {
    setRoomData(prevData => ({ ...prevData, currentTurn: data.currentTurn }));
  };

  socket.on('gameStarted', handleGameStarted);
  socket.on('nextTurn', handleNextTurn);

  // 清理事件监听,避免内存泄漏
  return () => {
    socket.off('gameStarted', handleGameStarted);
    socket.off('nextTurn', handleNextTurn);
  };
}, [roomData]);

// 游戏结束后的断开逻辑可以保留,但建议用socket.disconnect()前先检查连接状态
useEffect(() => {
  if (roomData && roomData.roomStatus === 2 && socket.connected) {
    socket.disconnect();
  }
}, [roomData]);

3. 优化连接逻辑

在连接前检查socket是否已经处于连接状态,避免重复调用connect():

if (!socket.connected) {
  socket.connect();
}

修复后的完整RoomLobby关键部分

import React, { useEffect, useState } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import axios from 'axios';
import { socket } from '../service/socket';

import ContentCopyIcon from '@mui/icons-material/ContentCopy';

const RoomLobby = () => {
  const [roomData, setRoomData] = useState(null);
  const [iconSize, setIconSize] = useState('default');
  const [dotCount, setDotCount] = useState(1);

  const { roomId } = useParams();
  const navigate = useNavigate();
  const location = useLocation();
  const userId = location.state?.userId;
  const userName = location.state?.userName;

  useEffect(() => {
    const fetchRoomData = async () => {
      try {
        const response = await axios.get(`http://localhost:3000/api/room/${roomId}`);
        setRoomData(response.data);
      } catch (error) {
        console.error('Error fetching room data', error);
        navigate('/');
      }
    };

    fetchRoomData();
  }, [roomId, navigate]);

  useEffect(() => {
    // 仅在未连接时建立连接
    if (!socket.connected) {
      socket.connect();
    }

    // 房间数据可用时加入房间
    if (roomData && roomData.roomCode) {
      socket.emit('joinRoom', roomData.roomCode);
    }

    // 组件卸载时断开连接
    return () => {
      socket.disconnect();
    };
  }, [roomData]);

  useEffect(() => {   
    const handleGameStarted = (updatedRoom) => {
      setRoomData(updatedRoom);
    };

    const handleNextTurn = (data) => {
      setRoomData(prevData => ({ ...prevData, currentTurn: data.currentTurn }));
    };

    socket.on('gameStarted', handleGameStarted);
    socket.on('nextTurn', handleNextTurn);

    // 清理事件监听
    return () => {
      socket.off('gameStarted', handleGameStarted);
      socket.off('nextTurn', handleNextTurn);
    };
  }, [roomData]);
  
  useEffect(() => {
    if (roomData && roomData.roomStatus === 2 && socket.connected) {
      socket.disconnect();
    }
  }, [roomData]);

  // 其他函数...

  return (
    {/* Layout */}
  );
};

export default RoomLobby;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 22:14:51