五子棋双人对战项目(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.rhkb.cn/news/437648.html

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

相关文章

【C语言指南】数据类型详解(上)——内置类型

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《C语言指南》 期待您的关注 目录 引言 1. 整型&#xff08;Integer Types&#xff09; 2. 浮点型&#xff08;Floating-Point …

java发送邮件email实战

1.首先在项目中增加依赖&#xff0c;在pom文件中添加如下坐标 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency>2.发邮件工具类如下 package com.example.demo.…

力扣 —— 多数元素 轮转数组

多数元素 题目(简单) 给定一个大小为 n 的数组 nums &#xff0c;返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 你可以假设数组是非空的&#xff0c;并且给定的数组总是存在多数元素。 示例 1&#xff1a; 输入&#xff1a; nums [3,2,3] 输…

第24天sql注入(小迪安全学习)

前置知识&#xff08;搭建环境失败&#xff0c;搞不了实验了&#xff0c;学下理论知识吧&#xff09; sql注入 脚本代码在实现代码与数据库进行数据通讯时&#xff08;从数据库中取出相关数据&#xff09;&#xff0c;将定义的SQL语句进行执行查询数据时其中的SQL语句能通过参…

VSOMEIP代码阅读整理(1) - 网卡状态监听

一. 概述 在routing进程所使用的配置文件中&#xff0c;存在如下配置项目&#xff1a;{"unicast" : "192.168.56.101",..."service-discovery" :{"enable" : "true","multicast" : "224.244.224.245",…

Oracle 表空间时间点恢复

已有一个数据库全备&#xff0c;在PDB中恢复被drop掉的表空间 1.新建表空间 create tablespace PITR_TBS datafile /u01/app/oracle/oradata/PRODCDB/PDBPROD2/PITR_TBS01.dbf size 10m; 2.使用RMAN备份. backup as compressed backupset database INCLUDE CURRENT CONTROLFI…

从零开始搭建UVM平台(八)-加入agent

书接上回&#xff1a; 从零开始搭建UVM平台&#xff08;一&#xff09;-只有uvm_driver的验证平台 从零开始搭建UVM平台&#xff08;二&#xff09;-加入factory机制 从零开始搭建UVM平台&#xff08;三&#xff09;-加入objection机制 从零开始搭建UVM平台&#xff08;四&…

Cortex微控制器软件接口标准(CMSIS)

Cortex微控制器软件接口标准 目前&#xff0c;软件开发已经是嵌入式系统行业公认的主要开发成本&#xff0c;通过将所有Cortex-M芯片供应商产品的软件接口标准化&#xff0c;能有效降低这一成本&#xff0c;尤其是进行新产品开发或者将现有项目或软件移植到基于不同厂商MCU的产…

react-问卷星项目(4)

项目实战 使用CSS 尽量不要使用内联CSS 内联style代码多&#xff0c;性能差&#xff0c;扩展性差外链css文件可复用代码&#xff0c;可单独缓存文件 元素内联style 和HTMl元素的style相似必须用JS写法&#xff0c;不能是字符串&#xff0c;里面必须是对象 <span style…

进阶数据库系列(十三):PostgreSQL 分区分表

概述 在组件开发迭代的过程中&#xff0c;随着使用时间的增加&#xff0c;数据库中的数据量也不断增加&#xff0c;因此数据库查询越来越慢。 通常加速数据库的方法很多&#xff0c;如添加特定的索引&#xff0c;将日志目录换到单独的磁盘分区&#xff0c;调整数据库引擎的参…

无人化焦炉四大车系统 武汉正向科技 工业机车无人远程控制系统

焦炉四大车无人化系统介绍 采用格雷母线光编码尺双冗余定位技术&#xff0c;炉门视觉定位自学习技术&#xff0c;wifi5G无线通讯技术&#xff0c;激光雷达安全识别技术&#xff0c;焦化智慧调度&#xff0c;手机APP监控功能。 焦炉四大车无人化系统功能 该系统能自动生成生产…

IDTL:茶叶病害识别数据集(猫脸码客 第205期)

Identifying Disease in Tea Leaves茶叶病害识别数据集 一、引言 在农业领域&#xff0c;茶叶作为一种重要的经济作物&#xff0c;其生产过程中的病害防治是确保茶叶质量和产量的关键环节。然而&#xff0c;传统的病害识别方法主要依赖于人工观察和经验判断&#xff0c;这不仅…

Nature Machine Intelligence 基于强化学习的扑翼无人机机翼应变飞行控制

尽管无人机技术发展迅速&#xff0c;但复制生物飞行的动态控制和风力感应能力&#xff0c;仍然遥不可及。生物学研究表明&#xff0c;昆虫翅膀上有机械感受器&#xff0c;即钟形感受器campaniform sensilla&#xff0c;探测飞行敏捷性至关重要的复杂气动载荷。 近日&#xff0…

STM32引脚PB3、PB4、PA15作为输入输出的特殊配置

一、问题描述 简单描述&#xff1a; 最近做的一个项目中&#xff0c;PB3端口配置为输入&#xff0c;不管外部输入是高电平还是低电平&#xff0c;一直读取到的是低电平。 调试过程&#xff1a;在撰写代码过程中&#xff0c;又发现新的问题&#xff0c;Enter按键无法控制屏幕数…

【Python】ftfy 使用指南:修复 Unicode 编码问题

ftfy&#xff08;fixes text for you&#xff09;是一个专为修复各种文本编码错误而设计的 Python 工具。它的主要目标是将损坏的 Unicode 文本恢复为正确的 Unicode 格式。ftfy 并非用于处理非 Unicode 编码&#xff0c;而是旨在修复因为编码不一致、解码错误或混合编码导致的…

物流行业中的AI平台架构与智能化应用

随着物流行业的迅速发展&#xff0c;尤其是电商、仓储、运输的需求日益增多&#xff0c;AI技术逐渐成为推动物流企业高效运营、提升服务水平的关键力量。AI平台架构为物流行业的各个环节提供了智能化解决方案&#xff0c;助力物流企业在仓储管理、运输调度、客户服务等方面实现…

Redis: Sentinel工作原理和故障迁移流程

Sentinel 哨兵几个核心概念 1 ) 定时任务 Sentinel 它是如何工作的&#xff0c;是如何感知到其他的 Sentinel 节点以及 Master/Slave节点的就是通过它的一系列定时任务来做到的&#xff0c;它内部有三个定时任务 第一个就是每一秒每个 Sentinel 对其他 Sentinel 和 Redis 节点…

【2023工业3D异常检测文献】Shape-Guided: 基于形状引导和双记忆库的异常检测方法

Shape-Guided Dual-Memory Learning for 3D Anomaly Detection 1、Background 提出了一个以形状为指导的专家学习框架&#xff0c;用于解决无监督3D异常检测的问题。 该方法建立在两个专门的专家模型及其协同作用的基础上&#xff0c;以从颜色和形状模态中定位异常区域。 第…

基于单片机跑步机控制系统设计

** 文章目录 前言概要功能设计设计思路 软件设计效果图 程序文章目录 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师&#xff0c;一名热衷于单片机技术探索与分享的博主、专注于 精通51/STM32/MSP430/AVR等单片机设计 主要对…

详细分析BigDecimal基本知识(附Demo)

目录 前言1. 基本知识2. Demo 前言 之所以深入了解这个函数的用法&#xff0c;发现还可这么使用 基本的Java知识推荐阅读&#xff1a; java框架 零基础从入门到精通的学习路线 附开源项目面经等&#xff08;超全&#xff09;【Java项目】实战CRUD的功能整理&#xff08;持续更…