0
点赞
收藏
分享

微信扫一扫

基于 SpringBoot + MyBatis 的在线五子棋对战

紫荆峰 2022-08-13 阅读 35

文章目录

1. 项目设计

前端 : HTML + CSS + JavaScript + Jquery + AJAX
后端 : Spring MVC + Spring Boot + MyBatis
在这里插入图片描述

2. 效果图展示

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3. 创建项目以及配置文件

3.1 创建项目

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.2 配置文件

3.2.1 在 application.properties 中添加配置文件

spring.datasource.url=jdbc:mysql://localhost:3306/onlineGobang?characterEncoding=utf8&useSSL=true
spring.datasource.username=root
spring.datasource.password=0000
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

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

3.2.2 在 resources 目录下创建mapper

mapper下添加 目录 **.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.onlinemusicserver.mapper."对应的Mapper"">

</mapper>

4. 数据库设计与实现

在这里插入图片描述
这里使用数据库存储每一个用户的信息, 初始的时候, 天梯分和场次都是默认的.

create database if not exists onlineGobang;

use onlineGobang;

drop table if exists user;
create table user(
    userId int primary key auto_increment,
    username varchar(20) unique,
    password varchar(255) not null,
    score int,
    totalCount int,
    winCount int
);

5. 登录注册模块

5.1 设计登录注册交互接口

请求
POST /user/login HTTP/1.1

{username: "",password: ""}

响应
{
	status: 1/-1,
	message: "",
	data: ""
}
请求
GET /user/logout HTTP/1.1

响应
HTTP/1.1 200
请求
POST /user/register HTTP/1.1

{username: "",password: ""}

响应
{
	status: 1/-1,
	message: "",
	data: ""
}

5.2 设置登录注册功能返回的响应类

@Data
public class ResponseBodyMessage<T> {
    private int status;
    private String message;
    private T data;

    public ResponseBodyMessage(int status, String message, T data) {
        this.status = status;
        this.message = message;
        this.data = data;
    }
}

5.3 使用 BCrypt 对密码进行加密

<!-- security依赖包 (加密)-->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-config</artifactId>
		</dependency>
@SpringBootApplication(exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
@Configuration
public class AppConfig implements WebMvcConfigurer {

    @Bean
    public BCryptPasswordEncoder getBCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

5.4 完成 MyBatis 操作

@Data
public class User {
    private int userId;
    private String username;
    private String password;
    private int score;
    private int totalCount;
    private int winCount;
}
@Mapper
public interface UserMapper {
    // 注册一个用户, 初始的天梯积分默认为1000, 场次默认为0
    int insert(User user);

    // 通过username查询当前用户是否存在
    User selectByName(String username);
}
<?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.gobang.mapper.UserMapper">
    <insert id="insert">
        insert into user values(null,#{username},#{password},1000,0,0)
    </insert>

    <select id="selectByName" resultType="com.example.gobang.model.User">
        select * from user where username = #{username}
    </select>
</mapper>

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    public int insert(User user){
        return userMapper.insert(user);
    }

    public User selectByName(String username){
        return userMapper.selectByName(username);
    }
}

5.5 后端的实现

5.5.1 登录功能后端实现

 @RequestMapping("/login")
    public ResponseBodyMessage<User> login(@RequestBody User user, HttpServletRequest request) {
        User truUser = userService.selectByName(user.getUsername());
        if (truUser == null) {
            System.out.println("登录失败!");
            return new ResponseBodyMessage<>(-1,"用户名密码错误!",user);
        }else {
            boolean flg = bCryptPasswordEncoder.matches(user.getPassword(),truUser.getPassword());
            if (!flg) {
                return new ResponseBodyMessage<>(-1,"用户名密码错误!",user);
            }
            System.out.println("登录成功!");
            HttpSession session = request.getSession(true);
            session.setAttribute(Constant.USER_SESSION_KEY,truUser);
            return new ResponseBodyMessage<>(1,"登录成功!",truUser);
        }
    }

5.5.2 注册功能后端实现

    @RequestMapping("/register")
    public ResponseBodyMessage<User> register(@RequestBody User user) {
        User truUser = userService.selectByName(user.getUsername());
        if (truUser != null) {
            return new ResponseBodyMessage<>(-1,"当前用户名已经存在!",user);
        } else{
            String password = bCryptPasswordEncoder.encode(user.getPassword());
            user.setPassword(password);
            userService.insert(user);
            return new ResponseBodyMessage<>(1,"注册成功!",user);
        }
    }

5.5.3 注销功能

@RequestMapping("/logout")
    public void userLogout(HttpServletRequest request, HttpServletResponse response) throws IOException, IOException {
        HttpSession session = request.getSession(false);
        // 拦截器的拦截, 所以不可能出现session为空的情况
        session.removeAttribute(Constant.USER_SESSION_KEY);
        response.sendRedirect("login.html");
    }

注意: 这里的Constant.USER_SESSION_KEY 是存储的 session 字符串, 由于该 字符串是不变的, 所以存入 Constant 类中.

5.6 前端的实现

5.6.1 登录前端实现

在这里插入图片描述

let loginButton = document.querySelector('#loginButton');
		loginButton.onclick = function() {
			let username = document.querySelector('#loginUsername');
			let password = document.querySelector('#loginPassword');
			if (username.value.trim() == ""){
                alert('请先输入用户名!');
                username.focus();
                return;
            }
            if (password.value.trim() == ""){
                alert('请先输入密码!');
                password.focus();
                return;
            }
            $.ajax({
                url: "user/login",
                method: "POST",
                data: JSON.stringify({username: username.value.trim(), password: password.value.trim()}),
                contentType: "application/json;charset=utf-8",
                success: function(data, status) {
                    if(data.status == 1) {
                        location.assign("index.html");
                    }else{
                        alert(data.message);
                        username.value="";
                        password.value="";
                        username.focus();
                    }
                }
			})
		}

5.6.2 注册前端实现

在这里插入图片描述


		let Reg = document.querySelector('#Reg');
		Reg.onclick = function() {
			let username = document.querySelector('#RegUsername');
			let password1 = document.querySelector('#RegPassword1');
			let password2 = document.querySelector('#RegPassword2');
			if(!$('#checkbox').is(':checked')) {
				alert("请勾选条款");
				return;
			}
			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(username.value.trim().length > 20) {
				alert("用户名长度过长");
				username.value="";
				username.focus();
				return;
			}
			if(password1.value.trim() != password2.value.trim()) {
                alert('两次输入的密码不同!');
                passwrod1.value="";
                password2.value="";
                return;
            }
			if(password1.value.trim().length > 255) {
				alert("当前密码长度过长!");
				password1.value="";
				password2.value="";
				password1.focus();
				return;
			}
			$.ajax({
                url: "user/register",
                method: "POST",
                data: JSON.stringify({username: username.value.trim(), password: password1.value.trim()}),
                contentType: "application/json;charset=utf-8",
                success: function(data,status){
                    if(data.status == 1) {
						alert(data.message);
						location.assign("login.html");
					}else{
						alert(data.message);
						username.value="";
                        password1.value="";
                        password2.value="";
                        username.focus();
					}
                }
            })
		}

5.7 添加拦截器

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 true;
        }
        response.sendRedirect("/login.html");
        return false;
    }
}
@Configuration
@EnableWebSocket
public class AppConfig implements WebSocketConfigurer,WebMvcConfigurer{
	@Override
    public void addInterceptors(InterceptorRegistry registry) {
        LoginInterceptor loginInterceptor = new LoginInterceptor();
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/**/login.html")
                .excludePathPatterns("/**/css/**.css")
                .excludePathPatterns("/**/images/**")
                .excludePathPatterns("/**/fonts/**")
                .excludePathPatterns("/**/js/**.js")
                .excludePathPatterns("/**/scss/**")
                .excludePathPatterns("/**/user/login")
                .excludePathPatterns("/**/user/register")
                .excludePathPatterns("/**/user/logout");
    }
}

6. 大厅界面

6.1 交互接口设计

{
	message: ' startMatch ' / ' stopMatch'
}
{
	status: '1' / '-1'  
	message: ' startMatch ' / ' stopMatch '
}
{
	status: '1' / '-1'
	message: 'matchSuccess'
}

6.2 用户加载前后交互接口

请求
GET /user/userInfo HTTP/1.1

响应
{
	status: 1/-1 (1 为成功, -1 为失败),
	message: "对应信息",
	data: "内容",  (用户信息)
}

6.3 前端和后端实现用户信息加载

6.3.1 后端的实现

    @RequestMapping("/userInfo")
    public ResponseBodyMessage<User> getUserInfo(HttpServletRequest request) {
        HttpSession session = request.getSession(false);
        User user = (User) session.getAttribute(Constant.USER_SESSION_KEY);
        if (user == null) {
            return new ResponseBodyMessage<>(-1,"当前用户不存在",null);
        }else{
            return new ResponseBodyMessage<>(1,"查找成功!", newUser);
        }
    }

6.3.2 前端的实现

在这里插入图片描述

      load();

      function load() {
        $.ajax({
          url: "user/userInfo",
          method: "GET",
          success:function(data) {
            if(data.status == 1) {
              let h2 = document.querySelector('#myname');
              h2.innerHTML = "你好! " + data.data.username;
              let game = document.querySelector('#gameMes');
              game.innerHTML = "天梯分数: " + data.data.score + " | " + "场数: " + data.data.totalCount + " | " + "获胜场数: "+ data.data.winCount;

            }else{
              alert(data.message);
              location.assign("login.html");
            }
          }
        })
      }

6.4 实现匹配功能的前端代码

        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("onerrot");
        }

        // 监听整个窗口关闭的事件, 当窗口关闭, 主动的去关闭websocket连接
        window.onbeforeunload = function() {
          websocket.close();
        }

        // 连接成功收到的响应
        websocket.onmessage = function(e) {
          // 先将Json格式 e 化为 响应对象
          let resp = JSON.parse(e.data);
          // 获取到 匹配按钮
          let play = document.querySelector('#beginPlay');
          // 等于-1是错误的起来, 打印错误的信息, 并跳转到登录页面
          if (resp.status == -1) {
            alert(resp.message);
            location.assign("login.html");
            return;
          }
          // 这里就都是正常的响应, 那么就判断是开始匹配, 还是结束匹配
          if (resp.message == 'startMatch') {
            //开始匹配
            console.log("开始匹配");
            play.innerHTML = '匹配中...(点击停止)';
          }else if(resp.message == 'stopMatch') {
            //结束匹配
            console.log("结束匹配");
            play.innerHTML = '开始匹配';
          }else if(resp.message == 'matchSuccess') {
            //匹配成功
            console.log("匹配成功");
            location.assign('room.html');
          }else{
            // 按理不会触发这个else
            alert(resp.message);
            console.log("收到非法响应");
          }
        }
        
        // 获取到匹配按钮
        let play = document.querySelector('#beginPlay');
        // 匹配按钮点击事件
        play.onclick = function() {
          // 判断当前 readyState 是否是OPEN状态的
          if (websocket.readyState == websocket.OPEN) {
            // 当前 readyState 处于OPEN 状态, 说明链接是好的
            if (play.innerHTML == '开始匹配') {
              // 发送开始匹配的请求
              websocket.send(JSON.stringify(
                {
                  message: 'startMatch',
                }
              ))
            }else if(play.innerHTML == '匹配中...(点击停止)'){
              // 发送停止匹配的请求
              websocket.send(JSON.stringify(
                {
                  message: 'stopMatch',
                }
              ))
            }
          }else{
            // 这里就是链接异常的情况
            alert('当前您的链接已经断开, 请重新登录');
            location.assign("login.html");
          }
        }

6.5 实现匹配功能的后端代码

@Configuration
@EnableWebSocket
public class AppConfig implements WebSocketConfigurer{

    @Autowired
    private MatchController matchController;
 
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(matchController,"/findMatch")
                .addInterceptors(new HttpSessionHandshakeInterceptor());
    }
}

6.5.1 创建在线状态

@Component
public class OnlineUserManager {
    // 这个哈希表是表示当前用户在游戏大厅的在线状态
    private ConcurrentHashMap<Integer, WebSocketSession> gameState = new ConcurrentHashMap<>();
    public void enterGameIndex(int userId, WebSocketSession webSocketSession) {
        gameState.put(userId,webSocketSession);
    }

    public void exitGameHall(int userId) {
        gameState.remove(userId);
    }

    public WebSocketSession getState(int userId) {
        return gameState.get(userId);
    }
}

6.5.2 创建房间对象


// 游戏房间
@Data
public class Room {
    private String roomId;
    private User user1;
    private User user2;
 	public Room() {
        this.roomId = UUID.randomUUID().toString();
	}
}

6.5.3 创建房间管理器


// 房间管理器
@Component
public class RoomManager {
    private ConcurrentHashMap<String,Room> rooms = new ConcurrentHashMap<>();
	
	public void insert(Room room) {
        rooms.put(room.getRoomId(),room);
    }
	
	public void remove(String roomId) {
        rooms.remove(roomId);
    }
	
    public Room findRoomByRoomId(String roomId) {
        return rooms.get(roomId);
    }

6.5.4 创建匹配队列

    // 创建匹配队列 按等级划分
    // 1. < 2000
    private Queue<User> simpleQueue = new LinkedList<>();
    // 2. >= 2000 && < 3000
    private Queue<User> normalQueue = new LinkedList<>();
    // 3. >= 3000
    private Queue<User> highQueue = new LinkedList<>();

// 匹配器, 这个类是用来完成匹配功能的
@Component
public class Matcher {
    // 创建匹配队列 按等级划分
    // 1. < 2000
    private Queue<User> simpleQueue = new LinkedList<>();
    // 2. >= 2000 && < 3000
    private Queue<User> normalQueue = new LinkedList<>();
    // 3. >= 3000
    private Queue<User> highQueue = new LinkedList<>();

    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private RoomManager roomManager;

    /**
     * 将当前玩家添加到匹配队列中
     * @param user
     */
    public void insert(User user) {
        // 按等级加入队列中
        if (user.getScore() < 2000) {
            synchronized (simpleQueue) {
                simpleQueue.offer(user);
                // 只要有用户进入了, 就进行唤醒
                simpleQueue.notify();
            }
        }else if (user.getScore() >= 2000 && user.getScore() < 3000) {
            synchronized (normalQueue) {
                normalQueue.offer(user);
                normalQueue.notify();
            }
        }else {
            synchronized (highQueue) {
                highQueue.offer(user);
                highQueue.notify();
            }
        }
    }

    /**
     * 将当前玩家匹配队列中删除
     * @param user
     */
    public void remove(User user) {
        // 按照当前等级去对应匹配队列中删除
        if (user.getScore() < 2000) {
            synchronized (simpleQueue){
                simpleQueue.remove(user);
            }
        }else if (user.getScore() >= 2000 && user.getScore() < 3000) {
            synchronized (normalQueue) {
                normalQueue.remove(user);
            }
        }else {
            synchronized (highQueue) {
                highQueue.remove(user);
            }
        }
    }

    /**
     * 这里使用3个线程去一直的进行查看是否有2个以上的人, 如果有进行匹配
     */
    public Matcher() {
        // 创建三个线程, 操作三个匹配队列
        Thread t1 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    handlerMatch(simpleQueue);
                }
            }
        };
        t1.start();
        Thread t2 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    handlerMatch(normalQueue);
                }
            }
        };
        t2.start();
        Thread t3 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    handlerMatch(highQueue);
                }
            }
        };
        t3.start();
    }

    private void handlerMatch(Queue<User> matchQueue) {
        synchronized (matchQueue) {
            try{
                // 1. 先查看当前队列中的元素个数, 是否满足两个
                // 这里使用while, 以防为0的时候, 被唤醒,然后没有再次判断导致进入下面操作.
                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.getState(player1.getUserId());
                WebSocketSession session2 = onlineUserManager.getState(player2.getUserId());
                // 再次判断是否为空
                if (session1 == null && session2 != null) {
                    matchQueue.offer(player2);
                    return;
                }
                if (session1 != null && session2 == null) {
                    matchQueue.offer(player1);
                    return;
                }
                if (session1 == null && session2 == null) {
                    return;
                }
                if (session1 == session2) {
                    matchQueue.offer(player1);
                    return;
                }
                // 4. 把两个玩家放入一个游戏房间中
                Room room = new Room();
                roomManager.insert(room,player1.getUserId(),player2.getUserId());

                // 5. 给玩家反馈信息, 通知匹配到了对手
                MatchResponse response1 = new MatchResponse();
                response1.setStatus(1);
                response1.setMessage("matchSuccess");
                String json1 = objectMapper.writeValueAsString(response1);
                session1.sendMessage(new TextMessage(json1));

                MatchResponse response2 = new MatchResponse();
                response2.setMessage("matchSuccess");
                response2.setStatus(1);
                String json2 = objectMapper.writeValueAsString(response2);
                session2.sendMessage(new TextMessage(json2));
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

6.5.5 写完 MatchController

@Component
public class MatchController extends TextWebSocketHandler {
    private ObjectMapper objectMapper = new ObjectMapper();

    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private Matcher matcher;

	// 连接成功的时候就会调用该方法
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 玩家上线
        // 1. 获取用户信息
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
        // 2. 判断当前用户是否已经登录
        if (onlineUserManager.getState(user.getUserId()) != null ) {
            // 当前用户已经登录
            MatchResponse message = new MatchResponse();
            message.setMessage("当前用户已经登录!");
            message.setStatus(-1);
            session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(message)));
            session.close();
            return;
        }
        // 3. 设置在线状态
        onlineUserManager.enterGameIndex(user.getUserId(),session);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 处理开始匹配 和 停止匹配
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
        String payload = message.getPayload();
        MatchRequest matchRequest = objectMapper.readValue(payload, MatchRequest.class);
        MatchResponse matchResponse = new MatchResponse();
        if (matchRequest.getMessage().equals("startMatch")) {
            // 进入匹配队列
            // 创建匹配队列, 加入用户
            matcher.insert(user);
            // 返回响应给前端
            matchResponse.setStatus(1);
            matchResponse.setMessage("startMatch");
        }else if(matchRequest.getMessage().equals("stopMatch")) {
            // 退出匹配队列
            // 创建匹配队列, 将用户移除
            matcher.remove(user);
            matchResponse.setMessage("stopMatch");
            matchResponse.setStatus(1);
        }else{
            matchResponse.setStatus(-1);
            matchRequest.setMessage("非法匹配");
            // 非法情况
        }
        session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(matchResponse)));
    }

    // 异常情况
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        // 玩家下线
        // 1. 获取用户信息
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
        WebSocketSession webSocketSession = onlineUserManager.getState(user.getUserId());
        if(webSocketSession == session) {
            // 2. 设置在线状态
            onlineUserManager.exitGameHall(user.getUserId());
        }
        matcher.remove(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.getState(user.getUserId());
        if(webSocketSession == session) {
            // 2. 设置在线状态
            onlineUserManager.exitGameHall(user.getUserId());
        }
        matcher.remove(user);
    }
}

6.6 大厅界面总结

  1. 这里要注意多线程环境下, 多个用户同时使用同一个哈希表的时候, 进行添加和删除的时候, 会有线程安全的问题, 那么这里就需要使用 ConcurrentHashMap
  2. 在多线程环境下, 按照等级分的队列, 在多线程环境下, 并发的进行入队的时候, 删除的队列中用户的时候, 也会有线程安全问题, 这里针对同一个队列就可以进行加锁.
  3. 由于创建3个线程循环的进入队列中查看是否满足2个用户, 如果当前的环境下, 用户特别少, 一直去循环的进入, 会造成CPU占用率特别高, 所以这里就使用wai()等待, 在有用户进入匹配队列的时候,再去唤醒notify().
  4. 防止多开, 多个地方登录同一个账号就会出现很多问题, 这里在进行连接的时候判断, 如果用户已经在线, 就不让该地方用户登录.
  5. 要想让 房间是第一无二, 就需要使用 UUID, 那么roomId也要使用 字符串的格式.

在这里插入图片描述

7. 房间界面

7.1 交互接口设计

ws://127.0.0.1:8080/game
{
	message: 'gameReady' 
	status: '1 / -1'  (1是正常响应, -1 是错误响应) 
	roomId: ' ' 
	thisUserId: ' ' (自己用户Id)
	thatUserId: ' ' (对方用户Id)
	whiteUser: ' ' (先手方)
}
{
	message: ' putChess ' 
	userId: ' '  (落子的用户Id)
	row: ' ' (落子的第几行)
	col: ' ' (落子的第几列)
}
{
	message: 'putChess;
	userId: ' '
	row: ' '
	col: ' '
	winner: ' ' (获胜者, 和用户Id一致, 如果没有获胜, 就是0)
}

7.2 实现房间界面前端代码

7.2.1 设置棋盘界面, 以及显示框.

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 href="css/game_room.css" rel="stylesheet" type="text/css" media="all" />
</head>
<body>
        <div class="container">
            <div class="one">
                <!-- 棋盘区域, 需要基于 canvas 进行实现 -->
                <canvas id="chess" width="450px" height="450px">
                </canvas>
                <!-- 显示区域 -->
                <div id="screen"> 等待玩家连接中... </div>
            </div>
        </div>
        <script src="js/script.js"></script>
</body>
</html>

game_room.css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
html, body {
    height: 100%;

    background-image: url(../images/bg.jpg);
    background-repeat: no-repeat;
    background-position: center;
    background-size: cover;
}
.container {
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
}

#screen {
    width: 450px;
    height: 50px;
    margin-top: 10px;
    background-color: #fff;
    font-size: 22px;
    line-height: 50px;
    text-align: center;
}
.backButton {
    width: 450px;
    height: 50px;
    font-size: 20px;
    color: white;
    background-color: orange;
    border: none;
    outline: none;
    border-radius: 10px;

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

.backButton:active {
    background-color: gray;
}

7.2.2 对应的js文件

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
//
// TODO

//
// 初始化一局游戏
//
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 = "#BFBFBF";
    // 背景图片
    let logo = new Image();
    logo.src = "image/sky.jpeg";
    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, 430);
            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) {
            // TODO 发送坐标给服务器, 服务器要返回结果

            oneStep(col, row, gameInfo.isWhite);
            chessBoard[row][col] = 1;
        }
    }

    // TODO 实现发送落子请求逻辑, 和处理落子响应逻辑. 
}

initGame();

7.2.3 初始化 websocket


let websocketUrl = 'ws://'+ location.host +'/game';
let websocket = new WebSocket(websocketUrl);

websocket.onopen = function() {
    console.log("房间链接成功!");
}
websocket.onclose = function() {
    console.log("房间断开链接");
}
websocket.onerror = function() {
    console.log("房间出现异常");
}
window.onbeforeunload = function() {
    websocket.close();
}
websocket.onmessage = function(e) {
    console.log(e.data);
    let resp = JSON.parse(e.data);

    if(resp.message != 'gameReady') {
        console.log("响应类型错误");
        location.assign("index.html");
        return;
    }
    if(resp.status == -1) {
        alert("游戏链接失败!");
        location.assign("index.html");
        return;
    }
    
    gameInfo.roomId == resp.roomId;
    gameInfo.thisUserId = resp.thisUserId;
    gameInfo.thatUserId = resp.thatUserId;
    gameInfo.isWhite = resp.whiteUser == resp.thisUserId;

    // 初始化棋盘
    initGame();

    // 设置显示内容
    setScreenText(gameInfo.isWhite);
}

7.2.4 落子时,发送落子请求

在这里插入图片描述

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

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

7.2.5 落子时, 发送落子响应

websocket.onmessage = function(e) {
        console.log(e.data);
        let resp = JSON.parse(e.data);
        if (resp.message != 'putChess') {
            console.log("响应类型错误!");
            location.assign("index.html")
            return;
        }

        if (resp.userId == gameInfo.thisUserId) {
            // 自己落子
            oneStep(resp.col, resp.row, gameInfo.isWhite);
            chessBoard[resp.row][resp.col] = 1;
        } else if (resp.userId == gameInfo.thatUserId) {
            // 别人落子
            oneStep(resp.col, resp.row, !gameInfo.isWhite);
            chessBoard[resp.row][resp.col] = 1;
        }else{
            // 落子异常
            console.log("userId 异常");
            return;
        }

        // 交换落子
        me = !me;
        setScreenText(me);

        // 判断游戏是否结束
        let screenDiv = document.querySelector('#screen');
        if (resp.winner != 0) {
            console.log(resp.winner+" " + gameInfo.thisUserId+" " + gameInfo.thatUserId);
            if (resp.winner == gameInfo.thisUserId) {
                screenDiv.innerHTML = "恭喜你, 获胜了!";
            }else if(resp.winner == gameInfo.thatUserId) {
                screenDiv.innerHTML = "游戏结束, 失败了!";
            }else {
                console.log("winner 错误");
                alert("当前 winner字段错误 winner = "+ resp.winner);
            }
            // location.assign('index.html');
            // 增加一个按钮, 返回游戏大厅
            let backBtn = document.createElement('button');
            backBtn.innerHTML = "返回游戏大厅";
            backBtn.className = "backButton";
            let one = document.querySelector('.one');
            backBtn.onclick = function() {
                location.assign("index.html");
            }
            one.appendChild(backBtn);
        }

7.3 实现房间界面后端代码

7.3.1 注册GameController

@Configuration
@EnableWebSocket
public class AppConfig implements WebSocketConfigurer,WebMvcConfigurer{

    @Autowired
    private GameController gameController;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(gameController,"/game")
                .addInterceptors(new HttpSessionHandshakeInterceptor());
    }

7.3.2 创建GameController

  1. afterConnectionEstablished 这个方法是在建立连接时候的方法.
  2. handleTextMessage 这个方法是接收发送的响应
  3. handleTransportError 这个方法是出现异常的时候执行的
  4. afterConnectionClosed 这个方法是关闭websocket的时候执行的
@Component
public class GameController extends TextWebSocketHandler {
    @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 {
    }
}

7.3.3 创建对应的响应类和请求类

// 客户端链接成功后, 返回的响应
@Data
public class GameReadyResponse {
    private String message;
    private int status;
    private String roomId;
    private int thisUserId;
    private int thatUserId;
    private int whiteUser;
}
// 落子的请求
@Data
public class GameRequest {
    private String message;
    private int userId;
    private int row;
    private int col;
}
//落子响应
@Data
public class GameResponse {
    private String message;
    private int userId;
    private int row;
    private int col;
    private int winner;
}

7.3.4 完成用户房间在线状态管理

// 这个哈希表是表示当前用户在游戏房间的在线状态
    private ConcurrentHashMap<Integer, WebSocketSession> roomState = new ConcurrentHashMap<>();

    public void enterGameRoom(int userId, WebSocketSession webSocketSession){
        roomState.put(userId,webSocketSession);
    }
    public void exitGameRoom(int userId) {
        roomState.remove(userId);
    }
    public WebSocketSession getRoomState(int userId) {
        return roomState.get(userId);
    }

7.3.5 添加 MyBatis 用来更新玩家积分

UserMapper

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

    // 总场数 + 1, 天梯分数 -50
    void userLose(int userId);

UserMapper.xml

    <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>

UserService

// 总场数 + 1, 获胜场数+1, 天梯分数 + 50
    public void userWin(int userId){
        userMapper.userWin(userId);
    }

    // 总场数 + 1, 天梯分数 -50
    public void userLose(int userId) {
        userMapper.userLose(userId);
    }

7.3.6 完成处理连接方法

 @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        GameReadyResponse readyResponse = new GameReadyResponse();

        // 获取用户信息
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
        // 判断当前是否已经进入房间
        Room room = roomManager.findRoomByUserId(user.getUserId());
        if (room == null) {
            readyResponse.setStatus(-1);
            readyResponse.setMessage("用户尚未匹配到!");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(readyResponse)));
            return;
        }
        // 判断当前是否多开
        if (onlineUserManager.getRoomState(user.getUserId()) != null || onlineUserManager.getState(user.getUserId()) != null) {
            readyResponse.setMessage("当前用户已经登录!");
            readyResponse.setStatus(-1);
            session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(readyResponse)));
            return;
        }

        // 上线
        onlineUserManager.enterGameRoom(user.getUserId(), session);

        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());
                }
                // 通知玩家1
                noticeGameReady(room,room.getUser1(),room.getUser2());
                // 通知玩家2
                noticeGameReady(room,room.getUser2(),room.getUser1());
                return;
            }
        }

        readyResponse.setStatus(-1);
        readyResponse.setMessage("房间已经满了");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(readyResponse)));
    }
private void noticeGameReady(Room room, User user1, User user2) throws IOException {
        GameReadyResponse resp = new GameReadyResponse();
        resp.setStatus(1);
        resp.setMessage("gameReady");
        resp.setRoomId(room.getRoomId());
        resp.setThisUserId(user1.getUserId());
        resp.setThatUserId(user2.getUserId());
        resp.setWhiteUser(room.getWhiteUser());

        WebSocketSession webSocketSession = onlineUserManager.getRoomState(user1.getUserId());
        webSocketSession.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
    }

7.3.7 完成处理连接断开的方法和连接异常的方法


    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        // 异常下线
        // 下线
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
        WebSocketSession exitSession = onlineUserManager.getRoomState(user.getUserId());
        if(exitSession == session) {
            onlineUserManager.exitGameRoom(user.getUserId());
        }
        System.out.println("当前用户: " + user.getUsername()+" 异常下线了");

        noticeThatUserWin(user);
    }
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 下线
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
        WebSocketSession exitSession = onlineUserManager.getRoomState(user.getUserId());
        if(exitSession == session) {
            onlineUserManager.exitGameRoom(user.getUserId());
        }
        System.out.println("当前用户: " + user.getUsername()+" 离开房间");

        noticeThatUserWin(user);
    }

    private void noticeThatUserWin(User user) throws IOException {
        Room room = roomManager.findRoomByUserId(user.getUserId());
        if (room == null) {
            System.out.println("房间已经关闭");
            return;
        }
        // 找到对手
        User thatUser = (user == room.getUser1()) ? room.getUser2() : room.getUser1();
        // 找到对手的状态
        WebSocketSession session = onlineUserManager.getRoomState(thatUser.getUserId());
        if (session == null) {
            // 都掉线了
            System.out.println("都掉线了, 无需通知");
            return;
        }
        // 这里通知对手获胜
        GameResponse gameResponse = new GameResponse();
        gameResponse.setMessage("putChess");
        gameResponse.setUserId(thatUser.getUserId());
        gameResponse.setWinner(thatUser.getUserId());
        session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(gameResponse)));

        // 更新玩家分数信息
        int winId = thatUser.getUserId();
        int loseId = user.getUserId();
        userService.userWin(winId);
        userService.userLose(loseId);
        // 释放房间对象
        roomManager.remove(room.getRoomId(),room.getUser1().getUserId(),room.getUser2().getUserId());
    }

7.3.8 在房间管理器中添加代码

    private ConcurrentHashMap<Integer,String> Ids = new ConcurrentHashMap<>();

    public void insert(Room room,int userId1, int userId2) {
        Ids.put(userId1,room.getRoomId());
        Ids.put(userId2,room.getRoomId());
    }

    public void remove(String roomId,int userId1, int userId2) {
        Ids.remove(userId1);
        Ids.remove(userId2);
    }

	public Room findRoomByUserId(int userId) {
        String roomId = Ids.get(userId);
        if (roomId == null) {
            return null;
        }
        return rooms.get(roomId);
    }

7.3.9 Room类添加棋盘代码

修改启动类

public class GobangApplication {

	public static ConfigurableApplicationContext context;

	public static void main(String[] args) {
		context = SpringApplication.run(GobangApplication.class, args);
	}

}
// 游戏房间
@Data
public class Room {
    private String roomId;
    private User user1;
    private User user2;
    private int whiteUser;

    private OnlineUserManager onlineUserManager;

    private RoomManager roomManager;

    private UserService userService;

    public Room() {
        this.roomId = UUID.randomUUID().toString();

        onlineUserManager = GobangApplication.context.getBean(OnlineUserManager.class);

        roomManager = GobangApplication.context.getBean(RoomManager.class);

        userService = GobangApplication.context.getBean(UserService.class);
    }

    // 为0就是为落子, 为1就是用户1落子, 为2就是用户2落子
    private int[][] board= new int[Constant.ROW][Constant.COL];

    private ObjectMapper objectMapper = new ObjectMapper();

}

7.3.10 实现handleTextMessage方法

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 获取用户对象
        User user = (User) session.getAttributes().get(Constant.USER_SESSION_KEY);
        // 根据 玩家 Id 获取房间对象
        Room room = roomManager.findRoomByUserId(user.getUserId());
        // 通过room对象处理这次请求
        room.putChess(message.getPayload());
    }

7.3.11 实现putChess方法

    // 这个方法是用来处理一次落子的操作
    public void putChess(String reqJson) throws IOException {
        // 1. 记录当前落子的位子
        GameRequest request = objectMapper.readValue(reqJson,GameRequest.class);
        GameResponse response = new GameResponse();
        // 1.1 判断当前落子是谁
        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. 进行胜负判定
        int winner = checkWinner(row,col,chess);
        // 3. 给房间中所有的客户端返回响应
        response.setMessage("putChess");
        response.setRow(row);
        response.setCol(col);
        response.setWinner(winner);
        response.setUserId(request.getUserId());

        WebSocketSession session1 = onlineUserManager.getRoomState(user1.getUserId());
        WebSocketSession session2 = onlineUserManager.getRoomState(user2.getUserId());
        // 这里对下线进行判断
        if (session1 == null) {
            // 玩家1下线
            response.setWinner(user2.getUserId());
            System.out.println("玩家1掉线");
        }
        if (session2 == null) {
            // 玩家2下线, 就认为玩家1获胜
            System.out.println("玩家2掉线");
        }
        String respJson = objectMapper.writeValueAsString(response);
        if (session1 != null) {
            session1.sendMessage(new TextMessage(respJson));
        }
        if (session2 != null) {
            session2.sendMessage(new TextMessage(respJson));
        }
        // 4. 如果当前获胜, 销毁房间
        if (response.getWinner() != 0) {
            System.out.println("游戏结束, 房间即将销毁");
            // 更新获胜方的信息
            int winId = response.getWinner();
            int LoseId = response.getWinner() == user1.getUserId() ? user2.getUserId() : user1.getUserId();
            userService.userLose(LoseId);
            userService.userWin(winId);
            // 销毁房间
            roomManager.remove(roomId,user1.getUserId(),user2.getUserId());
        }
    }

7.3.12 完成用户胜负判断

完成 checkWinner 方法

    // 谁获胜就返回谁的Id, 如果还没有获胜者, 就返回0
    private int checkWinner(int row, int col, int chess) {
        //  判断当前是谁获胜
        // 1. 一行五子连珠
        for (int i = col -4 ;i >= 0 && i <= col && i <= Constant.COL-5; i++) {
            if (board[row][i] == chess
            && board[row][i+1] == chess
            && board[row][i+2] == chess
            && board[row][i+3] == chess
            && board[row][i+4] == chess) {
                return chess == 1 ? user1.getUserId() : user2.getUserId();
            }
        }
        // 2. 一列五子连珠
        for (int i = row - 4; i >= 0 && i <= row && i <= Constant.ROW-5; i++) {
            if (board[i][col] == chess
            && board[i+1][col] == chess
            && board[i+2][col] == chess
            && board[i+3][col] == chess
            && board[i+4][col] == chess) {
                return chess == 1 ? user1.getUserId() : user2.getUserId();
            }
        }
        // 3. 斜着五子连珠 -> 左上到右下
        for (int i = row - 4, j = col - 4; i <= row && j <= col;j++,i++){
            try {
                if (board[i][j] == chess
                        && board[i+1][j+1] == chess
                        && board[i+2][j+2] == chess
                        && board[i+3][j+3] == chess
                        && board[i+4][j+4] == chess) {
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            }catch (ArrayIndexOutOfBoundsException e) {
                continue;
            }
        }
        // 4. 斜着五子连珠 -> 右上到左下
        for (int i = row+4,j=col-4; i>=row && j <= col; i--,j++) {
            try {
                if (board[i][j] == chess
                        && board[i-1][j+1] == chess
                        && board[i-2][j+2] == chess
                        && board[i-3][j+3] == chess
                        && board[i-4][j+4] == chess) {
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            }catch (ArrayIndexOutOfBoundsException e) {
                continue;
            }
        }
        return 0;
    }
举报

相关推荐

0 条评论