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

目录

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

1、websocket连接路径

2、构造请求、响应对象

二、用户在线状态管理

三、房间管理

1、房间类:

2、房间管理器:

四、匹配器(Matcher)

1、玩家实力划分

2、加入匹配队列(add)

3、移除匹配队列(remove)

4、创建三个线程

5、加入房间(handlerMatch)

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

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

(3)获取玩家的Session

(4)双方玩家加入房间

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

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

1、afterConnectionEstablished

2、afterConnectionClosed

3、handleTransportError

4、handleTextMessage

六、前端代码的逻辑处理

1、建立连接

2、发送请求

3、处理响应

七、梳理前后端交互流程

1、玩家的匹配

(1)建立连接

(2)玩家发起请求

(3)服务器处理请求

(4)客户端处理响应

2、玩家匹配成功

[2.1 后端处理请求](#2.1 后端处理请求)

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

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

[2.2 客户端处理响应](#2.2 客户端处理响应)


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

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

1、websocket连接路径

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

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

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

后端代码:

java 复制代码
@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就可以了。

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

java 复制代码
@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、房间类:

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

2、房间管理器:

java 复制代码
// 房间管理器类
// 这个类也希望有唯一实例
@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(大神)。

java 复制代码
    private Queue<User> normalQueue = new LinkedList<>();
    private  Queue<User> highQueue = new LinkedList<>();
    private Queue<User> veryHighQueue = new LinkedList<>();

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

2、加入匹配队列(add)

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

java 复制代码
    //  操作匹配队列的方法
    //  把玩家放到匹配队列中
    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、玩家在匹配的时候掉线了

java 复制代码
    //  当玩家点击停止匹配,就需要把该玩家从匹配队列删除
    //  当匹配成功后,玩家进入房间,也需要把该玩家从匹配队列删除
    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玩家,有就要把这两个玩家加入同一个房间中。

java 复制代码
    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个玩家

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

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

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

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

(3)获取玩家的Session

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

java 复制代码
                //  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)双方玩家加入房间

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

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

java 复制代码
                //  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连接成功后,就要进行校验用户信息,如果是新登录的玩家,就把该玩家设为游戏大厅在线状态,在这个方法里面,要处理用户多开的问题 和 未登录却直接访问游戏大厅的不合理操作。

java 复制代码
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 连接,也就意味着玩家下线了,要把当前玩家从游戏大厅的在线状态给删除掉;如果是在匹配中,不经要删除游戏大厅的在线状态,还要在匹配队列删除该玩家。

java 复制代码
        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

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

java 复制代码
    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:就要把玩家从匹配队列中删除,同时构造返回响应,返回给客户端

java 复制代码
    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连接了。

java 复制代码
        // 此处进行初始化 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" 数据。

java 复制代码
        // 给匹配按钮添加一个点击事件
        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,打印一个日志信息。

java 复制代码
        //一会重点来实现,要处理服务器返回的响应
        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'

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

相关推荐
编程重生之路32 分钟前
Springboot启动异常 错误: 找不到或无法加载主类 xxx.Application异常
java·spring boot·后端
politeboy42 分钟前
k8s启动springboot容器的时候,显示找不到application.yml文件
java·spring boot·kubernetes
刽子手发艺43 分钟前
WebSocket详解、WebSocket入门案例
网络·websocket·网络协议
世间万物皆对象7 小时前
Spring Boot核心概念:日志管理
java·spring boot·单元测试
qq_17448285759 小时前
springboot基于微信小程序的旧衣回收系统的设计与实现
spring boot·后端·微信小程序
代码小鑫11 小时前
A043-基于Spring Boot的秒杀系统设计与实现
java·开发语言·数据库·spring boot·后端·spring·毕业设计
真心喜欢你吖11 小时前
SpringBoot与MongoDB深度整合及应用案例
java·spring boot·后端·mongodb·spring
周全全11 小时前
Spring Boot + Vue 基于 RSA 的用户身份认证加密机制实现
java·vue.js·spring boot·安全·php
飞升不如收破烂~12 小时前
Spring boot常用注解和作用
java·spring boot·后端
计算机毕设源码qq-383653104112 小时前
(附项目源码)Java开发语言,215 springboot 大学生爱心互助代购网站,计算机毕设程序开发+文案(LW+PPT)
java·开发语言·spring boot·mysql·课程设计