网页五子棋对战——SSM框架

网页版五子棋对战

1.用户模块

用户的注册和登录

管理用户的天梯分数、比赛场数、获胜场数等信息

2.匹配模块

依据用户的天梯积分,实现匹配机制

3.对战模块

把两个匹配到的玩家放到一个游戏房间中,双方通过网页的形式来进行对战比赛

用到的关键技术点:

Java、Spring/Spring Boot/Spring MVC、HTML/CSS/AJAX、MySQL/MyBatis、WebSocket

我们之前学习过的服务器开发,主要是这样的模型:

客户端主动向服务器发起请求,服务器收到之后,返回一个响应。

如果客户端不主动发起请求,服务器是不能主动联系客户端的

我们是否需要,服务器主动给客户端发消息这样的场景呢?

需要!!“消息推送”

网页五子棋对战——SSM框架_第1张图片

当前已有的知识,主要是HTTP.HTTP自身难以实现这种消息推送效果的

HTTP要想实现类似的效果,就需要基于“轮询”的机制

网页五子棋对战——SSM框架_第2张图片

很明显,像这样的轮询操作,开销是比较大的,成本也是比较高的

如果轮询间隔时间长,玩家1落子之后,玩家2不能及时拿到结果

如果轮询间隔时间短,虽然即使性得到改善,但是玩家2 不得不浪费更多的机器资源(尤其是带宽)

因此,websocket就是一个消息推送机制

websocket报文格式

网页五子棋对战——SSM框架_第3张图片

websocket也是一个应用层的协议,下层是基于TCP的~

opcode描述了当前这个websocket报文是啥类型

表示当前这是一个文本帧还是一个二进制帧

表示当前这是一个ping帧,还是一个pong帧

payload len含义表示的是当前数据报携带的数据载荷的长度。这个字段本身就是一个变长的,一个websocket数据报能承载的载荷长度是非常长的

websocket握手过程(建立连接的过程)

使用网页端,尝试和服务器建立websocket连接

网页端就会先给服务器发起一个HTTP请求 这个HTTP请求中会带有特殊的Header

Connection:Upgrade

Upgrade:Websocket

这两个header其实就是告知服务器,我们要进行协议升级

如果服务器支持websocket,就会返回一个特殊的HTTP响应 这个响应的状态码是101(切换协议)

客户端和服务器之间就开始使用websocket来进行通信了

实现一个简单的websocket代码

编写服务器端(Java)

编写客户端(JS)

在这里插入图片描述

通过TestAPI重写了几个类,

在这里插入图片描述

光有这几个类还不够 需要把这几个类关联到路径

用户模块

完成注册登录,以及用户分数管理

使用数据库来保存上述用户信息

网页五子棋对战——SSM框架_第4张图片

使用MyBatis来连接并操作数据库

1.修改Spring的配置文件,使数据库可以被连接上(application.yml)

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/java_gobang?characterEncoding=utf8&useSSL=false
    username: root
    password: zy19991227
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/**Mapper.xml

2.创建实体类。用户 User

网页五子棋对战——SSM框架_第5张图片

3.创建Mapper接口

针对数据库进行哪些具体的操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yvvSwfi5-1688815713045)(C:\Users\zyx\AppData\Roaming\Typora\typora-user-images\image-20230329102600202.png)]

4.实现MyBatis的相关xml配置文件,来自动实现数据库操作
网页五子棋对战——SSM框架_第6张图片

约定前后端交互接口

登录的请求和相应

请求

POST /login HTTP/1.1

Content-Type: application/x-www-form-urlencoded

username=zhangsan&password=123

响应

HTTP/1.1 200 OK
Content-Type: application/json
{
​		userId: 1,
​        username:'zhangsan',
​		score: 1000,
​		totalCount: 0,
​		winCount: 0
}

如果登录失败,就返回一个无效的user对象。

如果这里的每个属性都是空的,像userId => 0

注册的请求和相应

请求

POST/register HTTP/1.1

Content-Type: application/x-www-form-urlencoded

username=zhangsan&password=123

响应

HTTP/1.1 200 OK
Content-Type: application/json
{
​		userId: 1,
​        username:'zhangsan',
​		score: 1000,
​		totalCount: 0,
​		winCount: 0
}

这个前后端交互的接口,在约定的时候,是有很多种交互方式的

这里约定好了之后,后续的前端或后端代码,都要严格地遵守这个约定来写代码

从服务器获取到当前登录的用户信息的请求和响应

程序运行过程中,用户登陆了之后,让客户端随时通过这个接口,来访问服务器,获取自身的信息

请求

GET/userInfo HTTP/1.1

响应

HTTP/1.1 200 OK
Content-Type: application/json
{
​		userId: 1,
​        username:'zhangsan',
​		score: 1000,
​		totalCount: 0,
​		winCount: 0
}

编写服务器代码

package com.example.java_gobang.api;

import com.example.java_gobang.model.User;
import com.example.java_gobang.model.UserMapper;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

@RestController
public class UserAPI {

    @Resource
    private UserMapper userMapper;

    @PostMapping("/login")    //请求使用的是POST
    @ResponseBody    //将java对象转为json格式的数据
    public Object login(String username, String password, HttpServletRequest req){
        //关键操作:根据username去数据库中进行查询
        //如果能找到匹配的用户,并且密码也一致,就认为登陆成功
        User user = userMapper.selectByName(username);
        System.out.println("[login] user=" + username);
        if(user == null || !user.getPassword().equals(password)){
            //登陆失败
            System.out.println("登陆失败");
            return new User();//无效对象
        }
        HttpSession httpSession = req.getSession(true);
        //参数true的含义:会话存在直接返回,会话不存在就创建一个
        //参数false的含义:会话存在直接返回,会话不存在就返回空
        httpSession.setAttribute("user",user);
        return user;
    }

    @PostMapping("/register")
    @ResponseBody
    public Object register(String username,String password){
        try {
            User user = new User();
            user.setUsername(username);
            user.setPassword(password);
            userMapper.insert(user);
            return user;
        } catch (org.springframework.dao.DuplicateKeyException e){
            User user = new User();
            return user;
        }
    }

    @GetMapping("/userInfo")
    @ResponseBody
    public Object getUserInfo(HttpServletRequest req) {
        try {
            HttpSession httpSession = req.getSession(false);
            User user = (User) httpSession.getAttribute("user");
            return user;
        } catch (NullPointerException e){
            return new User();
        }
    }
}

使用Postman测试登录:

用户名和密码都正确

网页五子棋对战——SSM框架_第7张图片

用户名和密码不正确(返回空值)

网页五子棋对战——SSM框架_第8张图片

使用Postman测试注册:

网页五子棋对战——SSM框架_第9张图片

使用Postman测试用户信息:

网页五子棋对战——SSM框架_第10张图片

编写 登录/注册 功能的前端页面

login.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/login.css">

head>
<body>
    <div class="nav">
        五子棋对战
    div>
    <div class="login-container">
        
        <div class="login-dialog">
            
            <h3>登录h3>
            
            <div class="row">
                <span>用户名span>
                <input type="text" id="username">
            div>
            
            <div class="row">
                <span>密码span>
                <input type="password" id="password">
            div>
            
            <div class="row">
                <button id="submit">提交button>
            div>
        div>
    div>
body>
html>

common.css

/* 公共样式*/
/*去除浏览器原有样式*/
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

html,body{
    height: 100%;

    background-image: url(../image/cat.jpg);
    background-repeat: no-repeat;
    background-position: center;
    background-size: cover;
}

.nav {
    height: 50px;
    background-color: rgba(5, 5, 28,0.7);
    color: white;
    line-height: 50px;
    padding-left: 20px;
}

login.css

.login-container {
    height: calc(100% - 50px);
    display: flex;
    justify-content: center;
    align-items: center;
}

.login-dialog {
    width: 400px;
    height: 400px;
    background-color: rgba(255,255,255,0.8);
    border-radius: 10px;
}

/*标题*/
.login-dialog h3 {
    text-align: center;
    padding: 50px 0;
}

/*针对一行操作样式*/
.login-dialog .row {
    width: 100%;
    height: 50px;
    display: flex;
    align-items: center;
    justify-content: center;
}

.login-dialog .row span {
    width: 100px;
    font-weight: 700;
}

#username,#password {
    width: 200px;
    height: 40px;

    font-size: 20px;
    line-height: 40px;
    padding-left: 10px;
    border: none;
    outline: none;
    border-radius: 10px;
}

#submit {
    width: 300px;
    height: 50px;

    background-color: rgb(0,129,0);
    color: whitesmoke;

    border: none;
    outline: none;
    border-radius: 10px;

    margin-top: 20px;
}

#submit:active {
    background-color: rgb(6,6,6);
}

实现登录的具体过程

使用ajax,使页面和服务器之间进行交互

<script src="./js/jquery.min.js">script>
    <script>
        // 通过 ajax 的方式实现登录过程
        let submitButton = document.querySelector('#submit');
        submitButton.onclick = function() {
            // 1. 先获取到用户名和密码
            let username = document.querySelector('#username').value;
            let password = document.querySelector('#password').value;
    
            $.ajax({
                method: 'post',
                url: '/login',
                data: {
                    username: username,
                    password: password
                },
                success: function(data) {
                    console.log(JSON.stringify(data));
                    if (data && data.userId > 0) {
                        // 登录成功, 跳转到游戏大厅
                        alert("登录成功!")
                        location.assign('/game_hall.html');
                    } else {
                        alert("登录失败! 用户名密码错误! 或者该账号正在游戏中!");
                    }
                }
            });
        }
    script>

实现注册的具体过程

使用ajax,使页面和服务器之间进行交互

<script src="js/jquery.min.js">script>
    <script>
        let usernameInput = document.querySelector('#username');
        let passwordInput = document.querySelector('#password');
        let submitButton = document.querySelector('#submit');

        submitButton.onclick = function() {
            $.ajax ({
            type: 'post',
            url: '/register',
            data: {
                username: usernameInput.value,
                password: passwordInput.value,
            },
            success: function(body){
                //如果注册成功,就会返回一个新注册好的用户对象
                if(body && body.username) {
                    //注册成功!
                    alert("注册成功!");
                    location.assign('/login.html');
                }else {
                    alert("注册失败!");
                }
            },
            error: function() {
                alert("注册失败!");
            }

        });
        }
    script>

匹配模块

让多个用户,在游戏大厅中能够进行匹配,系统会把实力相近的两个玩家凑成一桌,进行对战

约定前后端交互接口

网页五子棋对战——SSM框架_第11张图片

玩家发送匹配请求,这个事情是确定(点击了匹配按钮,就会发送匹配请求)服务器啥时候告知玩家匹配结果(到底排到了谁)
需要等待匹配结束的时候才告知
正因为服务器自己也不知道啥时候能够告知玩家匹配的结果,因此就需要依赖消息推送机制当服务器这里匹配成功之后,就主动的告诉当前排到的玩家‘你排到了

接下来约定的前后端交互接口,也是基于websocket来展开的

websocket可以传输文本数据,也能传输二进制数据

此处就直接设计成让websocket传输json格式的方式即可

匹配请求:

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

ws://127.0.0.1:8080/findMatch

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

在通过websocket传输请求数据的时候,数据中是不必带有用户身份信息的

当前用户的身份信息,在前面登陆完成后,就已经保存到HttpSession中了

websocket里,也是能拿到之前登录好的HttpSession中的信息的

匹配响应1:

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',
}

这个响应是真正匹配到对手之后,服务器主动推送回来的消息

匹配到的对手不需要在这个响应中体现,仍然都放到服务器这边来保存即可

匹配页面(游戏大厅页面)

网页五子棋对战——SSM框架_第12张图片

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>
            
            <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 
                + "
比赛场次:"
+ body.totalCount + " 获胜场数:" + body.winCount }, error: function() { alert("获取用户信息失败!"); } });
script> body> html>
.container {
    width: 100%;
    height: calc(100% - 50px);

    display: flex;
    align-items: center;
    justify-content: center;
}

#screen {
    width: 400px;
    height: 200px;
    font-size: 20px;
    background-color: gray;
    color: white;
    border-radius: 10px;

    text-align: center;
    line-height: 100px;
}

#match-button {
    width: 400px;
    height: 50px;
    font-size: 20px;
    color: white;
    background-color: orange;
    border: none;
    outline: none;
    border-radius: 10px;

    text-align: center;
    line-height: 50px;
    margin-top: 20px;
}

#match-button:active {
    background-color: bisque;
}

![在这里插入图片描述\

JSON字符串和JS对象的转换

JSON字符串转成JS对象

JSON.parse

JS对象转成JSON字符串

JSON.stringify

JSON字符串和Java对象的转换

JSON字符串转成Java对象

ObjectMapper.readValue

Java对象转成JSON字符串

ObjectMapper.writerValueAsString

在注册websocket API的时候,就需要把前面准备好的HttpSession给搞过来(搞到Websocket的Session中!)

用户登录就会给HttpSession中保存用户的信息

此处需要能够保存和表示用户上线和下线的状态

之所以要维护用户的在线状态,目的就是为了能够在代码中比较方便的获取到某个用户

当前的websocket会话,从而可以通过这个会话来给这个客户端发送消息,同时也可以感知到他的在线/离线状态

使用哈希表来保存当前用户的在线状态

key就是用户id

value就是用户当前使用的websocket会话

session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));

先通过ObjectMapperMatchResponse对象转成JSON字符串,然后再包装上一层TextMessage再进行传输

其中TextMessage就表示一个文本格式的websocket数据包

当前是使用HashMap来存储用户的在线状态

如果是多线程访问同一个HashMap就容易出现线程安全问题

如果同时有多个用户和服务器连接/断开连接,此时服务器就是并发的针对HashMap进行修改

private ConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>();

多开问题

当浏览器1建立websocket请求时,服务器这边会在OnlineUserManager中保存键值对:userId=1,WebSocketSession=session1

当浏览器2建立websocket请求时,服务器这边会在OnlineUserManager中保存键值对:userId=1,WebSocketSession=session2

这两次连接,尝试往哈希表中存储两个键值对,这两个键值对的key是一样的 后来的value会覆盖之前value

上述这种覆盖,就会导致第一个浏览器的连接“名存实亡”已经拿不到对应的WebSocketSession了,也就无法给这个浏览器推送数据了

多开会产生上述问题,我们的程序是否应该允许多开呢?

对于大部分游戏来说,都是不行的!都是禁止多开的,禁止同一个账号在不同的主机上登录!

因此我们呢要做的,不是直接解决会话覆盖的问题,而是从源头上禁止游戏多开!

1)账号登陆成功之后,禁止在其他地方再登录(采用这种方法)

2)账号登陆之后,后续其他位置的登录会把前面的登录给踢掉

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.先判定当前用户是否已经登陆过(已经是在线状态),如果是已经在线,就不该继续进行后续逻辑
            WebSocketSession tmpSession = onlineUserManager.getFromGameHall(user.getUserId());
            if(tmpSession != null) {
                //当前用户已经登陆了
                //针对这个情况要告知客户端,你这里重复登陆了
                MatchResponse response = new MatchResponse();
                response.setOk(false);
                response.setReason("当前禁止多开!");
                session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
                session.close();
                return;
            }

            //3.拿到了身份信息之后,就可以把玩家设置成上线状态了
            onlineUserManager.enterGameHall(user.getUserId(), session);
            System.out.println("玩家"+ user.getUsername() +"进入游戏大厅");
        }catch (NullPointerException e){
            e.printStackTrace();
            //出现空指针异常,说明当前用户的身份信息为空
            //把当前用户尚未登陆这个信息返回
            MatchResponse response = new MatchResponse();
            response.setOk(false);
            response.setReason("您尚未登录!不能进行后续匹配");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        }

    }

在连接建立逻辑这里,做出了判定;如果玩家已经登陆过,就不能再登录,同时关闭websocket连接

websocket连接关闭的过程中,也会触发afterConnectionClosed

在这个方法里,会有一个exitGameHall

匹配模块的目标

从带匹配的玩家中,选出分数尽量相近的的玩家

把所有玩家按照分数,分为三类:

Normal score<2000

High score>=2000 &&score<3000

VeryHigh score>=3000

给这三个等级,分配三个不同的队列

根据当前玩家的分数,来把这个玩家的用户信息,放到对应的队列里

接下来在搞一个专门的线程,去不停的扫描这个匹配队列

只要说队列里的元素(匹配中的玩家)凑成了一对,把这一对玩家取出来,放到一个游戏房间中

线程安全问题

网页五子棋对战——SSM框架_第13张图片

入队列:

网页五子棋对战——SSM框架_第14张图片

取元素:

网页五子棋对战——SSM框架_第15张图片

删除元素:

网页五子棋对战——SSM框架_第16张图片

使用到多线程的代码时,一定要时刻注意“线程安全”问题

使用sunchronized进行加锁

需要指定一个锁对象,到底针对谁进行加锁?只有多个线程在尝试针对同一个锁对象进行加锁的时候,才会有互斥效果

此处我们进行加锁的时候,如果多个线程访问的是不同的队列,不涉及线程安全问题。必须得是多个线程操作同一个队列,才需要加锁

因此在加锁的时候选取的锁对象,就是normalQueue,highQueue,veryHighQueue这三个队列对象本身

如果当前匹配队列中,就只有一个元素,或者没有元素,会出现什么效果?

网页五子棋对战——SSM框架_第17张图片

网页五子棋对战——SSM框架_第18张图片

在这个代码中,就会出现handlerMatch一进入方法就会快速返回,然后再次进入方法 循环速度飞快 但是却没有实质的意义。这个过程中CPU占用率会非常高(忙等)

在调用完handlerMatch之后,加上sleep(500)

这个方案确实可以,但是当有玩家匹配到之后,可能要500ms之后才能正在得到匹配的返回结果

通过sleep难以两全齐美,要么让玩家多等,要么让CPU多转

因此我们使用wait/notify

当真正有玩家进入匹配队列之后,就调用notify来唤醒线程

设计游戏房间管理

一个游戏服务器上,又同时存在了多个游戏房间~
需要一个“游戏房间管理器”管理多个游戏房间~

网页五子棋对战——SSM框架_第19张图片

键值对,给每个room也生成一个唯一的roomId~

以键值对(哈希表)在room manager中进行管理

UUID表示“世界上唯一的身份标识”

通过一系列的算法,能够生成一串字符串(一组十六进制表示的数字)

两次调用这个算法,生成的这个字符串都是不相同的

任意次调用,每次得到的结果都不相同

UUID内部具体如何实现的(算法实现细节)不去深究 Java中有现成的类

关于RoomManager 希望能够根据房间id找到房间对象,也希望能够根据玩家id,找到玩家所属的房间

通过调试目前代码 发现问题:

网页五子棋对战——SSM框架_第20张图片

问题1:

当前发现玩家点击匹配之后,匹配按钮的文本不发生改变

分析之前写过的代码,点击按钮的时候,仅仅是给服务器发送了websocket请求,告诉服务器我要开始匹配了~

服务器会立即返回一个响应,“进入匹配队列成功”,然后页面再修改按钮的文本

出现问题的原因:

服务器这边在处理匹配请求时,按理说,要立即返回一个**websocket**响应

实际上在服务器代码这里构造了响应对象,但是忘记sendMessage给发回去了

解决方法:

MatchAPI中加入如下两行代码

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

验证匹配功能的时候,模拟多个用户登录的情况,最好使用多个浏览器,避免同一个浏览器中的cookie/session信息干扰

问题2:

出现异常

网页五子棋对战——SSM框架_第21张图片

检查发现原因:

在创建objectMapper时,未对其进行实例化

更改之后 出现404:

网页五子棋对战——SSM框架_第22张图片

正常情况 因此我们目前还未创建 127.0.0.1:8080/game_room.html

验证多开处理

可以看到我们使用两个浏览器登录zhangsan的账号后,页面上并没有什么提示"多开"响应 仅仅是打开控制台才能看到

网页五子棋对战——SSM框架_第23张图片

但是在第二个浏览器窗口点击”开始匹配“按钮时,会显示 连接已断开请重新登录

网页五子棋对战——SSM框架_第24张图片

当前我们虽然能够禁止一个账户的多开效果(主要是禁止在多个客户端进行匹配),但是在界面上没有一个明确的提示

此处需要调整前端代码,出现多开时,给客户一个明显的提示

网页五子棋对战——SSM框架_第25张图片

更改之后,在第二次登录zhangsan账户时,会立马显示”当前和服务器的连接已经断开!请重新登录!“,并跳转回登录界面

网页五子棋对战——SSM框架_第26张图片

另外,这里修改了js代码,再刷新页面的时候要使用ctrl+f5 强制刷新,否则应用的还是旧版本的js代码

匹配模块小结

  1. 点击开始匹配之后

    1)先触发js中的按钮的点击事件回调

网页五子棋对战——SSM框架_第27张图片

​ 这里会发送一个websocket的请求给服务器

2)服务器处理这个匹配请求

网页五子棋对战——SSM框架_第28张图片

​ 此处的payload数据就是上面的websocket发送的JSON数据,将客户端发送的数据,服务端读了出来,然后对其进行一个解析,解析为一个MatchRequest对象,这个对象中就包含了一个关键的字段

网页五子棋对战——SSM框架_第29张图片

​ 拿message里面的内容进行判断,看是startMatch还是stopMatch

​ 使用matcher.add(user)把玩家加入到匹配队列中

网页五子棋对战——SSM框架_第30张图片

网页五子棋对战——SSM框架_第31张图片

​ 服务器立即给客户端返回一个响应,告知客户端,已经把用户加入到匹配队列中

在这里插入图片描述

​ 3)客户端收到服务器返回的响应之后,就会立即进行处理

​ 其中resp就是上面我们所受到的response对象

网页五子棋对战——SSM框架_第32张图片

网页五子棋对战——SSM框架_第33张图片

​ 4)匹配器的处理

网页五子棋对战——SSM框架_第34张图片

​ 由于当前只有一个玩家,点击了开始匹配,此时队列中也就只有一个元素 因此扫描线程 会在wait处堵塞

网页五子棋对战——SSM框架_第35张图片

​ 5)此时又有一个玩家也点击了匹配操作

​ 这里的流程同刚才的123一样

​ 当又有一个玩家点击匹配之后,就会从匹配队列中的wait中返回 于是继续执行匹配逻辑

匹配器匹配到多个玩家就会创建一个房间 把房间加入到房间管理器中

在这里插入图片描述

​ 给两个哈希表中都去添加键值对的内容

网页五子棋对战——SSM框架_第36张图片

​ 添加三组映射:

​ 1.房间ID到房间对象的映射

​ 2.玩家1的ID到房间ID的映射

​ 3.玩家2的ID到房间ID的映射
网页五子棋对战——SSM框架_第37张图片

对战模块

约定好前后端交互的接口

对战模块和匹配模块使用的是两套逻辑,使用的是不同的websocket的路径进行处理,可以做到更好的解耦合~

建立连接

ws://127.0.0.1:8080/game

建立连接响应

服务器要生成一些游戏的初始信息,通过这个响应告诉客户端

{
	message: 'gameReady', //消息的类别是游戏就绪
    ok: true,
    reason:'',
    roomId:'12345678', //玩家所处在的房间id
    thisUserId: 1, //玩家自己的id
    thatUserId: 2  //玩家对手的id
    whiteUser:1  //那个玩家执白子(先手)
}

这些都是玩家匹配成功之后,要有服务器生成的内容,把这个内容返回到浏览器中

针对“落子”的请求和响应

请求

此处更建议大家使用行和列 而不是坐标x和y

后面的代码中需要使用二维数组来表示这个棋盘 通过下表取二维数组元素[row][col] 如果使用x,y [y][x]感觉比较奇怪

{
    message: 'putChess',
    userId:1,
    row:0, 
    col:0, //落子的坐标,往哪一行,哪一列来落子
}

响应

{
    message: 'putChess',
    userId: 1,
    row: 0,
    col: 0,
    winner:0 //winner表示当前是否分出胜负 如果winner为0,表示胜负未分,还需要继续对战,如果winner非0,则表示当前的获胜方的id
}

以上交互接口的设计,其实也不一定非得按照我们写的这种格式约定,我们也就而已使用其他的约定方式

不管是哪种格式,只要能够解决我们的问题,只要简约方便就可以了

实现game_room.html

这个页面就是匹配成功之后,要跳转到的新页面

canvas是HTML5引入的一个标签 “画布” 可以在画布上画画

此处的棋盘和棋子都是画上去的

canvas这个标签有一组配套的js的canvas api,通过这个api就可以实现一些“画画”的效果

例如,展示一个棋盘,就画很多的直线,就能构成棋盘的网格

表示一个棋子,就画一个圆圈,并且填充上颜色

还需要响应点击事件,在鼠标落子的地方来画圆圈

阅读一下script.js

网页五子棋对战——SSM框架_第38张图片

表示当前游戏中的棋盘,通过这个棋盘来表示当前哪个位置有子了

当前玩家点击的时候,如果有子的位置就不能继续落子了

0表示空闲位置,非0表示有子了

网页五子棋对战——SSM框架_第39张图片

drawImage()是指把图片画上去

initChessBoard()绘制棋盘

网页五子棋对战——SSM框架_第40张图片

针对chess(棋盘canvas)设定了点击回调

点击回调中的事件参数 这里就会记录点击的实际位置(坐标)

网页五子棋对战——SSM框架_第41张图片

match.floor()这里是为了让点击操作能够对应到网格线上~

总体的棋盘尺寸是450px*450px 整个棋盘上是15行,15列 每一行每一列占用的尺寸就是30px

oneStep()走一步(里面会绘制一个棋子)

网页五子棋对战——SSM框架_第42张图片

最终实现的页面结果:

服务器实现连接游戏房间

之前已经写了一个OnlineUserManager对象了 也确实能够管理用户的在线状态 但是这个状态仅仅是局限于game_hall这个页面中 现在是在game_room

网页五子棋对战——SSM框架_第43张图片

之前在退出game_hall页面的时候,就会断开 的连接 也就会在服务器的OnlineUserManager中删除对应的元素

网页五子棋对战——SSM框架_第44张图片

因此玩家从游戏大厅页面离开之后,需要重新
网页五子棋对战——SSM框架_第45张图片

连接游戏房间逻辑梳理

网页五子棋对战——SSM框架_第46张图片

连接游戏房间的线程安全问题

但凡是服务器端得开发,尤其是多个客户端来并发访问服务器,访问同一个数据的时候,就可能引发线程安全问题

网页五子棋对战——SSM框架_第47张图片

这一段逻辑就可以视为多线程环境(两个客户端是并发连入的)

如果恰好是两个客户端同时执行到这个逻辑if(room.getUser1() == null),此时就会出现问题,玩家1和玩家2都会认为自己是先手方

因此就需要把这里的逻辑判定 使用保护起来 避免多个客户端都认为自己是玩家1

接下来需要考虑加锁对象是谁??

原则是,要竞争的资源是什么,就对谁加锁

(对谁加锁 针对这个对象访问的时候才有互斥效果)

在这个逻辑里是多个玩家/线程,在同时 访问/修改 同一个room对象~就需要针对room对象来加锁

网页五子棋对战——SSM框架_第48张图片

解决先手判定错误的bug

网页五子棋对战——SSM框架_第49张图片

客户端代码中尝试获取响应中的isWhite字段

网页五子棋对战——SSM框架_第50张图片

实际的响应数据中,根本就没有isWhite字段 有的只是whiteUser字段

发送落子请求

script.js里增加代码

function send(row, col) {
        let req = {
            message: 'putChess',
            userId: gameInfo.thisUserId,
            row: row,
            col: col
        };

        websocket.send(JSON.stringify(req));
    }

网页五子棋对战——SSM框架_第51张图片

注意:客户端和服务器两边的二维数组的区别

服务器这边的数组元素有三种状态:

服务器这边的二维数组,要起到的效果是进行判定胜负,要知道玩家1和玩家2的子落在哪里

网页五子棋对战——SSM框架_第52张图片

客户端这边的数组元素只有两种状态:

客户端的二维数组只是用来判定这个位置有没有子 无0有1,只是为了避免出现重复落子的情况 一个位置落子多次

网页五子棋对战——SSM框架_第53张图片

如果直接在客户端来判定胜负关系,是否可行呢?

不太可行 游戏中的关键逻辑一般还是要交给服务器来进行(防止外挂)

外挂的工作过程就是 篡改客户端这边的逻辑

发送落子响应

处理异常

发现此处有一个问题,空指针异常。深深的怀疑onlineUserManager为空

//要想给用户发送 websocket 数据,就需要获取到这个用户 WebSocketSession
        WebSocketSession session1 = onlineUserManager.getFromGameRoom(user1.getUserId());
        WebSocketSession session2 = onlineUserManager.getFromGameRoom(user2.getUserId());

当前这两个属性在Room这个类里面

//引入OnlineUserManager
@Autowired
private OnlineUserManager onlineUserManager;
//引入RoomManager,用来房间销毁
   private RoomManager roomManager;

但是Room类自身不是Spring组件,没有被注册进去,自然@Autowired无法生效

网页五子棋对战——SSM框架_第54张图片

如果这么写,就成了单例了,Room显然不应该是单例,应该是多例

此处Room是已经被我们手动管理起来了:RoomManager

在这里插入图片描述

当前显然,Room不应该作为Spring中的组件~又希望能够从Spring中拿到对应的onlineUerManagerroomManager 就需要通过手动注入的方式来获取到实例了

判定胜负

判定棋面上是否出现五子连珠

一行,一列,一个对角线

因此我们不需要判定整个棋盘,只需要以rowcol这个位置为中心,判定周围若干个格子

网页五子棋对战——SSM框架_第55张图片

先以一行 为例来考虑判定结果

网页五子棋对战——SSM框架_第56张图片

如果棋盘上出现了五子连珠,一定是和新落子的位置是相关的

五种一行五子连珠的情况:假设x是我们新落的子

  1. a b c d x
  2. a b c x d
  3. a b x c d
  4. a x b c d
  5. x a b c d

假设这一行中最左侧的点,

第一个点 r,c

第二个点 r,c+1

第三个点 r,c+2

第四个点 r,c+3

第五个点 r,c+4

最左边第一个点的运动范围,此处(row,col)这个位置是玩家这次的落子位置

第一种情况:

最左边点:r=row,c=col-4

第二种情况:

最左边点:r=row,c=col-3

第三种情况:

最左边点:r=row,c=col-2

第四种情况:

最左边点:r=row,c=col-1

第五种情况:

最左边点:r=row,c=col

//1.检查所有的行
        //  先遍历这五种情况
        for (int c = col - 4; c <= col ; c++) {
            //针对其中的一种情况,来判定这五个子是不是连在一起了
            //不光是这五个子得连着,而且要跟玩家落的子是一样的 才算获胜
            try {
                if(board[row][c] == chess
                        && board[row][c+1] == chess
                        && board[row][c+2] == chess
                        && board[row][c+3] == chess
                        && board[row][c+4] == chess) {
                    //构成了五子连珠!胜负已分
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            }catch (ArrayIndexOutOfBoundsException e) {
                //如果出现数组下标越界得情况,可以直接忽略这个异常
                continue;
            }
        }
一列 为例判定所有结果

坐标假设为 row,col,五种情况如下:

一 二 三 四 五

a a a a x

b b b x a

c c x b b

d x c c c

x d d d d

假设这一列中最上面的点,坐标为

第一个点:r,c

第二个点:r+1,c

第三个点:r+2,c

第四个点:r+3,c

第五个点:r+4,c

最上边第一个点的运动范围,此处(row,col)这个位置是玩家这次的落子位置

第一种情况:

最左边点:r=row-4,c=col

第二种情况:

最左边点:r=row-3,c=col

第三种情况:

最左边点:r=row-2,c=col

第四种情况:

最左边点:r=row-1,c=col

第五种情况:

最左边点:r=row,c=col

//2.检查所有列
        for(int r = row - 4;r <= row;r++) {
            try {
                if(board[r][col] == chess
                    && board[r+1][col] == chess
                    && board[r+2][col] == chess
                    && board[r+3][col] == chess
                    && board[r+4][col] == chess) {
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            }catch (ArrayIndexOutOfBoundsException e){
                continue;
            }
        }
左对角线 为例判定所有结果

网页五子棋对战——SSM框架_第57张图片

//3.检查左对角线
        for (int r = row - 4,c = col - 4; r <=  row && c <= col;r++,c++){
            try {
                if (board[r][c] == chess
                    && board[r + 1][c + 1] == chess
                    && board[r + 2][c + 2] == chess
                    && board[r + 3][c + 3] == chess
                    && board[r + 4][c + 4] == chess) {
                    return chess  == 1 ? user1.getUserId() : user2.getUserId();
                }
            }catch (ArrayIndexOutOfBoundsException e) {
                continue;
            }
        }
右对角线 为例判定所有结果

网页五子棋对战——SSM框架_第58张图片

//4.检查右对角线
        for (int r = row - 4,c = col + 4; r <=  row && c >= col;r++,c--){
            try {
                if (board[r][c] == chess
                        && board[r + 1][c - 1] == chess
                        && board[r + 2][c - 2] == chess
                        && board[r + 3][c - 3] == chess
                        && board[r + 4][c - 4] == chess) {
                    return chess  == 1 ? user1.getUserId() : user2.getUserId();
                }
            }catch (ArrayIndexOutOfBoundsException e) {
                continue;
            }
        }

胜负未分 直接返回0return 0;

更新玩家分数

问题:

1.玩家比赛完成之后,比赛的胜负场数和分数没有改变

2.玩家掉线的情况,需要通知对手,你自动获胜

​ 落子这里,对对方是否在线做过检测 但是这个检测是在落子的时候做的检测

刚才发现,当进行完一局游戏之后,分数没有顺利的被更新

刚才的逻辑中,主要是两部分:

1.一局游戏进行完之后,需要把信息写入数据库

2.返回到游戏大厅之后,要重新从数据库来获取

此处的关键点,就是数据库里面的内容对不对~

网页五子棋对战——SSM框架_第59张图片

客户端的这个代码,实现了从服务器获取玩家信息的操作

网页五子棋对战——SSM框架_第60张图片

网页五子棋对战——SSM框架_第61张图片

分析到这里,就知道了~当前从服务器拿信息的这个接口,获取到的user对象不是数据库中的最新对象,而是之前在登录过程中,往session里存的user对象

后续我们已经更新了数据库的内容,但是session里的user没有发生改变

此处的解决方案:

根据当前的session中拿到的user对象,重新查询数据库

获取到的user对象才返回给客户端

网页五子棋对战——SSM框架_第62张图片

更改:

网页五子棋对战——SSM框架_第63张图片

在进行了一局之后

调整弹窗

网页五子棋对战——SSM框架_第64张图片
网页五子棋对战——SSM框架_第65张图片

处理回退按钮的问题

assign 更换为replace

游戏界面中 点击回退直接回退到登陆界面

部署程序到云服务器

1.先把数据库中的数据给构造好

2.微调页面(websocket建立连接的url进行调整)

在这里插入图片描述

如果服务器就在浏览的本机上,可以这么写~

如果服务器程序部署到其他机器上,此时就不能使用127.0.0.1了,而需要指定不同机器的ip

服务器部署到那个机器上,就需要制定哪个ip

这个ip就要写成云服务器的外网ip

此处我们要修改代码,让这个ip能够适应不同的主机

3.打包,并进行上传

借助maven来打包

命名为 zyx_gobang

4.运行程序,通过外网进行访问

使用命令 java -jar zyx-gobang.jar 启动java包

你可能感兴趣的:(java,maven,spring,boot,servlet,mybatis,spring)