五子棋双人对战项目(4)——匹配模块(解读代码)

目录

一、约定前后端交互接口的参数

1、websocket连接路径

2、构造请求、响应对象

二、用户在线状态管理

三、房间管理

1、房间类:

2、房间管理器:

四、匹配器(Matcher)

1、玩家实力划分

2、加入匹配队列(add)

3、移除匹配队列(remove)

4、创建三个线程

5、加入房间(handlerMatch)

(1)检测匹配队列是否有2个玩家

(2)取出匹配队列中的玩家

(3)获取玩家的Session

(4)双方玩家加入房间

(5)加入房间成功后,给客户端返回响应

五、处理 websocket 请求、返回的响应(MatchAPI)

1、afterConnectionEstablished

2、afterConnectionClosed

3、handleTransportError

4、handleTextMessage

六、前端代码的逻辑处理

1、建立连接

2、发送请求

3、处理响应

七、梳理前后端交互流程

1、玩家的匹配

(1)建立连接

(2)玩家发起请求

(3)服务器处理请求

(4)客户端处理响应

2、玩家匹配成功

2.1 后端处理请求

 (1)服务器发现匹配队列有2个玩家

(2)两个玩家加入同一个游戏房间

2.2 客户端处理响应


一、约定前后端交互接口的参数

        在上一篇博客中,我们约定了前后端交互接口的参数,如图:

1、websocket连接路径

        对照着上面的约定,我们进行配置,前端代码如下:

        // 此处进行初始化 websocket,并且实现前端的匹配逻辑// 此处的路径必须写作 /findMatchlet websocketUrl = "ws://" + location.host + "/findMatch";let websocket = new WebSocket(websocketUrl);

        使用动态的方式配置路径,方便以后部署在云服务器上(云服务器上的主机号肯定不会是127.0.0.1,而端口号也可能不同,因此让websocket的 URL变为动态的更合理)。

        后端代码:

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

注意:连接建立完成之后,还要记得把之前连接的 HttpSession 拿到,添加到 websocketSession

2、构造请求、响应对象

        我们需要创建两个对象,用来接收和传送,也就是 MatchRequest 和 MatchResponse


二、用户在线状态管理

        为什么要维护 玩家 在线状态 呢?因为这样我们可以根据用户,获取该用户的Session,从而可以给 用户 传送数据。因为服务器是要给多个客户端发送数据的?我们怎么保证给指定的玩家发送数据呢?那么拿到该玩家的Session就可以了。

        也能进行判断 在线 / 离线,方便处理 不同状态下的操作。比如:后面进行比赛了,如果对手掉线了,我们是不是能直接判断当前玩家赢了?

@Component
public class OnlineUserManager {// 这个hash表就是用来表示当前用户在游戏大厅的在线状态private ConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>();// 这个hash表用来维护用户Id和房间页面的在线状态private ConcurrentHashMap<Integer, WebSocketSession> gameRoom = new ConcurrentHashMap<>();public void enterGameHall(int userId, WebSocketSession webSocketSession) {gameHall.put(userId, webSocketSession);}public void exitGameHall(int userId) {gameHall.remove(userId);}public WebSocketSession getFromGameHall(int userId) {return gameHall.get(userId);}public void enterGameRoom(int userId, WebSocketSession webSocketSession) {gameRoom.put(userId, webSocketSession);}public void exitGameRoom(int userId) {gameRoom.remove(userId);}public WebSocketSession getFromGameRoom(int userId) {return gameRoom.get(userId);}
}

        这里有2个hash表,一个是用来维护 玩家 处在游戏大厅的在线状态,一个是用来维护 玩家 处在游戏房间的在线状态

        其中,维护的是 玩家Id 和 Session 的映射关系

        分别有三个方法:增、删、查


三、房间管理

        为什么维护游戏房间呢?联机游戏,例如 LOL,每时每刻都会有非常多的玩家在进行对局,那么怎么管理这么多玩家呢?我们就可以把若干个玩家放在一个游戏房间里,每个游戏房间都是相互独立的,互不干扰,而且每一个房间在不同时间中的对局状态都会不一样。如图:

        这里,我们就这设置成 1个房间 有 2个玩家,因为五子棋的玩法是双人对弈的(后续也会进行扩展,例如观战功能)。使用 UUID 生成一个随机房间Id,类型是String。(为了保证房间Id不能重复,Java 内置的 UUID 类就已经能满足当前这个项目了)

1、房间类:

//  这个类表示一个游戏房间
@Data
public class Room {// 使用字符串类型来表示,方便生成唯一值private String roomId;private User user1;private User user2;public Room() {// 构造 Room 的时候生成一个唯一的字符串表示房间 id// 使用 UUID 来作为房间 idroomId = UUID.randomUUID().toString();}
}

2、房间管理器:

// 房间管理器类
// 这个类也希望有唯一实例
@Component
public class RoomManager {private ConcurrentHashMap<String, Room> rooms = new ConcurrentHashMap<>();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);}public Room getRoomByRoomId(String roomId) {return rooms.get(roomId);}public Room getRoomByUserId(int userId) {String roomId = userIdToRoomId.get(userId);if(roomId == null) {// userId -> roomId 映射关系不存在,直接返回 nullreturn null;}return rooms.get(roomId);}
}

        这里有 2个hash表,一个用来维护 房间Id 和 房间 的映射关系(希望有了房间Id,就能拿到这个房间),一个用来维护 用户Id 和 房间Id 的映射关系根据用户Id 拿到 房间Id,再根据 房间Id 拿到 房间)。

        提供的方法:增、删、查。(这里增、删需要同时维护上面2个hash表查有两种方法一是拿着房间Id,找到房间二是拿着用户Id,找到房间)。

注意:这里涉及到线程安全问题;我们想想,五子棋对战肯定是会有很多人在进行对战的,这时候,有多个客户端都对这两个hash表有修改、删除、查询的操作,在这种并发的情况下,就会涉及到线程安全问题。(因此使用ConcurrentHashMap,线程安全的hash表)


四、匹配器(Matcher)

        匹配器 用来处理整个匹配模块的功能。

1、玩家实力划分

        如今很多的竞技类游戏,都有段位,用来证明玩家的实力 的象征之一,也有其他的参数可以证明,例如战绩、KPI等等

        为了玩家的游戏体验、还有公平的游戏环境,所以要对不同水平的玩家进行划分

        因此,这里我们也设置一个段位的功能,用来划分不同实力区间的玩家,使用匹配三个队列进行划分:normalQueue(普通玩家)、highQueue(高水平玩家)、veryHighQueue(大神)

    private Queue<User> normalQueue = new LinkedList<>();private  Queue<User> highQueue = new LinkedList<>();private Queue<User> veryHighQueue = new LinkedList<>();

        既然这里要实现匹配功能,就是要给玩家分配对手,约定了上面这三种匹配队列,我们就可以把水平相当的玩家匹配到同一个房间中了。

2、加入匹配队列(add)

        根据上面的实力划分,就根据不同玩家的天梯区间积分,给加入到对应水平的匹配队列中了,代码如下:

    //  操作匹配队列的方法//  把玩家放到匹配队列中public void add(User user) {if(user.getScore() < 2000) {synchronized (normalQueue) {normalQueue.offer(user);normalQueue.notify();}log.info("把玩家 " + user.getUsername() + " 加入到了 normalQueue 中");} else if(user.getScore() >= 2000 && user.getScore() < 3000) {synchronized (highQueue) {highQueue.offer(user);highQueue.notify();}log.info("把玩家 " + user.getUsername() + " 加入到了 highQueue 中");} else {synchronized (veryHighQueue) {veryHighQueue.offer(user);veryHighQueue.notify();}log.info("把玩家 " + user.getUsername() + " 加入到了 veryHighQueue 中");}}

3、移除匹配队列(remove)

        既然有了加入匹配队列,对应的也要有删除,比如以下场景会用到:

1、玩家点击 停止匹配

2、玩家匹配成功,也需要把该玩家从匹配队列中删除

3、玩家在匹配的时候掉线了

    //  当玩家点击停止匹配,就需要把该玩家从匹配队列删除//  当匹配成功后,玩家进入房间,也需要把该玩家从匹配队列删除public void remove(User user) {if(user.getScore() < 2000) {synchronized (normalQueue) {normalQueue.remove(user);}log.info("玩家: " + user.getUsername() + " 在 normalQueue 队列被删除");} else if(user.getScore() >= 2000 && user.getScore() < 3000) {synchronized (highQueue) {highQueue.remove(user);}log.info("把玩家: " + user.getUsername() + " 在 highQueue 队列被删除");} else {synchronized (veryHighQueue) {veryHighQueue.remove(user);}log.info("把玩家: " + user.getUsername() + " 在 veryHighQueue 队列被删除");}}

4、创建三个线程

        创建三个线程,分别对这三个匹配队列进行扫描,看该队列中有没有2玩家,有就要把这两个玩家加入同一个房间中。

    public Matcher() {//  创建三个线程,分别针对这三个匹配队列,进行操作Thread t1 = new Thread() {@Overridepublic void run() {//  扫描normalQueuewhile(true) {handlerMatch(normalQueue);}}};t1.start();Thread t2 = new Thread() {@Overridepublic void run() {while (true) {handlerMatch(highQueue);}}};t2.start();Thread t3 = new Thread() {@Overridepublic void run() {while (true) {handlerMatch(veryHighQueue);}}};t3.start();}

        这三个线程是在构造方法中创建、启动的。一匹配就需要知道这三个匹配队列,分别有没有2个玩家,有就继续后面的操作,没有就要继续扫描。

5、加入房间(handlerMatch)

        这里涉及到忙等问题,在上篇博客有讲述。

(1)检测匹配队列是否有2个玩家

        在上面这三个线程扫描时,就会进来判断有没有玩家要不要加入匹配队列,判断逻辑如下:

                //  1、检测队列中元素个数是否达到 2//  队列的初始情况可能是 空//  如果往队列中添加一个元素,这个时候,仍然是不能进行后续匹配操作的//  因此在这里使用 while 循环检查更合理while (matchQueue.size() < 2) {matchQueue.wait();}

(2)取出匹配队列中的玩家

                //  2、尝试从队列中取出两个玩家User player1 = matchQueue.poll();User player2 = matchQueue.poll();log.info("匹配出两个玩家: " + player1.getUsername() + ", " + player2.getUsername());

(3)获取玩家的Session

        这里获取的玩家Session可能为null,因为玩家也可能在加入匹配队列后掉线了。

                //  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) {//  如果玩家1不在线了,就把玩家2放回匹配队列matchQueue.offer(player1);return;}//  当前能否排到两个玩家是同一个用户的情况吗?一个玩家入队列两次//  理论上也不会存在~//  1) 如果玩家下线,就会对玩家移除匹配队列//  2) 又禁止了玩家多开//  但是仍然在这里多进行一次判定,以免前面的逻辑出现 bug 时,带来严重的后果if(session1 == session2) {//  把其中的一个玩家放回匹配队列matchQueue.offer(player1);return;}

(4)双方玩家加入房间

                //  4、把这两个玩家放到同一个游戏房间中Room room = new Room();roomManager.add(room, player1.getUserId(), player2.getUserId());

(5)加入房间成功后,给客户端返回响应

                //  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));

五、处理 websocket 请求、返回的响应(MatchAPI)

        因为需要消息推送机制,所以我们使用了 websocket 协议,它既能满足消息推送机制,又能节省带宽资源,提高传输效率的协议。

        在建立 websocket 连接后,主要的方法有以下 4 个:

afterConnectionEstablished:在建立 websocket 连接时,要做的处理

handleTextMessage:接收玩家的 请求,返回对应的 响应。

handleTransportError:当 websocket 连接出现错误时,要做的处理

afterConnectionClosed:当 websocket 连接关闭时,要做的处理

1、afterConnectionEstablished

        建立websocket连接成功后,就要进行校验用户信息,如果是新登录的玩家,就把该玩家设为游戏大厅在线状态,在这个方法里面,要处理用户多开的问题未登录却直接访问游戏大厅的不合理操作

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、拿到了身份信息之后,进行判断当前用户是否已经登录过(在线状态),如果已经是在线,就不该继续进行后续逻辑if(onlineUserManager.getFromGameHall(user.getUserId()) != null|| onlineUserManager.getFromGameRoom(user.getUserId()) != null) {//  说明该用户已经登录了//  针对这个情况,要告知客户端,你这里重复登录了MatchResponse response = new MatchResponse();response.setOk(true);response.setReason("当前用户已经登录, 静止多开!");response.setMessage("repeatConnection");session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));// 此处直接关闭有些太激进了,还是返回一个特殊的 message,供客户端来进行处理//session.close();return;}onlineUserManager.enterGameHall(user.getUserId(), session);
//            System.out.println("玩家" + user.getUsername() + " 进入游戏大厅");log.info("玩家 {}",user.getUsername() + " 进入游戏大厅");} catch (NullPointerException e) {//e.printStackTrace();log.info("[MatchAPI.afterConnectionEstablished] 当前用户未登录");// 出现空指针异常,说明当前用户的身份信息为空,也就是用户未登录// 就把当前用户尚未登录,给返回回去MatchResponse response = new MatchResponse();response.setOk(false);response.setReason("您尚未登录,不能进行后续匹配");session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(response)));}}

2、afterConnectionClosed

        既然关闭了 websocket 连接,也就意味着玩家下线了,要把当前玩家从游戏大厅的在线状态给删除掉;如果是在匹配中,不经要删除游戏大厅的在线状态,还要在匹配队列删除该玩家。

        public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {// 玩家下线,删除 OnlineUserManager 中的该用户的Sessiontry {User user = (User) session.getAttributes().get("user");WebSocketSession tmpSession = onlineUserManager.getFromGameHall(user.getUserId());if(tmpSession == session) {onlineUserManager.exitGameHall(user.getUserId());}// 如果玩家正在匹配中,但WebSocket断开了,就应该把该玩家移除匹配队列log.info("Closed玩家: {}", user.getUsername() + " 下线");matcher.remove(user);} catch (NullPointerException e) {log.info("[MatchAPI.afterConnectionClosed] 当前用户未登录");}

3、handleTransportError

        既然连接出现错误了,那么也肯定要把玩家的游戏大厅在线状态给删除掉,如果在匹配,匹配队列也应该删掉该玩家,代码逻辑和关闭连接一样。

    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {// 玩家下线,删除 OnlineUserManager 中的该用户的Sessiontry {User user = (User) session.getAttributes().get("user");WebSocketSession tmpSession = onlineUserManager.getFromGameHall(user.getUserId());if(tmpSession == session) {onlineUserManager.exitGameHall(user.getUserId());}// 如果玩家正在匹配中,但WebSocket断开了,就应该把该玩家移除匹配队列log.info("Error玩家: {}", user.getUsername() + " 下线");matcher.remove(user);} catch (NullPointerException e) {log.info("[MatchAPI.handleTransportError] 当前用户未登录");}}

4、handleTextMessage

        这里才是真正的处理 websocket 请求、返回对应响应的逻辑。(处理开始匹配请求 和 停止匹配请求,返回对应响应)

        1、首先要拿到用户信息以及用户发来的请求,约定了发送过来的是:startMatch、stopMatch

startMatch:就要把玩家加入到匹配队列中,同时构造返回响应,返回给客户端

stopMatch:就要把玩家从匹配队列中删除,同时构造返回响应,返回给客户端

    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {//  实现处理开始匹配请求和停止匹配请求User user = (User) session.getAttributes().get("user");//  拿到客户端发给服务器的数据String payload = message.getPayload();//  当前传过来的数据是JSON格式的字符串,就需要把它转成 Java 对象:MatchRequestMatchRequest 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("非法的匹配请求");}String jsonString = objectMapper.writeValueAsString(response);session.sendMessage(new TextMessage(jsonString));}

六、前端代码的逻辑处理

1、建立连接

        通过指定 websocketURL,和后端的 websocket 路径保存一致,这样就能和后端建立websocket连接了。

        // 此处进行初始化 websocket,并且实现前端的匹配逻辑// 此处的路径必须写作 /findMatchlet websocketUrl = "ws://" + location.host + "/findMatch";let websocket = new WebSocket(websocketUrl);websocket.onopen = function () {console.log("onopen");}websocket.onclose = function () {console.log("onclose");}websocket.onerror = function () {console.log("onerror");}// 监听页面关闭事件,在页面关闭之前,手动调动这里的 websocket 的 close 方法window.onbeforeunload = function () {websocket.close();}//一会重点来实现,要处理服务器返回的响应websocket.onmessage = function (e) {//用来处理响应,下面会介绍}

        事件:

websocket.open连接建立时,客户端这边进行的操作

websocket.onclose连接关闭时,客户端这边的操作

websocket.onerror连接错误时,客户端这边的操作

websocket.onmessage发送请求

window.onbeforeunload箭头页面关闭事件,这里让页面关闭后,断开websocket连接

2、发送请求

        这里有个点击事件当点击 “开始匹配” 按钮,就会发送 “startMatch” 数据,当点击 匹配中...(点击停止匹配),就会发送 “stopMatch” 数据

        // 给匹配按钮添加一个点击事件let matchButton = document.querySelector('#match-button');matchButton.onclick = function () {//在触发 websocket 请求之前,先确认下 websocket 连接是否好着if (websocket.readyState == websocket.OPEN) {//如果当前 readyState 处在 OPPEN状态,说明连接是好着的//这里发送的数据有两种可能,开始匹配/停止匹配if (matchButton.innerHTML == '开始匹配') {console.log("开始匹配");websocket.send(JSON.stringify({message: 'startMatch',}))} else if (matchButton.innerHTML == '匹配中...(点击停止)') {console.log("停止匹配");websocket.send(JSON.stringify({message: 'stopMatch',}));}} else {//这是说明当前连接是异常的状态alert("当前您的连接已经断开! 请重新登录!");// location.assign('/login.html');location.replace("/login.html");}}

        开启这个点击事件的前提是websocket连接 是正常的,如果是异常状态,说明玩家掉线了,那就给个弹窗,然后返回到登录页面,进行重新登录

3、处理响应

        处理响应,说明玩家发送的 开始/停止匹配 请求后端收到了,并发送过来了。

        响应状态有2种,一种resp.ok == false:可能是客户端这里没有进行登录,直接进入游戏大厅页面,导致后端拿到的User=null,这时候就直接给出提示弹窗,然后返回登录页面

        另一种响应状态,resp.ok == true:这时候又会有五个分支

resp.message='startMatch'这时候就要把客户端的“开始匹配”按钮,转为“匹配中...(点击停止)”按钮

resp.message='stopMatch'这时候就要把客户端的“匹配中...(点击停止)”按钮,转为“开始匹配”按钮

resp.message='matchSuccess'说明匹配到对手了,进入游戏房间页面

resp.message='repeatConnection'说明用户多开情况,给出提示弹窗,跳转到登录页面

上面这些情况都不是说明出现了我们意料之外的bug,打印一个日志信息

        //一会重点来实现,要处理服务器返回的响应websocket.onmessage = function (e) {// 处理服务器返回的响应数据,这个响应就是针对 "开始匹配" / "结束匹配" 来应对的//解析得到的响应对象,返回的数据是一个 JSON 字符串,解析成 js 对象let resp = JSON.parse(e.data);let matchButton = document.querySelector("#match-button");if (!resp.ok) {console.log("游戏大厅中接收到了失败响应! " + resp.reason);alert("游戏大厅中接收到了失败响应! " + resp.reason);location.replace("/login.html");return;}if (resp.message == 'startMatch') {//开始匹配请求发起成功console.log("进入匹配队列成功");matchButton.innerHTML = '匹配中...(点击停止)';} else if (resp.message == 'stopMatch') {//结束匹配请求发起成功console.log("离开匹配队列成功");matchButton.innerHTML = '开始匹配';} else if (resp.message == 'matchSuccess') {//已经匹配到对手了console.log("匹配到对手! 进入游戏房间");// location.assign("/game_room.html");location.replace("/game_room.html");} else if (resp.message == 'repeatConnection') {// 多开的情况alert("当前检测到多开,请使用其他账号登录!");// location.assign("/login.html");location.replace("/login.html");} else {console.log("收到了非法的响应! message=" + resp.message);}}

七、梳理前后端交互流程

        以下流程保证是在预期逻辑下的匹配过程,不涉及异常等错误情况。

1、玩家的匹配

(1)建立连接

        进入登录页面,输入账号密码:

进入游戏页面,建立websocket连接:(后端拿到玩家信息,把该玩家设置为大厅在线状态

        执行到这一步,客户端、服务器的websocket连接建立成功。

(2)玩家发起请求

        玩家进入游戏大厅后,点击“开始匹配”按钮


        发送 websocket 请求:带有“startMatch”的数据

(3)服务器处理请求

        服务器接收到前端发来的 message=‘startMatch’ 字样信息,把该玩家加入到匹配队列中

        加入匹配队列:

        不断扫描线程,看匹配队列有没有2个玩家:

(这里只分析匹配队列有1个玩家的情况)

        构造响应数据,关键字样:ok=true,message=‘startMatch’把该响应数据发送给客户端

(4)客户端处理响应

        客户端接收到服务器的返回的响应校验响应数据:message=‘startMatch’,就把“开始匹配”按钮修改为“匹配中...(停止匹配)”(修改的是html文本信息)。

        修改后:

        以上就是每个玩家都会进入的匹配流程

2、玩家匹配成功

        当有玩家匹配成功时,就要把这两个玩家加入同一个游戏房间中。

        此时也会跳转到游戏房间页面,如图:(目前还没介绍对局模块,后面博客会介绍)

        流程介绍:玩家匹配流程上面已经介绍,下面主要介绍匹配成功的流程

2.1 后端处理请求

 (1)服务器发现匹配队列有2个玩家

        两个玩家都进行匹配,服务器会把用户都加进对应匹配队列中:

        扫描线程:

        发现匹配队列有2个玩家了:

        跳出这个循环,进行后面的逻辑操作。

(2)两个玩家加入同一个游戏房间

        先把两个玩家从匹配队列拿出来:

        获取到对应玩家的Session:

        把这两个玩家加入到同一个游戏房间:

        构造响应,给玩家返回响应:

        设置关键信息:message=‘matchSuccess’

2.2 客户端处理响应

        客户端这边拿到关键信息:message=‘matchSuccess’

        那就跳转到游戏房间页面。如图:

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

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

相关文章

解决ModuleNotFoundError: No module named ‘torchcrf‘

运行深度学习程序时候&#xff0c;出现报错&#xff1a;ModuleNotFoundError: No module named torchcrf 将 from torchcrf import CRF 改为 from TorchCRF import CRF

C#案例 | 基于C#语言在Excel中进行二次开发(一):简单系统搭建:打印输出“Hello Excel C#”

基于C#语言在Excel中进行二次开发&#xff08;一&#xff09;&#xff1a;简单系统搭建&#xff1a;打印输出”Hello Excel & C#” 实现效果第一步&#xff1a;前期准备第二步&#xff1a;打开VS 2022&#xff0c;创建项目第三步&#xff1a;程序界面设计 实现效果 在Exce…

《蓝桥杯算法入门》(C/C++、Java、Python三个版本)24年10月出版

推荐&#xff1a;《算法竞赛》&#xff0c;算法竞赛大全书&#xff0c;网购&#xff1a;京东 天猫  当当 文章目录 《蓝桥杯算法入门》内容简介本书读者对象作者简介联系与交流《蓝桥杯算法入门 C/C》版目录 《蓝桥杯算法入门 Java》版目录 《蓝桥杯算法入门 Python》版目录 …

STM32器件支持包安装,STLINK/JLINK驱动安装

一、支持包安装 1、离线安装 先下载支持包之后&#xff0c;再进行安装。如下图要安装STM32F1系列&#xff0c;双击 出现如下&#xff0c;会自动锁定安装路径&#xff0c;然后点击下一步&#xff0c;直接安装。 2、在线安装 首先需要电脑联网。如下。先点击第一个红框绿色按钮…

【EXCEL数据处理】000011 案列 EXCEL带有三角形图标的单元格转换,和文本日期格式转换。

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 【EXCEL数据处理】000011 案列 EXCEL带有三角形图标的单元格转换。使用…

[uni-app]小兔鲜-04推荐+分类+详情

热门推荐 新建热门推荐组件, 动态设置组件的标题 <template><!-- 推荐专区 --><view class"panel hot"><view class"item" v-for"item in list" :key"item.id">... ...<navigator hover-class"none&…

Pikachu-Cross-Site Scripting-DOM型xss

DOM型xss DOM型XSS漏洞是一种特殊类型的XSS,是基于文档对象模型 Document Object Model (DOM)的一种漏洞。是一个与平台、编程语言无关的接口&#xff0c;它允许程序或脚本动态地访问和更新文档内容、结构和样式&#xff0c;处理后的结果能够成为显示页面的一部分。 dom就是一…

物联网将如何影响全球商业?

互联网使人们能够交流&#xff0c;企业能够全天候不间断地跨洋跨洲持续运营。它重塑、颠覆并催生了新的产业&#xff0c;改变了人类与世界互动的方式。互联网曾经仅仅是一种方便、快捷、廉价的向世界各地发送信息的方式&#xff0c;而现在&#xff0c;只需打开或关闭任何连接到…

thinkphp6入门(25)-- 分组查询 GROUP_CONCAT

假设表名为 user_courses&#xff0c;字段为 user_id 和 course_name&#xff0c;存储每个用户选修的课程&#xff0c;想查询每个学生选修的所有课程 SQL 原生查询 SELECT user_id, GROUP_CONCAT(course_name) as courses FROM user_courses GROUP BY user_id; ThinkPHP 代码…

汇编语言知识(王爽第四版)

汇编语言&#xff0c;当然&#xff0c;我们学习是在c语言的基础上&#xff0c;那么&#xff0c;我们就先复习一下c语言的知识 C语言的基础&#xff0c;进制转换必不可少 数组&#xff0c;函数…… 接下来&#xff0c;我们学习了数据结构&#xff1a;顺序表&#xff0c;链表&…

Ubuntu/Debian网络配置(补充篇)

Ubuntu/Debian网络配置补充 在《Ubuntu/Debian网络配置 & Ubuntu禁用自动更新_ubuntu nmtui-CSDN博客》上总结的“配置网络”章节&#xff0c;对于新版本或者“最小化安装”场景&#xff0c;可能不适应&#xff0c;故此本文做一下补充&#xff0c;就不在原有文章上做更新了…

【数据结构】什么是平衡二叉搜索树(AVL Tree)?

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:数据结构 ⚙️操作环境:Visual Studio 2022 目录 &#x1f4cc;AVL树的概念 &#x1f4cc;AVL树的操作 &#x1f38f;AVL树的插入操作 ↩️右单旋 ↩️↪️右左双旋 ↪️↩️左右双旋 ↪️左单旋 &#x1f38f;AVL树的删…

平面电磁波(解麦克斯韦方程)

注意无源代表你立方程那个点xyzt处没有源&#xff0c;电场磁场也是这个点的。 j电流面密度&#xff0c;电流除以单位面积&#xff0c;ρ电荷体密度&#xff0c;电荷除以单位体积。 j方程组有16个未知数&#xff0c;每个矢量有三个xyz分量&#xff0c;即三个未知数&#xff0c;…

在idea使用nacos微服务

一.安装nacos 、依赖记得别放<dependencyManagement></dependencyManagement>这个标签去了 1.在linux拉取镜像安装 docker pull nacos/nacos-server:1.3.1 2.创建挂载目录 mkdir -p /usr/local/docker/nacos/init.d /usr/local/docker/nacos/logs 3.安装nacos…

GGHead:基于3D高斯的快速可泛化3D数字人生成技术

随着虚拟现实(VR)、增强现实(AR)和数字人技术的发展,对高质量、实时生成的3D头部模型的需求日益增长。传统的3D生成方法往往依赖于复杂的2D超分辨率网络或大量的3D数据,这不仅增加了计算成本,还限制了生成速度和灵活性。为了解决这些问题,研究人员开发了一种名为GGHead…

加密与安全_TOTP 一次性密码生成算法

文章目录 PreTOTP是什么TOTP 算法工作原理TOTP 生成公式TOTP 与 HOTP 的对比Code生成TOTP验证 TOTP使用场景小结 TOTP 与 HOTP 的主要区别TOTP 与 HOTP应用场景比较TOTP 与 HOTP安全性分析 Pre 加密与安全_HTOP 一次性密码生成算法 https://github.com/samdjstevens/java-tot…

gdb 调试 linux 应用程序的技巧介绍

使用 gdb 来调试 Linux 应用程序时&#xff0c;可以显著提高开发和调试的效率。gdb&#xff08;GNU 调试器&#xff09;是一款功能强大的调试工具&#xff0c;适用于调试各类 C、C 程序。它允许我们在运行程序时检查其状态&#xff0c;设置断点&#xff0c;跟踪变量值的变化&am…

指针 (5)

目录 1. 字符指针变量 2. 数组指针变量 3. ⼆维数组传参的本质 4. 函数指针变量 5.typedef 关键字 6 函数指针数组 7.转移表 计算器的⼀般实现 1. 字符指针变量 在指针的类型中我们知道有⼀种指针类型为字符指针 char* #include <stdio.h> int main() {char* ch …

VB.net读写NDEF标签URI智能海报WIFI蓝牙连接

本示例使用的发卡器&#xff1a;https://item.taobao.com/item.htm?ftt&id615391857885 Public Class Form1Dim oldpicckey(0 To 5) As Byte 卡片旧密码Dim newpicckey(0 To 5) As Byte 卡片新密码Function GetTagUID() As StringDim status As ByteDim myctrlword As …

矩阵系统源码搭建的具体步骤,支持oem,源码搭建

一、前期准备 明确需求 确定矩阵系统的具体用途&#xff0c;例如是用于社交媒体管理、电商营销还是其他领域。梳理所需的功能模块&#xff0c;如多账号管理、内容发布、数据分析等。 技术选型 选择适合的编程语言&#xff0c;如 Python、Java、Node.js 等。确定数据库类型&…