五子棋双人对战项目(5)——对战模块

目录

一、需求分析

二、约定前后端交互接口

三、实现游戏房间页面(前端代码)

game_room.html

game_room.css

srcipt.js

四、实现后端代码

GameAPI

Room

Mapper

五、线程安全问题


一、需求分析

        在对局中,玩家需要知道实时对局情况,所以需要用到消息推送机制,只要有玩家一落子,把落子情况发送给服务器,服务器收到后马上发送给对手,让对手玩家立马能知道我的落子位置。

        同时,我们还需要棋盘,这个可以使用 HTML5 引入的一个标签:canvas,称为画布,为什么这么说呢?因为它在网页上可以实现一个 “画画” 的效果,我们需要的棋盘,就可以使用这个标签,给它画上去。(同理,黑棋、白旗也可以画上去)。


二、约定前后端交互接口

        对战模块和匹配模块,使用的是两套逻辑,使用不同的 websocket 的路径进行处理,可以做到更好的解耦合。

        在匹配成功后,进入房间页面,服务器主动给客户端发送的响应:(客户端无需发送请求给服务器)

        针对 落子请求和响应:(我落子后,对方也要知道实时棋局情况,需要把请求发给服务器,服务器在返回响应给对方;对方落子后,也同理)


三、实现游戏房间页面(前端代码)

game_room.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>游戏房间</title><link rel="stylesheet" href="css/common.css"><link rel="stylesheet" href="css/game_room.css">
</head>
<body><div class="nav">五子棋对战</div><div class="container"><div><!-- 棋盘区域, 需要基于 canvas 进行实现 --><canvas id="chess" width="450px" height="450px"></canvas><!-- 显示区域 --><div id="screen"> 等待玩家连接中... </div></div></div><script src="js/script.js"></script>
</body>
</html>

game_room.css

#screen {width: 450px;height: 50px;margin-top: 10px;background-color: orange;font-size: 22px;line-height: 50px;text-align: center;color: white;border: none;outline: none;border-radius: 10px;
}/* 在 game_room.css 中添加下面的样式 */
.button {width: 450px;height: 50px;margin-top: 10px;background-color: green;font-size: 22px;line-height: 50px;text-align: center;color: white;border: none;outline: none;border-radius: 10px;
}.button:active{background-color: gray;
} 

srcipt.js

let gameInfo = {roomId: null,thisUserId: null,thatUserId: null,isWhite: true,
}//
// 设定界面显示相关操作
//function setScreenText(me) {let screen = document.querySelector('#screen');if (me) {screen.innerHTML = "轮到你落子了!";} else {screen.innerHTML = "轮到对方落子了!";}
}//
// 初始化 websocket
//
let websocketUrl = "ws://" + location.host + "/game";
let websocket = new WebSocket(websocketUrl);websocket.onopen = function () {console.log("连接游戏房间成功");
}websocket.close = function () {console.log("和游戏服务器连接断开");
}websocket.onerror = function () {console.log("和服务器的连接出现异常");
}websocket.onbeforeunload = function () {websocket.close();
}//处理服务器返回的响应数据 (游戏就绪响应)
websocket.onmessage = function (event) {console.log("[handlerGameReady] " + event.data);let resp = JSON.parse(event.data);if (!resp.ok) {alert("连接游戏失败! reason: " + resp.reason);// 如果出现连接失败的情况,回到游戏大厅// location.assign("/game_hall.html");location.replace("/game_hall.html");return;}if (resp.message == 'gameReady') {// 把后端返回的数据放进 gameInfo 对象中gameInfo.roomId = resp.roomId;gameInfo.thisUserId = resp.thisUserId;gameInfo.thatUserId = resp.thatUserId;// 判断自己是不是先手gameInfo.isWhite = (resp.whiteUser == resp.thisUserId);// 初始化棋盘initGame();//设置显示区域的内容(轮到谁落子了)setScreenText(gameInfo.isWhite);} else if (resp.message == 'repeatConnection') {console.log("检测到游戏多开!");alert("检测到游戏多开, 请使用其他账户进行登录!");// location.assign("/login.html");location.replace("/login.html");return;}
}//
// 初始化一局游戏
//
function initGame() {// 是我下还是对方下. 根据服务器分配的先后手情况决定let me = gameInfo.isWhite;// 游戏是否结束let over = false;let chessBoard = [];//初始化chessBord数组(表示棋盘的数组)for (let i = 0; i < 15; i++) {chessBoard[i] = [];for (let j = 0; j < 15; j++) {chessBoard[i][j] = 0;}}let chess = document.querySelector('#chess');let context = chess.getContext('2d');context.strokeStyle = "#FFFFFF";// 背景图片let logo = new Image();logo.src = "image/6.jpg";logo.onload = function () {context.drawImage(logo, 0, 0, 450, 450);initChessBoard();}// // 绘制棋盘网格// function initChessBoard() {//     for (let i = 0; i < 15; i++) {//         context.moveTo(15 + i * 30, 15);//         context.lineTo(15 + i * 30, 430);//         context.stroke();//         context.moveTo(15, 15 + i * 30);//         context.lineTo(435, 15 + i * 30);//         context.stroke();//     }// }// 绘制棋盘网格function initChessBoard() {for (let i = 0; i < 15; i++) {context.moveTo(15 + i * 30, 15);context.lineTo(15 + i * 30, 15 + 30 * 14);  // 这里确保到450context.stroke();context.moveTo(15, 15 + i * 30);context.lineTo(15 + 30 * 14, 15 + i * 30);  // 这里确保到450context.stroke();}}// 绘制一个棋子, me 为 truefunction oneStep(i, j, isWhite) {context.beginPath();context.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI);context.closePath();var gradient = context.createRadialGradient(15 + i * 30 + 2, 15 + j * 30 - 2, 13, 15 + i * 30 + 2, 15 + j * 30 - 2, 0);if (!isWhite) {gradient.addColorStop(0, "#0A0A0A");gradient.addColorStop(1, "#636766");} else {gradient.addColorStop(0, "#D1D1D1");gradient.addColorStop(1, "#F9F9F9");}context.fillStyle = gradient;context.fill();}chess.onclick = function (e) {if (over) {return;}if (!me) {return;}let x = e.offsetX;let y = e.offsetY;// 注意, 横坐标是列, 纵坐标是行let col = Math.floor(x / 30);let row = Math.floor(y / 30);// 客户端的棋盘状态只有两种,主要是用来判断当前棋盘有没有棋子,用来避免一个问题:同一个位置重复落子的情况if (chessBoard[row][col] == 0) {// 发送坐标给服务器, 服务器要返回结果send(row, col);}}function send(row, col) {let req = {message: 'putChess',userId: gameInfo.thisUserId,row: row,col: col};websocket.send(JSON.stringify(req));}// 之前 websocket.onmessage 主要是用来处理了游戏就绪响应,在游戏就绪之后,初始化完毕之后,也就不再有这个 游戏就绪响应 了// 就在这个 initGame 内部修改 websocket.onmessage 方法~~,让这个方法里面针 对落子响应 进行处理websocket.onmessage = function (event) {console.log("[handlerPutChess]: " + event.data);let resp = JSON.parse(event.data);if (resp.message != "putChess") {console.log("响应类型错误");return;}// 先判定当前这个响应时否为自己逻的子,还是对方落的子if (resp.userId == gameInfo.thisUserId) {// 我自己落的子// 根据我自己棋子的颜色,来绘制一个棋子oneStep(resp.col, resp.row, gameInfo.isWhite);} else if (resp.userId == gameInfo.thatUserId) {// 我的对手落的子oneStep(resp.col, resp.row, !gameInfo.isWhite);} else {// 响应错误! userId 是有问题的console.log("[handlerPutChess resp userId 错误");return;}// 给对应的位置设为 1,方便后续逻辑判定当前位置是否已经有棋子了chessBoard[resp.row][resp.col] = 1;// 交换双方的落子轮次me = !me;setScreenText(me);// 判定游戏是否结束let screenDiv = document.querySelector('#screen');if (resp.winner != 0) {if (resp.winner == gameInfo.thisUserId) {// alert('你赢了!');screenDiv.innerHTML = '你赢了!';} else if (resp.winner == gameInfo.thatUserId) {// alert('你输了');screenDiv.innerHTML = '你输了!';} else {alert('winner 字段错误 ' + resp.winner);}// 回到游戏大厅// location.assign('/game_hall.html');// 增加一个按钮,让玩家点击之后,再回到游戏大厅~let backButton = document.createElement('button');backButton.innerHTML = '回到游戏大厅'backButton.className = 'button';  // 添加样式类backButton.onclick = function() {location.replace('/game_hall.html');}let fatherDiv = document.querySelector('.container>div');fatherDiv.appendChild(backButton);}}
}

四、实现后端代码

1、WebSocketConfig 

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {@Autowiredprivate MatchAPI matchAPI;@Autowiredprivate GameAPI gameAPI;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {webSocketHandlerRegistry.addHandler(matchAPI, "/findMatch").addInterceptors(new HttpSessionHandshakeInterceptor());webSocketHandlerRegistry.addHandler(gameAPI, "/game").addInterceptors(new HttpSessionHandshakeInterceptor());}
}

2、GameAPI

@Slf4j
@Component
public class GameAPI extends TextWebSocketHandler {ObjectMapper objectMapper = new ObjectMapper();@Autowiredprivate RoomManager roomManager;@Autowiredprivate OnlineUserManager onlineUserManager;@Autowiredprivate UserMapper userMapper;@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {GameReadyResponse resp = new GameReadyResponse();// 1、先获取到用户的身份信息(从 HttpSession 里拿到)User user = (User) session.getAttributes().get("user");if (user == null) {resp.setOk(false);resp.setReason("用户尚未登录!");String jsonString = objectMapper.writeValueAsString(resp);session.sendMessage(new TextMessage(jsonString));return;}// 2、判定当前用户是否已经进入房间(拿着房间管理器进行查询)Room room = roomManager.getRoomByUserId(user.getUserId());if (room == null) {// 如果为 null,当前没有找到对应的房间,该玩家还没匹配到resp.setOk(false);resp.setReason("用户尚未匹配到");String jsonString = objectMapper.writeValueAsString(resp);session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(jsonString)));return;}// 3、判定当前是不是多开(该用户是不是已经在其他地方进行游戏了)//    前面多准备了一个 OnlineUserManagerif (onlineUserManager.getFromGameHall(user.getUserId()) != null|| onlineUserManager.getFromGameRoom(user.getUserId()) != null) {// 如果一个账号,一边是在游戏大厅,一边是在游戏房间,也视为多开~resp.setOk(true);resp.setReason("禁止多开游戏页面");resp.setMessage("repeatConnection");String jsonString = objectMapper.writeValueAsString(resp);session.sendMessage(new TextMessage(jsonString));return;}// 4、经过一些列校验都没问题后,设置当前玩家上线(房间中上线)onlineUserManager.enterGameRoom(user.getUserId(), session);synchronized (room) {// 5、把两个玩家加入到游戏房间中//    前面的创建房间/匹配过程,是在 game_hall.html 页面中完成的//    因此前面匹配到对手之后,需要经过页面跳转,来到 game_room.html 才算正式进入游戏房间(才算玩家准备就绪)//    当前这个逻辑是在 game_room.html 页面加载的时候进行的//    执行到当前逻辑,说明玩家已经页面跳转成功了//    页面跳转,其实是个大活~(很有可能出现 “失败” 的情况的)if (room.getUser1() == null) {// 第一个玩家尚未加入房间// 就把当前连上 WebSocket 的玩家作为 user1,加入到房间中room.setUser1(user);// 把先连入房间的玩家作为先手方room.setWhiteUser(user.getUserId());log.info("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家1");return;}if (room.getUser2() == null) {// 如果进入这个房间,说明玩家1 已经加入房间,现在要把玩家2 加入房间room.setUser2(user);log.info("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家2");// 当两个玩家都加入成功之后,就要让服务器,给这两个玩家都返回 WebSocket 的响应数据// 通知两个玩家说:游戏双方都已经准备好了// 通知玩家1noticeGameReady(room, room.getUser1(), room.getUser2());// 通知玩家2noticeGameReady(room, room.getUser2(), room.getUser1());return;}}// 6、此处如果又有一个玩家尝试连接同一个房间,就会提示报错//    这种情况理论上是不存在的,为了让程序更加健壮,还是做一个判定和提示resp.setOk(false);resp.setReason("当前房间已满,您不能加入房间");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));}private void noticeGameReady(Room room, User thisUser, User thatUser) throws IOException {GameReadyResponse resp = new GameReadyResponse();resp.setMessage("gameReady");resp.setOk(true);resp.setReason("");resp.setRoomId(room.getRoomId());resp.setThisUserId(thisUser.getUserId());resp.setThatUserId(thatUser.getUserId());resp.setWhiteUser(room.getWhiteUser());// 把当前的响应数据传回给对应的玩家WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(thisUser.getUserId());webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 1、先从 Session 拿到当前用户的身份信息User user = (User) session.getAttributes().get("user");if (user == null) {log.info("[handleTextMessage] 当前玩家尚未登录");return;}// 2、根据 玩家id 获取到房间对象Room room = roomManager.getRoomByUserId(user.getUserId());// 3、通过 room对象 来处理这次具体的请求room.putChess(message.getPayload());}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {User user = (User) session.getAttributes().get("user");if (user == null) {//此处就简单处理,在断开连接的时候就不给客户端返回响应了return;}WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());// 加上这个判定,目的是为了在多开的情况下,第二个用户退出连接动作,导致第一个登录在线的用户会话删除if (session == exitSession) {onlineUserManager.exitGameRoom(user.getUserId());log.info("当前这个用户 {}", user.getUsername() + " 游戏房间连接异常");}// 通知对手获胜noticeThatUserWin(user);}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {User user = (User) session.getAttributes().get("user");if (user == null) {//此处就简单处理,在断开连接的时候就不给客户端返回响应了return;}WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());// 加上这个判定,目的是为了在多开的情况下,第二个用户退出连接动作,导致第一个登录在线的用户会话删除if (session == exitSession) {onlineUserManager.exitGameRoom(user.getUserId());log.info("当前这个用户 {}", user.getUsername() + " 已经离开游戏房间");}// 通知对手获胜noticeThatUserWin(user);}private void noticeThatUserWin(User user) throws IOException {// 1、根据当前玩家,找到对应房间,再找到当前玩家的对手Room room = roomManager.getRoomByUserId(user.getUserId());if (room == null) {// 这个情况意味着房间已经被释放,也就没有对手了log.info("当前房间已经释放, 无需通知对手");return;}// 2、根据房间找到对手User thatUser = (user == room.getUser1()) ? room.getUser2() : room.getUser1();// 3、找到对手的在线状态WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(thatUser.getUserId());if(webSocketSession == null) {// 这就意味着对手也掉线了log.info("对手也已经掉线了, 无需通知");return;}// 4、构造一个响应,来通知对手,你是获胜方GameResponse resp = new GameResponse();resp.setMessage("putChess");resp.setUserId(thatUser.getUserId());resp.setWinner(thatUser.getUserId());webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));// 5、更新玩家的分数信息int winUserId = thatUser.getUserId();int loseUserId = user.getUserId();userMapper.userWin(winUserId);userMapper.userLose(loseUserId);// 6、释放房间对象roomManager.remove(room.getRoomId(), room.getUser1().getUserId(), room.getUser2().getUserId());}
}

(1)游戏房间准备就绪响应(GameReadyResponse )

// 客户端连接到游戏房间后,返回的响应
@Data
public class GameReadyResponse {private String message;private boolean ok;private String reason;private String roomId;private int thisUserId;private int thatUserId;private int whiteUser;
}

(2)“落子”请求(GameRequest )

// 这个类表示落子请求
@Data
public class GameRequest {private String message;private int userId;private int row;private int col;
}

(3)“落子”响应(GameResponse )

// 这个类表示一个落子响应
@Data
public class GameResponse {private String message;private int userId;private int row;private int col;private int winner;
}

(4)Room

@Slf4j
@Data
public class Room {// 使用字符串类型来表示,方便生成唯一值private String roomId;private User user1;private User user2;// 先手方的玩家 idprivate int whiteUser;// 行 | 列private static final int MAX_ROW = 15;private static final int MAX_COL = 15;// 这个二维数组表示棋盘(服务端这边数组的状态有三种,但客户端那边只有两种,主要是用来判断当前棋盘有没有棋子,用来避免一个问题:同一个位置重复落子的情况)// 约定:// 1) 使用 0 表示当前位置未落子(初始化好的二维数组,相对于 全都是0)// 2) 使用 1 表示 user1 的落子位置// 3) 使用 2 表示 user2 的落子位置private int[][] board = new int[MAX_ROW][MAX_COL];// 创建 ObjectMapper 用来转换JSONprivate ObjectMapper objectMapper = new ObjectMapper();// 引入 OnlineUserManager
//    @Autowiredprivate OnlineUserManager onlineUserManager;// 引入 RoomManager, 用于房间销毁
//    @Autowiredprivate RoomManager roomManager;// 引入 UserMapper, 用于更新用户数据
//    @Autowiredprivate UserMapper userMapper;// 通过这个方法来处理一次落子操作// 要做的事情:// 1、记录当前落子的位置// 2、进行胜负判定// 3、给客户端返回响应public void putChess(String reqJson) throws IOException {// 1、记录当前落子的位置GameRequest request = objectMapper.readValue(reqJson, GameRequest.class);GameResponse response = new GameResponse();// 当前这个棋子是玩家1落子,还是玩家2落子;根据玩家1 和 玩家2 来决定往数组中放1还是2int chess = request.getUserId() == user1.getUserId() ? 1 : 2;int row = request.getRow();int col = request.getCol();// 判断当前位置是不是已经有棋子了if (board[row][col] != 0) {// 在客户端已经针对重复落子进行判定过了,此处为了程序更加稳健,在服务器再判断一次log.info("当前位置 row: " + row + " col: " + col + " 已经有棋子了");}board[row][col] = chess;// 2、打印出当前的棋盘信息,方便来观察局势,也方便后面验证胜负关系的判定printBoard();// 3、进行胜负判定int winner = checkWinner(row, col, chess);// 4、给房间的所有客户端都返回响应response.setMessage("putChess");response.setUserId(request.getUserId());response.setRow(row);response.setCol(col);response.setWinner(winner);// 要想给用户发送 WebSocket 数据,就需要获取到这个用户的 WebSocketSessionWebSocketSession session1 = onlineUserManager.getFromGameRoom(user1.getUserId());WebSocketSession session2 = onlineUserManager.getFromGameRoom(user2.getUserId());// 万一当前查到的会话为空(玩家下线了), 特殊处理一下if (session1 == null) {// 玩家1 下线了,直接认为 玩家2 获胜response.setWinner(user2.getUserId());log.info("玩家: {}", user1.getUsername() + " 下线, 直接判定玩家1获胜");}if (session2 == null) {// 玩家2 下线了,直接认为 玩家1 获胜response.setWinner(user1.getUserId());log.info("玩家: {}", user2.getUsername() + " 下线, 直接判定玩家1获胜");}// 把响应构造成 JSON 字符串,通过 Session进行传输String respJson = objectMapper.writeValueAsString(response);if (session1 != null) {session1.sendMessage(new TextMessage(respJson));}if (session2 != null) {session2.sendMessage(new TextMessage(respJson));}// 5、如果当时胜负已分, 这个房间就已经失去存在的意义了,就把这个房间从房间管理器中删除if (response.getWinner() != 0) {log.info("游戏结束, 当前房间即将销毁! rommId= {}", roomId + " 获胜方为: " + response.getWinner());// 更新获胜方和失败方的信息int winUserId = response.getWinner();int loseUserId = (response.getWinner() == user1.getUserId()) ? user2.getUserId() : user1.getUserId();userMapper.userWin(winUserId);userMapper.userLose(loseUserId);// 销毁房间roomManager.remove(roomId, user1.getUserId(), user2.getUserId());}}private void printBoard() {// 打印出棋盘log.info("[打印棋盘信息] " + "roomId: {}", roomId);System.out.println("=======================================================");for (int r = 0; r < MAX_ROW; r++) {for (int c = 0; c < MAX_COL; c++) {// 针对一行之内的若干列,不要打印换行System.out.print(board[r][c] + " ");}// 遍历完一行之后,再换行System.out.println();}System.out.println("=======================================================");}// 使用这个方法,来判定当前落子后,是否分出胜负// 约定://  1) 如果 玩家1 获胜,就返回 玩家1 的userId//  2) 如果 玩家2 获胜,就返回 玩家2 的userId//  3) 如果 胜负未分,就返回 0private int checkWinner(int row, int col, int chess) {// 1、检查所有的行//   先遍历这五种情况for (int c = col - 4; c <= col; c++) {// 针对其中的一种情况,来判定这五个棋子是不是连在一起了// 不光是这五个字得连着,而且还得和玩家落的子是一样(才算是获胜)try {if (board[row][c] == chess&& board[row][c + 1] == chess&& board[row][c + 2] == chess&& board[row][c + 3] == chess&& board[row][c + 4] == chess) {// 构成 五子连珠! 胜负已分!log.info("行 五子连珠! 胜负已分!");return chess == 1 ? user1.getUserId() : user2.getUserId();}} catch (ArrayIndexOutOfBoundsException e) {// 如果出现数组下标越界的情况,就在这里直接忽略这个异常,继续下一次循环判断continue;}}// 2、检查所有的列for(int r = row - 4; r <= row; r++) {try {if (board[r][col] == chess&& board[r + 1][col] == chess&& board[r + 2][col] == chess&& board[r + 3][col] == chess&& board[r + 4][col] == chess) {// 构成 五子连珠! 胜负已分!log.info("列 五子连珠! 胜负已分!");return chess == 1 ? user1.getUserId() : user2.getUserId();}} catch (ArrayIndexOutOfBoundsException e) {// 如果出现数组下标越界的情况,就在这里直接忽略这个异常,继续下一次循环判断continue;}}// 3、检查所有主对角线 (左对角线,从左上往右下)for (int r = row - 4, c = col - 4; r <= row && c <= col; r++, c++) {try {if (board[r][c] == chess&& board[r + 1][c + 1] == chess&& board[r + 2][c + 2] == chess&& board[r + 3][c + 3] == chess&& board[r + 4][c + 4] == chess) {// 构成 五子连珠! 胜负已分!log.info("主对角线 五子连珠! 胜负已分!");return chess == 1 ? user1.getUserId() : user2.getUserId();}} catch (ArrayIndexOutOfBoundsException e) {// 如果出现数组下标越界的情况,就在这里直接忽略这个异常,继续下一次循环判断continue;}}// 4、检查所有副对角线 (右对角线, 从左下往右上)for (int r = row - 4, c = col + 4; r <= row && c >= col; r++, c--) {try {if (board[r][c] == chess&& board[r + 1][c - 1] == chess&& board[r + 2][c - 2] == chess&& board[r + 3][c - 3] == chess&& board[r + 4][c - 4] == chess) {// 构成 五子连珠! 胜负已分!log.info("副对角线 五子连珠! 胜负已分!");return chess == 1 ? user1.getUserId() : user2.getUserId();}} catch (ArrayIndexOutOfBoundsException e) {// 如果出现数组下标越界的情况,就在这里直接忽略这个异常,继续下一次循环判断continue;}}// 胜负未分,就直接返回 0 了return 0;}public Room() {// 构造 Room 的时候生成一个唯一的字符串表示房间 id// 使用 UUID 来作为房间 idroomId = UUID.randomUUID().toString();// 通过入口类记录中的 context,来手动获取到前面的 RoomManager 和 OnlineUserManageronlineUserManager = SpringGobangApplication.context.getBean(OnlineUserManager.class);roomManager = SpringGobangApplication.context.getBean(RoomManager.class);userMapper = SpringGobangApplication.context.getBean(UserMapper.class);}
}

(5)Mapper

@Mapper
public interface UserMapper {// 根据用户名,查询用户的详情信息,用于登录功能@Select("select * from user where user_name = #{username}")User selectByName(String username);// 往数据库里插入信息,用于注册功能@Insert("insert into user values (null, #{username}, #{password}, 1000, 0, 0);")void register(User userInfo);// 总比赛场数 + 1    获胜场数 + 1    天梯积分 + 30@Update("update user set total_count = total_count + 1, " +"win_count = win_count + 1, score = score + 30 where user_id = #{userId}")void userWin(int userId);// 总比赛场数 + 1    获胜场数 不变    天梯积分 - 30@Update("update user set total_count = total_count + 1, " +"score = score - 30 where user_id = #{userId}")void userLose(int userId);
}

五、线程安全问题

        针对同一个房间,既有查询,又有修改操作。

        因为该项目是多人联机游戏,所以会有多个玩家针对这一个房间同时进行 查询/修改 数据,所以存在线程安全问题,那么怎么解决呢?——核心操作:加锁

        我们先思考一下,要进行修改的对象是谁?是不是就是这个房间。

        每时每刻都有不同玩家进行游戏,那么也就说明房间是多个的,这些房间每个都是相互独立、互不干扰的。(保证这个房间玩家的操作,不会影响到别的房间里的游戏)

        所以我们针对 房间对象 进行加锁,这样我在进游戏时,拿到锁,执行完一段逻辑后,再释放掉,给对手进行分配,这样也不会影响到别的房间上的玩家。如图:

        这六个玩家访问的是同一个服务器,也就都会执行afterConnectionEstablished方法。

        此处保证玩家1 和 玩家2 互斥、玩家3 和 玩家4互斥、玩家5 和 玩家6互斥就行了(玩家1 和 玩家3之间 不必互斥)。

        经过上述思考,要想达到这种效果,针对 房间对象 加锁 就可以满足。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/55279.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Pandas数据类型

Pandas数据类型 学习目标 知道Pandas中都有哪些数据类型和数据结构&#xff0c;并知道数据类型和数据结构之间的关系知道时间日期类型作为索引的数据集可以基于时间范围来选取子集知道时间差类型索引的数据集可以基于时间差范围来选取子集 1 一般类型 Pandas数据类型Python…

商家营销工具架构升级总结

今年以来&#xff0c;商家营销工具业务需求井喷&#xff0c;需求数量多且耗时都比较长&#xff0c;技术侧面临很大的压力。因此这篇文章主要讨论营销工具前端要如何应对这样大规模的业务需求。 问题拆解 我们核心面对的问题主要如下&#xff1a; 1. 人力有限 我们除了要支撑存量…

redis 5的安装及启动(window)

最近看大模型的时候发现入手redis的同学没有练手的&#xff0c;而且大部分redis的文章要钱才能看&#xff0c;在这里我把路径和环境配置&#xff0c;启动给大家说一下 下载 redis5的获取链接在下面&#xff08;为什么是redis5&#xff0c;因为上个模型用的就是redis5&#xff…

基于单片机的两轮直立平衡车的设计

本设计基于单片机设计的两轮自平衡小车&#xff0c;其中机械部分包括车体、车轮、直流电机、锂电池等部件。控制电路板采用STC12C5A60S2作为主控制器&#xff0c;采用6轴姿态传感器MPU6050测量小车倾角&#xff0c;采用TB6612FNG芯片驱动电机。通过模块化编程完成了平衡车系统软…

【Ansys Fluent】计算数据导入tecplot傅里叶分析

来自&#xff1a;fluent计算数据导入tecplot进行傅里叶分析 首先在fluent计算结果中找到监测点压力曲线变化的输出文件&#xff0c;本例是pr0104.out&#xff0c;将文件后缀改为pr0104.txt&#xff0c;并用文本文档打开&#xff0c;将前几行的标题删除&#xff0c;只保留数据&…

Hive数仓操作(十)

一、Hive 分页查询 在大数据处理中&#xff0c;分页查询是非常常见的需求。Hive 提供了 LIMIT 和 OFFSET 关键字来方便地进行分页操作。本文将详细介绍它们的用法。 1. 基本用法 LIMIT&#xff1a;用于限制查询结果的行数。OFFSET&#xff1a;用于指定从哪一行开始检索。 2…

《动手学深度学习》笔记2.5——神经网络从基础→使用GPU (CUDA-单卡-多卡-张量操作)

目录 0. 前言 原书正文 1. 计算设备 (CPU和GPU) 补充&#xff1a;torch版本cuda报错的解决方案 2. 张量与GPU 3. 存储在GPU上 4. 复制&#xff08;多卡操作&#xff09; 5. 旁注 (CPU和GPU之间挪数据) 6. 神经网络与GPU 小结 0. 前言 课程全部代码&#xff08;pytorc…

ISO 21434车辆网络安全风险评估的全面流程解析

ISO 21434风险评估流程是一个系统性的过程&#xff0c;旨在帮助汽车制造商识别和评估与车辆网络安全相关的潜在风险&#xff0c;并制定相应的风险管理策略。以下是ISO 21434风险评估流程的清晰归纳和分点表示&#xff1a; 一、确定风险评估范围 范围界定&#xff1a;明确需要…

运动耳机哪个牌子的好?5大质量不凡的运动耳机测评力荐!

在快节奏的生活中&#xff0c;无论是晨跑、健身还是户外探险&#xff0c;音乐都成了许多人不可或缺的陪伴。运动耳机&#xff0c;作为一种专为运动场景设计的音频设备&#xff0c;旨在提供高质量音频体验的同时&#xff0c;保证佩戴的舒适度和运动的安全性。 &#xff08;上图为…

Spring之生成Bean

Bean的生命周期&#xff1a;实例化->属性填充->初始化->销毁 核心入口方法&#xff1a;finishBeanFactoryInitialization-->preInstantiateSingletons DefaultListableBeanFactory#preInstantiateSingletons用于实例化非懒加载的bean。 1.preInstantiateSinglet…

【JavaEE】——多线程常用类

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 引入&#xff1a; 一&#xff1a;Callable和FutureTask类 1&#xff1a;对比Runnable 2&#xff1a…

IT新秀系列:Go语言的兴起

Go语言&#xff08;Golang&#xff09;由谷歌于2007年发起&#xff0c;并于2009年正式开源。它的诞生背景可以追溯到互联网技术的高速发展时期。那时&#xff0c;软件开发面临着多核计算、大规模并发处理、部署和维护效率低下等挑战。作为一种新型的编程语言&#xff0c;Go主要…

秒懂Linux之线程

目录 线程概念 线程理解 地址空间&#xff08;页表&#xff0c;内存&#xff0c;虚拟地址&#xff09; 线程的控制 铺垫 线程创建 ​编辑 线程等待 线程异常 线程终止 代码 线程优点 线程缺点 线程特点 线程概念 线程是进程内部的一个执行分支&#xff0c;线程是C…

第 30 章 XML

第 30 章 XML 1.IE 中的 XML 2.DOM2 中的 XML 3.跨浏览器处理 XML 随着互联网的发展&#xff0c;Web 应用程序的丰富&#xff0c;开发人员越来越希望能够使用客户端来操作 XML 技术。而 XML 技术一度成为存储和传输结构化数据的标准。所以&#xff0c;本章就详细探讨一下 Ja…

云服务器部署k8s需要什么配置?

云服务器部署k8s需要什么配置&#xff1f;云服务器部署K8s需要至少2核CPU、4GB内存、50GBSSD存储的主节点用于管理集群&#xff0c;工作节点建议至少2核CPU、2GB内存、20GBSSD。还需安装Docker&#xff0c;选择兼容的Kubernetes版本&#xff0c;配置网络插件&#xff0c;以及确…

客运自助售票系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;乘客管理&#xff0c;司机管理&#xff0c;车票信息管理&#xff0c;订单信息管理&#xff0c;退票信息管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页&#xff0c;车票信息&#…

JSON的C实现(上)

JSON的C实现&#xff08;上&#xff09; JSON的C实现&#xff08;上&#xff09;前言JSON简介JSON的C实现思路小结 JSON的C实现&#xff08;上&#xff09; 前言 JSON是众多项目中较为常见的数据交换格式&#xff0c;为不同项目、系统间的信息交换提供了一个规范化标准。JSON…

SpringBoot3+Vue3开发后台管理系统脚手架

后台管理系统脚手架 介绍 在快速迭代的软件开发世界里&#xff0c;时间就是生产力&#xff0c;效率决定成败。对于构建复杂而庞大的后台系统而言&#xff0c;一个高效、可定制的后台脚手架&#xff08;Backend Scaffold&#xff09;无疑是开发者的得力助手。 脚手架 后台脚…

从0到1深入浅出构建Nest.Js项目

Nest (NestJS) 是一个用于构建高效、可扩展的 Node.js 服务器端应用程序的开发框架。它利用JavaScript 的渐进增强的能力&#xff0c;使用并完全支持 TypeScript &#xff08;仍然允许开发者使用纯 JavaScript 进行开发&#xff09;&#xff0c;并结合了 OOP &#xff08;面向对…

【Redis】知识点整理(源于javaguide)

一、什么是Redis Redis是一种开源的内存数据库&#xff0c;它支持键值存储&#xff0c;常被用作数据缓存、消息代理和队列等。它以高性能和支持多种数据结构而闻名&#xff0c;如字符串、哈希、列表、集合和有序集合。Redis也支持持久化&#xff0c;可以将数据存储在磁盘上&am…