项目-双人五子棋对战:匹配模块的实现(3)

完整代码见: 邹锦辉个人所有代码: 测试仓库 - Gitee.com

模块详细讲解

功能需求

匹配就类似于大家平常玩的王者荣耀这样的匹配功能, 当玩家点击匹配之后, 就会进入到一个匹配队列, 当匹配到足够数量的玩家后, 就会进入确认页.

在这里, 我们主要实现的是1 - 1匹配功能, 首先先有一个玩家点击匹配, 进入匹配队列, 然后如果有段位差不多的(就是根据我们之前讲到的天梯分数, 按分数来分段位, 后面也会实现)进入到匹配队列, 就匹配成功, 创建一个游戏房间, 双方进入游戏房间.

接下来我们来详细介绍一下具体的匹配实现原理.

具体原理

匹配这样的功能, 也需要依赖到我们之前讲到的消息推送.

  1. 玩家1点击匹配按钮, 就会告诉服务器, 我要进行匹配, 同时进入匹配队列.

  2. 玩家2(跟玩家1同一个段位)点击匹配按钮, 这时就完成了匹配.

  3. 此时服务器需要告诉玩家1匹配结果. (正是因为服务器自己也不确定, 啥时候告诉玩家匹配的结果, 因此就需要依赖消息推送机制, 当服务器匹配成功之后, 就主动告诉排到的玩家, 你排到了).

具体实现

前后端接口的约定

前后端的交互接口, 也是基于websocket来展开的, websocket可以传输文本数据, 也可以传输二进制数据, 此处就直接设计成让websocket传输json格式的文本数据即可.

匹配请求:

客户端通过websocket给服务器发送一个json格式的文本数据.

ws://127.0.0.1:8080/findMatch

{

message:'startMatch' / 'stopMatch', //开始/结束匹配

}

在通过websocket传输请求信息的时候, 数据中就不用包含用户的个人信息的, 因为此时个人信息在登录之后被存储在HttpSession中了. 通过websocket也是可以拿到之前登录的HttpSession里的信息的.

匹配响应:

ws://127.0.0.1:8080/findMatch

{

ok: true, //匹配成功

reason: ' ', //匹配如果失败, 失败的原因信息

message: 'startMatch' / 'stopMatch',

}

这个响应是客户端给服务器发送匹配请求之后, 服务器立即返回的匹配响应.

匹配响应2:

ws://127.0.0.1:8080/findMatch

{

ok: true,

reason: ' ',

message: 'matchSuccess'

}

这个响应是真正匹配到对手之后, 服务器主动推送回来的信息. 匹配到的对手不需要在这个响应中体现, 仍然都放到服务器这边来储存即可.

前端代码的实现

页面大致展示:

这里我们不难发现, 客户端的工作主要是发送两个请求, 即获取用户信息, 以及开始/结束匹配; 以及接收服务器响应, 根据响应改变按钮文本即可.(再次强调, 前端使用websocket的作用是建立连接, 发送和接收数据(请求和响应) ) 下面围绕这两个内容, 我们来实现一下前端代码.

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>游戏大厅</title>
    <link rel="stylesheet" href="css/common.css">
    <link rel="stylesheet" href="css/game_hall.css">
</head>
<body>
    <div class="nav">五子棋对战</div>
    <!-- 整个页面的容器元素 -->
    <div class="container">
        <!-- 这个 div 在 container 中是处于垂直水平居中这样的位置的 -->
        <div>
            <!-- 展示用户信息 -->
            <div id="screen"></div>
            <!-- 匹配按钮 -->
            <div id="match-button">开始匹配</div>
        </div>
    </div>

    <script src="js/jquery.min.js"></script>
    <script>
        //获取用户信息
        $.ajax({
            type: 'get',
            url: '/userInfo',
            success: function(body) {
                let screenDiv = document.querySelector('#screen');
                screenDiv.innerHTML = '玩家: ' + body.username + " 分数: " + body.score 
                    + "<br> 比赛场次: " + body.totalCount + " 获胜场数: " + body.winCount
            },
            error: function() {
                alert("获取用户信息失败!");
            }
        });

        // 此处进行初始化 websocket, 并且实现前端的匹配逻辑. 
        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. 
        window.onbeforeunload = function() {
            websocket.close();
        }

        // 处理服务器返回的响应数据. 这个响应就是针对 "开始匹配" / "结束匹配" 来对应的
        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);
                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.replace("/game_room.html");
            } else if (resp.message == 'repeatConnection') {
                alert("当前检测到多开! 请使用其他账号登录!");
                location.replace("/login.html");
            } else {
                console.log("收到了非法的响应! message=" + resp.message);
            }
        }

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

不难发现, 核心也就是发送请求和处理响应, 这个都是通过用户点击的按钮所发起的.

这里还有一个特殊的情况就是, 用户可能会同时登录多个账号(也就是我们常说的多开), 这时候也要进行处理(前端这里仅针对响应结果进行弹窗提示, 跳转页面处理), 还有一些防止未登录直接跳转至页面的特殊情况处理, 一会在后端代码中做详细讲解.

后端代码

我们知道, 前端已经通过websocket和后端进行了连接, 因此后端就需要通过websocket会话和前端保持连接, 通过这个可以向客户端发送信息. 然后前端发来的请求会以TextMessage的形式被后端接收到. 然后服务器进行响应, 响应也就包含刚才讲的(ok/reason/message)这三个参数.

下面来看一下后端WebSocket负责接收服务器数据的方法具体实现:

java 复制代码
@Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 实现处理开始匹配请求和停止匹配请求
        User user = (User) session.getAttributes().get("user");
        //获取到客户端给服务器的数据
        String payload = message.getPayload();
        // 当前这个数据载荷是一个JSON格式的字符串, 需要把它转换为 Java对象, MatchRequest(也就是字符串格式)
        // ObjectMapper就是完成类型转换的核心类
        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.setReason("非法的匹配请求");
        }
        //格式转换为JSON格式
        String jsonString = objectMapper.writeValueAsString(response);
        //返回响应
        session.sendMessage(new TextMessage(jsonString));
    }

这里与开始匹配/结束匹配相关的就是匹配队列, 它是在matcher类中实现的. 明天我们再来具体讲解一下这个, 以及对于一些特殊情况的处理.

相关推荐
为java加瓦14 小时前
Spring 方法注入机制深度解析:Lookup与Replace Method原理与应用
java·数据库·spring
_extraordinary_14 小时前
Java SpringIoC&DI --- @Bean,DI
java·开发语言
史迪奇_xxx14 小时前
9、C/C++ 内存管理详解:从基础到面试题
java·c语言·c++
hweiyu0014 小时前
Spring Boot 项目集成 Gradle:构建、测试、打包全流程教程
java·spring boot·后端·gradle
一勺菠萝丶14 小时前
Spring Boot 项目启动报错:`Could not resolve type id ... no such class found` 终极解决方案!
java·spring boot·后端
聪明的笨猪猪14 小时前
Java Redis “底层结构” 面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
Chris.Yuan77014 小时前
泛型学习——看透通配符?与PECS 法则
java·学习
这周也會开心15 小时前
云服务器安装JDK、Tomcat、MySQL
java·服务器·tomcat
hrrrrb16 小时前
【Spring Security】Spring Security 概念
java·数据库·spring
小信丶16 小时前
Spring 中解决 “Could not autowire. There is more than one bean of type“ 错误
java·spring