Files
webgame-backend/src/main/java/com/webgame/webgamebackend/adapter/ws/RoomWebSocketHandler.java
2026-07-07 09:58:42 +08:00

262 lines
9.5 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.webgame.webgamebackend.adapter.ws;
import com.webgame.webgamebackend.common.auth.AuthenticationProvider;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.webgame.webgamebackend.infrastructure.persistence.entity.GameRoomEntity;
import com.webgame.webgamebackend.infrastructure.persistence.repository.GameRoomRepository;
import com.webgame.webgamebackend.domain.room.RoomManager;
import com.webgame.webgamebackend.domain.engine.GameEngine;
import com.webgame.webgamebackend.domain.engine.GameEngineRegistry;
import com.webgame.webgamebackend.domain.engine.RoomContext;
import com.webgame.webgamebackend.adapter.ws.dto.WsInboundMessage;
import com.webgame.webgamebackend.adapter.ws.dto.WsMessage;
import com.webgame.webgamebackend.adapter.ws.dto.WsOutboundMessage;
import jakarta.annotation.PostConstruct;
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 处理器
*
* 重构后的职责:
* - 连接建立:认证 + 注册会话 + 取消断线倒计时(通过 RoomManager
* - 消息路由:房间管理消息 → RoomManager游戏消息 → GameEngine
* - 连接关闭:启动断线倒计时(通过 RoomManager
*
* <p>采用策略路由根据房间当前状态WAITING/PLAYING将消息分发给不同的处理器。
* WAITING 状态下只有房间管理消息ready, leave, start有效
* PLAYING 状态下游戏消息place_stone, resign, draw_*)由 GameEngine 处理。</p>
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class RoomWebSocketHandler extends TextWebSocketHandler {
private final RoomSessionManager sessionManager;
private final RoomManager roomManager;
private final GameEngineRegistry engineRegistry;
private final GameRoomRepository gameRoomRepository;
private final WsMessageRouter router;
private final AuthenticationProvider authProvider;
@PostConstruct
public void registerHandlers() {
// ===== 房间管理消息(所有状态) =====
// 准备/取消准备
router.register(WsInboundMessage.READY,
(roomId, userId, data, sm) ->
roomManager.toggleReady(roomId, userId));
// 离开房间
router.register(WsInboundMessage.LEAVE,
(roomId, userId, data, sm) ->
roomManager.leaveRoom(roomId, userId));
// 开始游戏(仅房主)
router.register(WsInboundMessage.START,
(roomId, userId, data, sm) ->
roomManager.startGame(roomId, userId));
// ===== 游戏消息PLAYING 状态) =====
// 落子
router.register(WsInboundMessage.PLACE_STONE, (roomId, userId, data, sm) -> {
GameRoomEntity room = gameRoomRepository.findById(roomId).orElse(null);
if (room != null && "playing".equals(room.getStatus())) {
// 通过 RoomManager 获取引擎的 getState 不行,需要直接找到引擎
// 改为委托模式RoomManager 提供 executeGameAction 方法
roomManager.executeGameAction(roomId, userId, "place_stone", data);
}
});
// 认输
router.register(WsInboundMessage.RESIGN, (roomId, userId, data, sm) -> {
roomManager.executeGameAction(roomId, userId, "resign", null);
});
// 求和请求
router.register(WsInboundMessage.DRAW_REQUEST, (roomId, userId, data, sm) -> {
roomManager.executeGameAction(roomId, userId, "draw_request", null);
});
// 回应求和
router.register(WsInboundMessage.DRAW_RESPONSE, (roomId, userId, data, sm) -> {
roomManager.executeGameAction(roomId, userId, "draw_response", data);
});
// 聊天
router.register(WsInboundMessage.CHAT, (roomId, userId, data, sm) -> {
String content = data != null ? data.getString("content") : null;
handleChat(roomId, userId, content);
});
log.info("[WS] 已注册 {} 个消息处理器", router.size());
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
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
String userId = authProvider.getUserIdByToken(token);
if (userId == null) {
session.close(CloseStatus.POLICY_VIOLATION);
return;
}
// 处理重连(在注册会话之前,避免广播到尚未完成握手的当前 session
roomManager.handleReconnect(roomId, userId);
// 注册会话
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 rawData = json.get("data");
JSONObject data = (rawData instanceof JSONObject) ? (JSONObject) rawData : null;
WsMessageHandler handler = router.get(type);
if (handler == null) {
sendError(session, "未知消息类型: " + type);
return;
}
handler.handle(roomId, userId, data, sessionManager);
} catch (Exception e) {
log.error("[WS] 处理消息异常, roomId={}, userId={}", roomId, userId, e);
sendError(session, e.getMessage());
}
}
@Override
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) {
// 通知 RoomManager 处理断线
roomManager.handleDisconnect(roomId, userId);
}
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 static final int CHAT_MAX_LENGTH = 500;
/**
* 聊天消息处理
*/
private void handleChat(String roomId, String userId, String content) {
if (content == null || content.isBlank()) {
return;
}
if (content.length() > CHAT_MAX_LENGTH) {
content = content.substring(0, CHAT_MAX_LENGTH);
}
String sanitized = content
.replace("<", "")
.replace(">", "");
broadcastToRoom(roomId, new WsMessage(WsOutboundMessage.CHAT_BROADCAST, Map.of(
"userId", userId,
"content", sanitized,
"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);
}
}
}
}