diff --git a/src/main/java/com/webgame/webgamebackend/ws/RoomSessionManager.java b/src/main/java/com/webgame/webgamebackend/ws/RoomSessionManager.java new file mode 100644 index 0000000..73296f8 --- /dev/null +++ b/src/main/java/com/webgame/webgamebackend/ws/RoomSessionManager.java @@ -0,0 +1,106 @@ +package com.webgame.webgamebackend.ws; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.WebSocketSession; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 房间 WebSocket 会话管理器 + * + * 维护 roomId → sessions 的映射,支持房间内广播(向房间内所有连接发送消息)。 + * 同时维护 sessionId → userId 的映射,用于识别会话所属用户。 + */ +@Slf4j +@Component +public class RoomSessionManager { + + /** roomId → 该房间的所有 WebSocket 会话 */ + private final Map> roomSessions = new ConcurrentHashMap<>(); + + /** sessionId → userId */ + private final Map sessionUsers = new ConcurrentHashMap<>(); + + /** sessionId → roomId */ + private final Map sessionRooms = new ConcurrentHashMap<>(); + + /** + * 将用户会话加入指定房间 + * + * @param roomId 房间 ID + * @param userId 用户 ID + * @param session WebSocket 会话 + */ + public void addSession(String roomId, String userId, WebSocketSession session) { + roomSessions.computeIfAbsent(roomId, k -> ConcurrentHashMap.newKeySet()).add(session); + sessionUsers.put(session.getId(), userId); + sessionRooms.put(session.getId(), roomId); + log.info("[WS会话] 加入房间, roomId={}, userId={}, sessionId={}, 当前房间连接数={}", + roomId, userId, session.getId(), roomSessions.get(roomId).size()); + } + + /** + * 移除会话 + * + * @param session WebSocket 会话 + */ + public void removeSession(WebSocketSession session) { + String roomId = sessionRooms.remove(session.getId()); + String userId = sessionUsers.remove(session.getId()); + if (roomId != null) { + Set sessions = roomSessions.get(roomId); + if (sessions != null) { + sessions.remove(session); + if (sessions.isEmpty()) { + roomSessions.remove(roomId); + } + } + } + log.info("[WS会话] 离开房间, roomId={}, userId={}, sessionId={}", + roomId, userId, session.getId()); + } + + /** + * 获取指定房间的所有会话 + * + * @param roomId 房间 ID + * @return 会话集合(可能为空) + */ + public Set getRoomSessions(String roomId) { + return roomSessions.getOrDefault(roomId, Set.of()); + } + + /** + * 获取会话对应的用户 ID + * + * @param session WebSocket 会话 + * @return 用户 ID,未找到返回 null + */ + public String getUserId(WebSocketSession session) { + return sessionUsers.get(session.getId()); + } + + /** + * 获取会话所在的房间 ID + * + * @param session WebSocket 会话 + * @return 房间 ID,未找到返回 null + */ + public String getRoomId(WebSocketSession session) { + return sessionRooms.get(session.getId()); + } + + /** + * 获取房间内连接总数 + * + * @param roomId 房间 ID + * @return 连接数 + */ + public int getRoomSessionCount(String roomId) { + Set sessions = roomSessions.get(roomId); + return sessions != null ? sessions.size() : 0; + } +} diff --git a/src/main/java/com/webgame/webgamebackend/ws/RoomWebSocketHandler.java b/src/main/java/com/webgame/webgamebackend/ws/RoomWebSocketHandler.java index 9558483..b24eada 100644 --- a/src/main/java/com/webgame/webgamebackend/ws/RoomWebSocketHandler.java +++ b/src/main/java/com/webgame/webgamebackend/ws/RoomWebSocketHandler.java @@ -1,32 +1,198 @@ package com.webgame.webgamebackend.ws; +import cn.dev33.satoken.stp.StpUtil; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.webgame.webgamebackend.service.GomokuGameService; +import com.webgame.webgamebackend.ws.dto.WsInboundMessage; +import com.webgame.webgamebackend.ws.dto.WsMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.socket.CloseStatus; import org.springframework.web.socket.TextMessage; import org.springframework.web.socket.WebSocketSession; import org.springframework.web.socket.handler.TextWebSocketHandler; +import java.io.IOException; +import java.net.URI; +import java.util.Map; + /** * 房间 WebSocket 处理器 * - * 管理游戏房间内的实时通信,包括玩家加入、离开、游戏状态同步等。 - * 后续任务将实现具体业务逻辑。 + * 处理客户端连接、断开与消息路由。消息类型路由到不同的业务逻辑。 + * 通过 URL 参数传递 roomId 和 token 完成认证与房间绑定。 */ +@Slf4j @Component +@RequiredArgsConstructor public class RoomWebSocketHandler extends TextWebSocketHandler { + private final RoomSessionManager sessionManager; + private final GomokuGameService gomokuGameService; + @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { - // 后续任务实现 + // 从 URL 参数中提取 roomId 和 token + URI uri = session.getUri(); + if (uri == null) { + session.close(CloseStatus.BAD_DATA); + return; + } + + String query = uri.getQuery(); + if (query == null) { + session.close(CloseStatus.BAD_DATA); + return; + } + + String roomId = getQueryParam(query, "roomId"); + String token = getQueryParam(query, "token"); + + if (roomId == null || token == null) { + session.close(CloseStatus.BAD_DATA); + return; + } + + // 通过 token 获取用户 ID + Object loginId = StpUtil.getLoginIdByToken(token); + if (loginId == null) { + session.close(CloseStatus.POLICY_VIOLATION); + return; + } + String userId = loginId.toString(); + + // 注册会话 + sessionManager.addSession(roomId, userId, session); + log.info("[WS] 连接建立, roomId={}, userId={}, sessionId={}", roomId, userId, session.getId()); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { - // 后续任务实现 + String payload = message.getPayload(); + String userId = sessionManager.getUserId(session); + String roomId = sessionManager.getRoomId(session); + + if (userId == null || roomId == null) { + sendError(session, "未加入房间"); + return; + } + + log.info("[WS] 收到消息, roomId={}, userId={}, payload={}", roomId, userId, payload); + + try { + JSONObject json = JSON.parseObject(payload); + String type = json.getString("type"); + Object data = json.get("data"); + + switch (type) { + case WsInboundMessage.PLACE_STONE -> { + // data 包含 row, col + int row = ((JSONObject) data).getIntValue("row"); + int col = ((JSONObject) data).getIntValue("col"); + gomokuGameService.placeStone(roomId, userId, row, col, sessionManager); + } + case WsInboundMessage.CHAT -> { + String content = ((JSONObject) data).getString("content"); + handleChat(roomId, userId, content); + } + case WsInboundMessage.READY -> { + gomokuGameService.toggleReady(roomId, userId, sessionManager); + } + case WsInboundMessage.RESIGN -> { + gomokuGameService.resign(roomId, userId, sessionManager); + } + case WsInboundMessage.DRAW_REQUEST -> { + gomokuGameService.requestDraw(roomId, userId, sessionManager); + } + case WsInboundMessage.DRAW_RESPONSE -> { + boolean accept = ((JSONObject) data).getBooleanValue("accept"); + gomokuGameService.respondToDraw(roomId, userId, accept, sessionManager); + } + case WsInboundMessage.LEAVE -> { + gomokuGameService.leaveRoom(roomId, userId, sessionManager); + } + case WsInboundMessage.START -> { + gomokuGameService.startGame(roomId, userId, sessionManager); + } + default -> sendError(session, "未知消息类型: " + type); + } + } catch (Exception e) { + log.error("[WS] 处理消息异常, roomId={}, userId={}", roomId, userId, e); + sendError(session, e.getMessage()); + } } @Override - public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { - // 后续任务实现 + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { + String userId = sessionManager.getUserId(session); + String roomId = sessionManager.getRoomId(session); + log.info("[WS] 连接关闭, roomId={}, userId={}, sessionId={}, status={}", + roomId, userId, session.getId(), status); + + if (userId != null && roomId != null) { + // 通知游戏服务玩家断开 + gomokuGameService.handleDisconnect(roomId, userId, sessionManager); + } + sessionManager.removeSession(session); + } + + // ==================== 辅助方法 ==================== + + /** + * 解析 URL Query 参数 + */ + private String getQueryParam(String query, String key) { + for (String param : query.split("&")) { + String[] pair = param.split("=", 2); + if (pair.length == 2 && pair[0].equals(key)) { + return pair[1]; + } + } + return null; + } + + /** + * 聊天消息处理(暂不持久化,直接广播) + */ + private void handleChat(String roomId, String userId, String content) { + // TODO: 后续可增加消息持久化 + broadcastToRoom(roomId, new WsMessage(WsOutboundMessage.CHAT_BROADCAST, Map.of( + "userId", userId, + "content", content, + "timestamp", System.currentTimeMillis() + ))); + } + + /** + * 向房间内所有会话广播消息 + */ + private void broadcastToRoom(String roomId, WsMessage message) { + String json = JSON.toJSONString(message); + for (WebSocketSession ws : sessionManager.getRoomSessions(roomId)) { + if (ws.isOpen()) { + try { + ws.sendMessage(new TextMessage(json)); + } catch (IOException e) { + log.error("[WS] 发送消息失败, sessionId={}", ws.getId(), e); + } + } + } + } + + /** + * 向指定会话发送错误消息 + */ + private void sendError(WebSocketSession session, String errorMsg) { + if (session.isOpen()) { + try { + String json = JSON.toJSONString( + new WsMessage(WsOutboundMessage.ERROR, Map.of("message", errorMsg))); + session.sendMessage(new TextMessage(json)); + } catch (IOException e) { + log.error("[WS] 发送错误消息失败", e); + } + } } }