[网页五子棋][匹配模块]处理开始匹配/停止匹配请求(匹配算法,匹配器的实现)
文章目录
- 处理开始匹配/停止匹配请求
- 匹配算法
- 实现匹配器(1)
- 完善匹配器的 TODO
- 实现匹配器(2)
- 实现 handlerMatch
- 线程安全
- 忙等问题
处理开始匹配/停止匹配请求
实现 handleTestMessage
- 先从会话中拿到当前玩家的信息
- 解析客户端发来的请求
- 判定请求的类型
- 如果是
startMatch
,则把用户加入到匹配队列 - 如果是
stopMatch
,则把用户对象从匹配队列中删除
- 如果是
- 此处需要实现一个匹配器对象,来处理匹配的实际逻辑
@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 (response.getMessage().equals("startMatch")) { // 进入匹配队列 // TODO 先创建一个类,来表示匹配队列,把当前用户给加进去 // 把玩家信息放入匹配队列之后,就可以返回一个响应给客户端了 response.setOk(true); response.setMessage("startMatch"); } else if (response.getMessage().equals("stopMatch")){ // 退出匹配队列 // TODO 先创建一个类表示匹配队列,把当前用户从队列中移除 // 移除之后,就可以返回一个响应给客户端了 response.setOk(true); response.setMessage("stopMatch"); } else { // 非法情况 response.setOk(false); response.setReason("非法的匹配请求!"); }
}
匹配算法
目标:从待匹配的玩家中,选出分数尽量相近的玩家
把整个所有的玩家,按照分数,划分为三类:
Normal
:socre < 2000
High
:score >= 2000 && score < 3000
VeryHigh
:score >= 3000
给这三个等级,分配三个不同的队列。根据当前玩家的分数,来把这个玩家的用户信息,放到对应的队列里面
接下来再搞一个专门的线程,去不停地扫描这个匹配队列。只要队列里面的元素 (匹配的玩家) 凑成了一对,就把这对玩家取出来,放到一个游戏房间中
- 当前的匹配实现,比较粗糙,只是简单的搞了三个段位的队列
- 如果想要匹配的更加精确,就可以多搞几个队列
实现匹配器(1)
创建 game.Matcher
- 在
Matcher
中创建三个队列(按上面分类) - 提供
add
方法,供MatchAPI
类来调用,用来把玩家加入匹配队列 - 提供
remove
方法,供MatchAPI
类来调用,用来把玩家移出匹配队列 - 同时
Matcher
找那个要记录OnlineUserManager
,来获取到玩家的session
package org.example.java_gobang.game; import org.example.java_gobang.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import java.util.LinkedList;
import java.util.Queue; // 这个类表示”匹配器“,这个类负责完成整个匹配功能
@Component
public class Matcher { // 创建三个匹配队列 private Queue<User> normalQueue = new LinkedList<>(); private Queue<User> highQUeue = new LinkedList<>(); private Queue<User> veryHighQueue = new LinkedList<>(); // 将用户在线状态这个管理器引入,方便我们后续随时能够得到相应玩家的会话信息,也可以判断用户的在线状态 @Autowired private OnlineUserManager onlineUserManager; // 操作匹配队列的方法: // 把玩家放到匹配队列中 public void add(User user) { if (user.getScore() < 2000) { normalQueue.offer(user); System.out.println("把玩家 " + user.getUsername() + " 加入到了 normalQueue 中!"); } else if (user.getScore() >= 2000 && user.getScore() < 3000) { highQUeue.offer(user); System.out.println("把玩家 " + user.getUsername() + " 加入到了 highQueue 中!"); }else { veryHighQueue.offer(user); System.out.println("把玩家 " + user.getUsername() + " 加入到了 veryHighQueue 中!"); } } // 当玩家点击停止匹配的时候,就需要把玩家从匹配队列中删除 public void remove(User user) { if (user.getScore() < 2000) { normalQueue.remove(user); System.out.println("把玩家 " + user.getUsername() + " 从 normalQueue 中删除了!"); } else if (user.getScore() >= 2000 && user.getScore() < 3000) { highQUeue.remove(user); System.out.println("把玩家 " + user.getUsername() + " 从 highQueue 中删除了!"); }else { veryHighQueue.remove(user); System.out.println("把玩家 " + user.getUsername() + " 从 veryHighQueue 中删除了!"); } }
}
完善匹配器的 TODO
在 MatchAPI
类中创建 Matcher
对象
@Autowired
private Matcher matcher;
然后完善 TODO
部分的逻辑
- 上面的红框改为:
macher.add(user)
- 下面的红框改为:
macher.remove(user)
针对连接异常和连接关闭的两个方法,我们也要进行相应处理
- 当在匹配的时候,突然连接关闭/断开了,相应的匹配就要停止了
- 停止匹配逻辑为:
matcher.remove(user);
实现匹配器(2)
修改 game.Matcher
,实现匹配逻辑
在 Matcher
的构造方法中,创建一个线程,使用该线程扫描每个队列,把每个队列的头两个元素取出来,匹配到一组中
// 匿名内部内的方式,继承 Thread 类
public Matcher() { // 创建三个线程,分别针对这三个匹配队列,进行操作 Thread t1 = new Thread() { @Override public void run() { // 扫描 normalQueue while (true) { handlerMatch(normalQueue); } } }; t1.start(); Thread t2 = new Thread() { @Override public void run() { // 扫描 highQueue while (true) { handlerMatch(highQUeue); } } }; t2.start(); Thread t3 = new Thread() { @Override public void run() { // 扫描 veryHighQueue while (true) { handlerMatch(veryHighQueue); } } };
}
实现 handlerMatch
private void handlerMatch(Queue<User> matchQueue) { try { // 1. 检测队列中元素个数是否达到 2 if (matchQueue.size() < 2) { return; } // 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. TODO 把这两个玩家放到一个游戏房间中 // 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); session1.sendMessage(new TextMessage(json2)); }catch (IOException e) { e.printStackTrace(); }
}
线程安全
我们主要涉及到要操作 normalQueue
、highQueue
、veryHighQueue
这三个队列,而这三个队列本身就是在多个线程中进行的
- 使用到多线程代码的时候,一定要时刻注意“线程安全”问题
此处我们使用 synchornized
进行加锁
- 指定一个“锁对象”
- 到底针对谁进行加锁
- 只有多个线程在尝试针对同一个锁对象进行加锁的时候,才会有互斥效果
- 此处进行加锁的时候,要明确
- 如果多个线程访问的是不同的队列,就不会涉及到线程安全问题
- 必须是多个线程操作同一个队列,才需要加锁
- 因此在加锁的时候选取的锁对象,就是
normalQueue
、highQueue
、veryHighQueue
这三个队列对象本身
- 对
add
和remove
里面的这些对队列的操作进行加锁操作
还要把 Macher
类中的 handlerMatch
这个方法全部加锁
忙等问题
如果当前匹配队列中,只有一个元素,或者没有元素,会出现什么效果?
- 在这个代码中,就会出现
handlerMatch
已进入方法就快速返回,然后再次进入方法… - 循环速度飞快,但是却没有什么实质的意义,这个过程中
CPU
占用率会非常高 - 这个就是我们说的——忙等
如何解决?
- 在调用完
handlerMatch
之后,加上个sleep(500)
合理吗
这个方法可以,但是不是很完美
- 当有玩家匹配到了之后,可能要
500ms
之后才能真正得到匹配的返回结果 - 通过
sleep
是难以两全其美的,要么就得让玩家多等,要么就得让CPU
多转
- 这里我们更好地选择就是使用
wait/notify
- 在扫描线程中,使用
wait
来等待 - 当真正有玩家进入匹配队列之后,就调用
notify
来唤醒
添加 wait
和 notify
- 在
handlerMatch
方法里面,当元素未达到 2 的时候,让其进行等待 - 当三个队列里面任意一个队列有元素加进来了,就进行通知
队列的初始情况可能是 0,如果往队列中添加一个元素,这个时候,让然是不能进行后续的匹配操作,因此在 handlerMatch
方法里面,元素未达到 2 的时候,使用 while
循环检查时更合理的