网页版五子棋——匹配模块(服务器端开发)
前一篇文章:网页版五子棋——匹配模块(客户端开发)-CSDN博客
目录
·前言
一、创建并注册 MatchAPI 类
1.创建 MatchAPI 类
2.注册 MatchAPI 类
二、实现用户管理器
三、创建匹配请求/响应
1.创建匹配请求
2.创建匹配响应
四、创建房间/房间管理器
1.创建房间类
2.实现房间管理器
五、实现匹配器
六、 实现 MatchAPI 中继承的方法
1.处理连接成功
2.处理开始匹配/取消匹配请求
3.处理连接异常
4.处理连接关闭
七、测试匹配功能
·结尾
·前言
前一篇文章中介绍了匹配模块中前后端交互接口的设计及匹配模块客服端代码的开发,在本篇文章里将继续对五子棋项目中匹配模块的代码进行编写,下面要进行介绍的内容就是服务器端代码的编写了,这里我们将实现按照天梯积分来把实力相近的两个玩家匹配到一起的细节逻辑,本篇文章中将新增的代码文件如下图圈起来的文件所示:
下面就开始本篇文章的内容介绍。
一、创建并注册 MatchAPI 类
1.创建 MatchAPI 类
创建 MatchAPI 类,继承自 TextWebSocketHandler 它是作为处理 WebSocket 请求的入口类,其中要重写的几个方法,及每个方法的用途在前面文章中已经进行了介绍,文章链接:网页版五子棋—— WebSocket 协议-CSDN博客 ,下面我们先把 MatchAPI 类的一个空架子搭好,具体的代码及详细介绍如下所示:
// 通过这个类来处理匹配功能中的 WebSocket 请求
@Component
public class MatchAPI extends TextWebSocketHandler {
// 创建 ObjectMapper 用于处理 JSON 数据
private ObjectMapper objectMapper = new ObjectMapper();
@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.注册 MatchAPI 类
修改 WebSocketConfig 类,把 MatchAPI 注册进去,修改后的 WebSocketConfig 类的具体代码及详细介绍如下所示:
// @EnableWebSocket 注解用来告诉 Spring 这是配置 WebSocket 的类
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
// 自动注入
@Autowired
private TestAPI testAPI;
@Autowired
private MatchAPI matchAPI;
@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());
}
}
二、实现用户管理器
创建 OnlineUserManager 类,这是用于管理当前用户在线状态的类,其中使用数据结构哈希表来实现用户与用户状态的映射,这里哈希表中 key 存的是用户的 id,value 存的是用户的 WebSocketSession,借助这个类我们一方面可以判断当前用户是否是在线状态,同时也可以方便的获取到 Session 从而给客户端进行响应,关于这个类需要完成的功能如下所示:
- 当玩家建立好 WebSocket 连接后,将键值对存入 OnlineUserManager 中;
- 当玩家断开 WebSocket 连接后,将键值对从 OnlineUserManager 中删除;
- 在玩家 WebSocket 连接完好的情况下,可以随时通过 userId 来查询到对应的会话,以便向客户端返回数据;
- 要注意线程安全的问题,如果多个用户和服务器建立连接/断开连接,此时服务器就是并发的在针对哈希表进行修改,传统的集合 HashMap 不是线程安全的,所以我们这里要使用 ConcurrentHashMap 来作为存储用户与用户状态映射的数据结构。
关于 OnlineUserManager 类的具体代码及详细介绍如下所示:
// 表现用户的在线状态
@Component
public class OnlineUserManager {
// 这个哈希表是用来表示当前用户在游戏大厅的在线状态.
private ConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>();
// 进入游戏大厅,调用这个方法,把玩家的 WebSocket 连接信息保存到哈希表中
public void enterGameHall(int userId, WebSocketSession webSocketSession) {
gameHall.put(userId, webSocketSession);
}
// 离开游戏大厅,调用这个方法,把玩家的 WebSocket 连接信息从哈希表中删除
public void exitGameHall(int userId) {
gameHall.remove(userId);
}
// 调用这个方法,把玩家的 WebSocket 连接信息从哈希表中获取到
public WebSocketSession getFromGameHall (int userId) {
return gameHall.get(userId);
}
}
创建完这个类后,就可以把它注入到 MatchAPI 中了,具体代码在下面再进行展示。
三、创建匹配请求/响应
1.创建匹配请求
创建 MatchRequest 类,这是用来接收客户端请求的类,其中的属性与前面文章中约定匹配模块中前后端交互接口的匹配请求一致,匹配请求的接口设计如下图所示:
关于 MatchRequest 类的具体代码如下所示:
// 表示一个 WebSocket 匹配请求
@Data
public class MatchRequest {
private String message;
}
@Data 注解会自动生成 get 与 set 方法。
2.创建匹配响应
创建 MatchResponse 类,这是用来给客户端返回响应的类,其中的属性与前面文章约定的匹配模块中前后端交互接口的匹配响应一致,匹配响应的接口设计如下图所示:
这两个匹配响应的数据格式一致所以可以共用一个类来表示,关于MatchResponse 类的具体代码如下所示:
// 表示一个 WebSocket 的匹配响应
@Data
public class MatchResponse {
private boolean ok;
private String reason;
private String message;
}
四、创建房间/房间管理器
1.创建房间类
在玩家匹配成功之后,需要把对战的两个玩家放入同一个房间对象中,下面我们来创建 Room 类,在匹配模块中我们需要做的事情如下:
- 一个房间要包含一个房间 ID,这里我们使用 UUID 来作为房间的唯一身份标识;
- 游戏房间中要记录对弈的玩家双方信息。
UUID 表示“世界上唯一的身份标识”,这是通过一系列算法生成的一串字符串(一组十六进制的数字),每次调用这个算法获取到的结果都不相同,所以这里我们可以使用 UUID 来作为我们房间的唯一身份标识,关于 UUID 内部具体是如何实现的我们不必深究,这是因为在 Java 中有现成的类可以帮我们生成 UUID,下面我来演示一下 UUID 的创建方式以及测试一下 UUID 每次调用生成的值是否真的不一样,测试代码及运行结果如下所示:
import java.util.UUID;
public class Test {
public static void main(String[] args) {
String id1 = UUID.randomUUID().toString();
String id2 = UUID.randomUUID().toString();
String id3 = UUID.randomUUID().toString();
String id4 = UUID.randomUUID().toString();
System.out.println("id1 = " + id1);
System.out.println("id2 = " + id2);
System.out.println("id3 = " + id3);
System.out.println("id4 = " + id4);
}
}
如上图所示,可以证明 UUID 生成的确实是唯一的身份标识,关于匹配模块中 Room 类的具体代码及详细介绍如下所示:
// 这个类表示一个游戏房间
@Data
public class Room {
// 使用字符串类型来表示, 方便生成唯一值.
private String roomId;
// 玩家一
private User user1;
// 玩家二
private User user2;
public Room() {
// 构造 Room 的时候生成一个唯一的字符串表示房间 id
// 使用 UUID 来作为房间 id
roomId = UUID.randomUUID().toString();
}
}
2.实现房间管理器
一局五子棋对战所进行的“场所”就可以称为是一个“对弈房间”,一个网页版五子棋的服务器上可以同时存在多个对弈房间,这时就需要一个“房间管理器”来管理多个“对弈房间”,“对弈房间”与“房间管理器”的关系如下图所示:
下面我们就来创建 RoomManager 类,用它管理所有 Room 对象,关于这个类我们要完成的功能如下:
- 使用数据结构哈希表,来保存所有房间对象,其中 key 存的是 roomId,value 存的是 Room 对象;
- 再使用一个哈希表,来保存 userId -> roomId 的映射,方便根据玩家来查找玩家所在的房间;
- 对于这两个哈希表分别提供增、删、查的方法。
- 要注意,多个玩家进行对弈时,服务器是并发的在对哈希表进行修改,传统的集合 HashMap 不能保证线程安全,所以我们这里使用 ConcurrentHashMap 来作为哈希表。
关于 RoomManager 类的具体代码及详细介绍如下所示:
// 房间管理器类
// 这个类希望有唯一实例
@Component
public class RoomManager {
// rooms 这个哈希表是用来存放 roomId 与 room 的映射
private ConcurrentHashMap<String, Room> rooms = new ConcurrentHashMap<>();
// userIdToRoomId 这个哈希表是用来存放 userId 与 roomId 的映射, 方便根据玩家来查找玩家所在房间
private ConcurrentHashMap<Integer, String> userIdToRoomId = new ConcurrentHashMap<>();
// 当有新的房间添加进来时, 要同时对这两个哈希表进都进行添加操作
public void add(Room room, int userId1, int userId2) {
rooms.put(room.getRoomId(), room);
userIdToRoomId.put(userId1, room.getRoomId());
userIdToRoomId.put(userId2, room.getRoomId());
}
// 当有房间被销毁时, 要同时对这两个哈希表进都进行删除操作
public void remove(String roomId, int userId1, int userId2) {
rooms.remove(roomId);
userIdToRoomId.remove(userId1);
userIdToRoomId.remove(userId2);
}
// 根据 roomId 来查找房间
public Room getRoomByRoomId(String roomId) {
return rooms.get(roomId);
}
// 根据 userId 来查找玩家所在房间
public Room getRoomByUserId(int userId) {
String roomId = userIdToRoomId.get(userId);
if (roomId == null) {
// userId -> roomId 映射关系不存在, 直接返回 null
return null;
}
return rooms.get(roomId);
}
}
五、实现匹配器
创建 Matcher 类,在这个类中,我们要做的就是根据进行匹配玩家的天梯分数,选出天梯分数尽量相近的两个玩家进行对弈,关于我们在这个类中要完成的功能如下所示:
- 创建三个队列,队列中存储 User 对象,每个队列代表着一个段位(此处约定:天梯分数 < 2000 的是普通玩家、天梯分数 2000-3000 的是高水平玩家、天梯分数 > 3000 的是超高水平玩家);
- 提供 add 方法,作用是让 MatchAPI 类来调用,把玩家加入到匹配队列;
- 提供 remove 方法,作用是让 MatchAPI 类来调用,把玩家移出匹配队列;
- Matcher 要在 OnlineUserManager 中获取玩家的 Session 来观察玩家的状态信息。
- 要注意,这里对每个队列进行的操作涉及到线程安全问题,这里我们需要对 add 方法和 remove 方法进行加锁操作;
- 此处进行加锁操作的时候要明确一点,在多个线程访问的是不同队列的时候是不涉及线程安全问题的,必须是多个线程操作同一个队列才会出现线程安全问题,所以多个线程操作同一个队列时就需要进行加锁操作,因此,我们在加锁时选取的加锁对象,就是创建的三个队列对象本身。
以上规定的匹配实现可能会略显简陋,如果想让匹配到的两个玩家天梯积分更加贴近,可以选择多创建几个队列来表示更多的段位。
介绍完 Matcher 的功能后,我们需要对 Matcher 的构造方法进行一个设计,在构造方法中,我们要对这三个队列分别创建一个线程,使用这个线程来扫描队列,把队列中的头两个元素取出来,匹配到一起,由于每个队列进行两个玩家的匹配操作都是一样的,所以我们可以把公共的操作封装成一个方法,这个方法叫作 handlerMatch ,在这个方法中我们需要注意以下几点:
- 由于 handlerMatch 方法在多线程环境中使用,因此需要考虑到线程安全问题,需要进行加锁操作;
- 每个队列分别使用队列对象本身作为锁对象即可;
- 在入口处需要使用 wait 来进行等待,当队列中的元素个数达到两个及以上,才能唤醒线程来消费队列;
- 在插入元素成功后要调用 notify 方法唤醒队列。
介绍到这里,Matcher 类的主要逻辑就都介绍完成了, 关于 Matcher 类的具体代码及详细介绍如下所示:
// 这个类表示 "匹配器", 通过这个类负责完成整个匹配功能
@Component
public class Matcher {
// 创建三个匹配队列
// 普通水平玩家
private Queue<User> normalQueue = new LinkedList<>();
// 高水平玩家
private Queue<User> highQueue = new LinkedList<>();
// 超高水平玩家
private Queue<User> veryHighQueue = new LinkedList<>();
// 注入 OnlineUserManager 来获取玩家的状态信息
@Autowired
private OnlineUserManager onlineUserManager;
// 创建 ObjectMapper 用于处理 JSON 数据
// 这里主要用于对给客户端返回匹配结果的响应进行转换
private ObjectMapper objectMapper = new ObjectMapper();
// 注入 RoomManager 来把匹配成功的两玩家放入同一个房间中
// 并保存好其中的映射关系
@Autowired
private RoomManager roomManager;
// 操作匹配队列的方法:
// 把玩家放到匹配队列中
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 中移除!");
}
}
public Matcher() {
// 创建三个线程, 分别对这三个匹配队列进行操作
Thread normalThread = new Thread(()->{
// 扫描 normalQueue
while (true) {
handleMatch(normalQueue);
}
});
normalThread.start();
Thread highThread = new Thread(()->{
// 扫描 highQueue
while (true) {
handleMatch(highQueue);
}
});
highThread.start();
Thread veryHighThread = new Thread(()->{
// 扫描 veryHighQueue
while (true) {
handleMatch(veryHighQueue);
}
});
veryHighThread.start();
}
// 处理匹配的相关逻辑
private void handleMatch(Queue<User> matchQueue) {
// 此时加锁的锁对象, 传进来的是 normalQueue 就是对 normalQueue进行加锁, 是 highQueue 就是对 highQueue 进行加锁
synchronized (matchQueue) {
try {
// 五子棋对战至少需要两位玩家
// 1. 检测匹配队列中的玩家个数是否达到 2
// 队列的初始情况可能是 空
// 如果往队列中添加一个元素, 这个时候, 仍然是不能进行后续匹配操作的
// 因此在这里使用 while 循环检查是更合理的
while (matchQueue.size() < 2) {
// 队列的玩家数量不足两人, 就直接返回
matchQueue.wait();
}
// 2. 尝试从队列中取出两个玩家
User player1 = matchQueue.poll();
User player2 = matchQueue.poll();
System.out.println("匹配出两个玩家: " + player1.getUsername() + " , " + player2.getUsername());
// 3. 获取玩家的 WebSocket 的会话
// 获取会话的目的是为了告诉玩家, 你匹配成功了
WebSocketSession session1 = onlineUserManager.getFromGameHall(player1.getUserId());
WebSocketSession session2 = onlineUserManager.getFromGameHall(player2.getUserId());
// 理论上来说, 我们这里匹配队列中的玩家一定是在线的状态,
// 这是因为前面的逻辑中已经进行了处理, 当玩家断开连接的时候就把玩家从匹配队列中移除了.
// 但是我们这里还是要做一次判定, 可以起到一个双重保险的作用
if (session1 == null) {
// 如果玩家1 现在不在线了, 就把玩家2 重新放回匹配队列中
matchQueue.offer(player2);
return;
}
if (session2 == null) {
// 如果玩家2 现在下线了, 就把玩家1 重新放回匹配队列中
matchQueue.offer(player1);
return;
}
// 当前能否排到两个玩家是同一个用户的情况吗? 一个玩家入队列两次?
// 理论上来说这是不会存在的, 这是因为前面的逻辑已经进行了处理:
// 1) 如果玩家下线, 就会对玩家进行移出匹配队列的操作
// 2) 并且禁止玩家多开.
// 但是这里仍然再做一次判定, 以免前面的逻辑出现 bug 时带来的严重后果
if (session1 == session2) {
// 把其中一个玩家放回匹配队列
matchQueue.offer(player1);
return;
}
// 4. 把这两个玩家放到一个游戏房间中
Room room = new Room();
roomManager.add(room, player1.getUserId(), player2.getUserId());
// 5. 给玩家反馈信息: 你匹配到对手了
// 通过 WebSocket 返回一个 message 为 'matchSuccess' 这样的响应
// 此处是要给两个玩家都返回 "匹配成功" 这样的信息
// 因此就需要返回两次
MatchResponse response1 = new MatchResponse();
response1.setOk(true);
response1.setMessage("matchSuccess");
String json1 = objectMapper.writeValueAsString(response1);
session1.sendMessage(new TextMessage(json1));
MatchResponse response2 = new MatchResponse();
response2.setOk(true);
response2.setMessage("matchSuccess");
String json2 = objectMapper.writeValueAsString(response2);
session2.sendMessage(new TextMessage(json2));
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
}
六、 实现 MatchAPI 中继承的方法
1.处理连接成功
这里我们来实现 afterConnectionEstablished 方法,我们要在 afterConnectionEstablished 方法中完成以下的工作:
- 通过参数中的 WebSocketSession 对象,拿到之前登录时设置的 User 信息;
- 使用 OnlineUserManager 来管理用户的在线状态;
- 判断当前用户是否已经在线,如果在线就直接返回错误(禁止同一个账号多开);
- 把玩家设置为上线状态。
关于 afterConnectionEstablished 方法的具体代码及详细介绍如下所示:
@Override
// 连接就绪后就会触发这个方法
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 玩家上线, 加入到 OnlineUserManager 中
// 1. 先获取到当前用户的身份信息(谁在游戏大厅中, 建立的连接)
// 此处的代码, 之所以能够 getAttributes, 全靠在注册 WebSocket 的时候,
// 加上的 .addInterceptors(new HttpSessionHandshakeInterceptor());
// 这个逻辑就把 HttpSession 中的 Attribute 都拿到 WebSocketSession 中了
// 在 Http 登录的逻辑中, 往 HttpSession 中存了 User 数据: httpSession.setAttribute("user",user);
// 此时就可以在 WebSocketSession 中把之前 HttpSession 里存的 User 对象给拿到了.
// 注意, 如果此处拿到的 user, 是可能为空的!!
// 如果之前用户没有通过 HTTP 来进行登录, 而是直接通过 /game_hall.html 这个 url 来访问游戏大厅页面
// 此时就会出现 user 为 null 的情况
try {
User user = (User) session.getAttributes().get("user");
// 2. 先判断当前用户是否已经登录过(已经是在线状态), 如果是已经在线, 就不该继续进行后续逻辑
WebSocketSession tmpSession = onlineUserManager.getFromGameHall(user.getUserId());
if (tmpSession != null) {
// 当前用户已经登录了!
// 此时就要告诉客户端, 你在这里的账号重复登录了
MatchResponse response = new MatchResponse();
response.setOk(false);
response.setReason("当前禁止多开!");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
session.close();
return;
}
// 3. 拿到身份信息之后, 就可以把玩家设置成在线状态了
onlineUserManager.enterGameHall(user.getUserId(), session);
System.out.println("玩家 " + user.getUsername() + "进入游戏大厅!");
} catch (NullPointerException e) {
e.printStackTrace();
// 出现空指针异常, 说明当前用户的身份信息为空,用户当前未登录
// 把当前用户未登录这个信息按照前面约定好的前后端交互接口的格式来进行返回.
MatchResponse response = new MatchResponse();
response.setOk(false);
response.setReason("您当前尚未登录! 不能进行后续匹配功能!");
// 先通过 ObjectMapper 把 MatchResponse 对象转成 JSON 字符串
// 然后再包装上一层 TextMessage, 再进行传输
// TextMessage 就表示一个文本格式的 WebSocket 数据包
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
}
}
2.处理开始匹配/取消匹配请求
这里我们来实现 handleTextMessage 方法,我们要在 handleTextMessage 方法中完成以下的工作:
- 通过参数中的 WebSocketSession 对象,拿到之前登录时设置的 User 信息;
- 解析客户端发来的请求;
- 根据约定的前后端交互接口,来判断请求的类型,如果是 startMatch,则把用户对象加入到匹配队列,如果是 stopMatch,则把用户对象从匹配队列中删除;
- 此处需要实例化一个匹配器对象,来处理匹配的实际逻辑。
关于 handleTextMessage 方法的具体代码及详细介绍如下所示:
@Override
// 客户端/服务器 给 服务器/客户端 发送信息通过这个方法就可以接收到信息
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 实现处理开始匹配请求和处理停止匹配请求.
User user = (User) session.getAttributes().get("user");
// 获取到客户端给服务器发送的数据
String payload = message.getPayload();
// 当前这个数据载荷是一个 JSON 格式的字符串, 需要把它转成 Java 对象 -> MatchRequest
MatchRequest request = objectMapper.readValue(payload, MatchRequest.class);
MatchResponse response = new MatchResponse();
if (request.getMessage().equals("startMatch")) {
// 进入匹配队列
matcher.add(user);
// 把玩家信息放入匹配队列之后,就可以返回一个响应给客户端了
response.setOk(true);
response.setMessage("startMatch");
} else if (request.getMessage().equals("stopMatch")) {
// 退出匹配队列
matcher.remove(user);
// 移除之后, 就可以返回一个响应给客户端了.
response.setOk(true);
response.setMessage("stopMatch");
} else {
response.setOk(false);
response.setMessage("非法的匹配请求");
}
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
}
3.处理连接异常
这里我们来实现实现 handleTransportError 方法,我们要在 handleTransportError 方法中完成以下的工作:
- 主要的工作是把玩家从 OnlineUserManager 的对象中进行移除;
- 退出的时候需要判断当前玩家退出的原因是不是因为多开的情况(一个 userId 对应到两个 WebSocket 连接),如果一个玩家开启了第二个 WebSocket 连接,那么这第二个 WebSocket 连接不会音响到玩家从 OnlineUserManager 中退出;
- 如果当前玩家在匹配队列中,就直接把玩家从匹配队列中进行移除。
关于 handleTransportError 方法的具体代码及详细介绍如下所示:
@Override
// 传输出现异常就会触发这个方法
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
// 玩家下线, 从 OnlineUserManager 中删除
try {
// 获取玩家信息
User user = (User) session.getAttributes().get("user");
WebSocketSession tmpSession = onlineUserManager.getFromGameHall(user.getUserId());
// 防止玩家多开的第二个连接把第一个连接也给踢掉
if (tmpSession == session) {
onlineUserManager.exitGameHall(user.getUserId());
}
// 如果玩家正在匹配中, WebSocket 连接断开了, 就应该移除匹配队列
matcher.remove(user);
} catch (NullPointerException e) {
e.printStackTrace();
MatchResponse response = new MatchResponse();
response.setOk(false);
response.setReason("您当前尚未登录! 不能进行后续匹配功能!");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
}
}
4.处理连接关闭
这里我们实现 afterConnectionClosed 方法,这个方法要做的工作与 handleTransportError 方法做的工作一致,所以我就不再进行描述,关于 afterConnectionClosed 方法的具体代码及详细介绍如下所示:
@Override
// 如果客户端/服务器关闭连接就会执行这个方法
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// 玩家下线, 从 OnlineUserManager 中删除
try {
// 获取玩家信息
User user = (User) session.getAttributes().get("user");
WebSocketSession tmpSession = onlineUserManager.getFromGameHall(user.getUserId());
// 防止玩家多开的第二个连接把第一个连接也给踢掉
if (tmpSession == session) {
onlineUserManager.exitGameHall(user.getUserId());
}
// 如果玩家正在匹配中, WebSocket 连接断开了, 就应该移除匹配队列
matcher.remove(user);
} catch (NullPointerException e) {
e.printStackTrace();
MatchResponse response = new MatchResponse();
response.setOk(false);
response.setReason("您当前尚未登录! 不能进行后续匹配功能!");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
}
}
七、测试匹配功能
编写完上述代码之后,我们就把五子棋项目匹配模块的代码编写完毕了,与客户端的交互代码在上一篇文章中已经编写完毕,启动服务器,在浏览器中输入:http://127.0.0.1:8080/login.html 进入登录页面,通过登录页面进入游戏大厅页面,在游戏大厅页面中测试我们的匹配功能,下面我来测试一下匹配功能是否存在问题,测试过程如下图所示:
此时我们还没有进行编写 game_room.html 页面的代码,所以会出现 404 找不到页面的错误,这是正常情况,经过测试,结果都符合预期,匹配功能就是一个正常的功能了。
·结尾
文章到这里就要结束了,本篇文章主要介绍了五子棋项目中匹配模块服务器端代码的编写,其中包括创建房间,匹配器,用户管理器,房间管理器等,还实现了 MatchAPI,本篇文章的内容比较多,其每部分代码之间都有所关联,要捋清楚匹配模块中代码的执行逻辑,那么到这,五子棋项目中的匹配模块的代码就都编写完成了,如果对本篇文章的内容有所疑惑,欢迎在评论区进行留言,如果感觉本篇文章还不错希望能收到你的三连支持,那么我们下一篇文章再见吧~~~