五子棋项目总结复习复盘逻辑

五子棋项目总结复习复盘逻辑

逻辑

项目 按着
用户登录——开始匹配——进入房间——开始对战——对战结束

在此过程,需要知道用户的状态,所以需要一个用户管理类
还需要知道房间,需要一个房间管理类

用户类+Mapper

User(id、账户名、密码、分数、总场数、赢场数)

package com.example.java_gobang.model;

public class User {
    private int userId;
    private String username;
    private String password;
    private int score;
    private int totalCount;
    private int winCount;

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getScore() {
        return score;
    }

    public void setScore(int score) {
        this.score = score;
    }

    public int getTotalCount() {
        return totalCount;
    }

    public void setTotalCount(int totalCount) {
        this.totalCount = totalCount;
    }

    public int getWinCount() {
        return winCount;
    }

    public void setWinCount(int winCount) {
        this.winCount = winCount;
    }
}

UserMapper

package com.example.java_gobang.model;

import org.apache.ibatis.annotations.Mapper;

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

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

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

    // 总比赛场数 + 1, 获胜场数 不变, 天梯分数 - 30
    void userLose(int userId);
}

insert

    <insert id="insert">
        insert into user values(null, #{username}, #{password}, 1000, 0, 0);
    </insert>

selectByName

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

userWin

    <update id="userWin">
        update user set totalCount = totalCount + 1, winCount = winCount + 1, score = score + 30
        where userId = #{userId}
    </update>

userLose

    <update id="userLose">
        update user set totalCount = totalCount + 1, score = score - 30
        where userId = #{userId}
    </update>

UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.java_gobang.model.UserMapper">
    <insert id="insert">
        insert into user values(null, #{username}, #{password}, 1000, 0, 0);
    </insert>

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

UserAPI

package com.example.java_gobang.api;

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

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

@RestController
public class UserAPI {
    @Resource
    private UserMapper userMapper;

    @PostMapping("/login")
    public Object login(String username, String password, HttpServletRequest req) {
        User user = userMapper.selectByName(username);
        System.out.println("[login] username = " + username);
        if (user == null || !user.getPassword().equals(password)) {
            System.out.println("登录失败");
            return new User();
        }
//        HttpSession httpSession = req.getSession(true);
//        httpSession.setAttribute("user",user);
        HttpSession httpSession = req.getSession(true);
        httpSession.setAttribute("user",user);
        return user;
    }

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

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

}

登录login

1. 根据用户名查询User对象

2. 对比User是否存在,密码是否正确

3. 如果正确存入到request的session中

    @PostMapping("/login")
    public Object login(String username, String password, HttpServletRequest req) {
        User user = userMapper.selectByName(username);
        System.out.println("[login] username = " + username);
        if (user == null || !user.getPassword().equals(password)) {
            System.out.println("登录失败");
            return new User();
        }
//        HttpSession httpSession = req.getSession(true);
//        httpSession.setAttribute("user",user);
        HttpSession httpSession = req.getSession(true);
        httpSession.setAttribute("user",user);
        return user;
    }

注册register

1. 创建User对象,set赋值

2. UserMapper插入

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

获取用户信息

1. 从req中获取session

2. 为了严谨,从数据库中再查一遍

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

WebSocket使用

WebSocketConfig

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

    @Autowired
    private MatchAPI matchAPI;

    @Autowired
    private GameAPI gameAPI;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
        webSocketHandlerRegistry.addHandler(testAPI,"/test");
        webSocketHandlerRegistry.addHandler(matchAPI,"/findMatch")
                .addInterceptors(new HttpSessionHandshakeInterceptor());
        webSocketHandlerRegistry.addHandler(gameAPI,"/game")
                .addInterceptors(new HttpSessionHandshakeInterceptor());
    }
}

用户状态管理类

当前用户在游戏大厅在线状态 gameHall

当前用户在游戏房间的在线状态 gameRoom

package com.example.java_gobang.game;

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

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

@Component
public class OnlineUserManager {
    // 这个哈希表就用来表示当前用户在游戏大厅在线状态.
    private ConcurrentHashMap<Integer, WebSocketSession> gameHall = new ConcurrentHashMap<>();
    // 这个哈希表就用来表示当前用户在游戏房间的在线状态.
    private ConcurrentHashMap<Integer, WebSocketSession> gameRoom = new ConcurrentHashMap<>();

    public void enterGameHall(int userId, WebSocketSession webSocketSession) {
        gameHall.put(userId, webSocketSession);
    }

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

    public WebSocketSession getFromGameHall(int userId) {
        return gameHall.get(userId);
    }

    public void enterGameRoom(int userId, WebSocketSession webSocketSession) {
        gameRoom.put(userId, webSocketSession);
    }

    public void exitGameRoom(int userId) {
        gameRoom.remove(userId);
    }

    public WebSocketSession getFromGameRoom(int userId) {
        return gameRoom.get(userId);
    }
}

匹配

匹配请求类 MatchRequest

public class MatchRequest {
    private String message = "";

    public String getMessage() {
        return message;
    }

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

匹配响应类 MatchResponse

public class MatchResponse {
    private boolean ok;
    private String reason;
    private String message;

    public boolean isOk() {
        return ok;
    }

    public void setOk(boolean ok) {
        this.ok = ok;
    }

    public String getReason() {
        return reason;
    }

    public void setReason(String reason) {
        this.reason = reason;
    }

    public String getMessage() {
        return message;
    }

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

匹配过程类

package com.example.java_gobang.game;

import com.example.java_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 {
    // 创建三个匹配队列
    private Queue<User> normalQueue = new LinkedList<>();
    private Queue<User> highQueue = new LinkedList<>();
    private Queue<User> veryHighQueue = new LinkedList<>();

    @Autowired
    private OnlineUserManager onlineUserManager;

    @Autowired
    private RoomManager roomManager;

    private ObjectMapper objectMapper = new ObjectMapper();

    // 操作匹配队列的方法.
    // 把玩家放到匹配队列中
    public void add(User user) {
        if (user.getScore() < 2000) {
            synchronized (normalQueue) {
                normalQueue.offer(user);
                normalQueue.notify();
            }
            System.out.println("把玩家 " + user.getUsername() + " 加入到了 normalQueue 中!");
        } else if (user.getScore() >= 2000 && user.getScore() < 3000) {
            synchronized (highQueue) {
                highQueue.offer(user);
                highQueue.notify();
            }
            System.out.println("把玩家 " + user.getUsername() + " 加入到了 highQueue 中!");
        } else {
            synchronized (veryHighQueue) {
                veryHighQueue.offer(user);
                veryHighQueue.notify();
            }
            System.out.println("把玩家 " + user.getUsername() + " 加入到了 veryHighQueue 中!");
        }
    }

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

    public Matcher() {
        // 创建三个线程, 分别针对这三个匹配队列, 进行操作.
        Thread t1 = new Thread() {
            @Override
            public void run() {
                // 扫描 normalQueue
                while (true) {
                    handlerMatch(normalQueue);
                }
            }
        };
        t1.start();

        Thread t2 = new Thread(){
            @Override
            public void run() {
                while (true) {
                    handlerMatch(highQueue);
                }
            }
        };
        t2.start();

        Thread t3 = new Thread() {
            @Override
            public void run() {
                while (true) {
                    handlerMatch(veryHighQueue);
                }
            }
        };
        t3.start();
    }

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

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

                // 5. 给玩家反馈信息: 你匹配到对手了~
                //    通过 websocket 返回一个 message 为 'matchSuccess' 这样的响应
                //    此处是要给两个玩家都返回 "匹配成功" 这样的信息.
                //    因此就需要返回两次
                MatchResponse response1 = new MatchResponse();
                response1.setOk(true);
                response1.setMessage("matchSuccess");
                String json1 = objectMapper.writeValueAsString(response1);
                session1.sendMessage(new TextMessage(json1));

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

1. 3个匹配队列

    private Queue<User> normalQueue = new LinkedList<>();
    private Queue<User> highQueue = new LinkedList<>();
    private Queue<User> veryHighQueue = new LinkedList<>();

2. 把用户加入队列方法 add

    public void add(User user) {
        if (user.getScore() < 2000) {
            synchronized (normalQueue) {
                normalQueue.offer(user);
                normalQueue.notify();
            }
            System.out.println("把玩家 " + user.getUsername() + " 加入到了 normalQueue 中!");
        } else if (user.getScore() >= 2000 && user.getScore() < 3000) {
            synchronized (highQueue) {
                highQueue.offer(user);
                highQueue.notify();
            }
            System.out.println("把玩家 " + user.getUsername() + " 加入到了 highQueue 中!");
        } else {
            synchronized (veryHighQueue) {
                veryHighQueue.offer(user);
                veryHighQueue.notify();
            }
            System.out.println("把玩家 " + user.getUsername() + " 加入到了 veryHighQueue 中!");
        }
    }

3. 把用户从匹配队列中删除

    public void remove(User user) {
        if (user.getScore() < 2000) {
            synchronized (normalQueue) {
                normalQueue.remove(user);
            }
            System.out.println("把玩家 " + user.getUsername() + " 移除了 normalQueue!");
        } else if (user.getScore() >= 2000 && user.getScore() < 3000) {
            synchronized (highQueue) {
                highQueue.remove(user);
            }
            System.out.println("把玩家 " + user.getUsername() + " 移除了 highQueue!");
        } else {
            synchronized (veryHighQueue) {
                veryHighQueue.remove(user);
            }
            System.out.println("把玩家 " + user.getUsername() + " 移除了 veryHighQueue!");
        }
    }

4. 匹配处理方法

1. 给对应队列上锁
2. 判断队列中元素是否大于等于2
3. 取出两个玩家
4.获取到玩家的websocketsession用户消息传递
5. 判断session是否下线且是否是同一个
6. 创建一个房间,把用户加入
7. 返回给两个用户的响应
    private void handlerMatch(Queue<User> matchQueue) {
        synchronized (matchQueue) {
            try {
                // 1. 检测队列中元素个数是否达到 2
                //    队列的初始情况可能是 空
                //    如果往队列中添加一个元素, 这个时候, 仍然是不能进行后续匹配操作的.
                //    因此在这里使用 while 循环检查是更合理的~~
                while (matchQueue.size() < 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) {
                    // 如果玩家1 现在不在线了, 就把玩家2 重新放回到匹配队列中
                    matchQueue.offer(player2);
                    return;
                }
                if (session2 == null) {
                    // 如果玩家2 现在下线了, 就把玩家1 重新放回匹配队列中
                    matchQueue.offer(player1);
                    return;
                }
                // 当前能否排到两个玩家是同一个用户的情况嘛? 一个玩家入队列了两次??
                // 理论上也不会存在~~
                // 1) 如果玩家下线, 就会对玩家移出匹配队列
                // 2) 又禁止了玩家多开.
                // 但是仍然在这里多进行一次判定, 以免前面的逻辑出现 bug 时带来严重的后果.
                if (session1 == session2) {
                    // 把其中的一个玩家放回匹配队列.
                    matchQueue.offer(player1);
                    return;
                }

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

                // 5. 给玩家反馈信息: 你匹配到对手了~
                //    通过 websocket 返回一个 message 为 'matchSuccess' 这样的响应
                //    此处是要给两个玩家都返回 "匹配成功" 这样的信息.
                //    因此就需要返回两次
                MatchResponse response1 = new MatchResponse();
                response1.setOk(true);
                response1.setMessage("matchSuccess");
                String json1 = objectMapper.writeValueAsString(response1);
                session1.sendMessage(new TextMessage(json1));

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

游戏过程中请求和响应

1. 游戏开始通知

package com.example.java_gobang.game;

// 客户端连接到游戏房间后, 服务器返回的响应.
public class GameReadyResponse {
    private String message;
    private boolean ok;
    private String reason;
    private String roomId;
    private int thisUserId;
    private int thatUserId;
    private int whiteUser;

    public String getMessage() {
        return message;
    }

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

    public boolean isOk() {
        return ok;
    }

    public void setOk(boolean ok) {
        this.ok = ok;
    }

    public String getReason() {
        return reason;
    }

    public void setReason(String reason) {
        this.reason = reason;
    }

    public String getRoomId() {
        return roomId;
    }

    public void setRoomId(String roomId) {
        this.roomId = roomId;
    }

    public int getThisUserId() {
        return thisUserId;
    }

    public void setThisUserId(int thisUserId) {
        this.thisUserId = thisUserId;
    }

    public int getThatUserId() {
        return thatUserId;
    }

    public void setThatUserId(int thatUserId) {
        this.thatUserId = thatUserId;
    }

    public int getWhiteUser() {
        return whiteUser;
    }

    public void setWhiteUser(int whiteUser) {
        this.whiteUser = whiteUser;
    }
}

2. 下棋请求

package com.example.java_gobang.game;

// 这个类表示 落子请求
public class GameRequest {
    private String message;
    private int userId;
    private int row;
    private int col;

    public String getMessage() {
        return message;
    }

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

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public int getRow() {
        return row;
    }

    public void setRow(int row) {
        this.row = row;
    }

    public int getCol() {
        return col;
    }

    public void setCol(int col) {
        this.col = col;
    }
}

3. 落子响应

package com.example.java_gobang.game;

// 这个类表示一个 落子响应
public class GameResponse {
    private String message;
    private int userId;
    private int row;
    private int col;
    private int winner;

    public String getMessage() {
        return message;
    }

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

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public int getRow() {
        return row;
    }

    public void setRow(int row) {
        this.row = row;
    }

    public int getCol() {
        return col;
    }

    public void setCol(int col) {
        this.col = col;
    }

    public int getWinner() {
        return winner;
    }

    public void setWinner(int winner) {
        this.winner = winner;
    }
}

房间

房间类

package com.example.java_gobang.game;

import com.example.java_gobang.JavaGobangApplication;
import com.example.java_gobang.model.User;
import com.example.java_gobang.model.UserMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
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.UUID;

// 这个类就表示一个游戏房间
public class Room {
    // 使用字符串类型来表示, 方便生成唯一值.
    private String roomId;

    private User user1;
    private User user2;

    // 先手方的玩家 id
    private int whiteUser;

    private static final int MAX_ROW = 15;
    private static final int MAX_COL = 15;
    // 这个二维数组用来表示棋盘
    // 约定:
    // 1) 使用 0 表示当前位置未落子. 初始化好的 int 二维数组, 就相当于是 全 0
    // 2) 使用 1 表示 user1 的落子位置
    // 3) 使用 2 表示 user2 的落子位置
    private int[][] board = new int[MAX_ROW][MAX_COL];

    // 创建 ObjectMapper 用来转换 JSON
    private ObjectMapper objectMapper = new ObjectMapper();

    // 引入 OnlineUserManager
    // @Autowired
    private OnlineUserManager onlineUserManager;

    // 引入 RoomManager, 用于房间销毁
    // @Autowired
    private RoomManager roomManager;

    private UserMapper userMapper;

    // 通过这个方法来处理一次落子操作.
    // 要做的事情:
    public void putChess(String reqJson) throws IOException {
        // 1. 记录当前落子的位置.
        GameRequest request = objectMapper.readValue(reqJson, 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 进行传输.
        String respJson = objectMapper.writeValueAsString(response);
        if (session1 != null) {
            session1.sendMessage(new TextMessage(respJson));
        }
        if (session2 != null) {
            session2.sendMessage(new TextMessage(respJson));
        }

        // 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 r = 0; r < MAX_ROW; r++) {
            for (int c = 0; c < MAX_COL; c++) {
                // 针对一行之内的若干列, 不要打印换行
                System.out.print(board[r][c] + " ");
            }
            // 每次遍历完一行之后, 再打印换行.
            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++) {
            // 针对其中的一种情况, 来判定这五个子是不是连在一起了~
            // 不光是这五个子得连着, 而且还得和玩家落的子是一样~~ (才算是获胜)
            try {
                if (board[row][c] == chess
                        && board[row][c + 1] == chess
                        && board[row][c + 2] == chess
                        && board[row][c + 3] == chess
                        && board[row][c + 4] == chess) {
                    // 构成了五子连珠! 胜负已分!
                    return chess == 1 ? user1.getUserId() : user2.getUserId();
                }
            } catch (ArrayIndexOutOfBoundsException e) {
                // 如果出现数组下标越界的情况, 就在这里直接忽略这个异常.
                continue;
            }
        }

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

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

        // 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;
    }


    public int getWhiteUser() {
        return whiteUser;
    }

    public void setWhiteUser(int whiteUser) {
        this.whiteUser = whiteUser;
    }

    public String getRoomId() {
        return roomId;
    }

    public void setRoomId(String roomId) {
        this.roomId = roomId;
    }

    public User getUser1() {
        return user1;
    }

    public void setUser1(User user1) {
        this.user1 = user1;
    }

    public User getUser2() {
        return user2;
    }

    public void setUser2(User user2) {
        this.user2 = user2;
    }

    public Room() {
        // 构造 Room 的时候生成一个唯一的字符串表示房间 id.
        // 使用 UUID 来作为房间 id
        roomId = UUID.randomUUID().toString();
        // 通过入口类中记录的 context 来手动获取到前面的 RoomManager 和 OnlineUserManager
        onlineUserManager = JavaGobangApplication.context.getBean(OnlineUserManager.class);
        roomManager = JavaGobangApplication.context.getBean(RoomManager.class);
        userMapper = JavaGobangApplication.context.getBean(UserMapper.class);
    }

    public static void main(String[] args) {
        Room room = new Room();
        System.out.println(room.roomId);
    }
}

1. 房间id、用户1和2、先手id、棋盘

2.下棋方法

    public void putChess(String reqJson) throws IOException {
        // 1. 记录当前落子的位置.
        GameRequest request = objectMapper.readValue(reqJson, 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 进行传输.
        String respJson = objectMapper.writeValueAsString(response);
        if (session1 != null) {
            session1.sendMessage(new TextMessage(respJson));
        }
        if (session2 != null) {
            session2.sendMessage(new TextMessage(respJson));
        }

        // 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());
        }
    }
1. 获取下棋请求中的棋子位置和用户
2. 打印棋子
3.胜负判断
4.响应构造
5.获取两个用户的session 判断是否还在线
6. 如果胜负出,给用户加减积分,移除room

房间管理类

所有房间,id-room

用户所对应的房间 userIdToRoomId

package com.example.java_gobang.game;

import org.springframework.stereotype.Component;

import java.util.concurrent.ConcurrentHashMap;

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

    public void add(Room room, int userId1, int userId2) {
        rooms.put(room.getRoomId(), room);
        userIdToRoomId.put(userId1, room.getRoomId());
        userIdToRoomId.put(userId2, room.getRoomId());
    }

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

    public Room getRoomByRoomId(String roomId) {
        return rooms.get(roomId);
    }

    public Room getRoomByUserId(int userId) {
        String roomId = userIdToRoomId.get(userId);
        if (roomId == null) {
            // userId -> roomId 映射关系不存在, 直接返回 null
            return null;
        }
        return rooms.get(roomId);
    }
}

匹配API

package com.example.java_gobang.api;

import com.example.java_gobang.game.MatchRequest;
import com.example.java_gobang.game.MatchResponse;
import com.example.java_gobang.game.Matcher;
import com.example.java_gobang.game.OnlineUserManager;
import com.example.java_gobang.model.User;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.ibatis.jdbc.Null;
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. 先获取到当前用户的身份信息(谁在游戏大厅中, 建立的连接)
        //    此处的代码, 之所以能够 getAttributes, 全靠了在注册 Websocket 的时候,
        //    加上的 .addInterceptors(new HttpSessionHandshakeInterceptor());
        //    这个逻辑就把 HttpSession 中的 Attribute 都给拿到 WebSocketSession 中了
        //    在 Http 登录逻辑中, 往 HttpSession 中存了 User 数据: httpSession.setAttribute("user", user);
        //    此时就可以在 WebSocketSession 中把之前 HttpSession 里存的 User 对象给拿到了.
        //    注意, 此处拿到的 user, 是有可能为空的!!
        //    如果之前用户压根就没有通过 HTTP 来进行登录, 直接就通过 /game_hall.html 这个 url 来访问游戏大厅页面
        //    此时就会出现 user 为 null 的情况
        try {
            User user = (User) session.getAttributes().get("user");

            // 2. 先判定当前用户是否已经登录过(已经是在线状态), 如果是已经在线, 就不该继续进行后续逻辑.
            if (onlineUserManager.getFromGameHall(user.getUserId()) != null
                    || onlineUserManager.getFromGameRoom(user.getUserId()) != null) {
                // 当前用户已经登录了!!
                // 针对这个情况要告知客户端, 你这里重复登录了.
                MatchResponse response = new MatchResponse();
                response.setOk(true);
                response.setReason("当前禁止多开!");
                response.setMessage("repeatConnection");
                session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
                // 此处直接关闭有些太激进了, 还是返回一个特殊的 message , 供客户端来进行判定, 由客户端负责进行处理
                // session.close();
                return;
            }

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

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 实现处理开始匹配请求和处理停止匹配请求.
        User user = (User) session.getAttributes().get("user");
        // 获取到客户端给服务器发送的数据
        String payload = message.getPayload();
        // 当前这个数据载荷是一个 JSON 格式的字符串, 就需要把它转成 Java 对象. MatchRequest
        MatchRequest request = objectMapper.readValue(payload, MatchRequest.class);
        MatchResponse response = new MatchResponse();
        if (request.getMessage().equals("startMatch")) {
            // 进入匹配队列
            matcher.add(user);
            // 把玩家信息放入匹配队列之后, 就可以返回一个响应给客户端了.
            response.setOk(true);
            response.setMessage("startMatch");
        } else if (request.getMessage().equals("stopMatch")) {
            // 退出匹配队列
            matcher.remove(user);
            // 移除之后, 就可以返回一个响应给客户端了.
            response.setOk(true);
            response.setMessage("stopMatch");
        } else {
            response.setOk(false);
            response.setReason("非法的匹配请求");
        }
        String jsonString = objectMapper.writeValueAsString(response);
        session.sendMessage(new TextMessage(jsonString));
    }

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

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

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

            // 这个代码之前写的草率了!!
            // 不应该在连接关闭之后, 还尝试发送消息给客户端
//            MatchResponse response = new MatchResponse();
//            response.setOk(false);
//            response.setReason("您尚未登录! 不能进行后续匹配功能!");
//            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        }
    }
}

1. 注入用户管理类 和 匹配类

2. 匹配连接之前

获取用户

判断是否多开

加入大厅状态中

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 玩家上线, 加入到 OnlineUserManager 中

        // 1. 先获取到当前用户的身份信息(谁在游戏大厅中, 建立的连接)
        //    此处的代码, 之所以能够 getAttributes, 全靠了在注册 Websocket 的时候,
        //    加上的 .addInterceptors(new HttpSessionHandshakeInterceptor());
        //    这个逻辑就把 HttpSession 中的 Attribute 都给拿到 WebSocketSession 中了
        //    在 Http 登录逻辑中, 往 HttpSession 中存了 User 数据: httpSession.setAttribute("user", user);
        //    此时就可以在 WebSocketSession 中把之前 HttpSession 里存的 User 对象给拿到了.
        //    注意, 此处拿到的 user, 是有可能为空的!!
        //    如果之前用户压根就没有通过 HTTP 来进行登录, 直接就通过 /game_hall.html 这个 url 来访问游戏大厅页面
        //    此时就会出现 user 为 null 的情况
        try {
            User user = (User) session.getAttributes().get("user");

            // 2. 先判定当前用户是否已经登录过(已经是在线状态), 如果是已经在线, 就不该继续进行后续逻辑.
            if (onlineUserManager.getFromGameHall(user.getUserId()) != null
                    || onlineUserManager.getFromGameRoom(user.getUserId()) != null) {
                // 当前用户已经登录了!!
                // 针对这个情况要告知客户端, 你这里重复登录了.
                MatchResponse response = new MatchResponse();
                response.setOk(true);
                response.setReason("当前禁止多开!");
                response.setMessage("repeatConnection");
                session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
                // 此处直接关闭有些太激进了, 还是返回一个特殊的 message , 供客户端来进行判定, 由客户端负责进行处理
                // session.close();
                return;
            }

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

3. 开始匹配

判断传递的字符是开始还是停止

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 实现处理开始匹配请求和处理停止匹配请求.
        User user = (User) session.getAttributes().get("user");
        // 获取到客户端给服务器发送的数据
        String payload = message.getPayload();
        // 当前这个数据载荷是一个 JSON 格式的字符串, 就需要把它转成 Java 对象. MatchRequest
        MatchRequest request = objectMapper.readValue(payload, MatchRequest.class);
        MatchResponse response = new MatchResponse();
        if (request.getMessage().equals("startMatch")) {
            // 进入匹配队列
            matcher.add(user);
            // 把玩家信息放入匹配队列之后, 就可以返回一个响应给客户端了.
            response.setOk(true);
            response.setMessage("startMatch");
        } else if (request.getMessage().equals("stopMatch")) {
            // 退出匹配队列
            matcher.remove(user);
            // 移除之后, 就可以返回一个响应给客户端了.
            response.setOk(true);
            response.setMessage("stopMatch");
        } else {
            response.setOk(false);
            response.setReason("非法的匹配请求");
        }
        String jsonString = objectMapper.writeValueAsString(response);
        session.sendMessage(new TextMessage(jsonString));
    }

4. 处理异常

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

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

5. 连接关闭【用户匹配成功会跳转页面、自动连接关闭】

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

            // 这个代码之前写的草率了!!
            // 不应该在连接关闭之后, 还尝试发送消息给客户端
//            MatchResponse response = new MatchResponse();
//            response.setOk(false);
//            response.setReason("您尚未登录! 不能进行后续匹配功能!");
//            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
        }
    }

游戏API

package com.example.java_gobang.api;

import com.example.java_gobang.game.*;
import com.example.java_gobang.model.User;
import com.example.java_gobang.model.UserMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
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;

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

    @Autowired
    private RoomManager roomManager;

    @Autowired
    private OnlineUserManager onlineUserManager;

    @Resource
    private UserMapper userMapper;

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

        // 1. 先获取到用户的身份信息. (从 HttpSession 里拿到当前用户的对象)
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            resp.setOk(false);
            resp.setReason("用户尚未登录!");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
            return;
        }

        // 2. 判定当前用户是否已经进入房间. (拿着房间管理器进行查询)
        Room room = roomManager.getRoomByUserId(user.getUserId());
        if (room == null) {
            // 如果为 null, 当前没有找到对应的房间. 该玩家还没有匹配到.
            resp.setOk(false);
            resp.setReason("用户尚未匹配到!");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
            return;
        }

        // 3. 判定当前是不是多开 (该用户是不是已经在其他地方进入游戏了)
        //    前面准备了一个 OnlineUserManager
        if (onlineUserManager.getFromGameHall(user.getUserId()) != null
                || onlineUserManager.getFromGameRoom(user.getUserId()) != null) {
            // 如果一个账号, 一边是在游戏大厅, 一边是在游戏房间, 也视为多开~~
            resp.setOk(true);
            resp.setReason("禁止多开游戏页面");
            resp.setMessage("repeatConnection");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
            return;
        }

        // 4. 设置当前玩家上线!
        onlineUserManager.enterGameRoom(user.getUserId(), session);

        // 5. 把两个玩家加入到游戏房间中.
        //    前面的创建房间/匹配过程, 是在 game_hall.html 页面中完成的.
        //    因此前面匹配到对手之后, 需要经过页面跳转, 来到 game_room.html 才算正式进入游戏房间(才算玩家准备就绪)
        //    当前这个逻辑是在 game_room.html 页面加载的时候进行的.
        //    执行到当前逻辑, 说明玩家已经页面跳转成功了!!
        //    页面跳转, 其实是个大活~~ (很有可能出现 "失败" 的情况的)
        synchronized (room) {
            if (room.getUser1() == null) {
                // 第一个玩家还尚未加入房间.
                // 就把当前连上 websocket 的玩家作为 user1, 加入到房间中.
                room.setUser1(user);
                // 把先连入房间的玩家作为先手方.
                room.setWhiteUser(user.getUserId());
                System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家1");
                return;
            }
            if (room.getUser2() == null) {
                // 如果进入到这个逻辑, 说明玩家1 已经加入房间, 现在要给当前玩家作为玩家2 了
                room.setUser2(user);
                System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家2");

                // 当两个玩家都加入成功之后, 就要让服务器, 给这两个玩家都返回 websocket 的响应数据.
                // 通知这两个玩家说, 游戏双方都已经准备好了.
                // 通知玩家1
                noticeGameReady(room, room.getUser1(), room.getUser2());
                // 通知玩家2
                noticeGameReady(room, room.getUser2(), room.getUser1());
                return;
            }
        }

        // 6. 此处如果又有玩家尝试连接同一个房间, 就提示报错.
        //    这种情况理论上是不存在的, 为了让程序更加的健壮, 还是做一个判定和提示.
        resp.setOk(false);
        resp.setReason("当前房间已满, 您不能加入房间");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
    }

    private void noticeGameReady(Room room, User thisUser, User thatUser) throws IOException {
        GameReadyResponse resp = new GameReadyResponse();
        resp.setMessage("gameReady");
        resp.setOk(true);
        resp.setReason("");
        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("user");
        if (user == null) {
            System.out.println("[handleTextMessage] 当前玩家尚未登录! ");
            return;
        }

        // 2. 根据玩家 id 获取到房间对象
        Room room = roomManager.getRoomByUserId(user.getUserId());
        // 3. 通过 room 对象来处理这次具体的请求
        room.putChess(message.getPayload());
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            // 此处就简单处理, 在断开连接的时候就不给客户端返回响应了.
            return;
        }
        WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());
        if (session == exitSession) {
            // 加上这个判定, 目的是为了避免在多开的情况下, 第二个用户退出连接动作, 导致第一个用户的会话被删除.
            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("user");
        if (user == null) {
            // 此处就简单处理, 在断开连接的时候就不给客户端返回响应了.
            return;
        }
        WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());
        if (session == exitSession) {
            // 加上这个判定, 目的是为了避免在多开的情况下, 第二个用户退出连接动作, 导致第一个用户的会话被删除.
            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());
    }
}

1. 注入房间管理和用户管理类

2. 连接之前

1. 先获取到用户的身份信息

2. 判定当前用户是否已经进入房间.

3. 判定当前是不是多开

4. 设置当前玩家上线!

5. 把两个玩家加入到游戏房间中.

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

        // 1. 先获取到用户的身份信息. (从 HttpSession 里拿到当前用户的对象)
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            resp.setOk(false);
            resp.setReason("用户尚未登录!");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
            return;
        }

        // 2. 判定当前用户是否已经进入房间. (拿着房间管理器进行查询)
        Room room = roomManager.getRoomByUserId(user.getUserId());
        if (room == null) {
            // 如果为 null, 当前没有找到对应的房间. 该玩家还没有匹配到.
            resp.setOk(false);
            resp.setReason("用户尚未匹配到!");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
            return;
        }

        // 3. 判定当前是不是多开 (该用户是不是已经在其他地方进入游戏了)
        //    前面准备了一个 OnlineUserManager
        if (onlineUserManager.getFromGameHall(user.getUserId()) != null
                || onlineUserManager.getFromGameRoom(user.getUserId()) != null) {
            // 如果一个账号, 一边是在游戏大厅, 一边是在游戏房间, 也视为多开~~
            resp.setOk(true);
            resp.setReason("禁止多开游戏页面");
            resp.setMessage("repeatConnection");
            session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
            return;
        }

        // 4. 设置当前玩家上线!
        onlineUserManager.enterGameRoom(user.getUserId(), session);

        // 5. 把两个玩家加入到游戏房间中.
        //    前面的创建房间/匹配过程, 是在 game_hall.html 页面中完成的.
        //    因此前面匹配到对手之后, 需要经过页面跳转, 来到 game_room.html 才算正式进入游戏房间(才算玩家准备就绪)
        //    当前这个逻辑是在 game_room.html 页面加载的时候进行的.
        //    执行到当前逻辑, 说明玩家已经页面跳转成功了!!
        //    页面跳转, 其实是个大活~~ (很有可能出现 "失败" 的情况的)
        synchronized (room) {
            if (room.getUser1() == null) {
                // 第一个玩家还尚未加入房间.
                // 就把当前连上 websocket 的玩家作为 user1, 加入到房间中.
                room.setUser1(user);
                // 把先连入房间的玩家作为先手方.
                room.setWhiteUser(user.getUserId());
                System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家1");
                return;
            }
            if (room.getUser2() == null) {
                // 如果进入到这个逻辑, 说明玩家1 已经加入房间, 现在要给当前玩家作为玩家2 了
                room.setUser2(user);
                System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家2");

                // 当两个玩家都加入成功之后, 就要让服务器, 给这两个玩家都返回 websocket 的响应数据.
                // 通知这两个玩家说, 游戏双方都已经准备好了.
                // 通知玩家1
                noticeGameReady(room, room.getUser1(), room.getUser2());
                // 通知玩家2
                noticeGameReady(room, room.getUser2(), room.getUser1());
                return;
            }
        }

        // 6. 此处如果又有玩家尝试连接同一个房间, 就提示报错.
        //    这种情况理论上是不存在的, 为了让程序更加的健壮, 还是做一个判定和提示.
        resp.setOk(false);
        resp.setReason("当前房间已满, 您不能加入房间");
        session.sendMessage(new TextMessage(objectMapper.writeValueAsString(resp)));
    }

    private void noticeGameReady(Room room, User thisUser, User thatUser) throws IOException {
        GameReadyResponse resp = new GameReadyResponse();
        resp.setMessage("gameReady");
        resp.setOk(true);
        resp.setReason("");
        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)));
    }

3. 处理下棋

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 1. 先从 session 里拿到当前用户的身份信息
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            System.out.println("[handleTextMessage] 当前玩家尚未登录! ");
            return;
        }

        // 2. 根据玩家 id 获取到房间对象
        Room room = roomManager.getRoomByUserId(user.getUserId());
        // 3. 通过 room 对象来处理这次具体的请求
        room.putChess(message.getPayload());
    }

4. 异常处理

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            // 此处就简单处理, 在断开连接的时候就不给客户端返回响应了.
            return;
        }
        WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());
        if (session == exitSession) {
            // 加上这个判定, 目的是为了避免在多开的情况下, 第二个用户退出连接动作, 导致第一个用户的会话被删除.
            onlineUserManager.exitGameRoom(user.getUserId());
        }
        System.out.println("当前用户 " + user.getUsername() + " 游戏房间连接异常!");

        // 通知对手获胜了
        noticeThatUserWin(user);
    }

5. 连接关闭

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        User user = (User) session.getAttributes().get("user");
        if (user == null) {
            // 此处就简单处理, 在断开连接的时候就不给客户端返回响应了.
            return;
        }
        WebSocketSession exitSession = onlineUserManager.getFromGameRoom(user.getUserId());
        if (session == exitSession) {
            // 加上这个判定, 目的是为了避免在多开的情况下, 第二个用户退出连接动作, 导致第一个用户的会话被删除.
            onlineUserManager.exitGameRoom(user.getUserId());
        }
        System.out.println("当前用户 " + user.getUsername() + " 离开游戏房间!");

        // 通知对手获胜了
        noticeThatUserWin(user);
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值