262 lines
9.5 KiB
Java
262 lines
9.5 KiB
Java
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);
|
||
}
|
||
}
|
||
}
|
||
}
|