文章目录

  • 网页版在线五子棋
    • 1. 项目介绍
    • 2. 项目演示
    • 3. 前置知识
      • 3.1 WebSocket
      • 3.2 代码示例
        • 3.2.1 服务器代码
        • 3.2.2 客户端代码
    • 4. 需求分析和概要设计
      • 4.1 用户模块
      • 4.2 匹配模块
      • 4.3 对战模块
    • 5. 项目创建
    • 6. 实现用户模块
      • 6.1 编写数据库代码
        • 6.1.1数据库设计
        • 6.1.2 配置MyBatis
        • 6.1.3 创建实体类
        • 6.1.4 创建UserMapper
        • 6.1.5 实现UserMapper.xml
      • 6.2 约定前后端交互
      • 6.3 服务器开发
      • 6.4 客户端开发
        • 6.4.1 登录页面
        • 6.4.2 注册页面
    • 7. 实现匹配模块
      • 7.1 约定前后端交互接口
      • 7.2 客户端开发
        • 7.2.1 实现页面基本属性
        • 7.2.2实现匹配功能
      • 7.3 服务器开发
        • 7.3.1 创建并注册MatchAPI类
        • 7.3.2 实现用户管理类
        • 7.3.3 创建匹配请求/响应对象
        • 7.3.4 处理连接成功
        • 7.3.5 处理开始匹配/取消匹配
        • 7.3.6 实现匹配器
        • 7.3.7 实现房间类
        • 7.3.8 实现房间管理器类
        • 7.3.9 处理连接关闭/异常
    • 8. 实现对战模块
      • 8.1 约定前后端交互
      • 8.2 客户端开发
        • 8.2.1 实现棋盘/棋子绘制
        • 8.2.2 初始化websocket
        • 8.2.3 发送落子请求
        • 8.2.4 处理落子响应
      • 8.3 服务器开发
        • 8.3.1 创建落子请求/响应对象
        • 8.3.2 处理连接成功
        • 8.3.3 实现通知玩家就绪
        • 8.3.4 玩家下线处理
        • 8.3.5 手动注入bean
        • 8.3.6 处理落子请求
        • 8.3.7 实现对弈功能
        • 8.3.8 打印棋盘
        • 8.3.9 判决胜负
        • 8.3.10 处理玩家中途退出
    • 9. 部署到云服务器上
      • 9.1 增添数据库
      • 9.2 微调代码
      • 9.3 打包
      • 9.4 运行
      • 9.5 验证
  • 总结

网页版在线五子棋

1. 项目介绍

实现一个网页版在线对战五子棋

支持以下功能:

  • 用户模块:用户注册、用户登录、用户天梯积分记录、用户比赛场数记录
  • 匹配模块:根据玩家天梯积分进行匹配
  • 对战模块:实现1v1的实时对战功能

核心技术:

  • Spring/SpringBoot/SpringMVC
  • Websocket
  • MyBatis
  • MySQL
  • HTML/CSS/JS/AJAX

2. 项目演示

3. 前置知识

3.1 WebSocket

如果你了解过Http协议,那么应该知道Http协议是无状态、无连接、单向的应用层协议。它采用了请求-响应模式,由客户端发送一个请求,由服务端返回一个响应。它有一个弊端就是服务端无法主动向客户端发起消息。这样就导致客户端想要获取服务端连续的状态变化很困难,大多是web程序将通过频繁的异步JavaScript和XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。

举个例子,我们在餐馆点餐后,有两种选择:
①时不时跑到前台询问老板我的菜做好没,老板说没有,我溜达一圈后又来问菜做好没……循环直到我的菜做好了,我端着菜找个位置坐下用餐
②我直接找个位置坐下,等菜做好后,老板端着菜过来递给我然后用餐
第一种做法(轮询)就是使用客户端(我)一直向服务器(老板)发送请求,检查数据是否发生了变化(菜做好没)。
第二种做法(websocket)就是服务器(老板)直接向客户端(我)发送消息(菜做好了)

为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,通过这个附加头信息完成握手过程.

请求头

返回头

3.2 代码示例

Spring中内置了websocket,我们可以直接使用。

3.2.1 服务器代码

创建TestAPI类:

这个类用来处理websocket请求,并返回响应。

每个方法中都带有一个 session 对象, 这个 session 和 Servlet 的 session 并不相同, 而是 WebSocket 内部搞的另外一组 Session.

通过这个 Session 可以给客户端返回数据, 或者主动断开连接.

@Component
/*** 这是一个测试类* 继承自TextWebSocketHandler的类是一个webSocket消息处理类*/
public class TestAPI extends TextWebSocketHandler {@Override//用户建立连接后触发的方法public void afterConnectionEstablished(WebSocketSession session) throws Exception {System.out.println("连接成功");}@Override//收到文本消息后触发的方法protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {System.out.println("收到消息 : " + message.getPayload());session.sendMessage(new TextMessage("我收到了你的消息" + message.getPayload()));}@Override//触发异常后触发的方法public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {System.out.println("连接异常");}@Override//关闭连接后触发的方法public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {System.out.println("关闭连接");}
}

创建WebSocketConfig类:

@Configuration
@EnableWebSocket//这个注释可以让Spring知道这是一个WebSocket配置类
public class WebSocketConfig implements WebSocketConfigurer {@Autowiredprivate TestAPI testAPI;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {//这个方法可以将一个消息处理器和一个路由关联上,访问这个路由后将使用testAPI的方法进行消息处理registry.addHandler(testAPI,"/test");}
}

3.2.2 客户端代码

创建test.html

<body><input type="text" id = "message"><input type="button" id = "submit" value="提交"><script>/* 创建一个websocket实例 */let url = "ws://127.0.0.1:8080/test"let websocket = new WebSocket(url)/* 给实例挂载一些回调函数 */websocket.onopen = function() {console.log("建立连接");}websocket.onmessage = function(e) {console.log("收到消息" + e.date);}websocket.onerror = function() {console.log("连接异常");}websocket.onclose = function() {console.log("连接关闭");}let input = document.querySelector('#message');let button = document.querySelector('#submit')button.onclick = function() {console.log("发送消息" + input.value);websocket.send(input.value);}</script>
</body>

启动服务器,观察效果:

这样服务器和客户端就实现了交互~

4. 需求分析和概要设计

整个项目分成以下三个模块

  • 用户模块
  • 匹配模块
  • 对战模块

4.1 用户模块

该模块主要用于用户登录、注册、记录一些用户比赛信息。

用MySQL存储数据。

客户端提供登录注册页面。

服务器基于Spring + MyBatis来实现增删查改。

4.2 匹配模块

用户登录成功,进入游戏大厅,大厅里显示玩家的比赛信息。

同时显示一个匹配按钮,当玩家按下开始匹配,将玩家加入匹配队列,同时开始匹配变为匹配中……(点击停止)停止匹配后从队列中将玩家移除。

如果匹配成功,将进入游戏房间。

通过websocket实现通讯“开始匹配”、“停止匹配”、“匹配成功”。

4.3 对战模块

玩家匹配成功,则进入游戏房间界面

每两个玩家在同一个游戏房间

在游戏房间中显示棋盘,玩家点解棋盘实现落子功能

当五子连珠时,显示你赢了/你输了

页面加载时和服务器建立 websocket 连接. 双方通过 websocket 来传输 “准备就绪”, “落子位置”, “胜负” 这样的信息.

  • 准备就绪: 两个玩家均连上游戏房间的 websocket 时, 则认为双方准备就绪.
  • 落子位置: 有一方玩家落子时, 会通过 websocket 给服务器发送落子的用户信息和落子位置, 同时服务器再将这样的信息返回给房间内的双方客户端. 然后客户端根据服务器的响应来绘制棋子位置.
  • 胜负: 服务器判定这一局游戏的胜负关系. 如果某一方玩家落子, 产生了五子连珠, 则判定胜负并返回胜负信息. 或者如果某一方玩家掉线(比如关闭页面), 也会判定对方获胜.

5. 项目创建

使用idea创建一个SpringBoot项目

引入SpringBoot / Spring MVC / MyBatis /lombok依赖

6. 实现用户模块

6.1 编写数据库代码

6.1.1数据库设计

create database if not exists java_gobang;use java_gobang;drop table if exists user;
create table user (userId int primary key auto_increment,username varchar(50) unique ,password varchar(50),score int,   -- 天梯积分totalCount int,  -- 比赛总场数winCount int  -- 获胜场数
);insert into user values(null,'zhangsan','123',1000,0,0);
insert into user values(null,'lisi','123',1000,0,0);
insert into user values(null,'wangwu','123',1000,0,0);

6.1.2 配置MyBatis

编写application.yml

spring:datasource:url: jdbc:mysql://127.0.0.1:3306/java_gobang?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8username: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Drivermybatis:mapper-locations: classpath:mapper/**Mapper.xml

6.1.3 创建实体类

@Data
public class User {private int userId;private String username;private String password;private int score;private int totalCount;private int winCount;
}

6.1.4 创建UserMapper

此类主要提供4个方法:

  • selectByName : 根据用户名查找用户信息,实现登录
  • insert :根据信息新增用户,用于注册
  • userWin :给获胜者修改游戏分数
  • userLose:给失败者修改游戏分数
@Mapper
public interface UserMapper {int insert(User user);User selectByName(String name);void userWin(int userId);void userLose(int userId);
}

6.1.5 实现UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.java_gobang.model.UserMapper"><insert id="insert">insert into user values(null, #{username}, #{password}, 1000, 0, 0);</insert><select id="selectByName" resultType="com.example.java_gobang.model.User">select * from user where username = #{username};</select><update id="userWin">update user set totalCount = totalCount + 1, winCount = winCount + 1, score = score + 30where userId = #{userId}</update><update id="userLose">update user set totalCount = totalCount + 1, score = score - 30where userId = #{userId}</update>
</mapper>

6.2 约定前后端交互

6.3 服务器开发

创建controller.UserController

实现三个方法:

  • login :实现用户登录逻辑
  • register : 实现用户注册逻辑
  • userInfo:实现登录成功后查找用户分数逻辑
@RestController
//这个类用来实现三个方法
//①注册  ②登录   ③获取用户信息
public class UserController {@Autowiredprivate UserMapper userMapper;@PostMapping("/login")public Object login(String username, String password, HttpServletRequest req){User user = userMapper.selectByName(username);if(user == null || !user.getPassword().equals(password)){return new User();}System.out.println("登录" + username);HttpSession session = req.getSession(true);session.setAttribute("user",user);return user;}@PostMapping("/register")public Object register(String username,String password){User user = null;try {user = new User();user.setUsername(username);user.setPassword(password);System.out.println("register" + username);int ret = userMapper.insert(user);System.out.println("受影响的行数" + ret);//可能会触发一个主键重复的异常}catch (org.springframework.dao.DuplicateKeyException e){user = new User();//System.out.println("用户名重复");}return user;}@GetMapping("/userInfo")public Object getUserInfo(HttpServletRequest req){try{HttpSession session = req.getSession(false);User user = (User) session.getAttribute("user");//保证用户的分数是数据库中最新的数据User newUser = userMapper.selectByName(user.getUsername());return newUser;}catch (NullPointerException e){return new User();}}
}

6.4 客户端开发

6.4.1 登录页面

创建login.html

<!-- 导航栏 --><div class="nav"><span>五子棋对战</span></div><div class="login-container"><div class="login-dialog"><!-- 标题 --><h2>登录</h2><div class="row"><span>用户名</span><input type="text" id = "username"></div><div class="row"><span>密码</span><input type="password" id="password"></div><div class="row-button"><button id="submit">提交</button></div><div class="register"><a href="register.html">注册</a></div></div></div>

创建css.common.css

html,body {height: 100%;background-image: url(../img/背景.jpg);background-size: cover;background-repeat: no-repeat;background-position: center;
}.nav{width: 100%;height: 50px;display: flex;background-color: rgba(51, 51, 51,0.4);color: white;padding-left: 20px;align-items: center;
}.container{height: calc(100% - 50px);display: flex;align-items: center;justify-content: center;width: 100%;
}

创建css.login.css

.login-container{height: calc(100% - 50px);display: flex;align-items: center;justify-content: center;width: 100%;
}.login-dialog{width: 400px;height: 320px;background-color: rgba(255,255,255,0.8);border-radius: 10px;
}.login-dialog h2{text-align: center;padding: 20px 0;
}.login-dialog .row{width: 100%;height: 50px;align-items: center;justify-content: center;display: flex;
}.login-dialog span{width: 100px;display: block;/* 字体加粗 */font-weight: 700;
}.row #username,#password{outline: none;border: none;width: 200px;height: 40px;font-size: 20px;text-indent: 10px;border-radius: 10px;
}.login-dialog .row-button{margin-top: 10px;
}.row-button #submit{width: 300px;border: none;height: 50px;color: white;background-color: rgb(0, 128, 0);font-size: 20px;border-radius: 10px;margin-left: 50px;
}.register a{align-items: center;margin-left: 50px;text-decoration: none;
}#submit:active{background-color: #666;
}

login.html中编写js代码,实现交互

<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script><script>let submit = document.querySelector('#submit');submit.onclick = function(){let username = document.querySelector('#username').value;let password = document.querySelector('#password').value;$.ajax({method:"post",url:"/login",data:{username : username,password : password},success: function(data){console.log(JSON.stringify(data));if(data && data.userId > 0){alert("登录成功");location.assign('game_hall.html');}else{alert("登录失败! 用户名或密码错误!")}}})}</script>

6.4.2 注册页面

创建register.html

<!-- 导航栏 --><div class="nav"><span>五子棋对战</span></div><div class="register-container"><div class="register-dialog"><!-- 标题 --><h2>注册</h2><div class="row"><span>用户名</span><input type="text" id = "username"></div><div class="row"><span>密码</span><input type="password" id="password"></div><div class="row-button"><button id="submit">提交</button></div></div></div>

css部分可以使用css.common.css部分

register.html中编写js代码实现交互

<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script><script>let submit = document.querySelector('#submit');submit.onclick = function(){let username = document.querySelector('#username').value;let password = document.querySelector('#password').value;$.ajax({method:"post",url:"/register",data:{username: username,password: password},success: function(data){console.log(JSON.stringify(data));if(data && data.username){alert("注册成功");location.assign('login.html');}else{alert("注册失败")}}})}</script>

7. 实现匹配模块

7.1 约定前后端交互接口

7.2 客户端开发

7.2.1 实现页面基本属性

创建 game_hall.html

screen用于显示玩家分数

button作为匹配按钮

<div class="nav">五子棋对战</div><div class="container"><!-- 这个用来存放用户的比赛信息 --><div><div id="screen"></div><button id="match-button">开始匹配</button></div></div>

创建game_hall.css

#screen {width: 400px;height: 200px;font-size: 20px;background-color: gray;color: white;border-radius: 10px;text-align: center;line-height: 100px;
}#match-button {width: 400px;height: 50px;font-size: 20px;color: white;background-color: orange;border: none;outline: none;border-radius: 10px;text-align: center;line-height: 50px;margin-top: 20px;
}#match-button:active {background-color: gray;
}

编写js代码获取用户信息

<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script><script>/* 获取用户信息 */$.ajax({method: 'get',url: '/userInfo',success: function(data) {let screen = document.querySelector('#screen');if(data.username == null){alert("当前尚未登录,请先登录!");location.replace("/login.html");}screen.innerHTML = '玩家: ' + data.username + ', 分数: ' + data.score + "<br> 比赛场次: " + data.totalCount + ", 获胜场次: " + data.winCount;}});

7.2.2实现匹配功能

编辑 game_hall.html 的 js 部分代码.

  • 点击匹配按钮, 就会进入匹配逻辑. 同时按钮上提示 “匹配中……(点击停止)” 字样.
  • 再次点击匹配按钮, 则会取消匹配.
  • 当匹配成功后, 服务器会返回匹配成功响应, 页面跳转到 game_room.html
/* 处理匹配功能 */let url = 'ws://' + location.host + '/findMatch';let websocket = new WebSocket(url);let button = document.querySelector('#match-button');/* 点击开始匹配 */button.onclick = function(){/* 这个可以判断websocket是否处于连接状态OPEN是一个常数1 ,readstate=1代表连接状态 */if(websocket.readyState == websocket.OPEN){if(button.innerHTML == '开始匹配'){console.log("开始匹配");/* JSON对象转为字符串 */websocket.send(JSON.stringify({message:'startMatch',}));}else if(button.innerHTML == '匹配中……(点击停止)'){console.log("停止匹配");websocket.send(JSON.stringify({message:'stopMatch',}));}}else{console.log("当前你的连接已经断开,请重新连接");location.replace('/login.html');}}/* 处理服务器的响应 *//* 这个函数是当收到来自服务器的消息时调用的 */websocket.onmessage = function(e){/* 字符串转为JSON对象 */let resp = JSON.parse(e.data);if(!resp.ok){console.log("游戏大厅发生错误" + resp.reason);location.replace('/login.html');return;}if(resp.message == 'startMatch'){console.log("进入匹配队列成功");button.innerHTML = '匹配中……(点击停止)'}else if(resp.message == 'stopMatch'){console.log("移除匹配队列成功");button.innerHTML = '开始匹配';}else if(resp.message == 'MatchSuccess'){console.log("匹配成功,进入游戏界面");location.replace('/game_room.html');}else if(resp.message == 'repeatConnection'){alert("检测到当前为多开,请使用其他账号登录");location.replace("/login.html");}else{console.log("非法的message" + resp.message);}}/* 监听窗口关闭事件,当窗口关闭时,主动断开websocket链接,防止还没断开链接就关闭窗口server报错 */window.onbeforeunload = function () {websocket.close();}

7.3 服务器开发

7.3.1 创建并注册MatchAPI类

创建 api.MatchAPI, 继承自 TextWebSocketHandler 作为处理 websocket 请求的入口类.

@Component
public class MatchAPI extends TextWebSocketHandler {private ObjectMapper objectMapper = new ObjectMapper();@Componentpublic class MatchAPI extends TextWebSocketHandler {}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {}
}

修改 config.WebSocketConfig, 把 MatchAPI 注册进去.

@Configuration
@EnableWebSocket//这个注释可以让Spring知道这是一个WebSocket配置类
public class WebSocketConfig implements WebSocketConfigurer {@Autowiredprivate TestAPI testAPI;@Autowiredprivate MatchAPI matchAPI;@Autowiredprivate GameAPI gameAPI;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {//这个方法可以将一个消息处理器和一个路由关联上,访问这个路由后将使用testAPI的方法进行消息处理registry.addHandler(testAPI,"/test");//拦截器,可以获取到HttpSession中的session供webSocket中的session使用registry.addHandler(matchAPI,"/findMatch").addInterceptors(new HttpSessionHandshakeInterceptor());}
}

7.3.2 实现用户管理类

创建 game.OnlineUserManager 类, 用于管理当前用户的在线状态. 本质上是 哈希表 的结构. key 为用户 id, value 为用户的 WebSocketSession.

  • 当玩家建立好 websocket 连接, 则将键值对加入 OnlineUserManager 中.
  • 当玩家断开 websocket 连接, 则将键值对从 OnlineUserManager 中删除.
  • 在玩家连接好的过程中, 随时可以通过 userId 来查询到对应的会话, 以便向客户端返回数据.

由于存在两个页面, 游戏大厅和游戏房间, 使用两个 哈希表 来分别存储两部分的会话.

涉及线程安全使用ConcurrentHashMap哈希表

@Component
//这个类用来管理用户的在线状态
public class OnlineUserManager {private ConcurrentHashMap<Integer, WebSocketSession> game_hall = new ConcurrentHashMap<>();private ConcurrentHashMap<Integer, WebSocketSession> game_room = new ConcurrentHashMap<>();//用户进入游戏大厅public void enterGameHall(int userId,WebSocketSession session){game_hall.put(userId,session);}//用户离开游戏大厅public void exitGameHall(int userId){game_hall.remove(userId);}//获取用户信息public WebSocketSession getGameHallSession(int userId){return game_hall.get(userId);}//用户进入游戏房间public void enterGameRoom(int userId,WebSocketSession session){game_room.put(userId,session);}//用户离开游戏房间public void exitGameRoom(int userId){game_room.remove(userId);}//获取用户信息public WebSocketSession getGameRoomSession(int userId){return game_room.get(userId);}}

7.3.3 创建匹配请求/响应对象

创建 game.MatchRequest

@Data
public class MatchRequest {private String message = "";
}

创建 game.MatchResponse

@Data
public class MatchResponse {private boolean ok = true;private String reason = "";private String message = "";
}

7.3.4 处理连接成功

实现MatchAPI中的afterConnectionEstablished方法

@Override//处理用户连接public void afterConnectionEstablished(WebSocketSession session) throws Exception {//session.getAttributes()获取到的是一个map,里面存放了了HttpSession中的getAttribute里的所有对象User user = (User) session.getAttributes().get("user");if(user == null){//玩家还未登陆就进入游戏大厅了MatchResponse response = new MatchResponse();response.setOk(false);response.setReason("[afterConnectionEstablished]玩家尚未登录!");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));return;}//检查玩家的上线状态(是否多开)//在给玩家设置上线状态时,需要先判断之前玩家是否已经登录过了if (onlineUserManager.getGameHallSession(user.getUserId()) != null|| onlineUserManager.getGameRoomSession(user.getUserId()) != null){MatchResponse response = new MatchResponse();response.setOk(true);response.setReason("当前游戏禁止多开");response.setMessage("repeatConnection");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));return;}//当玩家获取到身份信息后,就可以给玩家设置上线状态了onlineUserManager.enterGameHall(user.getUserId(),session);System.out.println("当前玩家" + user.getUsername() + "进入游戏大厅");}

7.3.5 处理开始匹配/取消匹配

实现MatchAPI中的 handleTextMessage

@Override//处理开始/取消匹配protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {User user = (User) session.getAttributes().get("user");if(user == null){System.out.println("[handleTextMessage]玩家尚未登录");return;}System.out.println("开始匹配" + user.getUserId() + "message" + message.toString());//将解析得到的JSON请求数据转换为一个MatchRequest对象MatchRequest request = objectMapper.readValue(message.getPayload(),MatchRequest.class);MatchResponse response = new MatchResponse();if(request.getMessage().equals("startMatch")){//加入匹配器中//TODOmatch.add(user);response.setMessage("startMatch");}else if(request.getMessage().equals("stopMatch")){//从匹配器中移除//TODOmatch.remove(user);response.setMessage("stopMatch");}else{response.setOk(false);response.setReason("非法的匹配请求");}session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));}

7.3.6 实现匹配器

创建 game.Matcher 类.

涉及线程安全需处理

@Component
//匹配器
public class Match {@Autowiredprivate OnlineUserManager onlineUserManager;@Autowiredprivate RoomManager roomManager;private ObjectMapper objectMapper = new ObjectMapper();//游戏玩家分为三档//第一档://2000以下(不含2000)//第二档://2000-3000(不含3000)//第三档://3000以上private Queue<User> normalQueue = new LinkedList<>();private Queue<User> highQueue = new LinkedList<>();private Queue<User> veryHighQueue = new LinkedList<>();public void add(User user){if(user.getScore() < 2000){synchronized (normalQueue){normalQueue.offer(user);normalQueue.notify();}System.out.println("玩家" + user.getUsername() + "进入normalQueue");}else if(user.getScore() >= 2000 && user.getScore() < 3000){synchronized (highQueue){highQueue.offer(user);highQueue.notify();}System.out.println("玩家" + user.getUsername() + "进入highQueue");}else{synchronized (veryHighQueue){veryHighQueue.offer(user);veryHighQueue.notify();}System.out.println("玩家" + user.getUsername() + "进入veryHighQueue");}}public void remove(User user){if(user.getScore() < 2000){synchronized (normalQueue){normalQueue.remove(user);}System.out.println("玩家" + user.getUsername() + "退出normalQueue");}else if(user.getScore() >= 2000 && user.getScore() < 3000){synchronized (highQueue){highQueue.remove(user);}System.out.println("玩家" + user.getUsername() + "退出highQueue");}else{synchronized (veryHighQueue){veryHighQueue.remove(user);}System.out.println("玩家" + user.getUsername() + "退出veryHighQueue");}}//启动三个线程循环调用各自的队列private Match(){new Thread(){@Overridepublic void run() {while(true){handlerMatch(normalQueue);}}}.start();new Thread(){@Overridepublic void run() {while(true){handlerMatch(highQueue);}}}.start();new Thread(){@Overridepublic void run() {while(true){handlerMatch(veryHighQueue);}}}.start();}private void handlerMatch(Queue<User> matchQueue){synchronized (matchQueue){try{//五子棋需要两个人,当队列中人数少于2时等待while(matchQueue.size() < 2){matchQueue.wait();}User user1 = matchQueue.poll();User user2 = matchQueue.poll();System.out.println("匹配出两个玩家" + user1.getUsername() +" " + user2.getUsername());WebSocketSession session1 = onlineUserManager.getGameHallSession(user1.getUserId());WebSocketSession session2 = onlineUserManager.getGameHallSession(user2.getUserId());if(session1 == null){matchQueue.offer(user2);return;}if(session2 == null){matchQueue.offer(user1);return;}//防止多开if (session1 == session2){matchQueue.add(user1);}// 将两个玩家加入对战房间Room room = new Room();roomManager.add(user1.getUserId(),user2.getUserId(),room);//给玩家1发送匹配成功的信息MatchResponse response1 = new MatchResponse();response1.setOk(true);response1.setMessage("MatchSuccess");session1.sendMessage(new TextMessage(objectMapper.writeValueAsString(response1)));//给玩家2发送匹配成功的信息MatchResponse response2 = new MatchResponse();response2.setOk(true);response2.setMessage("MatchSuccess");session2.sendMessage(new TextMessage(objectMapper.writeValueAsString(response2)));}catch (IOException | InterruptedException e){e.printStackTrace();}}}
}

7.3.7 实现房间类

匹配成功之后, 需要把对战的两个玩家放到同一个房间对象中.

创建 game.Room

  • 一个房间要包含一个房间 ID, 使用 UUID 作为房间的唯一身份标识.
  • 房间内要记录对弈的玩家双方信息.
  • 记录先手方的 ID
  • 记录一个 二维数组 , 作为对弈的棋盘.
  • 记录一个 OnlineUserManager, 以备后面和客户端进行交互.
@Data
public class Room {//由于Room不能是唯一的,所以不能注入到Spring中,从而也不可以用 Autowired注入这三个bean//因此我们需要手动注入这三个bean后续会说怎么处理private OnlineUserManager onlineUserManager;private RoomManager roomManager;private UserMapper userMapper;private ObjectMapper objectMapper = new ObjectMapper();private String roomId;private User user1;private User user2;// 先手方的用户 idprivate int whiteUserId = 0;// 棋盘, 数字 0 表示未落子位置. 数字 1 表示玩家 1 的落子. 数字 2 表示玩家 2 的落子private static final int MAX_ROW = 15;private static final int MAX_COL = 15;private int[][] chessBoard = new int[MAX_ROW][MAX_COL];public Room() {// 使用 uuid 作为唯一身份标识roomId = UUID.randomUUID().toString();}

7.3.8 实现房间管理器类

Room 对象会存在很多. 每两个对弈的玩家, 都对应一个 Room 对象.需要一个管理器对象来管理所有的 Room.

创建 game.RoomManager

  • 使用一个 Hash 表, 保存所有的房间对象, key 为 roomId, value 为 Room 对象
  • 再使用一个 Hash 表, 保存 userId -> roomId 的映射, 方便根据玩家来查找所在的房间.
  • 提供增, 删, 查的 API. (查包含两个版本, 基于房间 ID 的查询和基于用户 ID 的查询).
@Component
public class RoomManager {//存储所有的Room房间ConcurrentHashMap<String,Room> rooms = new ConcurrentHashMap<>();//存储用户和房间的关联关系ConcurrentHashMap<Integer ,String> userIdToRoomId = new ConcurrentHashMap<>();public void add(int user1Id,int user2Id,Room room){rooms.put(room.getRoomId(),room);userIdToRoomId.put(user1Id,room.getRoomId());userIdToRoomId.put(user2Id,room.getRoomId());}public void remove(int user1Id,int userId2,String roomId){rooms.remove(roomId);userIdToRoomId.remove(user1Id);userIdToRoomId.remove(userId2);}public Room getRoomByRoomId(String roomID){return rooms.get(roomID);}public Room getRoomByUserId(int userId){String roomId = userIdToRoomId.get(userId);if(roomId == null){return null;}return getRoomByRoomId(roomId);}
}

7.3.9 处理连接关闭/异常

实现MatchAPI中的afterConnectionClosed

    @Override//异常连接处理public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {User user = (User) session.getAttributes().get("user");try{WebSocketSession tmpSession = onlineUserManager.getGameHallSession(user.getUserId());if(tmpSession == session){onlineUserManager.exitGameHall(user.getUserId());}//TODO 从匹配器中移除match.remove(user);System.out.println("玩家"+ user.getUsername() +"离开游戏大厅");}catch (NullPointerException e){System.out.println("[handleTransportError]当前用户尚未登录");}}@Override//处理玩家断开连接public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {User user = (User) session.getAttributes().get("user");try{WebSocketSession tmpSession = onlineUserManager.getGameHallSession(user.getUserId());if(tmpSession == session){onlineUserManager.exitGameHall(user.getUserId());}//TODO 从匹配器中移除match.remove(user);System.out.println("玩家"+ user.getUsername() +"离开游戏大厅");}catch (NullPointerException e){System.out.println("[afterConnectionClosed]当前用户尚未登录");}}

8. 实现对战模块

8.1 约定前后端交互

8.2 客户端开发

创建 game_room.html, 表示对战页面.

<div class="nav">联机五子棋</div><div class="container"><div><canvas id="chess" width="450px" height="450px"></canvas><div id="screen">等待玩家连接中...</div></div></div><script src="js/script.js"></script>

创建 css/game_room.css

#screen {font-size: 22px;text-align: center;background-color: rgba(255,255,255,0,7);color: yellow;margin-bottom: 20px;
}

8.2.1 实现棋盘/棋子绘制

创建 js/script

这段代码可以直接复制粘贴,不需要深究其中含义

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
//
// TODO//
// 初始化一局游戏
//
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 = "#BFBFBF";// 背景图片let logo = new Image();logo.src = "image/sky.jpeg";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();}}// 绘制一个棋子, 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) {// TODO 发送坐标给服务器, 服务器要返回结果oneStep(col, row, gameInfo.isWhite);chessBoard[row][col] = 1;}}// TODO 实现发送落子请求逻辑, 和处理落子响应逻辑.
}initGame();

8.2.2 初始化websocket

在刚才代码中加入websocket

//使用location.host 是为了后续部署到云服务器上做准备的
//也可写作127.0.0.1:8080
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("和服务器的连接出现异常!");
}window.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.areplacessign("/game_hall.html");return;}if (resp.message == 'readyGame') {gameInfo.roomId = resp.roomId;gameInfo.thisUserId = resp.thisUserId;gameInfo.thatUserId = resp.thatUserId;gameInfo.isWhite = (resp.whiteUserId == resp.thisUserId);// 初始化棋盘initGame();// 设置显示区域的内容setScreenText(gameInfo.isWhite);} else if (resp.message == 'repeatConnection') {alert("检测到游戏多开! 请使用其他账号登录!");location.replace("/login.html");}
}

8.2.3 发送落子请求

修改刚刚的onclick方法

注释掉原有的 onStep 和 修改 chessBoard 的操作, 放到接收落子响应时处理.

实现 send , 通过 websocket 发送落子请求.

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);// 留到浏览器收到落子响应的时候再处理(收到响应再来画棋子)// oneStep(col, row, gameInfo.isWhite);// chessBoard[row][col] = 1;}}function send(row, col) {let req = {message: 'putChess',userId: gameInfo.thisUserId,row: row,col: col};websocket.send(JSON.stringify(req));}

8.2.4 处理落子响应

在 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 backBtn = document.createElement('button');backBtn.innerHTML = '回到大厅';backBtn.style.backgroundColor = "green";backBtn.style.width = "450px";backBtn.style.height = "50px";backBtn.style.border = "none";backBtn.style.borderRadius = "10px";backBtn.onclick = function() {location.replace('/game_hall.html');}let fatherDiv = document.querySelector('.container>div');fatherDiv.appendChild(backBtn);}}

8.3 服务器开发

创建 api.GameAPI , 处理 websocket 请求.

@Component
public class GameAPI extends TextWebSocketHandler {private ObjectMapper objectMapper = new ObjectMapper();@Autowiredprivate RoomManager roomManager;// 这个是管理 game 页面的会话@Autowiredprivate OnlineUserManager onlineUserManager;@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {}@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {}
}

修改 WebSocketConfig, 将 GameAPI 进行注册.

@Configuration
@EnableWebSocket//这个注释可以让Spring知道这是一个WebSocket配置类
public class WebSocketConfig implements WebSocketConfigurer {@Autowiredprivate TestAPI testAPI;@Autowiredprivate MatchAPI matchAPI;@Autowiredprivate GameAPI gameAPI;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {//这个方法可以将一个消息处理器和一个路由关联上,访问这个路由后将使用testAPI的方法进行消息处理registry.addHandler(testAPI,"/test");//拦截器,可以获取到HttpSession中的session供webSocket中的session使用registry.addHandler(matchAPI,"/findMatch").addInterceptors(new HttpSessionHandshakeInterceptor());registry.addHandler(gameAPI,"/game").addInterceptors(new HttpSessionHandshakeInterceptor());}
}

8.3.1 创建落子请求/响应对象

创建game.GameRequest

@Data
public class GameRequest {private String message = "pusChess";private int userId;private int row;private int col;
}

创建game.GameResponse

@Data
public class GameResponse {private String message = "putChess";private int userId;private int row;private int col;private int winner;//获胜者id
}

创建 game.GameReadyResponse

@Data
public class GameReadyResponse {private String message = "readyGame";private boolean ok = true;private String reason;private String roomId;private int thisUserId = 0;private int thatUserId = 0;private int whiteUserId = 0;
}

8.3.2 处理连接成功

实现 GameAPI 的 afterConnectionEstablished 方法.

@Override//处理用户连接房间成功public void afterConnectionEstablished(WebSocketSession session) throws Exception {GameReadyResponse resp = new GameReadyResponse();User user = (User) session.getAttributes().get("user");if(user == null){resp.setOk(false);resp.setReason("[afterConnectionEstablished]当前用户尚未登录");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}Room room = roomManager.getRoomByUserId(user.getUserId());if(room == null){resp.setOk(false);resp.setReason("用户匹配尚未成功,不能开始游戏");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}System.out.println("游戏连接 roomId = " + room.getRoomId() + " userID = " + user.getUserId());//判断游戏是否多开if(onlineUserManager.getGameHallSession(user.getUserId()) != null ||onlineUserManager.getGameRoomSession(user.getUserId()) != null){resp.setOk(false);resp.setReason("当前游戏禁止多开");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));return;}//更新用户会话//游戏大厅和游戏房间的会话是不一样的onlineUserManager.enterGameRoom(user.getUserId(),session);//一个房间有两个玩家,因此使用时需要考虑到线程安全synchronized (room){//设置use1为先手if(room.getUser1() == null){room.setUser1(user);room.setWhiteUserId(user.getUserId());System.out.println("玩家1" + user.getUsername() + "准备就绪");return;}if(room.getUser2() == null){room.setUser2(user);System.out.println("玩家2" + user.getUsername() + "准备就绪");//通知玩家1\2\游戏就绪了notifyGameReady(room,room.getUser1().getUserId(),room.getUser2().getUserId());notifyGameReady(room,room.getUser2().getUserId(),room.getUser1().getUserId());return;}resp.setOk(true);resp.setReason("房间已经满了");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));}}

8.3.3 实现通知玩家就绪

private void notifyGameReady(Room room,int thisUserId,int thatUserId) throws IOException {GameReadyResponse response = new GameReadyResponse();response.setOk(true);response.setThisUserId(thisUserId);response.setThatUserId(thatUserId);response.setWhiteUserId(room.getWhiteUserId());WebSocketSession session = onlineUserManager.getGameRoomSession(thisUserId);session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));}

8.3.4 玩家下线处理

也要注意多开

@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {User user = (User) session.getAttributes().get("user");if(user == null){return;}WebSocketSession session1 = onlineUserManager.getGameRoomSession(user.getUserId());if(session1 != session){System.out.println("当前会话不是游戏中玩家的会话");return;}System.out.println("连接出错 userId = " + user.getUserId());onlineUserManager.exitGameRoom(user.getUserId());noticeThatUserWin(user);}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {User user = (User) session.getAttributes().get("user");if(user == null){return;}WebSocketSession session1 = onlineUserManager.getGameRoomSession(user.getUserId());if(session1 != session){System.out.println("当前会话不是游戏中玩家的会话");return;}System.out.println("用户退出 userId = " + user.getUserId());onlineUserManager.exitGameRoom(user.getUserId());noticeThatUserWin(user);}

8.3.5 手动注入bean

在启动类中加入这个
修改room

8.3.6 处理落子请求

@Override//落子请求protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {User user = (User) session.getAttributes().get("user");if(user == null){return;}Room room = roomManager.getRoomByUserId(user.getUserId());room.putChess(message.getPayload());}

8.3.7 实现对弈功能

实现 room 中的 putChess 方法.

//用这个方法实现落子响应public void putChess(String message) throws IOException {GameRequest request = new GameRequest();GameResponse response = new GameResponse();request = objectMapper.readValue(message,GameRequest.class);int row = request.getRow();int col = request.getCol();//判断是谁下的字//做出约定://①如果是玩家一,则下的子为1,//②是玩家而,则下的子是2int chess = request.getUserId() == user1.getUserId() ? 1 : 2;if(chessBoard[row][col] != 0){System.out.println("下的子有误" + request);return;}//1.进行落子chessBoard[row][col] = chess;printBoard();//2.检查游戏是否结束int winner = checkWinner(chess,row,col);System.out.println(winner);//3.把响应写回给玩家response.setUserId(request.getUserId());response.setRow(row);response.setCol(col);response.setWinner(winner);//4.检查玩家的在线状态WebSocketSession session1 = onlineUserManager.getGameRoomSession(user1.getUserId());WebSocketSession session2 = onlineUserManager.getGameRoomSession(user2.getUserId());if(session1 == null){//玩家1掉线,玩家2自动获胜response.setWinner(user2.getUserId());System.out.println("玩家1掉线");}if(session2 == null){//玩家2掉线,玩家1自动获胜response.setWinner(user1.getUserId());System.out.println("玩家2掉线");}//传回响应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){//更新数据userMapper.userWin(response.getWinner() == user1.getUserId() ? user1.getUserId() : user2.getUserId());userMapper.userLose(response.getWinner() == user1.getUserId() ? user2.getUserId() : user1.getUserId());//销毁房间roomManager.remove(user1.getUserId(),user2.getUserId(),roomId);System.out.println("游戏结束,房间已销毁 roomId" + roomId + "获胜方" + response.getWinner());}}

8.3.8 打印棋盘

实现room中的PrintBoard

private void printBoard() {System.out.println("打印棋盘信息" + roomId);System.out.println("------------------------");for(int r = 0 ; r < MAX_ROW ; r++){for (int c = 0; c < MAX_COL; c++) {System.out.print(chessBoard[r][c] + " ");}System.out.println();}System.out.println("------------------------");}

8.3.9 判决胜负

实现room中的checkWinner

这个方法其实很简单

(假设为行,其余三种也是一样)当出现五子连珠时,这最后一步肯定在这个五个子中

的一个,那么我们只需判断每次落子后左边4个和右边4个是否和自己颜色一样即可。

private int checkWinner(int chess, int row, int col) {// 以 row, col 为中心for (int c = col - 4; c <= col; c++) {// 针对其中的一种情况, 来判定这五个子是不是连在一起了~// 不光是这五个子得连着, 而且还得和玩家落的子是一样~~ (才算是获胜)try {if (chessBoard[row][c] == chess&& chessBoard[row][c + 1] == chess&& chessBoard[row][c + 2] == chess&& chessBoard[row][c + 3] == chess&& chessBoard[row][c + 4] == chess) {// 构成了五子连珠! 胜负已分!return chess == 1 ? user1.getUserId() : user2.getUserId();}} catch (ArrayIndexOutOfBoundsException e) {// 如果出现数组下标越界的情况, 就在这里直接忽略这个异,继续循环下一组数据continue;}}// 2. 检查所有列for (int r = row - 4; r <= row; r++) {try {if (chessBoard[r][col] == chess&& chessBoard[r + 1][col] == chess&& chessBoard[r + 2][col] == chess&& chessBoard[r + 3][col] == chess&& chessBoard[r + 4][col] == chess) {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 (chessBoard[r][c] == chess&& chessBoard[r + 1][c + 1] == chess&& chessBoard[r + 2][c + 2] == chess&& chessBoard[r + 3][c + 3] == chess&& chessBoard[r + 4][c + 4] == chess) {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 (chessBoard[r][c] == chess&& chessBoard[r + 1][c - 1] == chess&& chessBoard[r + 2][c - 2] == chess&& chessBoard[r + 3][c - 3] == chess&& chessBoard[r + 4][c - 4] == chess) {return chess == 1 ? user1.getUserId() : user2.getUserId();}} catch (ArrayIndexOutOfBoundsException e) {continue;}}// 胜负未分, 就直接返回 0 了.return 0;

8.3.10 处理玩家中途退出

在 GameAPI 中

//如果玩家掉线通知对手获胜private void noticeThatUserWin(User user) throws IOException {Room room = roomManager.getRoomByUserId(user.getUserId());if(room == null){System.out.println("房间已经释放,无需通知");return;}User thatUser = room.getUser1() == user ? room.getUser2() : room.getUser1();WebSocketSession session = onlineUserManager.getGameRoomSession(thatUser.getUserId());if(session == null){//这情况意味着对手也掉线了System.out.println("该玩家已掉线,无需通知");return;}//发送响应通知对手GameResponse response = new GameResponse();response.setUserId(thatUser.getUserId());response.setWinner(thatUser.getUserId());session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));//更新玩家分数userMapper.userWin(thatUser.getUserId());userMapper.userLose(user.getUserId());//销毁房间roomManager.remove(user.getUserId(),thatUser.getUserId(),room.getRoomId());System.out.println("游戏结束,房间已销毁 roomId" + room.getRoomId() + "获胜方" + user.getUserId());}

9. 部署到云服务器上

9.1 增添数据库

将我们写的db.sql直接复制到云服务器上。

9.2 微调代码

9.3 打包

通过maven打包


9.4 运行

使用java -jar + 包名即可

9.5 验证

总结

此项目中包含了许多问题,如多开账号的处理、玩家突然掉线的处理、玩家按了回退之后的处理、多线程下线程安全的问题……但是作为一个项目来说,功能还是不太全面,后续预计将进行改善增添功能如:玩家观战、生成对局回放、生成AI对手等等,现在时间较紧迫,只能先做出这几个功能。

Spring项目-在线五子棋相关推荐

  1. SSM项目 —— 在线五子棋

    前言: 此项目为 ssm 项目.基于 springBoot.SpringMVC.MyBatis.websocket.html.css.js.ajax--等技术实现,如果大家有好的项目改进方法 or 附 ...

  2. java项目-第61期基于ssm项目在线心理测评系统

    java项目-第61期基于ssm项目在线心理测评系统 1.项目简述 该项目是基于一款心理测评系统,主要是测试会员的心理情况是否正常,仅仅做参考. 会员只需要进行选择答题,系统会根据答题结果 进行评分, ...

  3. php在线对弈,【图片】手把手开始制作HTML5在线五子棋对弈游戏【编程吧】_百度贴吧...

    该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 本次课题:制作HTML5在线五子棋对弈游戏. 预计开发周期:还没想好看心情,先预计7天完成. 备注:最近做学校课题有关数据挖掘的,有时候搞得没得头绪,做个 ...

  4. 基于spring的在线家教管理系统

    1.项目介绍 基于spring的在线家教管理系统2拥有三种角色 管理员:会员管理.教师管理.家教列表.发布家教需求.教师接单列表.辅导机构列表.试题列表等 教师:登录注册.个人信息修改.查看预约记录 ...

  5. 在Eclipse中使用Maven构建Spring项目

    最新版的Spring需要使用Maven构建,本文讲述怎么在Eclipse构建Maven项目,以配置Spring项目为例. maven简单介绍 maven是构建工具,也是构建管理工具.ant只是构建工具 ...

  6. spring 项目中集成 Protocol Buffers 示例

    http://blog.csdn.net/fangzhangsc2006/article/details/8687388 本文适用于了解spring框架,同时想在spring项目中使用Protocol ...

  7. Guava Cache探索及spring项目整合GuavaCache实例

    背景 对于高频访问但是低频更新的数据我们一般会做缓存,尤其是在并发量比较高的业务里,原始的手段我们可以使用HashMap或者ConcurrentHashMap来存储. 这样没什么毛病,但是会面临一个问 ...

  8. 【报错笔记】在eclipse中做Spring项目时,创建Spring容器时老是出错

    在eclipse中做Spring项目时,创建Spring容器时老是出错 写完这句代码无法导包,最后发现包导错了,我原来导的4.3.9的包,而且是后缀为其他的包,而且对JDK也有要求,我又下载了5.0. ...

  9. 【SpringBoot】Spring项目中value注解,@Value不能够读取到配置文件的值,无法成功注入值的问题汇总及解决

    Spring项目中value注解,@Value不能够读取到配置文件的值,无法成功注入值的问题汇总及解决 @Value注解 常规用法示例 我们都知道通过@Value()注解可以取到我们配置文件的内容,之 ...

最新文章

  1. VIL-100: 一个新的车道线检测数据集和基线模型(ICCV2021)
  2. shiro教程:session管理
  3. vector利用swap()函数进行内存的释放
  4. 【youcans 的 OpenCV 例程 200 篇】103. 陷波带阻滤波器消除周期噪声干扰
  5. C#中判断字符串相等的方法
  6. 折半查找(非递归与递归实现)
  7. Exchaneg 2013 集成OWAS
  8. OLAP -- ODS 项目总结 -- BI 中的关键
  9. AVRNET 学习笔记UDP部分
  10. win10打开蓝牙_WIN10蓝牙不能使用,开启蓝牙后不能识别到其它设备怎么办?
  11. Flash动画制作实例教程
  12. 《Spring》AOP实现原理
  13. 国产手机的18年历史
  14. 电商经验!补单防止骗子退款技巧
  15. 台式关掉计算机不断网,笔记本电脑在关掉屏幕后不断网设置方法
  16. 有关JAVA考试中数据库的题_全国2018年4月自考互联网数据库考试真题
  17. 仓库建设细节及注意事项
  18. 快手校园招聘工程笔试B卷-搭积木
  19. 剑指offer:Python 二进制中1的个数 0xffffffff是什么意思?
  20. git 乌龟 git sync介绍

热门文章

  1. Excel-资产负债表-模板
  2. 知名艺人联合签名强烈谴责虐猫事件
  3. 电动汽车热管理粘合剂和密封剂市场现状及未来发展趋势
  4. Nolan的分形分布估计软件Stable使用教程
  5. 决策引擎EngineX平台实践
  6. 招银网络---C++
  7. 钉钉考勤接口调用与OA系统数据对接(多线程版)
  8. Win10系统 IE11浏览器调用F12开发人员工具,打开后底部显示空白
  9. Bandizip6.27百度网盘
  10. 钉钉自定义机器人提示报警信息