项目实战--网页五子棋(对战功能)(7)
1. webSocket交互约定
上期我们完成了游戏房间的实体类以及前端界面,这期我们实现玩家对战功能,这里我们同样通过webSocket实现玩家和玩家之间的交互,连接建立成功时,约定服务器发送一些游戏的初始信息给客户端:
{
message: 'gameReady', //游戏消息类别,游戏就绪
ok: true,
errMsg: '',//错误信息
roomId: '', //游戏房间id
userId1: 1, //当前客户端玩家
userId2: 2, //对手玩家
isBlack: true //当前玩家是否拿黑子(先手)
}
约定玩家落子的请求和响应:
落子请求:
{
message: 'putChess',
userId: ,
row: , //落子坐标行
col: //落子坐标列
}
落子响应:
{
message: 'putChess',
userid: ,
row: , //落子坐标行
col: //落子坐标列
winnerId: //是否分出胜负,-1表示未分出胜负,否则表示获胜玩家id
}
2. 建立webSocket连接
2.1 客户端代码
我们在script.js文件中增加webSocket连接相关的代码
let webSocket = new WebSocket('ws://127.0.0.1:8080/game');
webSocket.onopen = function() {
console.log("连接game成功");
}
webSocket.onclose = function() {
console.log("连接关闭请重新登陆");
location.href = "/login.html";
}
webSocket.onerror = function() {
console.log("error");
}
//页面关闭时释放webSocket
window.onbeforeunload = function() {
webSocket.close();
}
//处理服务器发送的消息
webSocket.onmessage = function(e) {
//连接成功处理返回响应
let resp = JSON.parse(e.data);
if(resp.message != "gameReady" || !resp.ok) {
alert("进入游戏房间失败: " + resp.errMsg);
location.href = "/hall.html";
return;
}
resp.roomId = e.roomId;
resp.userId1 = e.userId1;
resp.userId2 = e.userId2;
resp.isBlack = e.isBlack;
//初始化棋盘
initGame();
//提示先手玩家落子
setScreenText(resp.isBlack);
}
2.2 服务器代码
创建webSocket
package org.ting.j20250110_gobang.websocket;
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;
@Component
public class GameWebSocket extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
}
}
注册webSocket:
package org.ting.j20250110_gobang.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import org.ting.j20250110_gobang.websocket.GameWebSocket;
import org.ting.j20250110_gobang.websocket.MatchWebSocket;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
MatchWebSocket matchWebSocket;
@Autowired
GameWebSocket gameWebSocket;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(matchWebSocket, "/findMatch") //注意路径和前端对应
//添加拦截器获取到session,方便获取session中的用户信息
.addInterceptors(new HttpSessionHandshakeInterceptor());
registry.addHandler(gameWebSocket, "/game")
//添加拦截器获取到session,方便获取session中的用户信息
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
}
3. 创建数据交互实体类
package org.ting.j20250110_gobang.game;
public class GameReadyResponse {
private String message; //游戏消息类别,游戏就绪
private boolean ok;
private String errMsg;//错误信息
private String roomId; //游戏房间id
private int userId1; //当前客户端玩家的id
private int userId2;//对手id
private boolean isBlack; //当前玩家是否拿黑子(先手)
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 getErrMsg() {
return errMsg;
}
public void setErrMsg(String errMsg) {
this.errMsg = errMsg;
}
public String getRoomId() {
return roomId;
}
public void setRoomId(String roomId) {
this.roomId = roomId;
}
public Integer getUserId1() {
return userId1;
}
public void setUserId1(Integer userId1) {
this.userId1 = userId1;
}
public Integer getUserId2() {
return userId2;
}
public void setUserId2(Integer userId2) {
this.userId2 = userId2;
}
public boolean isBlack() {
return isBlack;
}
public void setBlack(boolean black) {
isBlack = black;
}
}
package org.ting.j20250110_gobang.game;
public class GameRequest {
private int userId;
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;
}
private int row; //落子坐标行
private int col; //落子坐标列
}
package org.ting.j20250110_gobang.game;
public class GameResponse {
private String message;
private int userid;
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 getWinnerId() {
return winnerId;
}
public void setWinnerId(int winnerId) {
this.winnerId = winnerId;
}
private int row; //落子坐标行
private int col; //落子坐标列
private int winnerId; //是否分出胜负,-1表示未分出胜负,否则表示获胜玩家id
}
4. 维护用户在线状态
前面我们在OnlineUserManager中使用了一个哈希表来维护用户的在线状态,但是当用户匹配成功后会进入游戏房间页面,从游戏大厅页面进入游戏房间页面,这个操作会使得我们在游戏大厅页面的webSocket连接断开,连接断开时,根据我们之前的代码来看,是会把玩家从哈希表中移除的,也就是把玩家下线,那么这个时候如果有人使用这个账号是可以在另一个地方登录成功的,就会造成两个账号同时在线的情况,为了避免这种情况我们在OnlineUserManager中再添加另一个哈希表用来维护在游戏房间中的用户:
package org.ting.j20250110_gobang.game;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketSession;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class OnlineUserManager {
//使用ConcurrentHashMap保证线程安全
//维护大厅中在线用户
private Map<Integer, WebSocketSession> onlineUser = new ConcurrentHashMap<>();
//维护游戏房间中的用户
private Map<Integer, WebSocketSession> inRoomUser = new ConcurrentHashMap<>();
public void enterGameRoom(int userId, WebSocketSession session) {
//用户进入游戏房间
inRoomUser.put(userId, session);
}
public void exitGameRoom(int userId) {
//用户退出游戏房间
inRoomUser.remove(userId);
}
public WebSocketSession getFromRoom(int userId) {
//获取用户的websocket会话
return inRoomUser.get(userId);
}
public void enterGameHall(int userId, WebSocketSession session) {
//用户上线
onlineUser.put(userId, session);
}
public void exitGameHall(int userId) {
//用户下线
onlineUser.remove(userId);
}
public WebSocketSession getFromHall(int userId) {
//获取用户的websocket会话
return onlineUser.get(userId);
}
}
注意要更改之前玩家上线时代码的判断逻辑,还需要检测用户是否在游戏房间中: