4、选择项目存放路径,就可以创建出一个 SpringBoot 项目
1、创建 config 包,在 config 包中创建 LoginInterceptor 类
5、在 com.example.online_gobang.api 包中创建 UerAPI 类
3、在 UserAPI 类中添加 getUserInfo 方法
(2)获取 RoomManager 与 OnlineUserManager
(1)使用 canvas 绘制棋盘(game_room.html)
(2)绘制棋盘的 JavaScript 文件(script.js)
- 用户的注册和登录。
- 管理用户的天梯积分、比赛场数、获胜场数等信息。
- 游戏大厅页面
- 依据用户的天梯积分,实现匹配机制。
- 游戏房间页面
- 把两个匹配到的玩家放到一个游戏房间中,双方进行对战比赛。
- 前端:HTML、CSS、JavaScript、jQuery、Ajax
- 后端:Java、SpringBoot、WebSocket
- 数据库:MySQL、MyBatis










- #配置数据库
- spring.datasource.url=jdbc:mysql://127.0.0.1:3306/online_gobang?characterEncoding=utf8&serverTimezone=UTC
- spring.datasource.username=数据库用户名
- spring.datasource.password=数据库密码
- spring.datasource.driver-class-name=com.mysql.jdbc.Driver
-
- mybatis.mapper-locations=classpath:mybatis/**Mapper.xml

- create database if not exists online_gobang;
-
- use online_gobang;
-
- -- 用户表
- drop table if exists user;
- create table user(
- userId int primary key auto_increment comment '用户id',
- username varchar(50) unique comment '用户名',
- password varchar(255) not null comment '用户密码',
- score int comment '天梯积分',
- totalCount int comment '比赛总场数',
- winCount int comment '获胜场数'
- );
- 在 package com.example.online_gobang 目录下创建一个 tools 包,在这个包中存放整个项目要使用的工具类。
- 设计统一的响应体工具类,因为做任何操作时都需要响应,所以封装一个通用的响应工具类,这个工具类设计成一个泛型类。
- package com.example.online_gobang.tools;
-
- import lombok.Data;
-
- @Data
- public class ResponseBodyMessage
{ - private int status; // 状态码
- private String message; // 返回的信息(出错原因等)
- private T data; // 返回给前端的数据(因为返回的数据类型不确定,可能是 String,boolea,int ...,因此使用泛型)
-
- public ResponseBodyMessage(int status, String message, T data) {
- this.status = status;
- this.message = message;
- this.data = data;
- }
- }
- 这个类用来存储不变的常量。 例如:设置 session 对象中的 key 值,key 是一个不变的字符串。
- 如果在其他地方获取对应的 session 就可以通过这个类中的字符串进行获取。
- package com.example.online_gobang.tools;
-
- public class Constant {
- public static final String USER_SESSION_KEY = "user"; // 设置 session 中的 key 值
- }
使用 Bcrypt 对用户密码进行加密
- Bcrypt 是一款加密工具,可以比较方便地实现数据的加密工作。也可以简单理解为它内部自己实现了随机加盐处理 。
- 使用MD5加密,每次加密后的密文其实都是一样的,这样就方便了MD5通过大数据的方式进行破解。
- Bcrypt生成的密文是60位的,而MD5的是32位的,因此 Bcrypt 破解难度更大。
- <dependency>
- <groupId>org.springframework.securitygroupId>
- <artifactId>spring-security-webartifactId>
- dependency>
- <dependency>
- <groupId>org.springframework.securitygroupId>
- <artifactId>spring-security-configartifactId>
- dependency>
- @SpringBootApplication(exclude =
- {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
- package com.example.online_gobang.config;
-
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
- import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
- import org.springframework.web.socket.config.annotation.EnableWebSocket;
- import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
-
- /**
- * Created with IntelliJ IDEA.
- * Description:
- * User: 74646
- * Date: 2022-11-14
- * Time: 16:35
- */
- @Configuration
- @EnableWebSocket
- public class AppConfig implements WebSocketConfigurer,WebMvcConfigurer {
- @Bean
- public BCryptPasswordEncoder getBCryptPasswordEncoder(){
- return new BCryptPasswordEncoder();
- }
- }
- package com.example.online_gobang.config;
-
- import com.example.online_gobang.tools.Constant;
- import org.springframework.web.servlet.HandlerInterceptor;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import javax.servlet.http.HttpSession;
-
- public class LoginInterceptor implements HandlerInterceptor {
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- HttpSession session = request.getSession(false);
- if(session == null || session.getAttribute(Constant.USER_SESSION_KEY)==null){
- return false;
- }
- return true;
- }
- }
- @Configuration
- @EnableWebSocket
- public class AppConfig implements WebSocketConfigurer,WebMvcConfigurer {
- /**
- * 添加拦截器
- * @param registry
- */
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- // 登录之后才可以访问其他页面
- LoginInterceptor loginInterceptor = new LoginInterceptor();
- registry.addInterceptor(loginInterceptor).
- // 拦截所有的
- addPathPatterns("/**")
- //排除所有的JS
- .excludePathPatterns("/js/**.js")
- //排除images下所有的元素
- .excludePathPatterns("/images/**")
- .excludePathPatterns("/css/**.css")
- .excludePathPatterns("/fronts/**")
- .excludePathPatterns("/player/**")
- .excludePathPatterns("/login.html")
- .excludePathPatterns("/register.html")
- //排除登录和注册接口
- .excludePathPatterns("/login")
- .excludePathPatterns("/register")
- .excludePathPatterns("/logout");
- }
- }
- 请求:
- {
- post, // 使用 post 请求
- /login // 请求路径
- data:{ username, password } // 传入的数据
- }
-
- 响应:
- {
- "status": 200,
- "message": "登录成功",
- "data": {
- "id": xxxxx,
- "username": xxxxxx,
- "score": 1000,
- "totalCount": 0,
- "winCount": 0
- }
-
- }
-
- 响应设计字段解释:
- {
- 状态码为 200 表示成功,-200表示失败
- 状态描述信息,描述此次请求成功或者失败的原因
- 返回的数据,请求成功后,服务器返回给前端的数据
- }
- 请求:
- {
- post, // 使用 post 请求
- /register // 请求路径
- data:{ username, password } // 传入的数据
- }
-
- 响应:
- {
- "status": 200,
- "message": "注册成功",
- "data": {
- "id": xxxxx,
- "username": xxxxxx,
- "score": 1000,
- "totalCount": 0,
- "winCount": 0
- }
-
- }
-
- 响应设计字段解释:
- {
- 状态码为 200 表示成功,-200表示失败
- 状态描述信息,描述此次请求成功或者失败的原因
- 返回的数据,请求成功后,服务器返回给前端的数据
- }

- package com.example.online_gobang.model;
-
- import lombok.Data;
-
- @Data
- public class User {
- private int userId; // 用户id
- private String username; // 用户名
- private String password; // 密码
- private int score; // 天梯积分
- private int totalCount; // 比赛总场数
- private int winCount; // 获胜场数
- }
- package com.example.online_gobang.mapper;
-
- import com.example.online_gobang.model.User;
- import org.apache.ibatis.annotations.Mapper;
-
- @Mapper
- public interface UserMapper {
- /**
- * 数据库中插入用户信息,用于注册功能
- * @param user
- */
- void insert(User user);
-
- /**
- * 根据用户名,查询用户的详细信息,用户登录功能
- * @param username
- * @return
- */
- User selectByName(String username);
- }
- "1.0" encoding="UTF-8"?>
- mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
- <mapper namespace="com.example.online_gobang.mapper.UserMapper">
- <insert id="insert">
- insert into user values(null, #{username}, #{password}, 1000, 0, 0);
- insert>
-
- <select id="selectByName" resultType="com.example.online_gobang.model.User">
- select *
- from user
- where username = #{username};
- select>
- mapper>
- package com.example.online_gobang.api;
-
- import com.example.online_gobang.mapper.UserMapper;
- import com.example.online_gobang.model.User;
- import com.example.online_gobang.tools.Constant;
- import com.example.online_gobang.tools.ResponseBodyMessage;
- import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
- import org.springframework.web.bind.annotation.*;
-
- import javax.annotation.Resource;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpSession;
-
- @RestController
- public class UserAPI {
-
- @Resource
- private UserMapper userMapper;
-
- // 使用 BCrypt 对密码进行加密
- @Resource
- private BCryptPasswordEncoder bCryptPasswordEncoder;
-
- @PostMapping("/login")
- @ResponseBody
- public ResponseBodyMessage
login(@RequestParam String username,@RequestParam String password,HttpServletRequest request){ - /**
- * 根据 username 到数据库中进行查询
- * 如果能找到匹配的用户,并且密码也一致,就认为登录成功
- */
- User user = userMapper.selectByName(username);
- System.out.println("登录的用户:"+ username);
- if(user != null){
- System.out.println("登录成功");
-
- // 判断当前用户输入的密码(password) 与 数据库中查询到的密码(加密的密码,getPassword())是否匹配
- boolean flag = bCryptPasswordEncoder.matches(password,user.getPassword());
-
- if(!flag){
- // 密码不匹配,登录失败
- return new ResponseBodyMessage<>(-200,"用户名或密码错误",user);
- }
- // 如果登录成功就将信息写入到 session 中(在 session 中存储了一个用户信息对象,此后可以随时从 session 中将这个对象取出来进行一些操作)
- request.getSession().setAttribute(Constant.USER_SESSION_KEY,user);
- // 状态码为200,表示登录成功,并返回用户信息
- return new ResponseBodyMessage<>(200,"登录成功",user);
- }else {
- // 登录失败,返回一个空的对象
- System.out.println("登录失败");
- return new ResponseBodyMessage<>(-200,"用户名或密码错误",user);
- }
- }
-
- @RequestMapping("/register")
- @ResponseBody
- public ResponseBodyMessage
register(@RequestParam String username, @RequestParam String password){ - User user1 = userMapper.selectByName(username);
- if(user1 != null) {
- return new ResponseBodyMessage<>(-200,"当前用户已经存在",false);
- }else {
- User user = new User();
- String newPassword = bCryptPasswordEncoder.encode(password);
- user.setUsername(username);
- user.setPassword(newPassword);
- boolean flag = userMapper.insert(user);
- if(flag == true){
- return new ResponseBodyMessage<>(200,"注册成功",true);
- }else{
- return new ResponseBodyMessage<>(-200,"注册失败",false);
- }
- }
- }
- }


-
- $(function(){
- $("#register").click(function(){
- var username = $("#username").val();
- var password = $("#password").val();
- $.ajax({
- url: "/register",
- type: "POST",
- data:{
- "username":username,
- "password":password
- },
- dataType:"json",
- success: function(data){
- console.log(data);
- if(data.status == 200) {
- // alert("注册成功");
- location.assign("login.html");
- }else{
- alert("注册失败");
- $("#username").val("");
- $("#password").val("");
- $("#repassword").val("");
- }
- }
- })
- })
- });
-
- let register = document.querySelector('#register');
- console.log(register);
- register.onclick = function() {
- let username = document.querySelector('#username');
- let password = document.querySelector('#password');
- let repassword = document.querySelector('#repassword');
- if(username.value.trim() == ""){
- alert("请输入账号!");
- username.focus();
- return;
- }
- if(password.value.trim() == ""){
- alert('请输入密码!');
- password.focus();
- return;
- }
- if(repassword.value.trim() == ""){
- alert('请再次输入密码!');
- repassword.focus();
- return;
- }
- if(username.value.trim().length > 15) {
- alert("账号长度不可超过15个字符,请重新输入");
- username.value="";
- username.focus();
- return;
- }
- if(password.value.trim() != repassword.value.trim()) {
- alert('两次输入的密码不同,请重试输入!');
- passwrod.value="";
- repassword.value="";
- return;
- }
- if(password.value.trim().length > 255) {
- alert("当前密码长度过长!");
- password.value="";
- repassword.value="";
- password.focus();
- return;
- }
-
- }
-
-
- $(function(){
- $("#submit").click(function(){
- // 点击登录按钮,获取用户名和密码
- var username = $("#username").val();
- var password = $("#password").val();
-
- // 判断用户名和密码是否为空(使用 trim 方法,防止输入空格)
- if(username.trim() == "" || password.trim() == ""){
- alert("账号或密码不能为空");
- return;
- }
- // 如果用户名和密码不为空,使用 Ajax 传入请求
- $.ajax({
- type:"POST",
- url:"/login",
- data:{
- "username":username,
- "password":password
- },
- // 服务器返回的数据类型
- dataType:"json",
- // 请求成功,服务器返回数据
- success:function(data){
- console.log(data);
- // 如果状态码为 200,表示登录成功
- if(data.status == 200){
- alert("登录成功");
- // 跳转到游戏大厅页面
- location.assign('/game_hall.html');
- }else{
- alert("登录失败,账号或密码错误");
- // 登录失败,将用户名或密码置空
- $("#username").val("");
- $("#password").val("");
- }
- }
- });
- });
- });
-
- $(function () {
- $("#register").click(function () {
- window.location.href="register.html";
- });
- });
-
-
- 让多个用户在游戏大厅进行匹配,系统将天梯积分相近的两个玩家匹配到同一个房间中进行对战。
- 玩家发送匹配请求,这个事情是确定的(点击了匹配按钮,就会发送匹配请求) ,但是服务器什么时候告知玩家匹配结果是不确定的。
- 匹配功能需要依赖消息推送机制(websocket),当服务器匹配成功之后就会主动告诉当前进行匹配的所有玩家“你匹配成功了”。
消息推送机制:
- 消息推送就是服务器主动将数据发送给客户端(服务器主动发送请求),而 HTTP 协议必须是客户端主动将数据发送给服务器(客户端主动发送请求)。
- 匹配功能需要服务器主动给客户端发送数据,因此需要使用 websocket,接下来所设计的前后端交互接口都是基于 websocket 这样的交互方式进行的。
- 请求:
- {
- ws://127.0.0.1:8081/findMatch // 请求路径
- data:{ message:'startMatch' // 开始匹配
- 或 'stopMatch' // 结束匹配
- } // 请求内容
- }
- 响应:
- {
- ws://127.0.0.1:8081/findMatch // 响应路径
- data:{
- status: 200, // 匹配成功
- reason: '', // 如果匹配失败,失败原因的信息
- message: 'startMatch' // 开始匹配
- 或 'stopMatch' // 结束匹配
- } // 响应内容
- }
-
- 响应设计字段解释:
- {
- 状态码为 200 表示成功,-200表示失败
- 状态描述信息,描述此次请求成功或者失败的原因
- 返回的数据,请求成功后,服务器返回给前端的数据
- }
- 响应:
- {
- ws://127.0.0.1:8081/findMatch // 响应路径
- data:{
- status: 200, // 匹配成功
- reason: '', // 如果匹配失败,失败原因的信息
- message: 'matchSuccess'
- } // 响应内容
- }
-
- 响应设计字段解释:
- {
- 状态码为 200 表示成功,-200表示失败
- 状态描述信息,描述此次请求成功或者失败的原因
- 返回的数据,请求成功后,服务器返回给前端的数据
- }
- 请求:
- {
- get, // 使用 get 请求
- /userInfo // 请求路径
- }
-
- 响应:
- {
- "status": 200,
- "message": "相关信息",
- "data": {
- "id": xxxxx,
- "username": xxxxxx,
- "score": 1000,
- "totalCount": 0,
- "winCount": 0
- }
-
- }
-
- 响应设计字段解释:
- {
- 状态码为 200 表示成功,-200表示失败
- 状态描述信息,描述此次请求成功或者失败的原因
- 返回的数据,请求成功后,服务器返回给前端的数据
- }
- @RequestMapping("/userInfo")
- @ResponseBody
- public ResponseBodyMessage
getUserInfo(HttpServletRequest request){ - HttpSession session = request.getSession(false);
- User user = (User)session.getAttribute("user"); // 从会话中获取 User 对象
- User newUser = userMapper.selectByName(user.getUsername());
- if(newUser != null){
- return new ResponseBodyMessage<>(200,"获取成功",user);
- }else {
- System.out.println("没有该用户");
- return new ResponseBodyMessage<>(-200,"获取失败",user);
- }
- }
- 维护用户的在线状态,目的是为了能够方便获取到某个用户当前的 websocket 会话,从而可以通过这个会话来给这个客户端发送信息,同时也可以感知到当前玩家的在线/离线状态。
使用哈希表来保存当前用户的在线状态,当用户登录的时候, 就将用户状态添加到哈希表中。
- 玩家的在线状态是多线程的, 很多用户访问同一个哈希表就会出现线程安全的问题, 所以这里就使用 ConcurrentHashMap, 确保了线程安全问题。
- 这里存储的, key是用户的Id, value是对应的 websocket 会话。
提供三个方法:
- 玩家进入游戏大厅的时候, 将用户的状态存入哈希表中
- 玩家退出游戏大厅的时候, 将用户的状态从哈希表中删除
- 在游戏大厅获取当前用户的信息
- package com.example.online_gobang.game;
-
- import org.springframework.stereotype.Component;
- import org.springframework.web.socket.WebSocketSession;
-
- import java.util.concurrent.ConcurrentHashMap;
-
- @Component
- public class OnlineUserManager {
- // 这个哈希表用来表示当前用户在游戏大厅的在线状态
- private ConcurrentHashMap
gameHall = 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);
- }
-
- }
UUID:通用唯一识别码(Universally Unique Identifier)的缩写
- 表示“唯一的身份标识”。
- 通过一系列算法能够生成一串字符串(一组十六进制表示的数字)。
- 每次调用这个算法,得到的结果都是不相同的。
- Java 中有现成的类可以直接生成 UUID。
- package com.example.online_gobang.game;
-
- import com.example.online_gobang.model.User;
- import lombok.Data;
-
- import java.util.UUID;
-
- // 这个类表示一个游戏房间
- @Data
- public class Room {
- private String roomId;
-
- private User user1;
- private User user2;
-
- public Room() {
- // 构造 Room 的时候生成一个唯一的字符串表示房间 id.
- // 使用 UUID 来作为房间 id
- roomId = UUID.randomUUID().toString();
- }
- }

使用哈希表存储每个游戏房间并对其进行管理。
private ConcurrentHashMaprooms = new ConcurrentHashMap<>();
- key:roomId(每个游戏房间的id都是唯一的),value:Room。
使用哈希表维护玩家和游戏房间之间的关系。
private ConcurrentHashMapusrIdToRoomId = new ConcurrentHashMap<>();
- 考虑到线程安全问题,因此使用 ConcurrentHashMap。
- key:userId,value:roomId。
提供4个方法:
- 添加玩家进入到游戏房间中。
- 删除游戏房间中的玩家。
- 根据游戏房间的Id,获取对应的游戏房间。
- 通过用户Id,查找该玩家所在的游戏房间。
- package com.example.online_gobang.game;
-
- import org.springframework.stereotype.Component;
-
- import java.util.concurrent.ConcurrentHashMap;
-
- // 房间管理器
- @Component
- public class RoomManager {
- private ConcurrentHashMap
rooms = new ConcurrentHashMap<>(); - // 通过用户id 与房间id 维护玩家和房间之间的关系
- private ConcurrentHashMap
usrIdToRoomId = new ConcurrentHashMap<>(); - public void add(Room room, int userId1,int userId2){
- // 添加一个房间到房间管理器的同时,也将两个玩家的 userId 添加到 usrIdToRoomId 中,便于维护玩家和房间之间的关系
- rooms.put(room.getRoomId(),room);
- usrIdToRoomId.put(userId1,room.getRoomId());
- usrIdToRoomId.put(userId2,room.getRoomId());
- }
-
- public void remove(String roomId, int userId1,int userId2){
- // 移除一个房间的同时也要同时移除两个玩家的信息
- rooms.remove(roomId);
- usrIdToRoomId.remove(userId1);
- usrIdToRoomId.remove(userId2);
- }
-
- public Room getByRoomId(String roomId){
- return rooms.get(roomId);
- }
-
- // 通过用户id 查找对应的房间
- public Room getRoomByUserId(int userId){
- String rooId = usrIdToRoomId.get(userId);
- if(rooId == null){
- // rooId == null 表示游戏房间不存在
- // userId -> roomId 映射关系不存在,直接返回 null
- return null;
- }
- return rooms.get(rooId);
- }
- }
将所有玩家按照天梯积分划分成三类(基础分:1000):
- 业余水平:score < 2000
- 普通水平:score >= 2000 && score < 3000
- 大师水平:score >= 3000
// 1. 业余水平:score < 2000 private QueueamateurQueue = new LinkedList<>(); // 2. 普通水平:score >= 2000 && score < 3000 private QueuenormalQueue = new LinkedList<>(); // 3. 大师水平:score >= 3000 private QueuemasterQueue = new LinkedList<>();
- 给这三个等级分配三个不同的队列,根据当前玩家的天梯积分,将玩家的用户信息放到对应的队列中。
- 设置一个线程去不停的扫描匹配队列,只要队列中的元素(匹配中的玩家)积分相近,就将这一对玩家放到一个游戏房间中。
1、线程安全问题:
- 如果多个线程针对同一个队列进行并发修改操作(入队、出队)就会产生线程安装问题。如果是针对多个不同队列进行操作就不会产生线程安全问题。
解决办法:对创建的三个队列对象在进行队列操作时分别进行加锁(synchronized)。
2、忙等问题:
- 如果当前匹配队列中没有玩家或只有一个玩家在进行匹配,线程就会调用 handlerMatch 方法并且直接返回,然后再次调用 handlerMatch 方法 ...... 一直循环。这个过程中 CPU 占用率会非常高。
解决方法:
- 在判断当前队列中的元素(正在匹配的玩家)是否有2个以上的时候,如果当前队列中的玩家数 < 2,就调用 wait() 进行匹配等待,如果没有玩家进入匹配队列就会一直等。
- 直到有玩家进入匹配队列,就调用 notify() 唤醒线程,再次判断当前队列中是否有2个以上的玩家。
- package com.example.online_gobang.game;
-
- import com.example.online_gobang.model.User;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Component;
- import org.springframework.web.socket.TextMessage;
- import org.springframework.web.socket.WebSocketSession;
-
- import java.io.IOException;
- import java.util.LinkedList;
- import java.util.Queue;
-
- // 匹配器, 这个类是用来完成匹配功能的
- @Component
- public class Matcher {
- // 创建匹配队列 按等级划分
- // 1. 业余水平:score < 2000
- private Queue
amateurQueue = new LinkedList<>(); - // 2. 普通水平:score >= 2000 && score < 3000
- private Queue
normalQueue = new LinkedList<>(); - // 3. 大师水平:score >= 3000
- private Queue
masterQueue = new LinkedList<>(); -
- @Autowired
- private OnlineUserManager onlineUserManager;
-
- @Autowired
- private ObjectMapper objectMapper;
-
- @Autowired
- private RoomManager roomManager;
-
- /**
- * 将当前玩家添加到匹配队列中
- * @param user
- */
- public void add(User user) {
- // 按等级加入队列中
- if (user.getScore() < 2000) {
- synchronized (amateurQueue) {
- amateurQueue.offer(user);
- // 只要有用户进入了, 就进行唤醒
- amateurQueue.notify();
- }
- // 打印日志
- System.out.println("把玩家"+user.getUsername()+"加入到了amateurQueue 中");
- }else if (user.getScore() >= 2000 && user.getScore() < 3000) {
- synchronized (normalQueue) {
- normalQueue.offer(user);
- normalQueue.notify();
- }
- System.out.println("把玩家"+user.getUsername()+"加入到了normalQueue 中");
- }else {
- synchronized (masterQueue) {
- masterQueue.offer(user);
- masterQueue.notify();
- }
- System.out.println("把玩家"+user.getUsername()+"加入到了masterQueue 中");
- }
- }
-
- /**
- * 当玩家点击停止匹配
- * 就把当前玩家匹配队列中删除
- * @param user
- */
- public void remove(User user) {
- // 按照当前等级去对应匹配队列中删除
- if (user.getScore() < 2000) {
- synchronized (amateurQueue){
- amateurQueue.remove(user);
- }
- System.out.println("把玩家"+user.getUsername()+"移除了masterQueue");
- }else if (user.getScore() >= 2000 && user.getScore() < 3000) {
- synchronized (normalQueue) {
- normalQueue.remove(user);
- }
- System.out.println("把玩家"+user.getUsername()+"移除了normalQueue");
- }else {
- synchronized (masterQueue) {
- masterQueue.remove(user);
- }
- System.out.println("把玩家"+user.getUsername()+"移除了masterQueue");
- }
- }
-
- // 使用3个线程去一直的进行查看是否有2个以上的人, 如果有进行匹配
- public Matcher() {
- // 创建三个线程, 操作三个匹配队列
- Thread t1 = new Thread() {
- @Override
- public void run() {
- // 扫描 amateurQueue
- while (true) {
- handlerMatch(amateurQueue);
- }
- }
- };
- t1.start();
- Thread t2 = new Thread() {
- @Override
- public void run() {
- // 扫描 normalQueue
- while (true) {
- handlerMatch(normalQueue);
- }
- }
- };
- t2.start();
- Thread t3 = new Thread() {
- @Override
- public void run() {
- // 扫描 masterQueue
- while (true) {
- handlerMatch(masterQueue);
- }
- }
- };
- t3.start();
- }
-
- private void handlerMatch(Queue
matchQueue) { - // 因为三个队列都调用了 handlerMatch 方法,因此对这个方法里面的操作进行加锁即可。
- // 针对形参进行加锁(传入不同的实参就可以对不同的队列对象进行加锁)
- synchronized (matchQueue) {
- try{
- // 1. 先查看当前队列中的元素个数, 是否满足两个
- // 在往队列里添加一个元素后仍然不能进行后续匹配操作,
- // 因此使用 while 循环检测是否有两个元素添加到队列中更合理
- while (matchQueue.size() < 2) {
- // 玩家数 < 2 的时候, 就进行等待
- matchQueue.wait();
- }
- // 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 && session2 != null) {
- // 如果玩家1 掉线了,就把玩家2 重新放到匹配队列中
- matchQueue.offer(player2);
- return;
- }
- if (session1 != null && session2 == null) {
- // 如果玩家2 掉线了,就把玩家1 重新放到匹配队列中
- matchQueue.offer(player1);
- return;
- }
- if (session1 == null && session2 == null) {
- return;
- }
- if (session1 == session2) {
- // 如果两个玩家是同一个用户(一个玩家入队了两次,理论上不存在,但还是需要再判定一次)
- // 就把其中的一个玩家放回到匹配队列
- matchQueue.offer(player1);
- return;
- }
-
- // 4. 把两个玩家放入一个游戏房间中
- Room room = new Room();
- roomManager.add(room, player1.getUserId(), player2.getUserId());
-
- // 5. 给玩家反馈信息, 通知匹配到了对手
- // 给玩家1返回的响应
- MatchResponse response1 = new MatchResponse();
- response1.setStatus(200);
- response1.setMessage("matchSuccess");
- String json1 = objectMapper.writeValueAsString(response1);
- session1.sendMessage(new TextMessage(json1));
-
- // 给玩家2返回的响应
- MatchResponse response2 = new MatchResponse();
- response2.setMessage("matchSuccess");
- response2.setStatus(200);
- String json2 = objectMapper.writeValueAsString(response2);
- session2.sendMessage(new TextMessage(json2));
-
- } catch (IOException | InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- package com.example.online_gobang.game;
-
- import lombok.Data;
-
- // 表示 websocket 的一个匹配请求
- @Data
- public class MatchRequest {
- private String message;
- }
- package com.example.online_gobang.game;
-
- import lombok.Data;
-
- // 表示 websocket 的一个匹配响应
- @Data
- public class MatchResponse {
- private int status; // 状态码
- private String reason; // 响应内容(失败的原因)
- private String message; // 匹配信息
- }
- afterConnectionEstablished 方法:在游戏大厅建立连接
- handleTextMessage 方法:在游戏大厅中接收发送的响应
- handleTransportError 方法:处理玩家异常下线
- afterConnectionClosed 方法:处理玩家正常下线
- package com.example.online_gobang.api;
-
- import com.example.online_gobang.game.MatchRequest;
- import com.example.online_gobang.game.MatchResponse;
- import com.example.online_gobang.game.Matcher;
- import com.example.online_gobang.game.OnlineUserManager;
- import com.example.online_gobang.model.User;
- import com.example.online_gobang.tools.Constant;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Component;
- import org.springframework.web.socket.CloseStatus;
- import org.springframework.web.socket.TextMessage;
- import org.springframework.web.socket.WebSocketSession;
- import org.springframework.web.socket.handler.TextWebSocketHandler;
-
- // 通过这个类来处理匹配功能中的 websocket 请求
- @Component
- public class MatchAPI extends TextWebSocketHandler {
- private ObjectMapper objectMapper = new ObjectMapper();
-
- @Autowired
- private OnlineUserManager onlineUserManager;
-
- @Autowired
- private Matcher matcher;
-
- @Override
- public void afterConnectionEstablished(WebSocketSession session) throws Exception {
- // 玩家上线,加入到 OnlineUserManager 中
-
- // 1. 获取当前的用户信息(谁在游戏大厅创建连接)
- User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
-
- // 2. 判断当前用户是否已经登录
- MatchResponse response = new MatchResponse();
- if (onlineUserManager.getFromGameHall(user.getUserId()) != null ) {
- // 当前用户已经登录
- response.setMessage("当前用户已经登录!");
- response.setStatus(-200);
- response.setReason("禁止游戏多开");
- /**
- * 先通过 ObjectMapper 把 MathResponse 对象转成 JSON 字符串
- * 然后再包装上一层 TextMessage,再进行传输
- * TextMessage 就表示一个文本格式的 websocket 数据包
- */
- session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(response)));
- session.close();
- return;
- }
-
- // 3. 设置在线状态
- onlineUserManager.enterGameHall(user.getUserId(),session);
- System.out.println("玩家"+user.getUsername()+"进入游戏大厅");
- }
-
- @Override
- protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
- // 处理开始匹配 和 停止匹配
- User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
- // 获取到客户端给服务器发送的数据
- String payload = message.getPayload();
- // 当前这个数据载荷是一个 JSON 格式的字符串,需要将它转换成 Java 对象( MatchRequest )
- MatchRequest matchRequest = objectMapper.readValue(payload, MatchRequest.class); // 从客户端获取的数据
- MatchResponse matchResponse = new MatchResponse(); // 给客户端返回的数据
- if (matchRequest.getMessage().equals("startMatch")) {
- // 进入匹配队列, 加入用户
- matcher.add(user);
- // 返回响应给前端
- matchResponse.setStatus(200);
- matchResponse.setMessage("startMatch");
- }else if(matchRequest.getMessage().equals("stopMatch")) {
- // 退出匹配队列, 将用户移除
- matcher.remove(user);
- matchResponse.setMessage("stopMatch");
- matchResponse.setStatus(200);
- }else{
- // 非法情况
- matchResponse.setStatus(-200);
- matchRequest.setMessage("非法匹配");
- }
- session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(matchResponse)));
- }
-
- @Override
- public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
- // 玩家下线,从 OnlineUserManager 中删除
- // 1. 获取用户信息
- User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
- WebSocketSession webSocketSession = onlineUserManager.getFromGameHall(user.getUserId());
- if(webSocketSession == session) {
- // 2. 设置在线状态
- onlineUserManager.exitGameHall(user.getUserId());
- }
- // 如果玩家正在匹配中,而 websocket 连接断开了就应该移除匹配队列
- matcher.remove(user);
- }
-
- @Override
- public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
- // 玩家下线,从 OnlineUserManager 中删除
- // 1. 获取用户信息
- User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
- WebSocketSession webSocketSession = onlineUserManager.getFromGameHall(user.getUserId());
- if(webSocketSession == session) {
- // 2. 设置在线状态
- onlineUserManager.exitGameHall(user.getUserId());
- }
- // 如果玩家正在匹配中,而 websocket 连接断开了就应该移除匹配队列
- matcher.remove(user);
- }
- }
- @Configuration
- @EnableWebSocket
- public class AppConfig implements WebSocketConfigurer,WebMvcConfigurer {
-
- @Autowired
- private MatchAPI matchAPI;
-
- @Override
- public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
- registry.addHandler(matchAPI,"/findMatch").
- addInterceptors(new HttpSessionHandshakeInterceptor());
- }
- }
- $.ajax({
- type:'get',
- url:'/userInfo',
- success: function(data) {
- if(data.status == 200){
- let screenDiv = document.querySelector('#screen');
- screenDiv.innerHTML = "玩家: " + data.data.username + "
分数: " + data.data.score - + "
比赛场次: " + data.data.totalCount + "
获胜场数: " + data.data.winCount - }else{
- alert(data.message);
- location.assign("login.html")
- }
- }
- });
-
- // 进行初始化 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 的 close 方法.
- window.onbeforeunload = function() {
- websocket.close();
- }
-
- // 匹配成功后收到的响应
- websocket.onmessage = function(e) {
- // 处理服务器返回的响应数据.
- let resp = JSON.parse(e.data);
- // 获取到开始匹配按钮
- let matchButton = document.querySelector('#match-button');
- if(resp.status == -200) {
- console.log("游戏大厅中接收到了非法响应! " + resp.reason);
- alert(resp.message);
- location.assign("login.html");
- 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.assign("/game_room.html");
- }else {
- alert(resp.message);
- 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 == '开始匹配') {
- console.log("开始匹配");
- websocket.send(JSON.stringify({
- message: 'startMatch',
- }));
- } else if (matchButton.innerHTML == '匹配中...(点击停止)') {
- console.log("停止匹配");
- websocket.send(JSON.stringify({
- message: 'stopMatch',
- }));
- }
- } else {
- // 连接异常
- alert("当前您的连接已经断开! 请重新登录!");
- location.assign('/login.html');
- }
- }
-
- 请求:
- {
- ws://127.0.0.1:8081/game // 请求路径
- }
- 响应:
- {
- message: 'gameReady' // 消息的类别(游戏准备就绪)
- status: 200 // 200 是正常响应, -200 是异常响应
- reason: '' // 报错原因
- roomId: '' // 玩家所处的房间 Id
- thisUserId: 1 // 自己的用户Id
- thatUserId: 2 // 对手的用户Id
- whiteUser: 1 // 执白子的玩家先手 (1:先手; 2:后手)
- }
- 请求:
- {
- message: 'putChess'
- userId: 1 // 落子的用户id
- row: 0 // 落子的行
- col: 0 // 落子的列
- }
- 响应:
- {
- message: 'putChess'
- userId: 1 // 落子的用户id
- row: 0 // 落子的行
- col: 0 // 落子的列
- winner:0 // 为0时: 胜负未分; 非0时(获胜者的用户id): 胜负已分
- }
- @Configuration
- @EnableWebSocket
- public class AppConfig implements WebSocketConfigurer,WebMvcConfigurer {
- @Autowired
- private GameAPI gameAPI;
-
- @Override
- public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
- registry.addHandler(gameAPI,"/game")
- .addInterceptors(new HttpSessionHandshakeInterceptor());
- }
- }
- package com.example.online_gobang.game;
-
- import lombok.Data;
-
- // 客户端连接游戏房间后,服务器返回的响应
- @Data
- public class GameReadyResponse {
- private String message; // 消息的类别
- private int status; // 状态码
- private String roomId; // 房间id
- private int thisUserId; // 自己的用户id
- private int thatUserId; // 对手的用户id
- private int whiteUser; // 执白子的玩家先手
- }
- package com.example.online_gobang.game;
-
- import lombok.Data;
-
- // 落子请求
- @Data
- public class GameRequest {
- private String message;
- private int userId; // 玩家id
- private int row; // 落子的行
- private int col; // 落子的列
- }
- package com.example.online_gobang.game;
-
- import lombok.Data;
-
- // 落子响应
- @Data
- public class GameResponse {
- private String message;
- private int userId;
- private int row;
- private int col;
- private int winner; // 判断胜负(未分出胜负为0,分出胜负则为获胜者的id)
- }
使用哈希表来保存当前玩家在游戏房间的在线状态,当玩家进入游戏房间的时候, 就将玩家的状态添加到哈希表中。
- key是用户的Id, value是对应的 websocket 会话。
// 这个哈希表用来表示当前用户在游戏房间的在线状态 private ConcurrentHashMapgameRoom = new ConcurrentHashMap<>(); 提供三个方法
- 玩家进入游戏房间的时候, 将用户的状态存入哈希表中
- 玩家退出游戏房间的时候, 将用户的状态从哈希表中删除
- 在游戏房间获取当前用户的信息
- @Component
- public class OnlineUserManager {
- // 这个哈希表用来表示当前用户在游戏房间的在线状态
- private ConcurrentHashMap
gameRoom = new ConcurrentHashMap<>(); -
- // 玩家进入游戏房间
- 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);
- }
- }
- @Mapper
- public interface UserMapper {
- /**
- * 更新的数据:总场数 +1,获胜场数 +1,天梯积分 + 50
- * @param userId
- */
- void userWin(int userId);
-
- /**
- * 更新的数据:总场数 +1,获胜场数不变,天梯积分 - 50
- * @param userId
- */
- void userLose(int userId);
- }
- "1.0" encoding="UTF-8"?>
- mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
- <mapper namespace="com.example.online_gobang.mapper.UserMapper">
-
- <update id="userWin">
- update user set totalCount = totalCount + 1,winCount = winCount + 1,score = score + 50
- where userId = #{userId};
- update>
-
- <update id="userLose">
- update user set totalCount = totalCount + 1,score = score - 50
- where userId = #{userId};
- update>
-
- mapper>
Room 要注入Spring对象, 不能使用@Autowired @Resource注解. 需要使用context
- package com.example.online_gobang;
-
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.context.ConfigurableApplicationContext;
-
- public class OnlineGobangApplication {
-
- public static ConfigurableApplicationContext context;
-
- public static void main(String[] args) {
- context = SpringApplication.run(OnlineGobangApplication.class, args);
- }
-
- }
- // 这个类表示一个游戏房间
- @Data
- public class Room {
-
- private static final int ROW = 15;
- private static final int COL = 15;
-
- private OnlineUserManager onlineUserManager;
-
- // 引入 roomManager 用于房间销毁
- private RoomManager roomManager;
-
- public Room() {
- // 构造 Room 的时候生成一个唯一的字符串表示房间 id.
- // 使用 UUID 来作为房间 id
- roomId = UUID.randomUUID().toString();
- // 通过入口类中记录的 context 来手动获取到前面的 RoomManager 和 OnlineUserManager
- onlineUserManager = OnlineGobangApplication.context.getBean(OnlineUserManager.class);
- roomManager = OnlineGobangApplication.context.getBean(RoomManager.class);
- userService = JavaGobangApplication.context.getBean(UserService.class);
- }
- }
- 用 0 表示没有落棋子,用 1 表示玩家1落的棋子,用 2 表示玩家2落的棋子。
- // 这个类表示一个游戏房间
- @Data
- public class Room {
- private static final int ROW = 15;
- private static final int COL = 15;
-
- private OnlineUserManager onlineUserManager;
-
- // 引入 roomManager 用于房间销毁
- private RoomManager roomManager;
-
- // 用于将 JSON 格式的字符串转换 Java 对象
- private ObjectMapper objectMapper = new ObjectMapper();
-
- // 这个二维数组表示棋盘
- // 1) 使用 0 表示当前位置未落子. 初始化好的 int 二维数组, 就相当于是 全 0
- // 2) 使用 1 表示 user1 的落子位置
- // 3) 使用 2 表示 user2 的落子位置
- private int[][] board = new int[ROW][COL];
-
- // 处理一次落子的操作
- public void putChess(String payload) throws IOException {
- // 1. 记录当前落子的位置
-
- // 2. 打印出当前的棋盘信息, 方便来观察局势. 也方便后面验证胜负关系的判定.
- printBoard();
-
- // 3. 进行胜负判定
- int winner = checkWinner(row, col, chess);
-
- // 4. 给房间里的所有客户端返回响应
- response.setMessage("putChess");
- response.setUserId(request.getUserId());
- response.setRow(row);
- response.setCol(col);
- response.setWinner(winner);
-
- // 5. 如果当前胜负已分, 此时这个房间就失去存在的意义了. 就可以直接销毁房间. (把房间从房间管理器中给移除)
-
- // 打印棋盘内容
- private void printBoard() {
-
- }
-
- // 使用这个方法来判定当前落子是否分出胜负.
- // 约定如果玩家1 获胜, 就返回玩家1 的 userId
- // 如果玩家2 获胜, 就返回玩家2 的 userId
- // 如果胜负未分, 就返回 0
- private int checkWinner(int row, int col, int chess) {
- return 0;
- }
- }
- package com.example.online_gobang.game;
-
- import com.example.online_gobang.OnlineGobangApplication;
- import com.example.online_gobang.mapper.UserMapper;
- import com.example.online_gobang.model.User;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import lombok.Data;
- import org.springframework.web.socket.TextMessage;
- import org.springframework.web.socket.WebSocketSession;
-
- import java.io.IOException;
- import java.util.UUID;
-
- // 这个类表示一个游戏房间
- @Data
- public class Room {
- private String roomId;
- private User user1;
- private User user2;
- private int whiteUser; // 先手方的玩家id
-
- private static final int ROW = 15;
- private static final int COL = 15;
-
- private OnlineUserManager onlineUserManager;
-
- // 引入 roomManager 用于房间销毁
- private RoomManager roomManager;
-
- // 用于将 JSON 格式的字符串转换 Java 对象
- private ObjectMapper objectMapper = new ObjectMapper();
-
- // 引入 UserMapper,用于更新比赛数据
- private UserMapper userMapper;
-
- // 这个二维数组表示棋盘
- // 1) 使用 0 表示当前位置未落子. 初始化好的 int 二维数组, 就相当于是 全 0
- // 2) 使用 1 表示 user1 的落子位置
- // 3) 使用 2 表示 user2 的落子位置
- private int[][] board = new int[ROW][COL];
-
- public Room() {
- // 构造 Room 的时候生成一个唯一的字符串表示房间 id.
- // 使用 UUID 来作为房间 id
- roomId = UUID.randomUUID().toString();
- // 通过入口类中记录的 context 来手动获取到前面的 RoomManager 和 OnlineUserManager
- onlineUserManager = OnlineGobangApplication.context.getBean(OnlineUserManager.class);
- roomManager = OnlineGobangApplication.context.getBean(RoomManager.class);
- userMapper = OnlineGobangApplication.context.getBean(UserMapper.class);
- }
-
- // 处理一次落子的操作
- public void putChess(String payload) throws IOException {
- // 1. 记录当前落子的位置
- // 将 json 格式的字符串转换成 Java 对象
- GameRequest request = objectMapper.readValue(payload,GameRequest.class);
- GameResponse response = new GameResponse();
- // 当前这个子是玩家1 落的还是玩家2 落的. 根据这个玩家1 和 玩家2 来决定往数组中是写 1 还是 2
- int chess = request.getUserId() == user1.getUserId() ? 1 : 2;
- int row = request.getRow();
- int col = request.getCol();
- if (board[row][col] != 0) {
- System.out.println("当前位置: ("+row+" ," + col+" )" +"已经有子了");
- return;
- }
- board[row][col] = chess;
-
- // 2. 打印出当前的棋盘信息, 方便来观察局势. 也方便后面验证胜负关系的判定.
- printBoard();
-
- // 3. 进行胜负判定
- int winner = checkWinner(row, col, chess);
-
- // 4. 给房间里的所有客户端返回响应
- response.setMessage("putChess");
- response.setUserId(request.getUserId());
- response.setRow(row);
- response.setCol(col);
- response.setWinner(winner);
-
- // 要想给用户发送 websocket 数据, 就需要获取到这个用户的 WebSocketSession
- WebSocketSession session1 = onlineUserManager.getFromGameRoom(user1.getUserId());
- WebSocketSession session2 = onlineUserManager.getFromGameRoom(user2.getUserId());
-
- // 万一当前查到的会话为空(玩家已经下线了) 特殊处理一下
- if (session1 == null) {
- // 玩家1 已经下线了. 直接认为玩家2 获胜!
- response.setWinner(user2.getUserId());
- System.out.println("玩家1 掉线!");
- }
- if (session2 == null) {
- // 玩家2 已经下线. 直接认为玩家1 获胜!
- response.setWinner(user1.getUserId());
- System.out.println("玩家2 掉线!");
- }
- // 把响应构造成 JSON 字符串, 通过 session 进行传输.
- if (session1 != null) {
- session1.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
- }
- if (session2 != null) {
- session2.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
- }
-
- // 5. 如果当前胜负已分, 此时这个房间就失去存在的意义了. 就可以直接销毁房间. (把房间从房间管理器中给移除)
- if (response.getWinner() != 0) {
- // 胜负已分
- System.out.println("游戏结束! 房间即将销毁! roomId=" + roomId + " 获胜方为: " + response.getWinner());
- // 更新获胜方和失败方的信息.
- int winUserId = response.getWinner();
- int loseUserId = response.getWinner() == user1.getUserId() ? user2.getUserId() : user1.getUserId();
- userMapper.userWin(winUserId);
- userMapper.userLose(loseUserId);
- // 销毁房间
- roomManager.remove(roomId, user1.getUserId(), user2.getUserId());
- }
- }
-
- // 打印棋盘内容
- private void printBoard() {
- System.out.println("打印棋盘信息, 当前房间: " + roomId);
- System.out.println("====================================================");
- for (int i = 0; i < ROW; i++) {
- for (int j = 0; j < COL; j++) {
- System.out.print(board[i][j] + " ");
- }
- System.out.println();
- }
- System.out.println("====================================================");
- }
-
-
- // 使用这个方法来判定当前落子是否分出胜负.
- // 如果玩家1 获胜, 就返回玩家1的userId
- // 如果玩家2 获胜, 就返回玩家2的userId
- // 如果胜负未分, 就返回 0
- private int checkWinner(int row, int col, int chess) {
- // 判断当前是谁获胜
- // 1. 一行五子连珠
- for (int c = col -4; c <= col && c <= COL-5; 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) {
- // 构成五子连珠,chess == 1 获胜者是玩家1;chess == 2 获胜者是玩家2
- return chess == 1 ? user1.getUserId() : user2.getUserId();
- }
- }catch (ArrayIndexOutOfBoundsException e){
- // 如果出现数组下标越界的情况,就忽略这个异常
- continue;
- }
- }
- // 2. 一列五子连珠
- for (int r = row - 4; r <= row && r <= ROW-5; 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;
- }
- }
- // 3. 斜着五子连珠 -> 左上到右下(左对角线)
- for (int r = row - 4, c = col - 4; r <= row && c <= col; c++, r++){
- 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;
- }
- }
- // 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;
- }
- }
-
- // 胜负未分返回0
- return 0;
- }
- }
- afterConnectionEstablished 方法:游戏房间建立连接.
- handleTextMessage 方法:在游戏房间中接收发送的响应
- handleTransportError 方法:玩家异常下线
- afterConnectionClosed 方法:玩家正常下线
- package com.example.online_gobang.api;
-
- import com.example.online_gobang.game.*;
- import com.example.online_gobang.mapper.UserMapper;
- import com.example.online_gobang.model.User;
- import com.example.online_gobang.tools.Constant;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Component;
- import org.springframework.web.socket.CloseStatus;
- import org.springframework.web.socket.TextMessage;
- import org.springframework.web.socket.WebSocketSession;
- import org.springframework.web.socket.handler.TextWebSocketHandler;
-
- import javax.annotation.Resource;
- import java.io.IOException;
- import java.util.Random;
-
- @Component
- public class GameAPI extends TextWebSocketHandler {
-
- @Autowired
- private OnlineUserManager onlineUserManager;
-
- @Autowired
- private RoomManager roomManager;
-
- @Autowired
- private ObjectMapper objectMapper;
-
- @Resource
- private UserMapper userMapper;
-
- @Override
- public void afterConnectionEstablished(WebSocketSession session) throws Exception {
- GameReadyResponse gameReadyResponse = new GameReadyResponse();
-
- // 1. 获取玩家信息
- User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
- if(user == null){
- gameReadyResponse.setStatus(-200);
- gameReadyResponse.setReason("用户未登录");
- session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(gameReadyResponse)));
- return;
- }
- // 2. 判断当前玩家是否已经进入房间
- Room room = roomManager.getRoomByUserId(user.getUserId());
- if (room == null) {
- gameReadyResponse.setStatus(-200);
- gameReadyResponse.setMessage("玩家尚未匹配到!");
- session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(gameReadyResponse)));
- return;
- }
- // 3. 判断当前玩家是否多开(如果一个账号一边在游戏大厅一边在游戏房间,这种也视为多开)
- if (onlineUserManager.getFromGameHall(user.getUserId()) != null || onlineUserManager.getFromGameRoom(user.getUserId()) != null) {
- gameReadyResponse.setReason("禁止玩家多开游戏页面!");
- gameReadyResponse.setStatus(-200);
- gameReadyResponse.setMessage("repeatConnection");
- session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(gameReadyResponse)));
- return;
- }
-
- // 4. 设置当前玩家上线(进入游戏房间)
- onlineUserManager.enterGameRoom(user.getUserId(), session);
-
- // 5. 把两个玩家加入到游戏房间中
- synchronized (room) {
- if (room.getUser1() == null) {
- room.setUser1(user);
- System.out.println("玩家1 " + user.getUsername() + " 已经准备就绪");
- return;
- }
-
- if (room.getUser2() == null) {
- room.setUser2(user);
- System.out.println("玩家2 " + user.getUsername() + " 已经准备就绪");
-
- Random random = new Random();
- int num = random.nextInt(10);
- if (num % 2 == 0) {
- room.setWhiteUser(room.getUser1().getUserId());
- } else{
- room.setWhiteUser(room.getUser2().getUserId());
- }
-
- // 当两个玩家都加入成功后, 让服务器给这两个玩家都返回 websocket 的响应数据.
- // 通知玩家1
- noticeGameReady(room,room.getUser1(),room.getUser2());
- // 通知玩家2
- noticeGameReady(room,room.getUser2(),room.getUser1());
- return;
- }
- }
-
- // 6. 如果又有其他玩家连接到已经满了的房间,给出一个提示。(这种情况理论上不存在)
- gameReadyResponse.setStatus(-200);
- gameReadyResponse.setMessage("当前房间已满");
- session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(gameReadyResponse)));
- }
-
- private void noticeGameReady(Room room, User thisUser, User thatUser) throws IOException {
- GameReadyResponse resp = new GameReadyResponse();
- resp.setStatus(200);
- resp.setReason("");
- resp.setMessage("gameReady");
- resp.setRoomId(room.getRoomId());
- resp.setThisUserId(thisUser.getUserId());
- resp.setThatUserId(thatUser.getUserId());
- resp.setWhiteUser(room.getWhiteUser());
- // 把当前的响应数据传回给玩家.
- WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(thisUser.getUserId());
- webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
- }
-
- @Override
- protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
- // 1. 先从 session 中获取当前用户的身份信息
- User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
- if (user == null){
- System.out.println("[handleTextMessage]当前玩家"+ user.getUsername()+"未登录");
- return;
- }
- // 2. 根据玩家id 获取房间对象
- Room room = roomManager.getRoomByUserId(user.getUserId());
-
- // 通过room对象处理这次请求
- room.putChess(message.getPayload());
- }
-
- @Override
- public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
- // 玩家异常下线
- // 1. 获取玩家信息
- User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
- WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(user.getUserId());
- if(webSocketSession == session){
- // 2. 退出游戏房间
- onlineUserManager.exitGameRoom(user.getUserId());
- }
- System.out.println("当前用户: " + user.getUsername()+" 游戏房间连接异常!");
-
- // 通知对手获胜了
- noticeThatUserWin(user);
- }
-
- @Override
- public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
- // 玩家正常下线
- // 1. 获取玩家信息
- User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
- WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(user.getUserId());
- if(webSocketSession == session){
- // 2. 退出游戏房间
- onlineUserManager.exitGameRoom(user.getUserId());
- }
- System.out.println("当前用户: " + user.getUsername()+" 离开房间");
-
- // 通知对手获胜了
- noticeThatUserWin(user);
- }
-
- private void noticeThatUserWin(User user) throws IOException {
- // 1. 根据当前玩家, 找到玩家所在的房间
- Room room = roomManager.getRoomByUserId(user.getUserId());
- if (room == null) {
- // 这个情况意味着房间已经被释放了, 也就没有 "对手" 了
- System.out.println("当前房间已关闭, 无需通知对手!");
- return;
- }
-
- // 2. 根据房间找到对手
- User thatUser = (user == room.getUser1()) ? room.getUser2() : room.getUser1();
- // 3. 获取对手的在线状态
- WebSocketSession webSocketSession = onlineUserManager.getFromGameRoom(thatUser.getUserId());
- if (webSocketSession == null) {
- // 对手掉线了!
- System.out.println("对方掉线了, 无需通知!");
- return;
- }
- // 4. 构造一个响应, 来通知对手, 你是获胜方
- GameResponse resp = new GameResponse();
- resp.setMessage("putChess");
- resp.setUserId(thatUser.getUserId());
- resp.setWinner(thatUser.getUserId());
- webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
-
- // 5. 更新玩家的分数信息
- int winUserId = thatUser.getUserId();
- int loseUserId = user.getUserId();
- userMapper.userWin(winUserId);
- userMapper.userLose(loseUserId);
-
- // 6. 释放房间对象
- roomManager.remove(room.getRoomId(), room.getUser1().getUserId(), room.getUser2().getUserId());
- }
- }
- 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_room.css">
- head>
- <body>
- <div class="container">
- <div >
-
- <canvas id="chess" width="450px" height="450px">
- canvas>
-
- <div id="screen">等待玩家连接中...div>
- <div class="buttons">div>
- div>
- div>
- <script src="js/script.js">script>
- body>
- html>
- setScreenText 方法:用来将显示框中的内容, 根据当前是哪个玩家下的棋来修改内容。
- initGame 方法:用来初始画棋盘的, 棋盘大小为 15 * 15。
- oneStep 方法:当点击下子之后, 会绘制对应颜色的棋子。
- onmessage 方法:用来处理后端传输的响应(使用 isWhite 判断是否是先手方)。
- send方法: 通过 websocket 发送落子请求。
- 棋盘数组:0 表示该位置没有落子, 1表示该位置已经落子,避免一个位置重复落子。
- let gameInfo = {
- roomId: null,
- thisUserId: null,
- thatUserId: null,
- isWhite: true,
- }
-
-
- // 设定界面显示的相关操作
- function setScreenText(me) {
- let screen = document.querySelector('#screen');
- if (me) {
- screen.innerHTML = "轮到你落子了!";
- } else {
- screen.innerHTML = "轮到对方落子了!";
- }
- }
-
- // 初始化 websocket
- let websocketUrl = "ws://" + location.host + "/game";
- let websocket = new WebSocket(websocketUrl);
-
- websocket.onopen = function() {
- console.log("连接游戏房间成功!");
- }
-
- websocket.close = function() {
- console.log("和游戏服务器断开连接!");
- }
-
- websocket.onerror = function() {
- console.log("和服务器的连接出现异常!");
- }
-
- window.onbeforeunload = function() {
- websocket.close();
- }
-
- // 处理服务器返回的响应数据
- websocket.onmessage = function(event) {
- console.log("[handlerGameReady] " + event.data);
- let resp = JSON.parse(event.data);
-
- if(resp.message != 'gameReady') {
- console.log("响应类型错误");
- location.assign("game_hall.html");
- return;
- }
-
- if (resp.status == -200) {
- alert("连接游戏失败! reason: " + resp.reason);
- // 如果出现连接失败的情况, 回到游戏大厅
- location.assign("/game_hall.html");
- return;
- }
-
- // 游戏就绪
- if (resp.message == 'gameReady') {
- gameInfo.roomId = resp.roomId;
- gameInfo.thisUserId = resp.thisUserId;
- gameInfo.thatUserId = resp.thatUserId;
- // 判断先手方,如果执白子的Userid是自己,那么就是自己先手
- gameInfo.isWhite = (resp.whiteUser == resp.thisUserId);
-
- // 初始化棋盘
- initGame();
- // 设置显示区域的内容
- setScreenText(gameInfo.isWhite);
- } else if (resp.message == 'repeatConnection') { // 重复连接
- alert("检测到游戏多开(游戏大厅和游戏房间多开)! 请使用其他账号登录!");
- location.assign("/login.html");
- }
- }
-
- // 初始化一局游戏
- function initGame() {
- // 是我下还是对方下. 根据服务器分配的先后手情况决定
- let me = gameInfo.isWhite;
- // 游戏是否结束
- let over = false;
- let chessBoard = [];
- //初始化chessBord数组(表示棋盘的数组)
- for (let i = 0; i < 15; i++) {
- chessBoard[i] = [];
- for (let j = 0; j < 15; j++) {
- chessBoard[i][j] = 0;
- }
- }
- let chess = document.querySelector('#chess');
- let context = chess.getContext('2d');
- context.strokeStyle = "#000000";
- // 背景图片
- let logo = new Image();
- logo.src = "images/game_room2.png";
- logo.onload = function () {
- context.drawImage(logo, 0, 0, 450, 450);
- initChessBoard();
- }
-
- // 绘制棋盘网格
- function initChessBoard() {
- for (let i = 0; i < 15; i++) {
- context.moveTo(15 + i * 30, 15);
- context.lineTo(15 + i * 30, 435);
- context.stroke();
- context.moveTo(15, 15 + i * 30);
- context.lineTo(435, 15 + i * 30);
- context.stroke();
- }
- }
-
- // 绘制一个棋子, me 为 true
- function oneStep(i, j, isWhite) {
- context.beginPath();
- context.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI);
- context.closePath();
- var gradient = context.createRadialGradient(15 + i * 30 + 2, 15 + j * 30 - 2, 13, 15 + i * 30 + 2, 15 + j * 30 - 2, 0);
- if (!isWhite) {
- gradient.addColorStop(0, "#0A0A0A");
- gradient.addColorStop(1, "#636766");
- } else {
- gradient.addColorStop(0, "#D1D1D1");
- gradient.addColorStop(1, "#F9F9F9");
- }
- context.fillStyle = gradient;
- context.fill();
- }
-
- chess.onclick = function (e) {
- if (over) {
- return;
- }
- if (!me) {
- return;
- }
- let x = e.offsetX;
- let y = e.offsetY;
- // 注意, 横坐标是列, 纵坐标是行
- let col = Math.floor(x / 30);
- let row = Math.floor(y / 30);
- if (chessBoard[row][col] == 0) {
- // 发送坐标给服务器, 服务器要返回结果
- send(row, col);
- }
- }
-
- function send(row, col) {
- let req = {
- message: 'putChess',
- userId: gameInfo.thisUserId,
- row: row,
- col: col
- };
-
- websocket.send(JSON.stringify(req));
- }
-
- // 之前 websocket.onmessage 主要是用来处理了游戏就绪响应. 在游戏就绪之后, 初始化完毕之后, 也就不再有这个游戏就绪响应了.
- // 就在这个 initGame 内部, 修改 websocket.onmessage 方法, 让这个方法里面针对落子响应进行处理!
- websocket.onmessage = function(event) {
- console.log("[handlerPutChess] " + event.data);
-
- let resp = JSON.parse(event.data);
- if (resp.message != 'putChess') {
- console.log("响应类型错误!");
- location.assign("game_hall.html")
- return;
- }
-
- // 先判定当前这个响应是自己落的子, 还是对方落的子.
- if (resp.userId == gameInfo.thisUserId) {
- // 自己落的子
- oneStep(resp.col, resp.row, gameInfo.isWhite);
- } else if (resp.userId == gameInfo.thatUserId) {
- // 对手落的子
- oneStep(resp.col, resp.row, !gameInfo.isWhite);
- } else {
- // 响应错误! userId 是有问题的!
- console.log('[handlerPutChess] resp userId 错误!');
- return;
- }
-
- // 给对应的位置设为 1, 方便后续逻辑判定当前位置是否已经有子了.
- chessBoard[resp.row][resp.col] = 1;
-
- // 交换双方的落子轮次
- me = !me;
- setScreenText(me);
-
- // 判定游戏是否结束
- let screenDiv = document.querySelector('#screen');
- if (resp.winner != 0) {
- if (resp.winner == gameInfo.thisUserId) {
- // alert('你赢了!');
- screenDiv.innerHTML = '恭喜你,获胜了!';
- } else if (resp.winner = gameInfo.thatUserId) {
- // alert('你输了!');
- screenDiv.innerHTML = '很遗憾,你输了!';
- } else {
- alert("winner 字段错误! " + resp.winner);
- }
- // 回到游戏大厅
- let room = document.querySelector(".buttons");
- console.log(room)
- let backButton = document.createElement("button");
- backButton.innerHTML = '返回游戏大厅';
- backButton.className = "backButton";
- backButton.onclick = function() {
- location.assign('/game_hall.html');
- }
- room.appendChild(backButton);
- }
- }
- }