主要功能介绍
- 用户模块:
- 用户的注册和登录功能
- 管理用户的天梯分数、获胜场数及比赛场次等信息;
- 匹配模块
- 依据用户的天梯分数,实现匹配机制;
- 对战模式
- 把两个匹配的玩家放到一个游戏房间中,双方通过网页的形式来进行对战比赛。
所用技术栈:
后端:Spring、Spring Boot、Spring MVC
前端:HTML、CSS、JS、AJAX
数据库:MySQL、MyBatis
WebSocket
项目创建
最终项目整体目录结构如下:
认识WebSocket
WebSocket
是从 HTML5
开始支持的一种网页端和服务端保持长连接的 消息推送机制.
WebSocket
是实现消息推送的主要机制。像五子棋这样的程序, 或者聊天这样的程序, 都是非常依赖 “消息推送” 的. 如果只是使用原生的 HTTP 协议, 要想实现消息推送一般需要通过 “轮询” 的方式.轮询的成本比较高, 而且也不能及时的获取到消息的响应.而 WebSocket 则是更接近于 TCP 这种级别的通信方式. 一旦连接建立完成, 客户端或者服务器都可以主动的向对方发送数据.
原理解析
WebSocket
协议本质上是一个基于 TCP
的协议。为了建立一个 WebSocket
连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,通过这个附加头信息完成握手过程.
WebSocket报文格式
其是一个应用层协议,下层是基于TCP的。
FIN
: 为 1 表示要断开 websocket 连接.RSV1/RSV2/RSV3
: 保留位, 一般为 0.opcode
: 操作代码. 决定了如何理解后面的数据载荷,opcode描述了当前这个websocket 报文是啥类型。
0x0: 表示这是个延续帧. 当 opcode 为 0, 表示本次数据传输采用了数据分片, 当前收到的帧为其中一个分片.
0x1: 表示这是文本帧.
0x2: 表示这是二进制帧.
0x3-0x7: 保留, 暂未使用.
0x8: 表示连接断开.
0x9: 表示 ping 帧.
0xa: 表示 pong 帧.
0xb-0xf: 保留, 暂未使用.
mask
: 表示是否要对数据载荷进行掩码操作。从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。Payload length
:数据载荷的长度,单位是字节。为7位,或7+16位,或1+64位。Masking-key
:0或4字节(32位)所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-keypayload data
: 报文携带的载荷数据.
Spring 内置了 websocket
. 可以直接进行使用.
用户模块:用户模块主要负责用户的注册, 登录, 分数记录功能.
使用 MySQL 数据库存储数据.
客户端提供一个登录页面+注册页面.
服务器端基于 Spring + MyBatis 来实现数据库的增删改查.
匹配模块:用户登录成功, 则进入游戏大厅页面.
游戏大厅中, 能够显示用户的名字, 天梯分数, 比赛场数和获胜场数.
同时显示一个 “匹配按钮”.
点击匹配按钮则用户进入匹配队列, 并且界面上显示为 “取消匹配” .
再次点击则把用户从匹配队列中删除.
如果匹配成功, 则跳转进入到游戏房间页面.
页面加载时和服务器建立 websocket
连接. 双方通过 websocket
来传输 “开始匹配”, “取消匹配”, “匹配成功” 这样的信息.
对战模块:玩家匹配成功, 则进入游戏房间页面.
每两个玩家在同一个游戏房间中.
在游戏房间页面中, 能够显示五子棋棋盘. 玩家点击棋盘上的位置实现落子功能.
并且五子连珠则触发胜负判定, 显示 “你赢了” “你输了”.
页面加载时和服务器建立 websocket 连接. 双方通过 websocket 来传输 “准备就绪”, “落子位置”, “胜负” 这样的信息.
准备就绪: 两个玩家均连上游戏房间的 websocket 时, 则认为双方准备就绪.
落子位置: 有一方玩家落子时, 会通过 websocket 给服务器发送落子的用户信息和落子位置, 同时服务器再将这样的信息返回给房间内的双方客户端. 然后客户端根据服务器的响应来绘制棋子位置.
胜负: 服务器判定这一局游戏的胜负关系. 如果某一方玩家落子, 产生了五子连珠, 则判定胜负并返回胜负信息. 或者如果某一方玩家掉线(比如关闭页面), 也会判定对方获胜.
引入pom.xml依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
数据库设计
创建user表,表示用户信息和身份信息。初始化其天梯分数都为1000.
create database if not exists java_gobang;
use java_gobang;
drop table if exists user;
create table user(
userId int primary key auto_increment,
username varchar(50) unique,
password varchar(50),
score int, -- 天梯分数
totalCount int, -- 比赛总场次
winCount int -- 获胜场次
);
insert into user values(null, '张三', '123', 1000, 0, 0);
insert into user values(null, '李四', '123', 1000, 0, 0);
insert into user values(null, '王五', '123', 1000, 0, 0);
insert into user values(null, '赵六', '123', 1000, 0, 0);
配置 MyBatis
编辑 application.yml
如下:
# 配置数据库的连接字符串
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/java_gobang?characterEncoding=utf8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 设置 Mybatis 的 xml 保存路径
mybatis:
mapper-locations: classpath:mapper/**Mapper.xml
configuration: # 配置打印 MyBatis 执行的 SQL
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 配置打印 MyBatis 执行的 SQL
logging:
level:
com:
example:
demo: debug
用户模块
创建实体类model.User
:
@Data
public class User {
private int userId;
private String username;
private String password;
private int score;
private int totalCount;
private int winCount;
}
创建 model.UserMapper
接口:
@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);
}
在resource下面创建一个mapper文件,并在该文件下创建一个UserMapper.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.demo.model.UserMapper">
<insert id="insert">
insert into user values(null, #{username}, #{password}, 1000, 0, 0);
</insert>
<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>
<select id="selectByName" resultType="com.example.demo.model.User">
select * from user where username = #{username};
</select>
</mapper>
需要明确用户模块的前后端交互接口. 这里主要涉及到三个部分:
登录接口
请求:
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 的对象.
注册接口
请求:
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 的对象.
获取用户信息
请求:
GET /userInfo HTTP/1.1
响应:
HTTP/1.1 200 OK
Content-Type: application/json
{
userId: 1,
username: 'zhangsan',
score: 1000,
totalCount: 10,
winCount: 5
}
服务器开发
创建 api.UserAPI
类
主要实现三个方法:
login
: 用来实现登录逻辑.
register
: 用来实现注册逻辑.
getUserInfo
: 用来实现登录成功后显示用户分数的信息.
package com.example.demo.api;
import com.example.demo.model.User;
import com.example.demo.model.UserMapper;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* Created With IntelliJ IDEA
* Description:
* Users: yyyyy
* Date: 2022-08-17
* Time: 8:41
*/
@RestController
public class UserAPI {
@Resource
private UserMapper userMapper;
/**
* 登录
* @param username
* @param password
* @param request
* @return
*/
@RequestMapping("/login")
@ResponseBody
public Object login(@RequestParam String username, @RequestParam String password,
HttpServletRequest request){
User user = userMapper.selectByName(username);
if (user == null || !user.getPassword().equals(password)){
System.out.println("登录失败");
return new User();
}
HttpSession session = request.getSession(true);
session.setAttribute("user",user);
return user;
}
/**
* 注册功能
* @param username
* @param password
* @return
*/
@RequestMapping("/register")
@ResponseBody
public Object register(@RequestParam String username, @RequestParam 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;
}
}
@RequestMapping("/userInfo")
@ResponseBody
public Object getUserInfo(HttpServletRequest request){
try {
//处理获取到了session为空的情况
HttpSession session = request.getSession(false);
User user = (User) session.getAttribute("user");
//拿到user对象去数据库中找
User newUser = userMapper.selectByName(user.getUsername());
return newUser;
}catch (NullPointerException e){
return new User();
}
}
}
Postman
登录功能验证:
注册功能验证:
验证获取用户信息功能:
客户端开发
登录界面的设计
创建一个login.html
,在 login.html
中编写 js 代码
通过 jQuery 中的 AJAX 和服务器进行交互.
<!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">
五子棋对战
</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>
<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: '/login',
data:{
username: usernameInput.value,
password:passwordInput.value,
},
success: function (body) {
//请求执行成功之后的回调函数
if (body && body.userId > 0){
alert("登录成功!");
//重定向到游戏大厅页面
location.assign('/game_hall.html');
}else {
alert("登录失败!");
$("#username").val("");
$("#password").val("");
}
},
error: function () {
//请求执行失败之后的函数,登录失败之后,用户名和密码置为空
alert("登录失败!");
$("#username").val("");
$("#password").val