// server/websocket/minesweeperSocket.ts import { Server as SocketIOServer } from 'socket.io'; import { Server as HTTPServer } from 'http'; import dotenv from 'dotenv'; // 加载环境变量 dotenv.config(); interface Cell { isMine: boolean; isRevealed: boolean; isFlagged: boolean; neighborMines: number; isExploded?: boolean; } interface GameRoom { roomId: string; playerId: string; board: Cell[][]; difficulty: string; spectators: Set; highlightedCells: Set; // 存储需要闪烁的格子坐标 "row,col" invitePlayMode: boolean; // 是否邀请同玩模式 } // 存储所有游戏房间 const gameRooms = new Map(); // 获取 WebSocket 路径前缀 - 与客户端保持一致 function getSocketIoPath() { const envApiUrl = process.env.REACT_APP_API_BASE_URL; console.log('[Socket.IO Path Config] REACT_APP_API_BASE_URL:', envApiUrl); console.log('[Socket.IO Path Config] NODE_ENV:', process.env.NODE_ENV); if (!envApiUrl || envApiUrl.trim() === '') { // 如果没有设置环境变量,根据 NODE_ENV 决定 const path = process.env.NODE_ENV === 'production' ? '/api/socket.io' : '/socket.io'; console.log('[Socket.IO Path Config] Using default path:', path); return path; } // 根据Nginx转发规则调整生产环境路径 // Nginx转发到 http://localhost:5001/api/socket.io/,所以后端应该接受 /api/socket.io if (process.env.NODE_ENV === 'production') { console.log('[Socket.IO Path Config] Production mode, Nginx配置'); console.log('[Socket.IO Path Config] Frontend sends: /api/api/socket.io'); console.log('[Socket.IO Path Config] Nginx location = /api/api/socket.io/'); console.log('[Socket.IO Path Config] Nginx proxy_pass: http://localhost:5001/api/socket.io/'); console.log('[Socket.IO Path Config] Backend should accept: /api/socket.io'); // 使用 /api/socket.io 路径匹配Nginx转发结果 return '/api/socket.io'; } // 如果是完整 URL,检查是否包含路径 if (envApiUrl.startsWith('http://') || envApiUrl.startsWith('https://')) { const urlObj = new URL(envApiUrl); // 如果原始 URL 包含路径,将其作为前缀 if (urlObj.pathname && urlObj.pathname !== '/') { const path = `${urlObj.pathname}/socket.io`; console.log('[Socket.IO Path Config] From URL pathname:', path); return path; } } // 如果是相对路径(如 /api),将其作为前缀 if (envApiUrl.startsWith('/')) { const path = `${envApiUrl}/socket.io`; console.log('[Socket.IO Path Config] From relative path:', path); return path; } const path = '/socket.io'; console.log('[Socket.IO Path Config] Using fallback path:', path); return path; } export function setupMinesweeperSocket(httpServer: HTTPServer) { // 生产环境支持多种可能的路径,通过请求检测来确定 const possiblePaths = ['/api/socket.io', '/socket.io']; const socketIoPath = getSocketIoPath(); console.log('[Socket.IO] Socket.IO 路径配置:', socketIoPath); console.log('[Socket.IO] 支持的可能路径:', possiblePaths); console.log('[Socket.IO] HTTP Server listening on port:', httpServer.address()); const io = new SocketIOServer(httpServer, { path: socketIoPath, // 使用最常见的路径作为配置 cors: { origin: "*", methods: ["GET", "POST"] }, allowEIO3: true, transports: ['polling', 'websocket'] }); // 添加底层HTTP请求日志中间件 io.engine.on('initial_headers', (headers, req) => { console.log('[Socket.IO Engine] === INITIAL HEADERS ==='); console.log('[Socket.IO Engine] Method:', req.method); console.log('[Socket.IO Engine] URL:', req.url); console.log('[Socket.IO Engine] Headers:', JSON.stringify(req.headers, null, 2)); console.log('[Socket.IO Engine] Query:', req.url ? req.url.split('?')[1] : 'N/A'); console.log('[Socket.IO Engine] ========================'); }); io.engine.on('headers', (headers, req) => { console.log('[Socket.IO Engine] === RESPONSE HEADERS ==='); console.log('[Socket.IO Engine] Status:', headers.status || 200); console.log('[Socket.IO Engine] Headers:', JSON.stringify(headers, null, 2)); console.log('[Socket.IO Engine] ========================'); }); io.on('connection', (socket) => { console.log('[Socket.IO] >>> CONNECTION ESTABLISHED <<<'); console.log('[Socket.IO] 用户连接:', socket.id); console.log('[Socket.IO] 握手耗时:', Date.now() - (socket.handshake.time || Date.now()), 'ms'); console.log('[Socket.IO] 握手 URL:', socket.handshake.url); console.log('[Socket.IO] 握手查询参数:', socket.handshake.query); console.log('[Socket.IO] 握手 Origin:', socket.handshake.headers.origin); console.log('[Socket.IO] 握手 Referer:', socket.handshake.headers.referer); console.log('[Socket.IO] 握手 User-Agent:', socket.handshake.headers['user-agent']); console.log('[Socket.IO] 握手 Host:', socket.handshake.headers.host); console.log('[Socket.IO] 配置的 Socket.IO 路径:', socketIoPath); // 详细路径分析 - 规范化路径比较(处理末尾斜杠差异) const reqPath = socket.handshake.url ? socket.handshake.url.split('?')[0] : 'N/A'; const normalizedReqPath = reqPath.replace(/\/$/, ''); // 移除末尾斜杠 const normalizedConfigPath = socketIoPath.replace(/\/$/, ''); // 移除末尾斜杠 console.log('[Socket.IO] 路径分析:'); console.log(' - 原始请求路径:', reqPath); console.log(' - 规范化请求路径:', normalizedReqPath); console.log(' - 配置路径:', socketIoPath); console.log(' - 规范化配置路径:', normalizedConfigPath); console.log(' - 严格相等:', reqPath === socketIoPath); console.log(' - 规范化后相等:', normalizedReqPath === normalizedConfigPath); console.log(' - 请求路径.endsWith(配置路径):', reqPath.endsWith(socketIoPath)); console.log(' - 规范化请求.endsWith(规范化配置):', normalizedReqPath.endsWith(normalizedConfigPath)); // 使用规范化路径进行比较 if (normalizedReqPath !== normalizedConfigPath) { console.warn(`[Socket.IO] ⚠️ 警告: 请求路径与配置路径不匹配!`); console.warn(`[Socket.IO] 原始请求: ${reqPath}`); console.warn(`[Socket.IO] 规范化请求: ${normalizedReqPath}`); console.warn(`[Socket.IO] 配置路径: ${socketIoPath}`); console.warn(`[Socket.IO] 规范化配置: ${normalizedConfigPath}`); console.warn(`[Socket.IO] 差异: ${reqPath.replace(socketIoPath, '【配置路径】')}`); } // 创建游戏房间(玩家创建) socket.on('create-room', (data: { difficulty: string; roomId?: string; invitePlayMode?: boolean }) => { const roomId = data.roomId || generateRoomId(); const room: GameRoom = { roomId, playerId: socket.id, board: [], difficulty: data.difficulty, spectators: new Set(), highlightedCells: new Set(), invitePlayMode: data.invitePlayMode || false }; gameRooms.set(roomId, room); socket.join(roomId); console.log(`[Socket.IO] 房间创建: ${roomId}, 玩家: ${socket.id}, 邀请同玩模式: ${room.invitePlayMode}`); socket.emit('room-created', { roomId }); }); // 旁观者加入房间 socket.on('join-spectate', (data: { roomId: string }) => { const room = gameRooms.get(data.roomId); if (!room) { socket.emit('error', { message: '房间不存在' }); return; } room.spectators.add(socket.id); socket.join(data.roomId); console.log(`旁观者加入: ${socket.id} -> 房间: ${data.roomId}`); // 发送当前游戏状态给旁观者 socket.emit('game-state', { board: room.board, difficulty: room.difficulty }); }); // 获取房间信息 socket.on('get-room-info', (data: { roomId: string }) => { console.log('[服务器日志] === 获取房间信息开始 ==='); console.log('[服务器日志] 收到 get-room-info 事件:'); console.log('[服务器日志] - 房间ID:', data.roomId); console.log('[服务器日志] - 请求者ID:', socket.id); const room = gameRooms.get(data.roomId); if (!room) { console.log('[服务器日志] ❌ 房间不存在'); socket.emit('error', { message: '房间不存在' }); console.log('[服务器日志] === 获取房间信息结束 ==='); return; } // 构建房间信息 const roomInfo = { roomId: room.roomId, playerCount: 1, // 玩家数量固定为1 spectatorCount: room.spectators.size, difficulty: room.difficulty, invitePlayMode: room.invitePlayMode, gameState: 'playing' // 默认游戏状态为进行中 }; console.log('[服务器日志] ✅ 发送房间信息:'); console.log('[服务器日志] - 同玩模式:', roomInfo.invitePlayMode ? '开启' : '关闭'); console.log('[服务器日志] - 旁观者数量:', roomInfo.spectatorCount); socket.emit('room-info', roomInfo); console.log('[服务器日志] === 获取房间信息完成 ==='); }); // 玩家更新游戏状态 socket.on('update-game', (data: { roomId: string; board: Cell[][] }) => { const room = gameRooms.get(data.roomId); if (!room || room.playerId !== socket.id) { return; } room.board = data.board; // 广播给该房间的所有旁观者 socket.to(data.roomId).emit('game-state', { board: room.board, difficulty: room.difficulty, highlightedCells: Array.from(room.highlightedCells) }); // 清除闪烁的格子 if (room.highlightedCells.size > 0) { setTimeout(() => { room.highlightedCells.clear(); }, 100); } }); // 旁观者清除所有高亮 socket.on('clear-all-highlights', (data: { roomId: string }) => { const room = gameRooms.get(data.roomId); if (!room || !room.spectators.has(socket.id)) { return; } // 清除所有高亮格子 room.highlightedCells.clear(); // 通知玩家清除所有高亮 io.to(room.playerId).emit('clear-all-highlights'); // 通知所有旁观者清除所有高亮 io.to(data.roomId).emit('clear-all-highlights'); console.log(`旁观者清除所有高亮: 房间 ${data.roomId}`); }); // 旁观者点击格子 socket.on('spectator-click', (data: { roomId: string; row: number; col: number }) => { const room = gameRooms.get(data.roomId); if (!room || !room.spectators.has(socket.id)) { return; } const cellKey = `${data.row},${data.col}`; room.highlightedCells.add(cellKey); console.log(`旁观者点击: 房间 ${data.roomId}, 格子 (${data.row}, ${data.col})`); // 通知玩家该格子被旁观者点击 io.to(room.playerId).emit('spectator-suggest', { row: data.row, col: data.col }); // 3秒后移除闪烁 setTimeout(() => { room.highlightedCells.delete(cellKey); io.to(data.roomId).emit('clear-highlight', { row: data.row, col: data.col }); }, 3000); }); // 旁观者操作格子(同玩模式) socket.on('spectator-action', (data: { roomId: string; action: 'reveal' | 'flag' | 'chord'; row: number; col: number }) => { console.log('[服务器日志] === 旁观者操作开始 ==='); console.log('[服务器日志] 收到 spectator-action 事件:'); console.log('[服务器日志] - 房间ID:', data.roomId); console.log('[服务器日志] - 动作类型:', data.action); console.log('[服务器日志] - 格子坐标:', `(${data.row}, ${data.col})`); console.log('[服务器日志] - 旁观者ID:', socket.id); const room = gameRooms.get(data.roomId); if (!room) { console.log('[服务器日志] ❌ 房间不存在,操作失败'); console.log('[服务器日志] 当前存在的房间:', Array.from(gameRooms.keys())); return; } console.log('[服务器日志] 房间信息:'); console.log('[服务器日志] - 玩家ID:', room.playerId); console.log('[服务器日志] - 旁观者数量:', room.spectators.size); console.log('[服务器日志] - 同玩模式:', room.invitePlayMode); console.log('[服务器日志] - 当前旁观者:', Array.from(room.spectators)); if (!room.spectators.has(socket.id)) { console.log('[服务器日志] ❌ 旁观者不在房间中,操作失败'); return; } if (!room.invitePlayMode) { console.log('[服务器日志] ❌ 房间未开启同玩模式,操作失败'); return; } console.log(`[服务器日志] ✅ 旁观者操作有效: 房间 ${data.roomId}, 动作: ${data.action}, 格子 (${data.row}, ${data.col})`); // 转发操作给玩家 console.log('[服务器日志] 转发操作给玩家:', room.playerId); io.to(room.playerId).emit('spectator-action', { action: data.action, row: data.row, col: data.col }); console.log('[服务器日志] === 旁观者操作完成 ==='); }); // 玩家更新难度 socket.on('update-difficulty', (data: { roomId: string; difficulty: string; config?: any }) => { const room = gameRooms.get(data.roomId); if (!room || room.playerId !== socket.id) { return; } console.log(`玩家更新难度: 房间 ${data.roomId}, 从 ${room.difficulty} 变更为 ${data.difficulty}`); console.log('配置信息:', data.config); room.difficulty = data.difficulty; // 广播难度更新给房间内所有旁观者 io.to(data.roomId).emit('difficulty-updated', { difficulty: data.difficulty, config: data.config // 传递配置信息给旁观者 }); }); // 玩家切换同玩模式 socket.on('toggle-invite-play-mode', (data: { roomId: string; invitePlayMode: boolean }) => { console.log('[服务器日志] === 切换同玩模式开始 ==='); console.log('[服务器日志] 收到 toggle-invite-play-mode 事件:'); console.log('[服务器日志] - 房间ID:', data.roomId); console.log('[服务器日志] - 同玩模式:', data.invitePlayMode ? '开启' : '关闭'); console.log('[服务器日志] - 玩家ID:', socket.id); const room = gameRooms.get(data.roomId); if (!room || room.playerId !== socket.id) { console.log('[服务器日志] ❌ 房间不存在或玩家不匹配,操作失败'); console.log('[服务器日志] === 切换同玩模式结束 ==='); return; } // 更新房间的同玩模式状态 room.invitePlayMode = data.invitePlayMode; console.log(`[服务器日志] ✅ 同玩模式已更新: ${data.invitePlayMode ? '开启' : '关闭'}`); // 广播同玩模式更新给房间内所有旁观者 console.log('[服务器日志] 广播同玩模式更新给旁观者'); io.to(data.roomId).emit('invite-play-mode-updated', { invitePlayMode: data.invitePlayMode }); console.log('[服务器日志] === 切换同玩模式完成 ==='); }); // 断开连接 socket.on('disconnect', () => { console.log('用户断开连接:', socket.id); // 清理房间数据 gameRooms.forEach((room, roomId) => { if (room.playerId === socket.id) { // 玩家离开,关闭房间 io.to(roomId).emit('room-closed'); gameRooms.delete(roomId); console.log(`房间关闭: ${roomId}`); } else if (room.spectators.has(socket.id)) { // 旁观者离开 room.spectators.delete(socket.id); } }); }); }); return io; } // 生成唯一房间ID function generateRoomId(): string { return Math.random().toString(36).substring(2, 10).toUpperCase(); }