五子棋项目总结复习复盘逻辑
逻辑
项目 按着
用户登录——开始匹配——进入房间——开始对战——对战结束
在此过程,需要知道用户的状态,所以需要一个用户管理类
还需要知道房间,需要一个房间管理类
用户类+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);
}