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

news/2024/10/7 16:50:44/

目录

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

websocket%E8%BF%9E%E6%8E%A5%E8%B7%AF%E5%BE%84-toc" style="margin-left:40px;">1、websocket连接路径

2、构造请求、响应对象

二、用户在线状态管理

三、房间管理

1、房间类:

2、房间管理器:

四、匹配器(Matcher)

1、玩家实力划分

2、加入匹配队列(add)

3、移除匹配队列(remove)

4、创建三个线程

5、加入房间(handlerMatch)

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

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

(3)获取玩家的Session

(4)双方玩家加入房间

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

websocket%20%E8%AF%B7%E6%B1%82%E3%80%81%E8%BF%94%E5%9B%9E%E7%9A%84%E5%93%8D%E5%BA%94(MatchAPI)-toc" style="margin-left:0px;">五、处理 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 客户端处理响应


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

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

websocket%E8%BF%9E%E6%8E%A5%E8%B7%AF%E5%BE%84">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%20%E8%AF%B7%E6%B1%82%E3%80%81%E8%BF%94%E5%9B%9E%E7%9A%84%E5%93%8D%E5%BA%94(MatchAPI)">五、处理 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.ppmy.cn/news/1535782.html

相关文章

指针(6)

目录 1. 回调函数是什么&#xff1f; 2. qsort 使⽤举例 使⽤qsort函数排序整型数据 ​编辑3. qsort函数的模拟实现 1. 回调函数是什么&#xff1f; 回调函数就是⼀个通过函数指针调⽤的函数。 如果你把函数的指针&#xff08;地址&#xff09;作为参数传递给另⼀个函数&…

高防服务器的优劣势有哪些?

高防服务器是专门用于防御分布式拒绝服务攻击和其他网络攻击所设计的服务器&#xff0c;高防服务器可以用于保护企业网站和应用不会受到网络攻击&#xff0c;但是高防服务器咋某些方面还是有着一些不足的&#xff0c;下面我们就来一起了解一下吧&#xff01; 高防服务器通常都具…

php基础语法

PHP 是一种常用于 Web 开发的服务器端脚本语言&#xff0c;具有易于学习、灵活性强等特点。以下是 PHP 的基础语法详细介绍。 1. PHP 基本语法 PHP 文件扩展名&#xff1a;PHP 文件通常以 .php 作为扩展名。 PHP 标签&#xff1a;PHP 代码通常嵌入到 HTML 中&#xff0c;PHP …

如何使用Redisson的布隆过滤器?

封装布隆过滤器 /*** 创建布隆过滤器** param filterName - 过滤器名称* param expectedInsertions - 预测插入数量* param falsePositiveRate - 误判率* author 付聪* time 2024-09-20 17:24:10*/ <T> RBloomFilter<T> createBloomFilter(String filterName, Lon…

自动驾驶系列—自动驾驶背后的数据通道:通信总线技术详解与应用场景分析

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

使用微服务Spring Cloud集成Kafka实现异步通信(消费者)

1、本文架构 本文目标是使用微服务Spring Cloud集成Kafka实现异步通信。其中Kafka Server部署在Ubuntu虚拟机上&#xff0c;微服务部署在Windows 11系统上&#xff0c;Kafka Producer微服务和Kafka Consumer微服务分别注册到Eureka注册中心。Kafka Producer和Kafka Consumer之…

私家车开车回家过节会发生什么事情

自驾旅行或者是自驾车回家过节路程太远。长途奔袭的私家车损耗很大。新能源汽车开始涉足电力系统和燃电混动的能源供应过渡方式。汽车在路途中出现零件故障。计划的出发日程天气原因。台风是否会提醒和注意。汽车的油站供应链和电力充电桩的漫长充电过程。高速公路的收费站和不…

Docker版MKVtoolnix的安装及中文显示

本文是应网友 kkkhi 要求折腾的&#xff0c;只研究了 MKVtoolnix 的安装及中文显示&#xff0c;未涉及到软件的使用&#xff1b; 什么是 MKVtoolnix &#xff1f; MKVToolnix 是一款功能强大的多媒体处理工具&#xff0c;用于在 Linux、其他 Unix 系统和 Windows 上创建、修改和…