完整代码见: 邹锦辉个人所有代码: 测试仓库 - 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的作用是建立连接, 发送和接收数据(请求和响应) ) 下面围绕这两个内容, 我们来实现一下前端代码.
<!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负责接收服务器数据的方法具体实现:
@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类中实现的. 明天我们再来具体讲解一下这个, 以及对于一些特殊情况的处理.