Socket.io+REST API项目Fetch请求偶发重复执行问题求助
Fetch请求偶发重复执行问题排查与解决
我正在开发一个基于Socket.io和REST API的游戏项目,目前遇到异常:Fetch请求有时会执行两次,有时正常执行一次。已经尝试调整代码,但始终没能定位根源。以下是相关代码,希望找到解决方案(这是我的大学作业)。
客户端代码
socket.on('connect', () => { socket.emit('joinGame', { gameID: gameID, page: page }); socket.on('role', (data) => { if (data.host === socket.id){ //Determina a quien le toca primero fetch(`http://localhost:3000/players/turn?idG=${gameID}`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }) .then(res => res.json()) .then(data => { currentPlayerFront = data.name localStorage.setItem('currentPlayer', currentPlayerFront); console.log('currentPlayerFront: ', currentPlayerFront) socket.emit('sendCurrentPlayer', { gameID: gameID, player: currentPlayerFront}); if (currentPlayerFront === getCurrentPlayer()){ diceButton.disabled = false; } }) .catch(error => { console.error(error) }) } }) })
服务端Socket.io事件处理代码
io.on('connection', (socket) => { // JoinRoom: un jugador se une a la partida, busca la patida por su gameID y genera un emit para actualizar a los juagadores en la lista de juagadores de la partida socket.on('joinRoom', async (data) => { socket.join(data.gameID); const game = await Game.findOne({ gameID: data.gameID }); if (game) { io.to(data.gameID).emit('updatePlayers', { gameID: data.gameID, players: game.players, playerCount: game.players.length, }); players.push({ id: socket.id, name: game.players[game.players.length - 1].name, page: data.page, gameID: data.gameID }); } }); // determineHost: determina cual jugador es el host de la partida segun su socket.id socket.on('determineHost', async (data) => { const game = await Game.findOne({ gameID: data.gameID }) if (game) { if (!game.host){ game.host = socket.id await game.save() } io.to(data.gameID).emit('role', { host: game.host }) } }) // startGame: redirige a todos los jugadores a game.html y coloca a todos los jugadores en la primera casilla con su color correspondiente socket.on('startGame', async (data) => { const game = await Game.findOne({ gameID: data.gameID }) if (game) { game.start = true game.host = null await game.save() } players.length = 0 const html = fs.readFileSync(path.join(__dirname, '..', 'views', 'game.html'), 'utf8'); io.to(data.gameID).emit('startGame', {site: `http://localhost:3000/game?gameID=${data.gameID}`, html: html }); }) // JoinGame: al Generarse una conexion en el front-end se une a uan partida con id socket.on('joinGame', async (data) => { socket.join(data.gameID) const game = await Game.findOne({ gameID: data.gameID }) if (game){ if (game.host == null){ game.host = socket.id await game.save() io.to(data.gameID).emit('role', { host: game.host }) } } players.push({ id: socket.id, page: data.page, gameID: data.gameID }) }) socket.on('sendCurrentPlayer', (data) => { io.to(data.gameID).emit('sendCurrentPlayer', { player: data.player }) }) socket.on('newQuestion', (data) => { io.to(data.gameID).emit('updateQuestion', {question: data.question , player: data.player, gameID: data.gameID}) }) // Escuchar evento para actualizar la posición del jugador socket.on('updatePlayerPosition', (data) => { // Emitir el evento a todos los clientes conectados io.to(data.gameID).emit('updatePlayerPosition', { player: data.player, points: data.points}); }); socket.on('clearTimer', (data) => { io.to(data.gameID).emit('clearTimer'); }) // NextTurn: Accediendo a la base de datos le notifica a los jugadores quien es el siguiente en jugar socket.on('nextTurn', async (data) => { const game = await Game.findOne({ gameID: data.gameID }) if (game && game.start) { console.log('nuevo turno: ', game.turn) let currentPlayer = {id: players[game.turn].id, name: game.players[game.turn].name} console.log(currentPlayer) io.to(data.gameID).emit('getCurrentPlayer', currentPlayer) } }) // Blockbuttons: bloquea los botones de respuesta para los jugadores que no tienen el turno socket.on('blockbuttons', (data) => { const game = Game.findOne({ gameID: data.gameID }) if (game) { let currentPlayer = {id: players[game.turn].id} io.to(data.gameID).emit('blockbuttons', currentPlayer) } }) // Answer: verifica si la respuesta es correcta o incorrecta y emite el evento correspondiente socket.on('answer', (data) => { if (data.response === 'correct') { io.to(data.gameID).emit('correctAnswer') } else if (data.response === 'incorrect') { io.to(data.gameID).emit('incorrectAnswer') } }) // EndGame: al finalizar el juego determina quien es el ganador y redirige a los jugadores a la pagina de resultados socket.on('endGame', async (data) => { const game = await Game.findOne({ gameID: data.gameID }) if (game) { game.winner = data.player.name await game.save() io.to(data.gameID).emit('cleanBoard', {winner: game.winner}) } }) })
游戏回合判定API路由代码
router.get('/turn', async (req, res) => { const game = await Game.findOne({ gameID: req.query.idG }) if (game && game.start) { if (game.players.length != 0) { game.turn = Math.floor(Math.random() * game.players.length) await game.save() console.log('turno: ' , game.turn) res.status(200).json({ name: game.players[game.turn].name }) } else { res.status(404).send('No hay jugadores') } } else { res.status(404).send('Juego no encontrado y no empezado') } })
问题根源分析
- Socket.io事件重复绑定:客户端在
socket.on('connect')内部绑定socket.on('role'),如果Socket连接发生重连(比如网络波动),会重复绑定role事件监听器。每次触发role事件时,所有绑定的监听器都会执行,导致Fetch请求多次发送。 - 服务端重复触发
role事件:服务端的joinGame和determineHost都可能触发role事件,如果这两个逻辑被多次执行(比如客户端重复发送joinGame),会导致客户端多次收到role事件,进而重复执行Fetch。 - API非幂等:当前
/turn接口每次请求都会随机修改game.turn,即使是重复请求也会改变回合结果,放大了重复请求的负面影响。
解决方案
客户端修复
将role事件监听器改为只执行一次,避免重复绑定导致多次触发Fetch:
// 只绑定一次role事件监听器,确保逻辑仅执行一次 socket.once('role', (data) => { if (data.host === socket.id){ fetch(`http://localhost:3000/players/turn?idG=${gameID}`, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }) .then(res => res.json()) .then(data => { currentPlayerFront = data.name localStorage.setItem('currentPlayer', currentPlayerFront); console.log('currentPlayerFront: ', currentPlayerFront) socket.emit('sendCurrentPlayer', { gameID: gameID, player: currentPlayerFront}); if (currentPlayerFront === getCurrentPlayer()){ diceButton.disabled = false; } }) .catch(error => { console.error(error) }) } }) socket.on('connect', () => { socket.emit('joinGame', { gameID: gameID, page: page }); })
服务端优化
在joinGame逻辑中增加判断,确保role事件仅在首次设置host时触发,避免重复发送:
socket.on('joinGame', async (data) => { socket.join(data.gameID) const game = await Game.findOne({ gameID: data.gameID }) if (game){ // 仅当host未设置时才赋值并触发role事件 if (game.host == null){ game.host = socket.id await game.save() io.to(data.gameID).emit('role', { host: game.host }) } } players.push({ id: socket.id, page: data.page, gameID: data.gameID }) })
API幂等性优化
修改/turn接口,只有当game.turn未初始化时才随机生成,重复请求不会改变回合结果:
router.get('/turn', async (req, res) => { const game = await Game.findOne({ gameID: req.query.idG }) if (game && game.start) { if (game.players.length !== 0) { // 仅在turn未设置时才生成随机值 if (game.turn === undefined || game.turn === null) { game.turn = Math.floor(Math.random() * game.players.length) await game.save() console.log('turno: ' , game.turn) } res.status(200).json({ name: game.players[game.turn].name }) } else { res.status(404).send('No hay jugadores') } } else { res.status(404).send('Juego no encontrado y no empezado') } })
内容的提问来源于stack exchange,提问作者Mateo Nicolas Suarez
相关产品推荐
相关产品推荐

