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;
问题根源
- 无依赖数组的useEffect:第二个
useEffect没有指定依赖数组,这会导致每次组件渲染(包括state更新、props变化)都会执行该effect。每次执行都会调用socket.connect(),而之前的cleanup函数会执行socket.disconnect(),导致连接被频繁断开又重新创建,每次重连都会生成新的socket ID,所以后端反复打印新连接日志。 - 多处调用disconnect:多个useEffect都调用
socket.disconnect(),比如组件卸载、roomData变化、游戏结束时,这会加剧连接的频繁断开重连。 - 事件监听未清理:第三个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
相关产品推荐
相关产品推荐

