feat: 添加 WebSocket Handler 与会话管理器
This commit is contained in:
@@ -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<String, Set<WebSocketSession>> roomSessions = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/** sessionId → userId */
|
||||||
|
private final Map<String, String> sessionUsers = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/** sessionId → roomId */
|
||||||
|
private final Map<String, String> 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<WebSocketSession> 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<WebSocketSession> 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<WebSocketSession> sessions = roomSessions.get(roomId);
|
||||||
|
return sessions != null ? sessions.size() : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,32 +1,198 @@
|
|||||||
package com.webgame.webgamebackend.ws;
|
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.stereotype.Component;
|
||||||
import org.springframework.web.socket.CloseStatus;
|
import org.springframework.web.socket.CloseStatus;
|
||||||
import org.springframework.web.socket.TextMessage;
|
import org.springframework.web.socket.TextMessage;
|
||||||
import org.springframework.web.socket.WebSocketSession;
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 房间 WebSocket 处理器
|
* 房间 WebSocket 处理器
|
||||||
*
|
*
|
||||||
* 管理游戏房间内的实时通信,包括玩家加入、离开、游戏状态同步等。
|
* 处理客户端连接、断开与消息路由。消息类型路由到不同的业务逻辑。
|
||||||
* 后续任务将实现具体业务逻辑。
|
* 通过 URL 参数传递 roomId 和 token 完成认证与房间绑定。
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class RoomWebSocketHandler extends TextWebSocketHandler {
|
public class RoomWebSocketHandler extends TextWebSocketHandler {
|
||||||
|
|
||||||
|
private final RoomSessionManager sessionManager;
|
||||||
|
private final GomokuGameService gomokuGameService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
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
|
@Override
|
||||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
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
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user