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

news2024/12/25 0:56:54

目录

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

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,并且实现前端的匹配逻辑
        // 此处的路径必须写作 /findMatch
        let websocketUrl = "ws://" + location.host + "/findMatch";
        let websocket = new WebSocket(websocketUrl);

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

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private MatchAPI matchAPI;

    @Override
    public 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 来作为房间 id
        roomId = 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 映射关系不存在,直接返回 null
           return 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() {
            @Override
            public void run() {
                //  扫描normalQueue
                while(true) {
                    handlerMatch(normalQueue);
                }
            }
        };
        t1.start();

        Thread t2 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    handlerMatch(highQueue);
                }
            }
        };
        t2.start();

        Thread t3 = new Thread() {
            @Override
            public 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 中的该用户的Session
        try {
            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 中的该用户的Session
        try {
            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 对象: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("非法的匹配请求");
        }

        String jsonString = objectMapper.writeValueAsString(response);
        session.sendMessage(new TextMessage(jsonString));
    }

六、前端代码的逻辑处理

1、建立连接

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

        // 此处进行初始化 websocket,并且实现前端的匹配逻辑
        // 此处的路径必须写作 /findMatch
        let 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.coloradmin.cn/o/2186115.html

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

相关文章

golang grpc初体验

grpc 是一个高性能、开源和通用的 RPC 框架&#xff0c;面向服务端和移动端&#xff0c;基于 HTTP/2 设计。目前支持c、java和go&#xff0c;分别是grpc、grpc-java、grpc-go&#xff0c;目前c版本支持c、c、node.js、ruby、python、objective-c、php和c#。grpc官网 grpc-go P…

Linux相关概念和重要知识点(11)(进程调度、Linux内核链表)

1.Linux调度算法 上篇文章我粗略讲过queue[140]的结构&#xff0c;根据哈希表&#xff0c;我们可以将40个不同优先级的进程借助哈希桶链入queue[140]中。调度器会根据queue的下标来进行调度。但这个具体的调度过程是怎样的呢&#xff1f;以及runqueue和queue[140]的关系是什么…

谷歌给到的185个使用生成式AI的案例

很多公司从利用AI回答问题&#xff0c;进而使用AI进行预测&#xff0c;向使用生成式AI Agent转变。AI Agent的独特之处在于它们可以采取行动以实现特定目标&#xff0c;比如引导购物者找到合适的鞋子&#xff0c;帮助员工寻找合适的健康福利&#xff0c;或在护理人员交接班期间…

python之输入输出

1、输入 Python在控制台输入内容&#xff0c;需要使用input函数。input函数会在控制台等待用户输入&#xff0c;直到用户按下了回车键才算完成输入。 注意&#xff1a;input函数接收的内容为字符串。 str1 input("请输入内容\n") print(str1) print(type(str1))1…

Python酷库之旅-第三方库Pandas(132)

目录 一、用法精讲 591、pandas.DataFrame.plot方法 591-1、语法 591-2、参数 591-3、功能 591-4、返回值 591-5、说明 591-6、用法 591-6-1、数据准备 591-6-2、代码示例 591-6-3、结果输出 592、pandas.DataFrame.plot.area方法 592-1、语法 592-2、参数 592-…

9.28学习笔记

1.ping 网址 2.ssh nscc/l20 3.crtl,打开vscode的setting 4.win 10修改ssh配置文件及其密钥权限为600 - 晴云孤魂 - 博客园 整体来看&#xff1a; 使用transformer作为其主干网络&#xff0c;代替了原先的UNet 在latent space进行训练&#xff0c;通过transformer处理潜…

查缺补漏----该不该考虑不可屏蔽中断

可以看看这个视频&#xff1a; 讨论中断时&#xff0c;该不该考虑不可屏蔽中断&#xff1f;_哔哩哔哩_bilibili 首先要知道一个概念&#xff1a;可屏蔽中断和不可屏蔽中断 可屏蔽中断&#xff1a; 可屏蔽中断是可通过中断屏蔽字来启用或禁用的中断。对于多级中断而言&#…

①EtherCAT转ModbusTCP, EtherCAT/Ethernet/IP/Profinet/ModbusTCP协议互转工业串口网关

EtherCAT/Ethernet/IP/Profinet/ModbusTCP协议互转工业串口网关https://item.taobao.com/item.htm?ftt&id822721028899 协议转换通信网关 EtherCAT 转 ModbusTCP GW系列型号 MS-GW15 简介 MS-GW15 是 EtherCAT 和 Modbus TCP 协议转换网关&#xff0c;为用户提供一种 …

map_set的使用

map_set的使用 关联式容器树形结构的关联式容器setset的介绍set的使用 multisetmultiset的介绍multiset的使用 mapmap的介绍map的使用键值对 multimapmultimap的介绍 &#x1f30f;个人博客主页&#xff1a;个人主页 关联式容器 在初阶阶段&#xff0c;我们已经接触过STL中的部…

黑科技外绘神器:一键扩展图像边界

黑科技外绘神器&#xff1a;一键扩展图像边界 Diffusers Image Outpaint✨是一个开源工具&#xff0c;能智能扩展图像边界&#xff0c;创造完美视觉效果&#x1f3de;️。用户可自定义风格&#xff0c;生成高清图像&#x1f929;&#xff0c;应用场景广泛&#xff0c;释放你的…

大模型~合集6

我自己的原文哦~ https://blog.51cto.com/whaosoft/11566566 # 深度模型融合&#xff08;LLM/基础模型/联邦学习/微调等&#xff09; 23年9月国防科大、京东和北理工的论文“Deep Model Fusion: A Survey”。 深度模型融合/合并是一种新兴技术&#xff0c;它将多个深度学习模…

爬虫——爬取小音乐网站

爬虫有几部分功能&#xff1f;&#xff1f;&#xff1f; 1.发请求&#xff0c;获得网页源码 #1.和2是在一步的 发请求成功了之后就能直接获得网页源码 2.解析我们想要的数据 3.按照需求保存 注意&#xff1a;开始爬虫前&#xff0c;需要给其封装 headers {User-…

本地化测试对游戏漏洞修复的影响

本地化测试在游戏开发的质量保证过程中起着至关重要的作用&#xff0c;尤其是在修复bug方面。当游戏为全球市场做准备时&#xff0c;它们通常会被翻译和改编成各种语言和文化背景。这种本地化带来了新的挑战&#xff0c;例如潜在的语言错误、文化误解&#xff0c;甚至是不同地区…

C++ 双端队列(deque)的深入理解

前言&#xff1a; 双端队列deque看起来是一个相当牛的容器&#xff0c;表面看起来将list和vector进行结合起来&#xff0c;形成了一个看起来很完美的容器&#xff0c;但是事实不是这样&#xff0c;要是deque如此完美&#xff0c;数据结构也就没list和vector的事情了&#xff0c…

多系统萎缩患者必看!这些维生素助你对抗病魔

亲爱的朋友们&#xff0c;今天我们来聊聊一个相对陌生但重要的健康话题——多系统萎缩&#xff08;MSA&#xff09;。这是一种罕见的神经系统疾病&#xff0c;影响着患者的自主神经系统、运动系统和平衡功能。面对这样的挑战&#xff0c;科学合理的饮食和营养补充显得尤为重要。…

暴力数据结构——AVL树

1.认识AVL树 AVL树最先发明的⾃平衡⼆叉查找树,AVL可以是⼀颗空树,或者具备下列性质的⼆叉搜索树&#xff1a; • 它的左右⼦树都是AV树&#xff0c;且左右⼦树的⾼度差的绝对值不超过1 • AVL树是⼀颗⾼度平衡搜索⼆叉树&#xff0c; 通过控制⾼度差去控制平衡 AVL树整体结点…

路由交换实验指南

案例 01&#xff1a;部署使用 eNSP 平台实验需求&#xff1a; 安装华为 eNSP 网络模拟平台打开 eNSP 平台&#xff0c;新建拓扑并绘制网络能够成功启动交换机、计算机设备 实验步骤&#xff1a; 安装华为 eNSP 网络模拟平台启动安装程序 配置安装内容 防护墙允许 eNSP 程序的…

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

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

从零开始实现RPC框架---------项目介绍及环境准备

一&#xff0c;介绍 RPC&#xff08;Remote Procedure Call&#xff09;远程过程调⽤&#xff0c;是⼀种通过⽹络从远程计算机上请求服务&#xff0c;⽽不需要 了解底层⽹络通信细节。RPC可以使⽤多种⽹络协议进⾏通信&#xff0c; 如HTTP、TCP、UDP等&#xff0c; 并且在 TCP/…

匿名方法与Lambda表达式+泛型委托

匿名方法 和委托搭配使用&#xff0c;方便我们快速对委托进行传参&#xff0c;不需要我们定义一个新的函数&#xff0c;直接用delegate关键字代替方法名&#xff0c;后面跟上参数列表与方法体。 格式&#xff1a;delegate(参数列表){方法体} lambda表达式 是匿名方法的升级…