前一篇文章:网页版五子棋——对战模块(服务器端开发①)-CSDN博客
项目源代码:Java: 利用Java解题与实现部分功能及小项目的代码集合 - Gitee.com
目录
·前言
一、创建并注册 GameAPI 类
1.创建 GameAPI 类
2.注册 GameAPI 类
二、实现 GameAPI 中继承的方法
1.通知玩家就绪
2.处理连接成功
3.处理落子请求
4.通知对手获胜
5.处理玩家退出
三、测试对战功能
·结尾
·前言
在前一篇文章中介绍了五子棋项目中核心部分有关落子操作相关的逻辑,本篇文章将继续对五子棋项目中对战模块的服务器端代码进行编写,下面我们要进行 WebSocket 请求入口类的编写,实现其继承的方法,还有对整个对战模块功能的测试,本篇文章中将要新增的代码文件如下图圈起来的文件所示:
下面就开始本篇文章的内容介绍:
一、创建并注册 GameAPI 类
1.创建 GameAPI 类
创建 GameAPI 类,继承自 TextWebSocketHandler 它是作为处理 WebSocket 请求的入口类,其中要重写的几个方法,及每个方法的用途在前面文章已经进行了介绍,文章链接:网页版五子棋—— WebSocket 协议_网页可以实现websocket吗-CSDN博客 ,下面我们先把 GameAPI 类的一个空架子搭好,并且这里要准备几个对象如下所示:
- 准备一个 ObjectMapper 对象,用来处理 JSON 数据;
- 注入 RoomManager 对象,用来获取玩家所在房间,还有进行释放房间的操作;
- 注入 OnlineUserManager 对象,用来获取当前玩家的在线状态,还有获取玩家的连接信息用于判断当前玩家是否多开,和给玩家返回响应;
- 注入 UserMapper 对象,用于更新对局结束后玩家的信息。
GameAPI 类空架子的代码及详细介绍如下所示:
// 通过这个类来处理对战模块中的 WebSocket 请求
@Component
public class GameAPI extends TextWebSocketHandler {
// 创建 RoomManager 对象, 用来获取玩家所在房间,还有进行释放房间的操作
@Autowired
private RoomManager roomManager;
// 创建 ObjectMapper 对象, 用来处理 JSON 数据
private ObjectMapper objectMapper = new ObjectMapper();
// 创建 OnlineUserManager 用来管理玩家的状态信息
@Autowired
private OnlineUserManager onlineUserManager;
// 创建 UserMapper 对象,用于更新对局结束后玩家的信息
@Autowired
private UserMapper userMapper;
// 连接就绪后就会触发这个方法
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
}
// 客户端/服务器 给 服务器/客户端 发送信息通过这个方法就可以接收到信息
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
}
// 传输出现异常就会触发这个方法
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
}
// 如果客户端/服务器关闭连接就会执行这个方法
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
}
}
2.注册 GameAPI 类
修改 WebSocketConfig 类,把 GameAPI 注册进去,修改后的 WebSocketConfig 类的具体代码及详细介绍如下所示:
// @EnableWebSocket 注解用来告诉 Spring 这是配置 WebSocket 的类
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
// 自动注入
@Autowired
private TestAPI testAPI;
@Autowired
private MatchAPI matchAPI;
@Autowired
private GameAPI gameAPI;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 当客户端连接 /test 这样的路径后就会触发 testAPI 进而调用其内部的方法
registry.addHandler(testAPI,"/test");
// 当客户端连接 /findMatch 这样的路径后就会触发 matchAPI 进而调用其内部的方法
registry.addHandler(matchAPI,"/findMatch")
// 把之前登录过程中往 HttpSession 中存放的 User 对象, 放到 WebSocket 的 session 中
// 方便后面代码可以获取到当前的用户信息
.addInterceptors(new HttpSessionHandshakeInterceptor());
// 当客户端连接 /game 这样的路径后就会触发 gameAPI 进而调用其内部的方法
registry.addHandler(gameAPI, "/game")
// 把之前登录过程中往 HttpSession 中存放的 User 对象, 放到 WebSocket 的 session 中
// 方便后面代码可以获取到当前的用户信息
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
二、实现 GameAPI 中继承的方法
1.通知玩家就绪
这里我们在 GameAPI 中写一个通知玩家就绪的方法 —— noticeGameReady() ,通过这个方法我们来给连接游戏房间页面成功的两个玩家返回 “准备就绪” 的响应,关于这个方法的具体代码及详细介绍如下所示:
// 用来给客户端返回 "准备就绪" 的响应
private void noticeGameReady(Room room, User thisUser, User thatUser) throws IOException {
// 构造 GameReadyResponse 作为返回的响应对象, 来返回响应
GameReadyResponse response = new GameReadyResponse();
response.setMessage("gameReady");
response.setOk(true);
response.setReason("");
response.setRoomId(room.getRoomId());
response.setThisUserId(thisUser.getUserId());
response.setThatUserId(thatUser.getUserId());
response.setWhiteUser(room.getWhiteUser());
// 把当前的响应数据传回给对应的玩家
// 获取当前玩家的连接信息
WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(thisUser.getUserId());
// 通过连接给当前玩家返回响应
webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
}
2.处理连接成功
这里我们来实现 GameAPI 中的 afterConnectionEstablished 方法,我们要在 afterConnectionEstablished 方法中完成以下工作:
- 需要检测用户的登录状态,从 Session 中拿到当前用户的信息;
- 需要判断当前玩家是否在房间中;
- 需要对多开进行判定,如果玩家已经在游戏中,就不能再进行连接;
- 需要把两个玩家放到对应的房间对象中,当两个玩家都建立了连接,房间就放满了,这时调用 noticeGameReady 方法,来通知两个玩家双方都准备就绪;
- 如果有第三个玩家尝试加入房间,就需要给出提示:“房间已经满了”;
在编写代码之前,我们要注意在上面需要完成的工作中,把两个玩家放到对应的房间对象过程中会涉及到线程安全问题,我们在这里会规定先进入房间的玩家是先手,当两个玩家对同一个房间对象进行操作时就会出现如下图所示的情况: 此时,玩家1 与 玩家2 都认为自己是先手方,就会出现线程安全的问题,为了解决这个问题,我们就需要进行加锁操作,下面我们就需要考虑对谁进行加锁,这里我们加锁的原则是对竞争的资源进行加锁,很显然,此时两位玩家竞争的资源就是这个房间对象,所以在执行上图中代码逻辑时,要在我们用 synchronized 对 room 进行加锁操作。
解决完线程安全的问题后,我们就开始进行实现 afterConnectionEstablished 方法代码的编写,关于这个方法的具体代码及详细介绍如下所示:
// 连接就绪后就会触发这个方法
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
GameReadyResponse response = new GameReadyResponse();
// 1. 获取用户的身份信息. (从 HttpSession 里拿到当前用户的对象)
User user = (User) session.getAttributes().get("user");
if (user == null) {
response.setOk(false);
response.setReason("用户尚未登录!");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
return;
}
// 2. 判断当前用户是否已经进入房间. (用房间管理器进行查询)
Room room = roomManager.getRoomByUserId(user.getUserId());
if (room == null) {
// 如果为 null ,当前没有找到对应的房间, 该玩家没有匹配成功.
response.setOk(false);
response.setReason("用户尚未匹配到对手!");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
return;
}
// 3. 判断当前是不是多开 (该用户是不是已经在其他地方进入游戏了)
// 前面准备了一个 OnlineUserManager
if (onlineUserManager.getFromGameHall(user.getUserId()) != null
|| onlineUserManager.getFromGameRoom((user.getUserId())) != null) {
// 如果一个账号, 一边在游戏大厅, 一边在游戏房间, 也视为多开
response.setOk(true);
response.setReason("禁止多开游戏页面");
response.setMessage("repeatConnection");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
return;
}
// 4. 设置当前玩家上线!
onlineUserManager.enterGameRoom(user.getUserId(), session);
// 5. 把两个玩家加入到游戏房间中.
// 前面的创建房间/匹配过程, 是在 game_hall.html 页面中完成的.
// 因此前面匹配到对手之后, 需要经过页面跳转, 来到 game_room.html 才算是正式进入游戏房间
// 当前这个逻辑是在 game_room.html 页面加载的时候进行的.
// 执行到当前逻辑, 说明玩家已经页面跳转成功了!!
// 页面跳转, 要进行很多的操作, 很可能出现 "失败" 的情况
synchronized (room) {
if (room.getUser1() == null) {
// 第一个玩家尚未加入房间.
// 就把当前连上 WebSocket 的玩家作为 user1, 加入到房间中.
room.setUser1(user);
// 把先连入房间的玩家作为先手方.
room.setWhiteUser(user.getUserId());
System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家1 ");
return;
}
if (room.getUser2() == null) {
// 如果进入这个逻辑, 说明玩家1 已经加入到房间, 现在要把当前玩家作为玩家2
room.setUser2(user);
System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家2 ");
// 当两个玩家都加入成功之后, 就要让服务器给这两个玩家都返回 WebSocket 的响应数据
// 通知这两个玩家, 游戏双方都已经准备好了.
// 通知玩家1
noticeGameReady(room, room.getUser1(), room.getUser2());
// 通知玩家2, 注意这里的传参顺序
noticeGameReady(room, room.getUser2(), room.getUser1());
return;
}
}
// 6. 此处如果又有玩家尝试连接同一个房间, 就提示报错.
// 这种情况理论上是不存在的, 为了让程序更加的健壮, 这里再做一个判断和提示.
response.setOk(false);
response.setReason("当前房间人数已满,您不能加入房间!");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
}
3.处理落子请求
这里我们来实现 GameAPI 中的 handleTextMessage 方法,我们要在 handleTextMessage 方法中完成以下工作:
- 需要检测用户的登录状态,从 Session 中拿到当前用户的信息;
- 需要根据玩家的 id 来获取到房间对象;
- 需要解析客户端发来的请求,通过房间对象调用 putChess 方法来处理这次具体的请求。
关于 handleTextMessage 方法的具体代码及详细介绍如下所示:
// 客户端/服务器 给 服务器/客户端 发送信息通过这个方法就可以接收到信息
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
GameReadyResponse response = new GameReadyResponse();
// 1. 先从 session 里拿到当前用户的身份信息
User user = (User) session.getAttributes().get("user");
if (user == null) {
System.out.println("[handleTextMessage] 当前玩家尚未登录! ");
response.setOk(false);
response.setReason("用户尚未登录!");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
return;
}
// 2. 根据玩家 id 获取到房间对象
Room room = roomManager.getRoomByUserId(user.getUserId());
// 3. 通过 room 对象来处理这次具体的请求
room.putChess(message.getPayload());
}
4.通知对手获胜
在进行五子棋对战的过程中,一方玩家很可能在进行对弈过程中由于网络或自身的原因退出了当前游戏页面,此时我们就需要通知另一方玩家获胜,不要让另一方玩家一直处于等待,下面我们在 GameAPI 中写一个通知对手获胜的方法 —— noticeThatUserWin(),在这个方法中,我们要完成以下的工作:
- 根据当前玩家找到玩家所在的房间,如果房间不存在,说明对局已经正常结束,就不用进行后续的操作了;
- 根据房间寻找对手,查看对手在线状态,如果对手也掉线了,那就不用再进行通知;
- 构造响应,通知对手获胜;
- 判断胜负后更改玩家的信息;
- 释放房间对象。
关于 noticeThatUserWin 方法的具体代码及详细介绍如下所示:
// 通知对手获胜
private void noticeThatUserWin(User user) throws IOException {
// 1. 根据当前玩家, 找到玩家所在的房间
Room room = roomManager.getRoomByUserId(user.getUserId());
if (room == null) {
// 这个情况意味着房间已经释放了, 也就没有 "对手" 了
System.out.println("当前房间已经释放, 无需通知对手!");
return;
}
// 2. 根据房间找到对手
User thatUser = (user == room.getUser1() ? room.getUser2() : room.getUser1());
// 3. 找到对手的在线状态
WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(thatUser.getUserId());
if (webSocketSession == null) {
// 说明对手也掉线了
System.out.println("对手已经掉线, 无需通知");
return;
}
// 4. 构造一个响应, 来通知对手: 它是获胜方
GameResponse response = new GameResponse();
response.setMessage("putChess");
response.setUserId(thatUser.getUserId());
response.setWinner(thatUser.getUserId());
webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
// 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());
}
5.处理玩家退出
这里我们要实现 GameAPI 中 handleTransportError 与 afterConnectionClosed 两个方法,在玩家下线或对局结束退出会触发这个方法,这两个方法中做的工作是一致的,需要完成以下的工作:
- 主要的工作是把玩家从 OnlineUserManager 的对象中进行移除;
- 退出的时候需要判断当前玩家退出的原因是不是因为多开的情况(一个 userId 对应到两个 WebSocket 连接),如果一个玩家开启了第二个 WebSocket 连接,那么这第二个 WebSocket 连接不会影响到玩家从 OnlineUserManager 中退出;
- 如果当前玩家退出游戏,就需要调用 noticeThatUserWin 方法,来判断对局是否是正常结束,如果对局没有结束是玩家主动退出,就需要通知对手获胜。
关于 handleTransportError 与 afterConnectionClosed 方法的具体代码及详细介绍如下所示:
// 传输出现异常就会触发这个方法
@Override
public 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 (exitSession == session) {
// 加上这个判断, 目的是为了避免在多开的情况下, 第二个用户退出连接动作,导致第一个用户受到影响
onlineUserManager.exitGameRoom(user.getUserId());
// 通知对手获胜了
noticeThatUserWin(user);
}
System.out.println("当前玩家: " + user.getUsername() + " 游戏房间连接异常");
}
// 如果客户端/服务器关闭连接就会执行这个方法
@Override
public 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 (exitSession == session) {
// 加上这个判断, 目的是为了避免在多开的情况下, 第二个用户退出连接动作,导致第一个用户受到影响
onlineUserManager.exitGameRoom(user.getUserId());
// 通知对手获胜了
noticeThatUserWin(user);
}
System.out.println("当前玩家: " + user.getUsername() + " 已经离开游戏房间");
}
三、测试对战功能
编写完上述代码之后,我们就把五子棋项目中对战模块的代码编写完毕了,与客户端交互的代码在上一篇文章中已经编写完毕,下面我们来启动服务器,在浏览器中输入:http://127.0.0.1:8080/login.html 进入登录页面,通过登录页面进入游戏大厅页面,在游戏大厅页面中点击匹配,进入游戏房间测试对战功能,下面我来测试一下对战功能是否存在问题,测试过程如下图所示:
经过上述的测试,结果都符合预期,对战功能就是一个正常的功能了。
·结尾
文章到这里就要结束了,本篇文章主要介绍了 GameAPI 类的代码编写,作为 WebSocket 连接请求的入口类,其中包含的方法是客户端进入游戏房间页面后第一时间要进行调用的,这里需要对很多情况进行判断,防止对局过程中出现问题,那么到这,五子棋项目中的对战模块就编写完成了,结合前面用户模块与匹配模块,网页版五子棋这个项目就算是大功告成了,文章中新增模块对前面代码的一些细微修改可能在文章中并没有很好的体现出来,如果大家对本项目感兴趣,也欢迎在我的码云中获取项目的完整代码,项目源码链接:Java: 利用Java解题与实现部分功能及小项目的代码集合 - Gitee.com ,如果对文章内容有所疑惑,欢迎在评论区进行留言,如果感觉本篇文章还不错希望能收到你的三连支持,那么我们下一篇文章再见吧~~~