React客户端重复连接Socket.io服务器问题及玩家分配咨询
问题描述
连接React客户端或刷新浏览器时,Socket服务器的io.on('connection')会触发两次。已尝试关闭Strict Mode、清理useEffect、重新编译,但问题仍未解决。请问是否可以通过这两次连接手动分配player1和player2?
server/index.ts
import express from 'express'; import { createServer } from "http"; import { Server } from 'socket.io'; const app = express(); const httpServer = createServer(app); const io = new Server(httpServer, { cors: { origin: "http://localhost:5173", } }); type GameStateType = [string, string, string, string, string, string, string, string, string]; let isXTurn: Boolean; let gameState:GameStateType = ['','','','','','','','','']; const players = {}; const victoryConditions = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ]; io.on('connection', (socket) => { isXTurn = true; let gameOver = false; gameState = ['','','','','','','','','']; console.log(`用户已连接,ID: ${socket.id}`); console.log(socket.rooms); // Set { <socket.id> } socket.join("room1"); console.log(socket.rooms); if (!players[socket.id]) { players[socket.id] = socket.id; console.log(players); } socket.on('tileClicked', (id) => { gameState[id] = isXTurn ? 'X' : 'O'; for (let i = 0; i < victoryConditions.length; i++) { const condition = victoryConditions[i]; if (gameState[condition[0]] === gameState[condition[1]] && gameState[condition[1]] === gameState[condition[2]] && gameState[condition[0]] !== '') { console.log('游戏结束'); socket.broadcast.emit('gameOver', true); gameOver = true; } } console.log(id); console.log(isXTurn); console.log('游戏结束?'+gameOver); socket.emit('turnChange', isXTurn); socket.broadcast.emit('turnChange', isXTurn); if (gameOver) { isXTurn = undefined; } else { isXTurn = !isXTurn; } }); }); httpServer.listen(3000, () => { console.log('服务器运行在 http://localhost:3000'); });
client/TicTacToe.ts
import { useEffect, useState } from "react" import Board from "./Board" import GameOver from "./GameOver" import Reset from "./Reset" import { io } from "socket.io-client" const socket = io("http://localhost:3000") const TicTacToe = () => { const [gameOver, setGameOver] = useState(false); const [xTurn, setXTurn] = useState<boolean | undefined>(); const [letterIcon, setLetterIcon] = useState('fa-solid fa-x fa-5x'); const [isClicked, setIsClicked] = useState(false); const [winner, setWinner] = useState<string | null>(null); useEffect(() => { if (gameOver && winner === null) { setWinner(!xTurn ? 'X' : 'O'); } }, [gameOver, xTurn, winner]); useEffect(() => { const handleGameOver = (arg: boolean) => { setGameOver(arg); console.log("游戏结束") } socket.on("gameOver", handleGameOver); return () => { socket.off("gameOver", handleGameOver) } }, []); useEffect(() => { const handleTurnChange = (arg: boolean | undefined) => { console.log("切换回合") console.log(arg) setXTurn(arg); if (arg === null && gameOver) { setLetterIcon('hidden'); } if (isClicked && arg !== undefined && !gameOver) { setLetterIcon(!arg ? 'fa-solid fa-x fa-5x' : 'fa-solid fa-o fa-5x'); } else { setLetterIcon('hidden'); } } socket.on("turnChange", handleTurnChange); return () => { socket.off("turnChange", handleTurnChange) } }, [gameOver, isClicked]); return ( <div className="ticTacToe"> <h1>井字棋</h1> <GameOver isGameOver={gameOver} gameWinner={winner}/> <Reset /> <Board letterIcon={letterIcon} setIsClicked={setIsClicked} gameOver={gameOver}/> </div> ) } export default TicTacToe
client/TicTacToeContext.tsx
import {createContext, useState} from 'react'; import { TicTacToeContextProps } from '../types/tictactoe'; import { io } from 'socket.io-client'; const socket = io("http://localhost:3000") export const TicTacToeContext = createContext({ isXTurn: true, }) export const TicTacToeContextProvider: React.FC<TicTacToeContextProps> = (props) => { const [isXTurn, setIsXTurn] = useState(true) socket.on('changeTurn', (isPlayerTurn) => { setIsXTurn(isPlayerTurn); }) return ( <TicTacToeContext.Provider value={{ isXTurn }}> {props.children} </TicTacToeContext.Provider> ); }
client/Tile.tsx
import { TileProps } from "../types/tictactoe" const Tile = ({ id, letterIcon, handleClick, children }: TileProps) => { return ( <div id={String(id)} className={'tile'} onClick={handleClick}> <i className={ letterIcon }></i> {children} </div> ) } export default Tile
client/Board.tsx
import { useState } from 'react'; import Tile from './Tile'; import { io } from 'socket.io-client'; import { BoardTypes } from '../types/tictactoe'; const socket = io('http://localhost:3000') const Board:React.FC<BoardTypes> = ({letterIcon, setIsClicked, gameOver}) => { const [tileStates, setTileStates] = useState(Array(9).fill(null)); const handleTileClick = (index: number) => { const newTileStates = [...tileStates]; const allNull = tileStates.every(element => element === null); console.log('点击格子') console.log('已点击?'+isClicked) console.log('全为空?'+allNull) if (!newTileStates[index] || allNull) { !gameOver && setIsClicked(true); newTileStates[index] = letterIcon; setTileStates(newTileStates); socket.emit('tileClicked', index); console.log(index) } } return ( <> <div className="board"> {tileStates.map((tileLetterIcon, index) => ( <Tile letterIcon={tileLetterIcon} key={index} handleClick={() => handleTileClick(index)} /> ))} </div> </> ); } export default Board;
回答
首先,两次连接的核心原因是客户端重复创建Socket实例:你在TicTacToe.ts、TicTacToeContext.tsx、Board.tsx中分别创建了独立的socket实例,每个实例都会触发一次服务器的connection事件,导致多重复连接。先修复这个问题,再处理玩家分配。
第一步:修复重复连接
- 创建全局单例Socket:在客户端新建
client/socket.ts文件,统一管理Socket实例:
import { io } from 'socket.io-client'; export const socket = io("http://localhost:3000");
- 在所有需要使用Socket的组件中导入这个单例,替换原有独立创建的代码:
// 比如TicTacToe.ts、Board.tsx中替换为 import { socket } from './socket';
- 移除无效监听:比如
TicTacToeContext.tsx中的changeTurn事件,服务器并未发送该事件,属于无效逻辑。
第二步:手动分配Player1和Player2
修复重复连接后,服务器每次客户端连接只会触发一次connection,此时可通过房间内玩家数量分配角色:
// 修改server/index.ts的connection逻辑 io.on('connection', (socket) => { // 仅当房间为空时初始化游戏状态,避免新玩家重置现有游戏 const roomPlayers = io.sockets.adapter.rooms.get("room1"); if (!roomPlayers || roomPlayers.size === 0) { isXTurn = true; gameState = ['','','','','','','','','']; } console.log(`用户已连接,ID: ${socket.id}`); socket.join("room1"); const currentPlayerCount = io.sockets.adapter.rooms.get("room1")?.size || 0; // 分配角色 let playerRole = ''; if (currentPlayerCount === 1) { playerRole = 'X'; // Player1 players[socket.id] = { id: socket.id, role: 'X' }; } else if (currentPlayerCount === 2) { playerRole = 'O'; // Player2 players[socket.id] = { id: socket.id, role: 'O' }; } else { playerRole = 'spectator'; // 超过2人设为观众 players[socket.id] = { id: socket.id, role: 'spectator' }; } console.log(`玩家 ${socket.id} 角色: ${playerRole}`); socket.emit('assignRole', playerRole); // 通知客户端角色 // 原有tileClicked等逻辑保持不变... });
客户端接收角色并处理:
// 在TicTacToe.ts中添加角色监听 useEffect(() => { const handleAssignRole = (role: string) => { if (role === 'X') { setLetterIcon('fa-solid fa-x fa-5x'); setXTurn(true); } else if (role === 'O') { setLetterIcon('fa-solid fa-o fa-5x'); setXTurn(false); } }; socket.on('assignRole', handleAssignRole); return () => socket.off('assignRole', handleAssignRole); }, []);
额外优化建议
- 服务器的
isXTurn、gameState是全局变量,多房间场景下会冲突,建议将游戏状态绑定到房间,比如用对象存储每个房间的状态:const roomGames = { room1: { isXTurn: true, gameState: [...], gameOver: false } } - 调整客户端
useEffect依赖项,避免重复绑定监听,比如gameOver监听不需要依赖gameOver状态
内容的提问来源于stack exchange,提问作者gpk96gpk
相关产品推荐
相关产品推荐

