【 五子棋对战平台(java_gobang) 】

news2025/1/18 20:16:54

文章目录

  • 一、核心功能及技术
  • 二、效果演示
  • 三、创建项目
    • 扩展:WebSocket 框架知识
  • 四、需求分析和概要设计
  • 五、数据库设计与配置 Mybatis
  • 六、实现用户模块功能
    • 6.1 数据库代码编写
    • 6.2 前后端交互接口
    • 6.3 服务器开发
    • 6.4 客户端开发
  • 七、实现匹配模块功能
    • 7.1 前后端交互接口
    • 7.2 客户端开发
    • 7.3 服务器开发
  • 八、实现对战模块功能

一、核心功能及技术

核心功能:

  1. 注册,登录,退出
  2. 大厅记录用户相关信息,如天梯分数
  3. 将分数相差不大的同水平选手进行匹配
  4. 大厅开始匹配,成功后两名玩家进入同一游戏房间,可随时取消匹配
  5. 多玩家可同时在线,两两玩家随机对弈
  6. 实时记录玩家游戏信息,管理游戏场数,分数等
  7. 无论哪一方玩家对弈时退出或掉线,另一方自动获胜

核心技术:

  1. Spring/SpringBoot/SpringMVC
  2. WebSocket
  3. MySQL
  4. MyBatis
  5. HTML/CSS/JS/AJAX(canvas API)

注意: WebSocket 和 canvas API 是实现本项目的两个核心技术,前者我们后续会稍许讲解,后者涉及到前端知识,我们就不多赘述,大家可参考以下链接去了解更多关于 canvas 的知识点!https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API


二、效果演示

  1. 登录
    在这里插入图片描述

  2. 注册
    在这里插入图片描述

  3. 游戏大厅
    在这里插入图片描述

  4. 游戏房间,及分出胜负后的效果
    在这里插入图片描述


三、创建项目

创建一个springboot项目,具体步骤与前面的博客记录一样,这里就不再重复赘述,大家自行参考以下博客:

1.springboot项目的基本创建

2.添加mybatis框架支持

注意: 除开以前的老几样框架,本次项目我们还引入了一个新的知识点 WebSocket 如下:

在这里插入图片描述

扩展:WebSocket 框架知识

参考另一篇博客 https://editor.csdn.net/md/?articleId=130700875


四、需求分析和概要设计

整个项目分成以下模块:

  1. 用户模块
  2. 匹配模块
  3. 对战模块

用户模块

  1. 用户模块主要负责用户的注册, 登录,退出,分数记录功能.

  2. 使用 MySQL 数据库存储用户数据.

  3. 客户端提供一个登录页面+注册页面.

  4. 服务器端基于 Spring + MyBatis 来实现数据库的增删改查.

匹配模块

  1. 注册后,用户登录成功, 则进入游戏大厅页面.

  2. 游戏大厅中, 能够显示用户的名字, 天梯分数, 比赛场数和获胜场数.

  3. 同时显示一个 “匹配按钮”.

  4. 点击匹配按钮则用户进入匹配队列, 并且界面上开始匹配按钮显示为 “取消匹配” .

  5. 再次点击取消匹配“”则把用户从匹配队列中删除.

  6. 如果匹配成功, 则跳转进入到游戏房间页面.

  7. 游戏大厅页面加载时和服务器建立 websocket 连接. 双方通过 websocket 来传输 “开始匹配”, “取消匹配”, “匹配成功” 这样的信息.

对战模块

  1. 玩家匹配成功, 则进入游戏房间页面.

  2. 每两个玩家在同一个游戏房间中.

  3. 在游戏房间页面中, 能够显示五子棋棋盘. 玩家点击棋盘上的位置实现落子功能.

  4. 并且五子连珠则触发胜负判定, 显示 “你赢了” “你输了”.

  5. 游戏房间页面加载时和服务器建立 websocket 连接. 双方通过 websocket 来传输 “准备就绪”, “落子位置”, “胜负” 这样的信息.

准备就绪: 两个玩家均连上游戏房间的 websocket 时, 则认为双方准备就绪.
落子位置: 有一方玩家落子时, 会通过 websocket 给服务器发送落子的用户信息和落子位置, 同时服务器再将这样的信息返回给房间内的双方客户端. 然后客户端根据服务器的响应来绘制棋子位置.
胜负: 服务器判定这一局游戏的胜负关系. 如果某一方玩家落子, 产生了五子连珠, 则判定胜负并返回胜负信息. 或者如果某一方玩家掉线(比如关闭页面), 也会判定对方获胜.


五、数据库设计与配置 Mybatis

在这里插入图片描述

创建如下:

create database if not exists java_gobang DEFAULT CHARACTER SET utf8;

use java_gobang;

drop table if exists user;
create table user(
                     userId int primary key auto_increment,
                     username varchar(50) unique,
                     password varchar(500),
                     score int, -- 天梯分数
                     totalCount int, -- 比赛总场次
                     winCount int -- 获胜场次
);

注意: 为了避免部署项目时云服务器数据库不支持中文,创建数据库时先提前设置好字符集。并且我们会将密码进行加密,所以密码的长度我们设置大一点

配置 Mybatis:

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

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

logging:
  pattern:
    console: "[%-5level] - %msg%n"

创建 mapper 目录,保存 .xml 文件,xml文件配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.java_gobang.mapper.UserMapper">

<mapper>

六、实现用户模块功能

前面已经配置好了Mybatis文件,下面我们具体来实现 !!

注意: 用户模块不会涉及到消息推送,我们还是通过AJAX向后端发送请求处理后端响应即可 !

6.1 数据库代码编写

  1. 创建实体类
    创建 model 目录,添加 User 实体类,添加 @Data 注解,提供get,set方法
@Data
public class User {
    private int userId;
    private String username;
    private String password;
    private int score;
    private int totalCount;
    private int winCount;
 }
  1. 创建 UserMapper 接口
    创建 mapper 接口目录,添加 UserMapper 接口类

此处主要提供四个方法:

  1. selectByName: 根据用户名查找用户信息. 用于实现登录.
  2. insert: 新增用户. 用户实现注册.
  3. userWin: 用于给获胜玩家修改分数.
  4. userLose: 用户给失败玩家修改分数.
package com.example.java_gobang.mapper;

import com.example.java_gobang.model.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
    // 往数据库里插入一个用户. 用于注册功能.
    void insert(User user);

    // 根据用户名, 来查询用户的详细信息. 用于登录功能
    User selectByName(String username);

    // 总比赛场数 + 1, 获胜场数 + 1, 天梯分数 + 30
    void userWin(int userId);

    // 总比赛场数 + 1, 获胜场数 不变, 天梯分数 - 30
    void userLose(int userId);
}
  1. .xml文件实现UserMapper 接口
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.java_gobang.mapper.UserMapper">
    <insert id="insert">
        insert into user values(null, #{username}, #{password}, 1000, 0, 0);
    </insert>

    <select id="selectByName" resultType="com.example.java_gobang.model.User">
        select * from user where username = #{username};
    </select>
    
    <update id="userWin">
        update user set totalCount = totalCount + 1, winCount = winCount + 1, score = score + 30
        where userId = #{userId}
    </update>
    
    <update id="userLose">
        update user set totalCount = totalCount + 1, score = score - 30
        where userId = #{userId}
    </update>
</mapper>

6.2 前后端交互接口

需要明确用户模块的前后端交互接口. 这里主要涉及到三个部分.

  1. 登录接口

请求:
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: 10,
winCount: 5
}
如果登录失败, 返回的是一个 userId 为 0 的对象.

  1. 注册接口

请求:
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: 10,
winCount: 5
}
如果注册失败(比如用户名重复), 返回的是一个 userId 为 0 的对象.

  1. 获取用户信息

请求:
GET /userInfo HTTP/1.1
响应:
HTTP/1.1 200 OK
Content-Type: application/json
{
userId: 1,
username: ‘zhangsan’,
score: 1000,
totalCount: 10,
winCount: 5
}


6.3 服务器开发

创建 api.UserAPI 类

主要实现四个方法:

  1. login: 用来实现登录逻辑.
  2. register: 用来实现注册逻辑.
  3. getUserInfo: 用来实现登录成功进入大厅后显示用户信息.
  4. logout:用来实现退出游戏功能

代码如下:

@RestController
public class UserAPI {

    @Resource
    private UserMapper userMapper;

    //需要提前添加 BCrypt 依赖
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();

    //登录更新 加上了密码加密
    @PostMapping("/login")
    @ResponseBody
    public Object login(String username, String password, HttpServletRequest request){

        // 查询用户是否在数据库中存在
        User user = userMapper.selectByName(username);

        // 没有查到
        if(user == null) {
            System.out.println("登录失败!");
            return new User();
        }else {

            //查到了,但密码不一样
            if(!bCryptPasswordEncoder.matches(password,user.getPassword())) {
                return new User();
            }
            // 匹配成功,创建 session
            request.getSession().setAttribute("user",user);
            return user;
        }
    }


    //注册更新,加上了密码加密
    @PostMapping("/register")
    @ResponseBody
    public Object register(String username,String password){

        User user1 = userMapper.selectByName(username);
        if(user1 != null){
            System.out.println("当前用户已存在");
            return new User();
        }else{
            User user2 = new User();
            user2.setUsername(username);
            String password1 = bCryptPasswordEncoder.encode(password);
            user2.setPassword(password1);
            userMapper.insert(user2);
            return user2;
        }
    }

    @RequestMapping("/logout")
    public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException {
        HttpSession session = request.getSession(false);
        try{
            session.removeAttribute("user");
            response.sendRedirect("login.html");
        }catch (NullPointerException e){
            System.out.println("session.removeAttribute()这里没有设置拦截器,直接访问logout页面退出会空指针异常");
        }
    }


    @GetMapping("/userInfo")
    @ResponseBody
    public Object getUserInfo(HttpServletRequest req) {
        try {
            HttpSession httpSession = req.getSession(false);
            User user = (User) httpSession.getAttribute("user");//拿到的是登录的用户信息
            // 拿着这个 user 对象, 去数据库中找, 找到最新的数据
            User newUser = userMapper.selectByName(user.getUsername());
            return newUser;
        } catch (NullPointerException e) {
            return new User();
        }
    }
}

注意: 上述逻辑实现,以及密码加密解密等操作和在线音乐平台的实现一样,若不懂大家可以去参考音乐博客 !!


6.4 客户端开发

  1. login.html 登录页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
    <link rel="stylesheet" href="css/common.css">
    <link rel="stylesheet" href="css/login.css">
</head>
<body id="body">
<div class="nav">
    <img src="image/touxiang.png" alt="">
    <span>五子棋对战平台</span>
    <!-- 空白元素, 用来占位置 -->

</div>

<div class="login-container">
    <div class="login-dialog">
        <!-- 登录界面的对话框 -->
        <h3>用户登录</h3>
        <!-- 这个表示一行 -->
        <div class="row">
            <span>用户名</span>
            <input type="text" id="username" placeholder="请输入用户名">
        </div>
        <div class="row">
            <span>密码</span>
            <input type="password" id="password" placeholder="请输入密码">
        </div>
        <!-- 提交按钮 -->
        <div class="submit-row1">
            <button id="submit">登录</button>
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
            <input type="button" id="submit2" value="注册" onclick="toregister()">
        </div>
    </div>
</div>

<script src="./js/jquery.min.js"></script>

<style>
    #body {
        background-image: url("image/roombeijing.png");
        background-size:100% 100%;
        background-attachment: fixed;
    }
</style>

<script type="text/javascript">
    function toregister() {
        window.location.href = "register.html";
    }
</script>
<script>

    let submitButton = document.querySelector("#submit");
    submitButton.onclick = function (){
        let usernameInput = document.querySelector("#username");
        let passwordInput = document.querySelector("#password");

        if(usernameInput.value.trim() == ""){
            alert("请输入用户名!");
            usernameInput.focus();
            return;
        }
        if(passwordInput.value.trim() == ""){
            alert('请输入密码!');
            passwordInput.focus();
            return;
        }

        //通过 jQuery 中的 AJAX 和服务器进行交互
        $.ajax({
            type: 'post',
            url: '/login',
            data: {
                username: usernameInput.value,
                password: passwordInput.value,
            },
            success: function (body){
                //请求执行成功之后的回调函数
                //判定当前是否登录成功
                // 登录成功返回 当前的 User对象,失败返回一个空的 User 对象
                if(body && body.userId > 0){
                    // alert("登录成功!");
                    location.assign('/game_hall.html');
                }else{
                    alert("用户名或密码错误!")
                }
            },
            error: function (){
                //请求执行失败之后的回调函数
                alert("登录失败!")
            }
        });
    }
</script>

</body>
</html>
  1. register.html 注册页面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>注册页面</title>
    <link rel="stylesheet" href="css/common.css">
    <link rel="stylesheet" href="css/login.css">
</head>
<body>
<div class="nav">
    <img src="image/touxiang.png" alt="">
    <span>五子棋对战平台</span>
</div>

<!-- 版心 -->
<div class="login-container">
    <!-- 中间的登陆框 -->
    <div class="login-dialog">
        <h3>用户注册</h3>
        <div class="row">
            <span>用户名</span>
            <input type="text" id="username" placeholder="请输入用户名">
        </div>
        <div class="row">
            <span>密码</span>
            <input type="password" id="password" placeholder="请输入密码">
        </div>
        <div class="row">
            <span>确认密码</span>
            <input type="password" id="password2" placeholder="请确认密码">
        </div>
        <div class="submit-row2">
            <button id="submit">注册</button>
            &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
            <input type="button" id="submit2" value="返回" onclick="toregister()">
        </div>
    </div>
</div>

<script src="./js/jquery.min.js"></script>

<script type="text/javascript">
    function toregister() {
        window.location.href = "login.html";
    }
</script>

<script>

    let submitButton = document.querySelector("#submit");

    submitButton.onclick = function () {
        let username = document.querySelector("#username");
        let password1 = document.querySelector("#password");
        let password2 = document.querySelector("#password2");
        if (username.value.trim() == "") {
            alert("请输入用户名!");
            username.focus();
            return;
        }
        if (password1.value.trim() == "") {
            alert('请输入密码!');
            password1.focus();
            return;
        }
        if (password2.value.trim() == "") {
            alert('请再次输入密码!');
            password2.focus();
            return;
        }
        if (password1.value.trim() != password2.value.trim()) {
            alert('两次输入的密码不同!');
            passwrod1.value = "";
            password2.value = "";
            return;
        }

        $.ajax({
            type: 'post',
            url: '/register',
            data: {
                username: username.value,
                password: password1.value,
            },
            success: function (body) {
                //请求执行成功之后的回调函数
                if (body && body.username) {
                    alert("注册成功!");
                    location.assign('/login.html');
                } else {
                    alert("注册失败!当前用户已经存在!")
                }
            },
            error: function () {
                //请求执行失败之后的回调函数
                alert("注册失败!")
            }
        });
    }
</script>

</body>
</html>

七、实现匹配模块功能

7.1 前后端交互接口

先通过 WebSocket 将前后端连接起来

前端初始化 websocket ,连接:

ws://127.0.0.1:8080/findMatch

后端创建匹配类 MatchAPI,并创建 WebSocketConfig 类实现WebSocketConfigurer接口,来连接匹配类 MatchAPI 和 前端,实现如下:

package com.example.java_gobang.config;

import com.example.java_gobang.api.GameAPI;
import com.example.java_gobang.api.MatchAPI;
import com.example.java_gobang.api.TestAPI;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Autowired
    private TestAPI testAPI;

    @Autowired
    private MatchAPI matchAPI;

    @Autowired
    private GameAPI gameAPI;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
        webSocketHandlerRegistry.addHandler(testAPI, "/test");
        webSocketHandlerRegistry.addHandler(matchAPI, "/findMatch")
                //在注册websocket API的时候,就需要把前面准备的 Httpsession(用户登录会给Httpsession保存用户信息) 搞过来(搞到websocket的session中)
                //因为在匹配中,你需要把用户相关的信息发送给服务器,服务器根据此信息进行水平匹配
                .addInterceptors(new HttpSessionHandshakeInterceptor());
        webSocketHandlerRegistry.addHandler(gameAPI, "/game")
                .addInterceptors(new HttpSessionHandshakeInterceptor());
    }
}

注意: 第一个参数为后端匹配的类,第二个参数也必须和前端规定的路径保持一致 !!

在 addHandler 之后, 再加上一个 .addInterceptors(new HttpSessionHandshakeInterceptor()) 代码, 这样可以把之前登录过程中往 HttpSession 中存放的数据(主要是 User 对象), 放到 WebSocket 的 session 中. 方便后面的代码中获取到当前用户信息.

实现连接后,我们来看具体的请求和响应 !!


请求:
{
message: ‘startMatch’ / ‘stopMatch’,
}

响应1: (收到请求后立即响应)
{
ok: true, // 是否成功. 比如用户 id 不存在, 则返回 false
reason: ‘’, // 错误原因
message: ‘startMatch’ / ‘stopMatch’
}

响应2: (匹配成功后的响应)
{
ok: true, // 是否成功. 比如用户 id 不存在, 则返回 false
reason: ‘’, // 错误原因
message: ‘matchSuccess’,
}

注意:

  1. 页面这端拿到匹配响应之后, 就跳转到游戏房间.
  2. 如果返回的响应 ok 为 false, 则弹框的方式显示错误原因, 并跳转到登录页面

7.2 客户端开发

创建 game_hall.html 游戏大厅页面, 主要包含:

  1. #screen 用于显示玩家的分数信息
  2. button#match-button 作为匹配按钮.

game_hall.html 的 js 部分代码功能:

  1. 点击匹配按钮, 就会进入匹配逻辑. 同时按钮上提示 “匹配中…(点击取消)” 字样.
  2. 再次点击匹配按钮, 则会取消匹配.
  3. 当匹配成功后, 服务器会返回匹配成功响应, 页面跳转到 game_room.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">
    <img src="image/touxiang.png" alt="">
    <span>五子棋对战大厅</span>
    <div class="spacer"></div>
    <a href="logout">退出登录[Logout]</a>

</div>
    <!-- 整个页面的容器元素 -->
    <div class="container">
        <!-- 这个 div 在 container 中是处于垂直水平居中这样的位置的 -->
        <div>
            <!-- 展示用户信息 -->
            <div id="screen"></div>
            <!-- 匹配按钮 -->
            <div id="match-button">开 始 匹 配 ( Play Game )</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 = '您的信息如下:' + '<br> 姓名: ' + body.username + "," + "天梯分数: " + body.score
                    + "<br> 比赛场次: " + body.totalCount + "," + "获胜场数: " + body.winCount
            },
            error: function() {
                alert("获取用户信息失败!");
            }
        });

        // 此处进行初始化 websocket, 并且实现前端的匹配逻辑. 
        // 此处的路径必须写作 /findMatch, 千万不要写作 /findMatch/ 
        let websocketUrl = 'ws://' + location.host + '/findMatch';//location.host访问 game_hall 同样的IP和端口号,不在此处写死,更加灵活
        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) {
            // 处理服务器返回的响应数据. 这个响应就是针对 "开始匹配" / "结束匹配" 来对应的
            // 解析得到的响应对象. 返回的数据是一个 JSON 字符串, 解析成 js 对象 JSON.stringify反之
            //扩展:JSON 转换为 Java对象:objectMapper.readValue  反之:writeValueAsString
            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 = '开 始 匹 配 ( Play Game )';
            } else if (resp.message == 'matchSuccess') {
                // 已经匹配到对手了. 
                console.log("匹配到对手! 进入游戏房间!");
                // location.assign("/game_room.html");
                location.replace("/game_room.html");//避免用户在浏览器使用回退功能造成逻辑出错,我们在此使用 replace,不会回退到上一历史页面
            } 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 == '开 始 匹 配 ( Play Game )') {
                    console.log("开 始 匹 配 ( Play Game )");
                    websocket.send(JSON.stringify({
                        message: 'startMatch',
                    }));
                } else if (matchButton.innerHTML == '匹 配 中 ! ! ( 点 击 停 止 )') {
                    console.log("停止匹配");
                    websocket.send(JSON.stringify({
                        message: 'stopMatch',
                    }));
                }
            } else {
                // 这是说明连接当前是异常的状态
                alert("当前您的连接已经断开! 请重新登录!");
                location.replace('/login.html');
            }
        }
    </script>
</body>
</html>

7.3 服务器开发

创建 api.MatchAPI, 继承自 TextWebSocketHandler 作为处理 websocket 请求的入口类,具体如何实现与前端实现 websocket 连接,前面已经写好了的

// 通过这个类来处理匹配功能中的 websocket 请求
@Component
public class MatchAPI extends TextWebSocketHandler {
    //处理JSON格式
    private ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private Matcher matcher;

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
       
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
       
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    
    }
}

下面我们就来考虑如何实现这几个重写方法 !!!先做好其他准备

  1. 实现用户管理器

创建 game.OnlineUserManager 类, 用于管理当前用户的在线状态. 本质上是 哈希表 的结构. key 为用户 id, value 为用户的 WebSocketSession.

借助这个类, 一方面可以判定用户是否是在线, 同时也可以进行方便的获取到 Session 从而给客户端回话.

  1. 当玩家建立好 websocket 连接, 则将键值对加入 OnlineUserManager 中.
  2. 当玩家断开 websocket 连接, 则将键值对从 OnlineUserManager 中删除.
  3. 在玩家连接好的过程中, 随时可以通过 userId 来查询到对应的会话, 以便向客户端返回数据.

由于存在两个页面, 游戏大厅和游戏房间, 使用两个 哈希表 来分别存储两部分的会话.

代码实现如下:

package com.example.java_gobang.game;

import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;

import java.util.HashMap;
import java.util.concurrent.ConcurrentHashMap;

//这个类用来管理 用户在大厅和游戏房间里的状态
@Component
public class OnlineUserManager {
    // 这个哈希表就用来表示当前用户在游戏大厅在线状态.
    // 避免同时有多个用户并发和服务器建立/断开连接,这里采用线程安全的 ConcurrentHashMap
    private ConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>();
    // 这个哈希表就用来表示当前用户在游戏房间的在线状态.
    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);
    }
}

  1. 创建匹配请求/响应对象

创建 game.MatchRequest 类

package com.example.java_gobang.game;

// 这是表示一个 websocket 的匹配请求
public class MatchRequest {
    private String message = "";

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

创建 game.MatchResponse 类

// 这是表示一个 websocket 的匹配响应
@Data
public class MatchResponse {
    private boolean ok;
    private String reason;
    private String message;
 }
  1. 处理连接成功

实现 afterConnectionEstablished 方法.

  1. 通过参数中的 session 对象, 拿到之前登录时设置的 User 信息.

  2. 先判定用户是否是已经在线, 如果在线则直接返回出错 (禁止同一个账号多开).

  3. 使用 onlineUserManager 来管理用户的在线状态,设置玩家的上线状态.

代码实现如下:

@Override
 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. 先判定当前用户是否已经登录过(已经是在线状态), 如果是已经在线, 就不该继续进行后续逻辑.禁止多开(同一账号登录多处)
            // 如果使用浏览器多开,会使用户的状态在hash表中同一个key对应两个value,而后一个value会将前一个value覆盖,
            // 所以第一个浏览器的连接就会失效,拿不到websocketsession,也就无法给它推送数据了
            if (onlineUserManager.getFromGameHall(user.getUserId()) != null
                    || onlineUserManager.getFromGameRoom(user.getUserId()) != null) {
                // 当前用户已经登录了!!
                // 针对这个情况要告知客户端, 你这里重复登录了.
                MatchResponse response = new MatchResponse();
                response.setOk(true);
                response.setReason("您已登录,当前禁止多开!");
                response.setMessage("repeatConnection");
                // TestMessage 表示一个文本格式的 websocket 数据包
                session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
                // 此处直接关闭有些太激进了, 还是返回一个特殊的 message response.setMessage("repeatConnection"), 供客户端来进行判定, 由客户端负责进行处理
                // 并且这里的直接关闭会触发后面的 afterConnectionClosed,通过用户id使先登录的浏览器也会断开websocket连接,不科学,我们只需要禁止重复登录就行
                // session.close();
                return;
            }

            // 3. 拿到了身份信息之后, 就可以把玩家设置成在线状态了
            onlineUserManager.enterGameHall(user.getUserId(), session);
            System.out.println("玩家 " + user.getUsername() + " 进入游戏大厅!");
        } catch (NullPointerException e) {
            System.out.println("[MatchAPI.afterConnectionEstablished] 当前用户未登录!");
            // e.printStackTrace(); 不直接打印异常调用栈了,我们在控制台自定义日志输出
            // 出现空指针异常, 说明当前用户的身份信息是空, 用户未登录呢.
            // 把当前用户尚未登录这个信息给返回回去~~
            MatchResponse response = new MatchResponse();
            response.setOk(false);
            response.setReason("您尚未登录! 不能进行后续匹配功能!");

            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        }
    }
  1. 处理开始匹配/取消匹配请求

实现 handleTextMessage 方法

  1. 先从会话中拿到当前玩家的信息.
  2. 解析客户端发来的请求
  3. 判定请求的类型, 如果是 startMatch, 则把用户对象加入到匹配队列. 如果是 stopMatch, 则把用户对象从匹配队列中删除.
  4. 此处需要实现一个 匹配器类Matcher, 来处理匹配的实际逻辑.
    @Override
    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.setReason("非法的匹配请求");
        }
        String jsonString = objectMapper.writeValueAsString(response);
        session.sendMessage(new TextMessage(jsonString));
    }
  1. 实现匹配器

创建 game.Matcher 类.并按照如下要求实现

  1. 在 Matcher 中创建三个队列 (队列中存储 User 对象), 分别表示不同的段位的玩家.
    (此处约定 <2000 一档, 2000-3000 一档, >3000 一档)
  2. 提供 add 方法, 供 MatchAPI 类来调用, 用来把玩家加入匹配队列.
  3. 提供 remove 方法, 供 MatchAPI 类来调用, 用来把玩家移出匹配队列.
  4. 同时 Matcher 找那个要记录 OnlineUserManager, 来获取到玩家的 Session.
  5. 在 Matcher 的构造方法中, 创建一个线程, 使用该线程扫描每个队列, 把每个队列的头两个元素取出来, 匹配到一组中.
  6. 实现 handlerMatch 处理匹配方法

1.由于 handlerMatch 在单独的线程中调用. 因此要考虑到多用户访问队列的线程安全问题. 需要加上锁.
2.每个队列分别使用队列对象本身作为锁即可.
3.在入口处使用 wait 来等待, 直到队列中达到 2 个元素及其以上, 才唤醒线程消费队列.插入成功后要通知唤醒上面的等待逻辑.
4.需要给上面的插入队列元素, 删除队列元素也加上锁

代码实现如下:

// 这个类表示 "匹配器", 通过这个类负责完成整个匹配功能
@Component
public class Matcher {
    // 创建三个匹配队列
    private Queue<User> normalQueue = new LinkedList<>();
    private Queue<User> highQueue = new LinkedList<>();
    private Queue<User> veryHighQueue = new LinkedList<>();

    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private RoomManager roomManager;

    private ObjectMapper objectMapper = new ObjectMapper();

    // 操作匹配队列的方法.
    // 把玩家放到匹配队列中
    //对于不同队列同时进行添加和移除操作是不会产生线程不安全的,而是对同一队列进行,所以要对同一队列对象进行加锁
    public void add(User user) {
        if (user.getScore() < 2000) {
            synchronized (normalQueue) {
                normalQueue.offer(user);
                normalQueue.notify();//有玩家进入匹配队列时,就唤醒该队列 ,对应handlerMatch方法中的 wait
            }
            System.out.println("把玩家 " + user.getUsername() + " 加入到了 normalQueue匹配队列中!");
        } else if (user.getScore() >= 2000 && user.getScore() < 3000) {
            synchronized (highQueue) {
                highQueue.offer(user);
                highQueue.notify();
            }
            System.out.println("把玩家 " + user.getUsername() + " 加入到了 highQueue匹配队列中!");
        } else {
            synchronized (veryHighQueue) {
                veryHighQueue.offer(user);
                veryHighQueue.notify();
            }
            System.out.println("把玩家 " + user.getUsername() + " 加入到了 veryHighQueue匹配队列中!");
        }
    }

    // 当玩家点击停止匹配的时候, 就需要把玩家从匹配队列中删除
    public void remove(User user) {
        if (user.getScore() < 2000) {
            synchronized (normalQueue) {
                normalQueue.remove(user);
            }
            System.out.println("把玩家 " + user.getUsername() + " 移除了 normalQueue匹配队列!");
        } else if (user.getScore() >= 2000 && user.getScore() < 3000) {
            synchronized (highQueue) {
                highQueue.remove(user);
            }
            System.out.println("把玩家 " + user.getUsername() + " 移除了 highQueue匹配队列!");
        } else {
            synchronized (veryHighQueue) {
                veryHighQueue.remove(user);
            }
            System.out.println("把玩家 " + user.getUsername() + " 移除了 veryHighQueue匹配队列!");
        }
    }

    public Matcher() {
        // 创建三个线程, 分别针对这三个匹配队列, 进行操作.
        Thread t1 = new Thread() {
            @Override
            public void run() {
                // 扫描 normalQueue
                while (true) { //在这里循环速度极快,一进入handlerMatch就快速返回,但是当队列中的个数小于2时,这样快速的扫描就没有什么意义,且CPU占用率很高,即出现了忙等
                    handlerMatch(normalQueue);
                    //针对上面的忙等,可以在调用完 handlerMatch 后,进行sleep
                    //这样做可以,但是不完美,比如玩家已经匹配到对手,却还要等待休眠结束后,才能进行游戏
                }
            }
        };
        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();
    }

    private void handlerMatch(Queue<User> matchQueue) {
        synchronized (matchQueue) {
            try {
                // 1. 检测队列中元素个数是否达到 2
                //    队列的初始情况可能是 空
                //    如果往队列中添加一个元素, 这个时候, 仍然是不能进行后续匹配操作的.
                //    因此在这里使用 while 循环检查是更合理的~~ 只有当 size大于2后,才能进行后续操作
                while (matchQueue.size() < 2) {
                    matchQueue.wait();//队列中的数目一直小于2时,即一直还没有玩家加入队列直到数目达到2以上,就进行线程等待
                }
                // 2. 尝试从队列中取出两个玩家
                User player1 = matchQueue.poll();
                User player2 = matchQueue.poll();
                System.out.println("匹配出两个玩家: " + player1.getUsername() + ", " + player2.getUsername());
                // 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) {
                    // 如果玩家2 现在下线了, 就把玩家1 重新放回匹配队列中
                    matchQueue.offer(player1);
                    return;
                }
                // 当前能否排到两个玩家是同一个用户的情况嘛? 一个玩家入队列了两次??
                // 理论上也不会存在~~
                // 1) 如果玩家下线, 就会对玩家移出匹配队列
                // 2) 又禁止了玩家多开.
                // 但是仍然在这里多进行一次判定, 以免前面的逻辑出现 bug 时带来严重的后果.
                if (session1 == session2) {
                    // 把其中的一个玩家放回匹配队列.
                    matchQueue.offer(player1);
                    return;
                }

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

                // 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));
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

通过上述匹配器进行玩家匹配,成功匹配出两名玩家后,我们就将玩家加入到一个游戏房间中进行后续具体的游戏操作。所以接下来我们需要完成游戏房间的设置

  1. 创建房间类

创建 game.Room 类

  1. 一个房间要包含一个房间 ID, 使用 UUID 作为房间的唯一身份标识.
  2. 房间内要记录对弈的玩家双方信息.
  3. 记录先手方的 ID
  4. 记录一个 二维数组 , 作为对弈的棋盘.
  5. 记录一个 OnlineUserManager, 以备后面和客户端进行交互.
  6. 当然, 少不了 ObjectMapper 来处理 json
@Data
public class Room {
    private String roomId;
    // 玩家1
    private User user1;
    // 玩家2
    private User user2;
    // 先手方的用户 id
    private int whiteUserId = 0;
    // 棋盘, 数字 0 表示未落子位置. 数字 1 表示玩家 1 的落子. 数字 2 表示玩家 2 的落子
    private static final int MAX_ROW = 15;
    private static final int MAX_COL = 15;
    private int[][] chessBoard = new int[MAX_ROW][MAX_COL];

    private ObjectMapper objectMapper = new ObjectMapper();

    private OnlineUserManager onlineUserManager;



    public Room() {
        // 构造 Room 的时候生成一个唯一的字符串表示房间 id.
        // 使用 UUID 来作为房间 id
        roomId = UUID.randomUUID().toString();

        // 通过入口类中记录的 context 来手动获取到前面的 RoomManager 和 OnlineUserManager
        onlineUserManager = JavaGobangApplication.context.getBean(OnlineUserManager.class);
        roomManager = JavaGobangApplication.context.getBean(RoomManager.class);
        userMapper = JavaGobangApplication.context.getBean(UserMapper.class);
    }
}

具体的游戏操作,我们在后面的对战模块进行补写 !!

  1. 实现房间管理器

Room 对象会存在很多. 每两个对弈的玩家, 都对应一个 Room 对象.需要一个管理器对象RoomMannager 来管理所有的 Room.

创建 game.RoomManager

  1. 使用一个 Hash 表, 保存所有的房间对象, key 为 roomId, value 为 Room 对象
  2. 再使用一个 Hash 表, 保存 userId -> roomId 的映射, 方便根据玩家来查找所在的房间.
  3. 提供增, 删, 查的 API. (查包含两个版本, 基于房间 ID 的查询和基于用户 ID 的查询).

代码实现如下:

// 房间管理器类.
// 这个类也希望有唯一实例.
@Component
public class RoomManager {
    private ConcurrentHashMap<String, Room> rooms = new ConcurrentHashMap<>();

    //将玩家id 和 游戏房间id 的映射关系存储起来
    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);
    }
    
    //通过玩家id 找到玩家所在的游戏房间
    public Room getRoomByUserId(int userId) {
        String roomId = userIdToRoomId.get(userId);
        if (roomId == null) {
            // userId -> roomId 映射关系不存在, 直接返回 null
            return null;
        }
        return rooms.get(roomId);
    }
}
  1. 处理连接关闭

实现 afterConnectionClosed

  1. 主要的工作就是把玩家从 onlineUserManager 中退出.
  2. 退出的时候要注意判定, 当前玩家是否是多开的情况(一个userId, 对应到两个 websocket 连接). 如果一个玩家开启了第二个 websocket 连接, 那么这第二个 websocket 连接不会影响到玩家从 OnlineUserManager 中退出.
  3. 如果玩家当前在匹配队列中, 则直接从匹配队列里移除.

代码实现如下:

@Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        try {
            // 玩家下线, 从 OnlineUserManager 中删除
            User user = (User) session.getAttributes().get("user");
            WebSocketSession tmpSession = onlineUserManager.getFromGameHall(user.getUserId());
            if (tmpSession == session) {
                onlineUserManager.exitGameHall(user.getUserId());
            }
            // 如果玩家正在匹配中, 而 websocket 连接断开了, 就应该移除匹配队列
            matcher.remove(user);
        } catch (NullPointerException e) {
            System.out.println("[MatchAPI.afterConnectionClosed] 当前用户未登录!");
            // e.printStackTrace();
            // 不应该在连接关闭之后, 还尝试发送消息给客户端
//            MatchResponse response = new MatchResponse();
//            response.setOk(false);
//            response.setReason("您尚未登录! 不能进行后续匹配功能!");
//            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        }
    }
}
  1. 理连接异常

实现 handleTransportError. 逻辑同上.

@Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        try {
            // 玩家下线, 从 OnlineUserManager 中删除
            User user = (User) session.getAttributes().get("user");
            WebSocketSession tmpSession = onlineUserManager.getFromGameHall(user.getUserId());
            if (tmpSession == session) {
                onlineUserManager.exitGameHall(user.getUserId());
            }
            // 如果玩家正在匹配中, 而 websocket 连接断开了, 就应该移除匹配队列
            matcher.remove(user);
        } catch (NullPointerException e) {
            System.out.println("[MatchAPI.handleTransportError] 当前用户未登录!");
//            e.printStackTrace();

//            MatchResponse response = new MatchResponse();
//            response.setOk(false);
//            response.setReason("您尚未登录! 不能进行后续匹配功能!");
//            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));

            //上述websocket连接都已经断开了,还怎么能发送响应呢 !!!
        }
    }

八、实现对战模块功能

至此,上述逻辑能成功实现当多个玩家开始匹配游戏,两两玩家能被分配到一个游戏房间中进行游戏,至于如何实现游戏逻辑,我们接着来看 !!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/532536.html

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

相关文章

构建新一代智慧园区移动应用以推动数字转型

随着智慧城市的建设和智慧园区的崛起&#xff0c;智慧园区数字一体化建设成为园区发展的重心&#xff0c;当然数字转型离不开移动应用的整合服务。 在过去的几年中&#xff0c;智慧园区移动应用已经发展成为园区管理和服务的重要手段之一&#xff0c;为企业和员工提供了更加便…

知行之桥EDI系统2023版功能介绍——概览页面

登录知行之桥EDI系统2023版&#xff0c;即可看到概览页面。默认情况下&#xff0c;会显示过去7天的各项指标。用户可以在页面右上角&#xff1a;显示过去的数据 下拉列表中手动选择需要的时间段&#xff0c;如&#xff1a;24小时、3天、7天等。 关键指标的自定义配置 概览页面…

如何将exe注册为windows服务,直接从后台运行

如何将exe注册为windows服务&#xff0c;直接从后台运行 使用instsrvsrvanywindow64位系统安装配置 window32位系统安装 使用instsrvsrvany 这是地址&#xff1a;链接: 网盘地址 提取码: h2za 复制这段内容后打开百度网盘手机App&#xff0c;操作更方便哦 window64位系统 安…

[OOD设计] - 电梯系统设计

明确主要需求 首先需要设计电梯系统的基本工作流程&#xff0c;一个简单电梯系统主要就是两个主要功能&#xff1a; 乘客在电梯外按下按钮时&#xff0c;电梯系统会驱动一个电梯来接人乘客在电梯内部按下楼层按钮时&#xff0c;电梯系统会驱动该电梯到达指定楼层 根据需求来…

泰克Tektronix AFG31021 任意波函数发生器产品资料

AFG31021是一款高质量、多功能的任意波形发生器&#xff0c;可以生成高精度、高分辨率的波形信号。该产品的主要特点包括&#xff1a; 可以生成任意波形信号&#xff0c;内置多种标准波形&#xff0c;如正弦波、方波、三角波、锯齿波等&#xff0c;也可以通过用户自定义来生成…

支付从业者转型路在何方?

近来&#xff0c;整个支付行业&#xff0c;已经“卷”出了新高度。 营销上电销卷地推&#xff0c;工单卷电销&#xff0c;POS机具则是退押金卷不退押金&#xff0c;无押金卷退押金”&#xff0c;互相“卷”得不亦乐乎。 与此同时&#xff0c;支付圈子里聊的永远是“成本上升”…

微信小程序是怎么做的?

微信小程序是一种轻量级的应用&#xff0c;它可以在微信内部直接使用&#xff0c;无需下载和安装。那么&#xff0c;微信小程序是怎么做的呢&#xff1f; 微信小程序制作的大概步骤 微信小程序制作主要包括以下几个步骤&#xff1a; ①注册小程序账号 ②在小程序制作工具创…

5th-Generation Mobile Communication Technology(一)

目录 一、5G/NR 1、 快速参考&#xff08;Quick Reference&#xff09; 2、5G Success 3、5G Challenges 4、Qualcomm Videos 二、PHY and Protocol 1、Frame Structure 2、Numerology 3、Waveform 4、Frequency Band 5、BWP 6、Synchronization 7、Beam Management 8、CSI Fra…

matmul/mm 函数用法介绍

介绍torch.matmul之前先介绍torch.mm函数, mm和matmul都是torch中矩阵乘法函数&#xff0c;mm只能作用于二维矩阵&#xff0c;matmul可以作用于二维也能作用于高维矩阵 mm函数使用 x torch.rand(4, 9) y torch.rand(9, 8) print(torch.mm(x,y).shape)torch.Size([4, 8]) m…

Linux Shell 实现一键部署VMware Workstation

VMware Workstation 前言 VMware Workstation Pro 是业界标准的桌面 Hypervisor&#xff0c;用于在 Linux 或 Windows PC 上运行虚拟机 download VMware_Workstation VMware_Workstation WindowsVMware_Workstation linux文档downloaddownload参考 Linux 各系统下载使用参…

Scrapy 基本使用

一、Scrapy框架使用 1. 创建scrapy项目&#xff08;不能有汉字&#xff0c;不能数字开头&#xff09; scrapy startproject Baidu 2. 创建爬虫文件 1. cd Baidu 2. scrapy genspider wenda www.baidu.com 注意&#xff1a; parse()是执行了start_url之后要执行的方法&#…

Camunda 外部任务(External Task)如何使用

Camunda的外部任务&#xff08;External Task&#xff09;是一种可分配和可重试的任务&#xff0c;与传统的用户任务不同&#xff0c;它可以在 Camunda 流程引擎之外的应用程序中执行。 外部任务可以用于将复杂的业务逻辑委托给外部系统&#xff0c;例如第三方服务或自定义的应…

Java简便模拟验证码

package staticStu.util;import java.util.Random;public class Login {public static void main(String[] args) {// 开发一个验证码// 1、定义一个变量记录验证码String code"";// 2、定义一个变量记住全部验证码字符String data"abcdefghijklmnopqrstuvwxyzA…

mongodb wireshark显示

mongodb wireshark展示为ssl&#xff0c;只需要禁用tls&#xff0c;然后把当前会话解码为mongo就可以

鸿蒙Hi3861学习十四-Huawei LiteOS-M(AP模式)

一、简介 参看上一章&#xff1a;鸿蒙Hi3861学习十四-Huawei LiteOS-M&#xff08;STA模式&#xff09;_t_guest的博客-CSDN博客 二、API介绍 RegisterWifiEvent 函数功能&#xff1a; 为指定的WIFI事件注册回调函数。当WIFIEVENT中定义的WIFI事件发生时&#xff0c;将调用已注…

MySQL体系架构

一、 MySQL体系架构 MySQL体系架构可分为物理架构和逻辑架构。 1、MySQL物理体系架构 1.1 配置文件 auto.cnf: 配置了MySQL Server的UUIDmy.cnf: MySQL的配置文件 1.2 其他重要文件 -basedirdir_name: MySQL安装的二进制文件目录-datadirdir_name: MySQL的数据目录和-pid-…

使用 Conv1D-LSTM 进行时间序列预测:预测多个未来时间步【优化】

未优化之前的版本见下&#xff0c;作者当时主要是为Mark这个项目&#xff0c;未进行深入分析。 使用 Conv1D-LSTM 进行时间序列预测&#xff1a;预测多个未来时间步 Introduction 通常有许多时间序列预测方法&#xff0c;例如 ARIMA、SARIMA 和 Holtz-winters&#xff0c;但是…

计算机网络基础(子网划分)

文章目录 一、网络设备二、IP地址分类A类地址B类地址C类地址特殊IP地址 三、划分子网根据子网掩码判断主机数根据子网和主机判断子网掩码根据IP和子网掩码判断子网数 四、场景运用 一、网络设备 1.集线器&#xff08;hub&#xff09;&#xff1a;不可以隔离冲突域&#xff0c;…

Redis内存优化——ZSet类型介绍及底层原理详解

系列文章目录 Redis内存优化——String类型介绍及底层原理详解 Redis内存优化——Hash类型介绍及底层原理详解 Redis内存优化——List类型介绍及底层原理详解 Redis内存优化——Set类型介绍及底层原理详解 Redis内存优化——ZSet类型介绍及底层原理详解 文章目录 系列文章目录…

Ingress:k8s集群进出流量的总管

Ingress&#xff1a;k8s集群进出流量的总管 Service 对象&#xff0c;它是 Kubernetes 内置的负载均衡机制&#xff0c;使用静态 IP 地址代理动态变化的 Pod&#xff0c;支持域名访问和服务发现&#xff0c;是微服务架构必需的基础设施。 Service 很有用&#xff0c;但也只能说…