当前位置: 首页> 财经> 金融 > 怎样在网上卖东西_如何开通企业邮箱_南京seo代理_seo最新快速排名

怎样在网上卖东西_如何开通企业邮箱_南京seo代理_seo最新快速排名

时间:2025/8/26 20:04:29来源:https://blog.csdn.net/2301_76161469/article/details/145324747 浏览次数:0次
怎样在网上卖东西_如何开通企业邮箱_南京seo代理_seo最新快速排名

目录

时序图

约定前后端交互接口

前端页面

game_hall.html

game_hall.css

获取用户信息

约定前后端交互接口

controller 层接口设计

service 层接口设计

前端请求

功能测试

前端实现

服务端实现

OnlineUserManager

创建请求/响应对象

处理连接成功

处理开始/结束匹配请求

Matcher

处理连接关闭

处理连接异常

创建游戏房间

修改前端逻辑

验证匹配功能


在实现用户匹配模块时需要使用到 WebSocket

当玩家发送请求时,服务器返回开始匹配响应:

 

 两个玩家匹配成功后,服务器推送 匹配成功 消息:

WebSocket 相关知识可参考:WebSocket_websocket csdn-CSDN博客

时序图

我们来理解一下匹配过程: 

1. 用户点击 开始匹配 按钮后,发送开始匹配请求给服务器

2. 服务器对用户信息进行校验,校验通过后,将用户放入匹配队列进行匹配,并返回开始匹配响应

3. 服务器为用户匹配到对手后,推送匹配成功消息给用户

4. 若用户在匹配过程中点击停止匹配,服务器则将用户从匹配队列移除,并返回停止匹配响应

约定前后端交互接口

前后端约定的交互接口,也是基于 WebSocket

[请求] ws://127.0.0.1:8080/findMatch

{"message": "START"/ "STOP" // 开始/结束匹配
}

在通过 WebSocket 传输请求数据时,数据中可以不必带有用户身份信息

当前用户的身份信息,在登录完成后,就自动保存到 HttpSession 中了,而在 WebSocket 中,可以拿到之前登录时保存的 HttpSession 信息 

[响应]

{"code": 200,"data": {"matchMessage": "START" / "STOP","rival": null},"errorMessage": ""
}

客户端向服务器发送匹配请求后,服务器立即返回匹配响应,表示已经开始 / 结束匹配 

由于此时并未匹配到对手,因此 rival 为空

 匹配到对手后,服务器主动推送信息:

{"code": 200,"data": {"matchMessage": "SUCCESS","rival": {"name": 'lisi', "score": 1000}},"errorMessage": ""
}

我们首先来实现前端页面 

前端页面

前端页面的内容比较简单: 一个 div 用于显示用户信息,一个 button 用于进行匹配

game_hall.html

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>游戏大厅</title><link rel="stylesheet" href="/css/common.css"><link rel="stylesheet" href="/css/game_hall.css">
</head>
<body><div class="nav">五子棋</div><div class="container"><div><!-- 显示用户信息 --><div id="screen"></div><!-- 匹配按钮 --><div id="match-button">开始匹配</div></div></div>
</body>
</html>

添加 css 样式

game_hall.css

#screen {width: 400px;height: 250px;background-color:antiquewhite;font-size: 20px;color: gray;border-radius: 10px;text-align: center;line-height: 100px;
}#match-button {width: 400px;height: 50px;background-color: antiquewhite;font-size: 20px;color: gray;border-radius: 10px;text-align: center;line-height: 50px;margin-top: 10px;
}

要在页面上显示用户相关信息,因此,需要从后端获取用户信息

接下来,我们就继续实现用户信息的获取

获取用户信息

约定前后端交互接口

[请求] GET /getUserInfo

[响应]

{"code": 200,"data": {"userId": 1,"userName": "zhangsan","score": 1000,"totalCount": 0,"winCount": 0},"errorMessage": ""
}

由于登录时将相关用户信息存储到 session 中了,因此,可以直接从 HttpSession 中获取用户信息,不必传递相关参数

controller 层接口设计

返回的响应类型:

@Data
public class UserInfoResult implements Serializable {/*** 用户 id*/private Long userId;/*** 用户名*/private String userName;/*** 天梯分数*/private Long score;/*** 总场数*/private Long totalCount;/*** 获胜场次*/private Long winCount;
}

controller 接口主要完成的功能是:

1. 打印日志

2. 从 request 中获取 session

3. 调用 service 层方法进行业务逻辑处理

3. 构造响应并返回

若从 session 中获取用户信息失败,表明当前用户需要重新登录,因此,我们可以在 CommonResult 中添加  noLogin 方法,用于处理用户未登录的情况

    public static <T> CommonResult<T> noLogin() {CommonResult result = new CommonResult();result.code = 401;result.errorMessage = "用户未登录";return result;}

 getUserInfo:

  /*** 从 session 中获取用户信息* @param request* @return*/@RequestMapping("/getUserInfo")public CommonResult<UserInfoResult> getUserInfo(HttpServletRequest request) {// 日志打印log.info("getUserInfo 从 HttpSession 中获取用户信息");// 检查当前请求是否已经有会话对象,如果没有现有的会话,则返回 nullHttpSession session = request.getSession(false); // 业务逻辑处理UserInfoDTO userInfoDTO = userService.getUserInfo(session);// 构造响应并返回if (null == userInfoDTO) {return CommonResult.noLogin();}return CommonResult.success(convertToUserInfoResult(userInfoDTO));}

UserInfoDTO:

@Data
public class UserInfoDTO implements Serializable {/*** 用户 id*/private Long userId;/*** 用户名*/private String userName;/*** 天梯分数*/private Long score;/*** 总场数*/private Long totalCount;/*** 获胜场次*/private Long winCount;
}

类型转化:

    /*** 将 UserInfoDTO 转化为 UserInfoResult* @param userInfoDTO* @return*/private UserInfoResult convertToUserInfoResult(UserInfoDTO userInfoDTO) {// 参数校验if (null == userInfoDTO) {throw new ControllerException(ControllerErrorCodeConstants.GET_USER_INFO_ERROR);}// 构造 UserInfoResultUserInfoResult userInfoResult = new UserInfoResult();userInfoResult.setUserId(userInfoDTO.getUserId());userInfoResult.setUserName(userInfoDTO.getUserName());userInfoResult.setScore(userInfoDTO.getScore());userInfoResult.setTotalCount(userInfoDTO.getTotalCount());userInfoResult.setWinCount(userInfoDTO.getWinCount());// 返回return userInfoResult;}

添加错误码:

public interface ControllerErrorCodeConstants {// ---------------------- 用户模块错误码 ----------------------ErrorCode REGISTER_ERROR = new ErrorCode(100, "注册失败");ErrorCode LOGIN_ERROR = new ErrorCode(101, "登录失败");ErrorCode GET_USER_INFO_ERROR = new ErrorCode(102, "获取用户信息失败");
}

service 层接口设计

定义业务接口:

    /*** 获取用户信息* @param session* @return*/UserInfoDTO getUserInfo(HttpSession session);

getUserInfo() 要实现的逻辑:

1. 校验 session 是否为 null

2. 从 session 中获取用户信息

3. 构造响应并返回

    @Overridepublic UserInfoDTO getUserInfo(HttpSession session) {// 参数校验if (null == session) {return null;}// 从 session 中获取用户信息UserInfo userInfo = (UserInfo) session.getAttribute(USER_INFO);if (null == userInfo) {return null;}// 构造响应并返回UserInfoDTO userInfoDTO = new UserInfoDTO();userInfoDTO.setUserId(userInfo.getUserId());userInfoDTO.setUserName(userInfo.getUserName());userInfoDTO.setScore(userInfo.getScore());userInfoDTO.setTotalCount(userInfo.getTotalCount());userInfoDTO.setWinCount(userInfo.getWinCount());return userInfoDTO;}

若从 session 中获取用户信息失败,直接返回 null

前端请求

    <script src="js/jquery.min.js"></script><script>$.ajax({url: "/getUserInfo",type: "GET",success: function(result) {console.log(result)if(result.code == 200) {let screenDiv = document.querySelector('#screen');var user = result.data;screenDiv.innerHTML = '玩家:' + user.userName + ' 分数:' + user.score + '<br>比赛场次:' + user.totalCount + ' 获胜场数:' + user.winCount;}else if(result.code == 401) {location.assign("/login.html");}}}); </script>

功能测试

运行程序,登录后进入 http://127.0.0.1:8080/game_hall.html:

用户信息正确显示

接下来,我们继续实现匹配功能

前端实现

添加点击事件,onclick() 需要完成的功能:

1. 判断连接是否正常

2. 判断当前是 开始匹配 还是 停止匹配

3. 若是开始匹配,则发送开始匹配请求

4. 若是停止匹配,则发送停止匹配请求

        // 为匹配按钮添加点击事件let matchBtn = document.querySelector('#match-button');matchBtn.onclick = function() {// 在发送 websocket 请求前,先判断连接是否正常if(webSocket.readyState == webSocket.OPEN) {if(matchBtn.innerHTML == "开始匹配") {console.log("开始匹配");webSocket.send(JSON.stringify({message: "START",}));} else if(matchBtn.innerHTML == "匹配中...(点击停止)") {console.log("停止匹配");webSocket.send(JSON.stringify({message: "STOP",}));}} else {alert("当前连接已断开!请重新登录");location.replace("/login.html");}}

webSocket.readyState 是 WebSocket 对象的一个属性,表示 WebSocket 连接的当前状态

WebSocket.CONNECTING:WebSocket 正在连接中,表示 WebSocket 对象正在尝试建立连接,但连接还未成功建立

WebSocket.OPEN:WebSocket连接已经建立并且可以进行通信,此时可以通过 WebSocket 发送和接收消息

WebSocket.CLOSING:WebSocket 正在关闭中,此时的 WebSocket 连接已经开始关闭过程,但仍然可以发送和接收一些消息

WebSocket.CLOSIN:WebSocket 连接已经关闭或无法建立连接,此时 WebSocket 不再可用,无法发送和接收消息

创建 WebSocket 实例,并挂载回调函数:

        // 初始化 webSocketlet webSocketUrl = 'ws://127.0.0.1/findMatch';let webSocket = new WebSocket(webSocketUrl);// 处理服务器响应webSocket.onmessage = function(e) {}// 监听页面关闭事件,在页面关闭之前,手动调用 webSocker 的 close 方法// 防止连接还没断开就关闭窗口window.onbeforeunload = function() {webSocket.close();}

 onmessage() 中主要是对服务器返回的数据进行处理:

首先需要根据返回 code 进行处理:

code 为 200,响应成功,继续判断 matchMessage

code 不为 200,出现异常情况,直接跳转到 登录页面

在此时对于 code 不为 200 的情况,我们就先直接让其跳转到登录页面,后续再进行更细致的处理 

若 code 为 200,则继续根据 matchMessage 进行处理:

若返回的 matchMessage START,表示服务器开始进行匹配,将显示的内容修改为 正在进行匹配

若返回的 matchMessage STOP,表示服务器已停止进行匹配,将显示的内容修改为 开始匹配

若返回的 matchMessage SUCCESS,表示服务器已经匹配到对手,显示对手相关信息,跳转到游戏房间页面

        webSocket.onmessage = function(e) {// 处理服务器返回的响应数据let resp = JSON.parse(e.data);console.log(resp);let matchButton = document.querySelector("#match-button");if (resp.code != 200) {console.log("发生错误: " + resp.errorMessage);location.replace("/login.html");return;}if (resp.data.matchMessage == 'START') {console.log("成功进入匹配队列");matchButton.innerHTML = "匹配中...(点击停止)";} else if (resp.data.matchMessage == 'STOP') {console.log("结束匹配");matchButton.innerHTML = "开始匹配";} else if (resp.data.matchMessage == 'SUCCESS') {// 成功匹配到对手, 进入游戏房间console.log(resp.data.rival);alert("匹配到对手:" + resp.data.rival.name + " 分数:" + resp.data.rival.score);location.replace("/game_room.html");} else {console.log("接收到非法响应!" + resp.data);}}

  

服务端实现

首先需要创建 MatchHandler,继承自 TextWebSocketHandler,作为处理 WebSocket 请求的入口类:

@Component
@Slf4j
public class MatchHandler extends TextWebSocketHandler {/*** 连接建立* @param session* @throws Exception*/@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {}/*** 处理匹配请求和停止匹配请求* @param session* @param message* @throws Exception*/@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {}/*** 处理异常情况* @param session* @param exception* @throws Exception*/@Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {}/*** 连接关闭* @param session* @param status* @throws Exception*/@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {}
}

接着,创建 WebSocketConfig,注册 MatchHanlder

@EnableWebSocket
@Configuration
public class WebSocketConfig implements WebSocketConfigurer {@Autowiredprivate MatchHandler matchHandler;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(matchHandler, "/findMatch").addInterceptors(new HttpSessionHandshakeInterceptor()); // 添加拦截器}
}

addHandler 之后,添加 addInterceptors(new HttpSessionHandshakeInterceptor()),其作用是将之前登录过程中存放在 HttpSession 中的数据(主要是 UserInfo),存放到 WebSocket 的 session 中,方便后续获取当前用户信息

OnlineUserManager

注册完成后,我们需要创建一个 Manager 类来管理用户的在线状态

借助这个类,一方面可以判定用户是否在线,另一方面是可以从中获取到 Session 给客户端回话

因此,我们可以使用 哈希表 这样的结果来对用户在线状态进行管理,其中 key 用户 idvalue 为用户的 WebSocketSession

那么,可以使用 HashMap 来存储用户在线状态吗?

若使用 HashMap 来进行存储的话,同时有多个用户和服务器建立连接/断开连接,此时服务器就是在并发的针对 HashMap 进行修改,就很可能会出现线程安全问题,因此使用 HashMap 是不适合的,而 ConcurrentHashMap 更适合当前的场景

ConcurrentHashMap 能够做到读数据不加锁,且在进行写操作时锁的粒度更小,可以允许多个修改操作并发进行

对于用户的在线状态,用户可能在 游戏大厅 中,也可能在 游戏房间 中,因此,我们创建两个 ConcurrentHashMap,分别对 游戏大厅用户在线状态游戏房间用户在线状态 进行管理 

@Component
public class OnlineUserManager {/*** 游戏大厅用户在线状态*/private ConcurrentHashMap<Long, WebSocketSession> hallMap = new ConcurrentHashMap<>();/*** 游戏房间用户在线状态*/private ConcurrentHashMap<Long, WebSocketSession> roomMap = new ConcurrentHashMap<>();}

OnlineUserManager 需要提供的主要功能有: 

当玩家建立好 WebSocket 连接时(进入游戏大厅 / 游戏房间),将键值对加入到OnlineUserManager 中

当玩家断开 WebSocket 连接时(离开游戏大厅 / 游戏房间),将键值对从 OnlineUserManager 中删除

在玩家连接建立好之后,能够随时通过 userId 来查询到对应的会话,以便向客户端返回数据

@Component
public class OnlineUserManager {/*** 游戏大厅用户在线状态*/private ConcurrentHashMap<Long, WebSocketSession> hallMap = new ConcurrentHashMap<>();/*** 游戏房间用户在线状态*/private ConcurrentHashMap<Long, WebSocketSession> roomMap = new ConcurrentHashMap<>();/*** 用户进入游戏房间,将用户信息存储到 roomMap 中* @param userId* @param session*/public void enterGameRoom(Long userId, WebSocketSession session) {roomMap.put(userId, session);}/*** 用户退出游戏房间,将用户信息从 roomMap 中删除* @param userId*/public void exitGameRoom(Long userId) {roomMap.remove(userId);}/*** 从 roomMap 中获取用户信息* @param userId* @return*/public WebSocketSession getFromRoom(Long userId) {return roomMap.get(userId);}/*** 用户进入游戏大厅,将用户信息存储到 hallMap 中* @param userId* @param session*/public void enterGameHall(Long userId, WebSocketSession session) {hallMap.put(userId, session);}/*** 用户退出游戏大厅,将用户信息从 hallMap 中删除* @param userId*/public void exitGameHall(Long userId) {hallMap.remove(userId);}/*** 从 hallMap 中获取用户信息* @param userId* @return*/public WebSocketSession getFromHall(Long userId) {return hallMap.get(userId);}
}

创建请求/响应对象

根据约定的前后端交互接口,来创建对应的请求/响应对象:

请求:

@Data
public class MatchParam implements Serializable {/*** 匹配信息*/private String message;
}

响应:

@Data
public class MatchResult implements Serializable {/*** 匹配结果*/private String matchMessage;private Rival rival;@Datapublic static class Rival {/*** 对手姓名*/private String name;/*** 天梯分数*/private Long score;}public MatchResult() {}public MatchResult(String matchMessage) {this.matchMessage = matchMessage;}
}

创建好之后 ,我们就可以开始处理 WebSocket 连接建立成功后的业务逻辑了

处理连接成功

 连接建立后(afterConnectionEstablished)需要处理的业务逻辑:

1. 从 session 中获取登录时存储的用户信息(UserInfo)

2. 使用 OnlineUserManager 来管理用户状态

3. 判断当前用户是否已经在线

4. 设置玩家的上线状态

其中,在设置玩家的上线状态之前,需要先判定用户是否已经在线,从而防止用户多开

什么是多开?

一个用户,同时打开多个浏览器,同时进行登录,进入游戏大厅/游戏房间,也就是一个账号登录两次

那么,多开会造成什么问题呢?

例如:

 

当 浏览器1 与 服务器 建立 WebSocket  连接成功时,服务器会在 OnlineUserManager 中保存键值对 userId: 1, WebSocketSession: Session1

而当 浏览器2 与 服务器 建立 WebSocket 连接成功时,服务器也会在 OnlineUserManager 中保存键值对  userId: 1, WebSocketSession: Session2

上述两次连接建立成功后,哈希表中存储的 key 是相同的,因此,后一次的 value(session2)会覆盖之前的 value(session1)

这种覆盖就会导致第一个浏览器的连接虽然未断开,但是服务器已经拿不到对应的 session 了,也就无法向这个浏览器推送数据

那么,应该如何禁止多开呢?

需要实现 账号登录成功之后,禁止该账号在其他地方再次登录,因此,在 WebSocket 连接建立之后,需要判断当前账号是否已经登录过(处于在线状态),若处于在线状态,则断开此次连接

    @Autowiredprivate OnlineUserManager onlineUserManager;@Overridepublic void afterConnectionEstablished(WebSocketSession session) throws Exception {// 从 session 中获取用户信息UserInfo userInfo = (UserInfo) session.getAttributes().get(USER_INFO);// 用户是否登录if (null == userInfo) {session.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(CommonResult.noLogin())));return;}// 判断用户是否处于在线状态if (null != onlineUserManager.getFromHall(userInfo.getUserId())) {session.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(CommonResult.repeatConnection())));return;}// 将用户设置为在线状态onlineUserManager.enterGameHall(userInfo.getUserId(), session);// 日志打印log.info("玩家 {} 进入游戏大厅", userInfo.getUserName());}

在使用 WebSocket sendMessage 方法发送数据时,先将需要返回的数据封装为 CommonResult 对象,再使用 JacksonUtil writeValueAsString 方法将 CommonResult 对象转化为 JSON 字符串,然后再包装上一层 TextMessage(表示一个文本格式的 WebSocket 数据包),进行传输

之前在实现用户登录时,我们在 UserService 中定义 session 的 key 为 USER_INFO,我们将其导入进来:

import static com.example.gobang_system.service.UserService.USER_INFO;

此外,我们约定 code 402 时,用户尝试多开,在 CommonResult 中添加方法:

    public static <T> CommonResult<T> repeatConnection() {CommonResult result = new CommonResult();result.code = 402;result.errorMessage = "禁止多开游戏界面!";return result;}

连接建立成功后,我们就可以处理 开始/ 结束 匹配请求

处理开始/结束匹配请求

开始/结束匹配请求在 handleTextMessage 中进行处理

handleTextMessage 实现:

1. 从 session 中拿到玩家信息

2. 解析客户端发送的请求

3. 判断请求类型,若是 START,则将玩家加入到匹配队列中;若是 STOP,则将玩家从匹配队列中删除

为了方便对用户匹配状态的管理,我们可以创建一个枚举类 MatchStatusEnum

@AllArgsConstructor
@Getter
public enum MatchStatusEnum {START(1, "开始匹配"),STOP(2, "停止匹配"),SUCCESS(3, "匹配成功");private final Integer code;private final String message;public static MatchStatusEnum forName(String name) {for (MatchStatusEnum matchStatus : MatchStatusEnum.values()) {if (matchStatus.name().equalsIgnoreCase(name)) {return matchStatus;}}return null;}
}

此外,我们还需要一个 匹配器(Matcher),来处理匹配的具体逻辑

    @Autowiredprivate Matcher matcher;@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 处理匹配请求和停止匹配请求UserInfo userInfo = (UserInfo) session.getAttributes().get(USER_INFO);// 用户是否登录if (null == userInfo) {session.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(CommonResult.noLogin())));return;}// 获取用户端发送的数据String payload = message.getPayload();// 将 JSON 字符串转化为 java 对象MatchParam matchParam = JacksonUtil.readValue(payload, MatchParam.class);if (MatchStatusEnum.START.name().equalsIgnoreCase(matchParam.getMessage())) {// 开始匹配matcher.addUserInfo(userInfo);session.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(CommonResult.success(new MatchResult(MatchStatusEnum.START.name())))));} else if (MatchStatusEnum.STOP.name().equalsIgnoreCase(matchParam.getMessage())) {// 结束匹配matcher.removeUserInfo(userInfo);session.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(CommonResult.success(new MatchResult(MatchStatusEnum.STOP.name())))));} else {session.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(CommonResult.fail(400, "错误的匹配信息"))));}}

接下来,我们就来实现匹配器

Matcher

在 Matcher 中创建三个队列(队列中存储 UserInfo 对象),分别表示不同段位的玩家(约定 score < 2000 一档;2000 <= score < 3000 一档,3000 <= score 一档)

提供 add 方法,供 MatcherHandler 调用,用于将玩家加入匹配队列

提供 remover 方法,供 MatcherHandler 调用,用于将玩家移除匹配队列

@Component
@Slf4j
public class Matcher {@Autowiredprivate OnlineUserManager onlineUserManager;public static final int HIGH_SCORE = 2000;public static final int VERY_HIGH_SCORE = 3000;// 创建三个队列,分别用于匹配不同天梯分数的玩家private Queue<UserInfo> normalQueue = new LinkedList<>(); // score < 2000private Queue<UserInfo> highQueue = new LinkedList<>(); // 2000 <= score < 3000private Queue<UserInfo> veryHighQueue = new LinkedList<>(); // 3000 <= score/*** 将玩家放入匹配队列* @param userInfo*/public void addUserInfo(UserInfo userInfo) {// 参数校验if (null == userInfo) {return;}// 放入对应队列进行匹配if (userInfo.getScore() < HIGH_SCORE) {offer(normalQueue, userInfo);} else if (userInfo.getUserId() >= HIGH_SCORE&& userInfo.getUserId() < VERY_HIGH_SCORE) {offer(highQueue, userInfo);} else {offer(veryHighQueue, userInfo);}}/*** 将玩家从匹配队列中移除* @param userInfo*/public void removeUserInfo(UserInfo userInfo) {// 参数校验if (null == userInfo) {return;}// 从对应队列中移除玩家if (userInfo.getScore() < HIGH_SCORE) {remove(normalQueue, userInfo);} else if (userInfo.getUserId() >= HIGH_SCORE&& userInfo.getUserId() < VERY_HIGH_SCORE) {remove(highQueue, userInfo);} else {remove(veryHighQueue, userInfo);}}
}

 注意,由于是在 多线程情况 下进行 添加元素 和 移除元素 操作,因此在将玩家加入匹配队列和将玩家从匹配队列移除时,都需要对其操作进行加锁(直接对队列进行加锁即可)

此外,当有玩家加入到队列中时,需要唤醒对应线程从而进行匹配

offer:

    /*** 将玩家放入匹配队列中* @param queue* @param userInfo*/private void offer(Queue<UserInfo> queue, UserInfo userInfo) {try {synchronized (queue) {// 将玩家添加到队列中queue.offer(userInfo);// 唤醒线程进行匹配queue.notify();}} catch (Exception e) {log.warn("向队列 {} 中添加玩家 {} 异常, e: ", queue.getClass().getName(),userInfo.getUserName(), e);}}

 remove:

   /*** 从队列中移除玩家信息* @param queue* @param userInfo*/private void remove (Queue<UserInfo> queue, UserInfo userInfo) {try {synchronized (queue) {// 将玩家从队列中移除queue.remove(userInfo);}} catch (Exception e) {log.warn("从队列 {} 中移除玩家 {} 异常, e: ", queue.getClass().getName(),userInfo.getUserName(), e);}}

Matcher 的构造方法中,创建三个线程,用来扫描对应队列,将每个队列中的头两个元素取出来,匹配到一组:

    public Matcher() {// 创建扫描线程,进行匹配Thread normalThread = new Thread(() -> {while (true) {handlerMatch(normalQueue);}});Thread highThread = new Thread(() -> {while (true) {handlerMatch(highQueue);}});Thread veryHighThread = new Thread(() -> {while (true) {handlerMatch(veryHighQueue);}});// 启动线程normalThread.start();highThread.start();veryHighThread.start();}

实现 handlerMatch

由于handlerMatch 在单独的线程中调用,也需要考虑到访问队列的线程安全问题,因此,也需要对其进行加锁

在入口处使用 wait 进行等待,直到队列中存在两个以上的元素,唤醒线程消费队列

    private void handlerMatch(Queue<UserInfo> queue) {try {synchronized (queue) {// 若队列中为空 或 队列中只有一个玩家信息,阻塞等待while (queue.size() <= 1) {queue.wait();}// 取出两个玩家进行匹配UserInfo user1 = queue.poll();UserInfo user2 = queue.poll();}} catch (InterruptedException e) {log.warn("Matcher 被提前唤醒", e);} catch (Exception e) {log.error("Matcher 处理异常", e);}}

若队列中两个玩家匹配成功,此时需要获取玩家对应的 session,从而通知两个玩家匹配成功

但在通知之前,需要判断两个玩家是否都在线

理论上来,匹配队列中的玩家一定是在线状态(前面的逻辑中进行了处理,当玩家断开连接时就将玩家从匹配队列中移除了),但这里再进行一次判断,避免前面的逻辑出现问题时带来的严重后果

1. 若两个玩家都已下线,此次匹配结束

2. 若一个玩家下线,则将另一个玩家放回匹配队列中重新进行匹配

3. session1 = session2,也就是得到的两个 session 相同,说明同一个玩家两次进入匹配队列,此时也需要将玩家放回匹配队列中

上述 session1 = session2 的情况理论上也不会出现,在之前的逻辑中,当用户下线时,就将其从匹配队列中移除,且禁止了用户多开,在此处再次进行校验,也是为了避免前面的逻辑出现问题时带来的严重后果

                // 判断两个玩家当前是否都在线if (null == session1 && null == session2) {// 两个玩家都已下线return;}if (null == session1) {// 玩家1 已下线, 将 玩家2 重新放回匹配队列queue.offer(user2);log.info("玩家 {} 重新进行匹配", user2.getUserName());return;}if (null == session2) {// 玩家2 已下线, 将 玩家1 重新放回匹配队列queue.offer(user1);log.info("玩家 {} 重新进行匹配", user1.getUserName());return;}if (session1 == session2) {// 两个 session 相同,同一个玩家两次进入匹配队列queue.offer(user1);return;}

若两个不同的玩家都在线,此时需要将这两个玩家放入同一个游戏房间(后续实现),并向匹配成功的两个玩家发送响应数据:

                // TODO 为上述匹配成功的两个玩家创建游戏房间// 通知玩家匹配成功session1.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(CommonResult.success(convertToMatchSuccessResult(user2)))));session2.sendMessage(new TextMessage(JacksonUtil.writeValueAsString(CommonResult.success(convertToMatchSuccessResult(user1)))));}

构造匹配成功响应类型: 

    private MatchResult convertToMatchSuccessResult(UserInfo rivalInfo) {// 参数校验if (null == rivalInfo) {return null;}// 构造 MatchResultMatchResult.Rival rival = new MatchResult.Rival();rival.setName(rivalInfo.getUserName());rival.setScore(rivalInfo.getScore());MatchResult result = new MatchResult();result.setMatchMessage(MatchStatusEnum.SUCCESS.name());result.setRival(rival);// 返回return result;}

处理连接关闭

afterConnectionClosed 连接断开时会将玩家从 onlineUserManager 中移除

    @Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {log.info("匹配连接断开, code: {}, reason: {}",status.getCode(), status.getReason());// 玩家下线logoutFromHall(session);}

当连接出现异常时,也需要将玩家从 onlineUserManager 中移除,因此,我们在 logoutFromHall() 方法中实现对应逻辑

    private void logoutFromHall(WebSocketSession session) {try {if (null == session) {return;}UserInfo userInfo = (UserInfo) session.getAttributes().get(USER_INFO);if (null == userInfo) {return;}onlineUserManager.exitGameHall(userInfo.getUserId());matcher.removeUserInfo(userInfo);log.info("玩家 {} 从游戏大厅退出", userInfo.getUserName());} catch (Exception e) {log.warn("玩家 {} 退出游戏大厅时发生异常e: ", e);}}

在 连接建立成功 时,我们对用户的在线状态进行了判定:若玩家已经登录过了,此时就不能再进行登录了,返回对应响应,客户端接收到消息时,就会关闭 WebSocket 连接

而在 WebSocket 连接关闭过程中,会触发 afterConnectionClosed,从而调用 logoutFromHall 方法,但在方法中,会调用 onlineUserManager.exitGameHall(userInfo.getUserId()) 方法,将玩家从 游戏大厅 中删除,但是

当 浏览器1 与 服务器 建立 WebSocket  连接成功时,服务器会在 OnlineUserManager 中保存键值对 userId: 1, WebSocketSession: Session1

而当 浏览器2 与 服务器 建立  WebSocket  连接成功时,由于我们判定其为多开情况,因此未在 OnlineUserManager 保存键值对 userId: 1, WebSocketSession: Session2

但是从 session1 session2 中获取到的 userId 都为 1

此时,在 断开 浏览器2 与 服务器 之间的连接时,就会将  OnlineUserManager 中保存键值对 userId: 1, WebSocketSession: Session1 删除,此时也就出现了异常

因此,在断开连接时,需要进行进一步处理,从而保证在断开多开连接时,不影响原有连接

获取  OnlineUserManager 中保存的 session 信息(onlineSession),判断其与当前需要断开的 session 是否相同,若相同,则移除对应 session 信息,若不同,则不进行移除

    private void logoutFromHall(WebSocketSession session) {try {if (null == session) {return;}UserInfo userInfo = (UserInfo) session.getAttributes().get(USER_INFO);if (null == userInfo) {return;}// 存储的 sessionWebSocketSession onlineSession = onlineUserManager.getFromHall(userInfo.getUserId());// 判断获取到的 session 信息 与 onlineUserManager 中存储的 session 是否相同// 避免关闭多开时将玩家信息错误删除if (session == onlineSession) {// 将玩家从游戏大厅移除onlineUserManager.exitGameHall(userInfo.getUserId());}// 若玩家正在进行匹配,而 WebSocket 连接断开// 需要将其从匹配队列中移除matcher.removeUserInfo(userInfo);log.info("玩家 {} 从游戏大厅退出", userInfo.getUserName());} catch (Exception e) {log.warn("玩家 {} 退出游戏大厅时发生异常e: ", e);}}

 

处理连接异常

连接异常时与连接关闭时的处理逻辑基本相同:

    @Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {// 打印错误信息log.error("匹配过程中出现异常: ", exception);logoutFromHall(session);}

创建游戏房间

接着,我们需要继续完成匹配成功时,为两个玩家创建游戏房间相关逻辑

我们创建 Room 类,表示游戏房间,游戏房间需要包含:

1. 房间 ID,房间 ID 必须是唯一值,作为房间的唯一身份标识,因此,可以使用 UUID 作为房间ID

2. 对弈玩家双方信息(user1 和 user2)

3. 记录先手方 ID

4. 使用一个二维数组,作为对弈的棋盘

@Data
public class Room {private static final int MAX_ROW = 15;private static final int MAX_COL = 15;/*** 房间 id*/private String roomId;/*** 玩家1*/private UserInfo user1;/*** 玩家2*/private UserInfo user2;/*** 先手玩家 id*/private Long whiteUserId;/*** 棋盘*/private int[][] board = new int[MAX_ROW][MAX_COL];public Room() {// 使用 uuid 作为房间唯一标识roomId = UUID.randomUUID().toString();}
}

每当两个玩家匹配成功时,都会创建一个游戏房间,即 Room 对象会存在很多

因此,我们需要创建一个管理器来管理所有的 Room

可以使用一个 Hash 表来保存所有的房间对象,key 为 roomId,value 为 Room 对象,方便通过 房间 id 找到对应房间信息

再使用一个 Hash 表来保存 userId -> roomId 的映射,方便根据玩家 id 查找对应房间

@Component
public class RoomManager {/*** key: roomId* value: Room 对象*/private ConcurrentHashMap<String, Room> rooms = new ConcurrentHashMap<>();/*** key: userId* value: roomId* 方便根据玩家查询对应房间*/private ConcurrentHashMap<Long, String> userIdToRoomId = new ConcurrentHashMap<>();
}

提供对应的增、删、查方法:

@Component
public class RoomManager {/*** key: roomId* value: Room 对象*/private ConcurrentHashMap<String, Room> rooms = new ConcurrentHashMap<>();/*** key: userId* value: roomId* 方便根据玩家查询对应房间*/private ConcurrentHashMap<Long, String> userIdToRoomId = new ConcurrentHashMap<>();/*** 创建游戏房间* @param room* @param userId1* @param userId2*/public void add(Room room, Long userId1, Long userId2) {rooms.put(room.getRoomId(), room);userIdToRoomId.put(userId1, room.getRoomId());userIdToRoomId.put(userId2, room.getRoomId());}/*** 删除游戏房间* @param roomId* @param userId1* @param userId2*/public void remove(String roomId, Long userId1, Long userId2) {rooms.remove(roomId);userIdToRoomId.remove(userId1);userIdToRoomId.remove(userId2);}/*** 通过房间号获取游戏房间* @param roomId* @return*/public Room getRoomByRoomId(String roomId) {return rooms.get(roomId);}/*** 通过玩家 id 获取游戏房间* @param userId* @return*/public Room getRoomByUserId(Long userId) {String roomId = userIdToRoomId.get(userId);if (null == roomId) {return null;}return rooms.get(roomId);}
}

在 Matcher 中注入 RoomManager:

    @Autowiredprivate RoomManager roomManager;

完善匹配逻辑: 

                // 为上述匹配成功的两个玩家创建游戏房间Room room = new Room();roomManager.add(room, user1.getUserId(), user2.getUserId());

修改前端逻辑

在前面,对于 code != 200 的情况,我们直接让页面跳转到登录页面,在这里,我们对其进行更进一步的处理:

code = 401:跳转到登录页面

code = 402:提示用户多开

code 为其他值:打印异常信息

        webSocket.onmessage = function(e) {// 处理服务器返回的响应数据let resp = JSON.parse(e.data);console.log(resp);let matchButton = document.querySelector("#match-button");if (resp.code != 200) {if (resp.code == 401) {// 用户未登录} else if (resp.code == 402) {alert("检测到当前账号游戏多开!请检查登录情况!");} else {alert("异常情况:" + resp.errorMessage);}location.replace("/login.html");return;}if (resp.data.matchMessage == 'START') {console.log("成功进入匹配队列");matchButton.innerHTML = "匹配中...(点击停止)";} else if (resp.data.matchMessage == 'STOP') {console.log("结束匹配");matchButton.innerHTML = "开始匹配";} else if (resp.data.matchMessage == 'SUCCESS') {// 成功匹配到对手, 进入游戏房间console.log(resp.data.rival);alert("匹配到对手:" + resp.data.rival.name + " 分数:" + resp.data.rival.score);location.replace("/game_room.html");} else {console.log("接收到非法响应!" + resp.data);}}

验证匹配功能

实现完成后,运行程序,验证匹配功能是否正常

使用两个浏览器(或是无痕式窗口),登录两个账号:

点击开始匹配,观察打印信息:

结束匹配:

再新开一个窗口,尝试登录上述其中一个账号:

点击确定,跳转到登录页面:

 两个玩家都点击开始匹配:

匹配成功,显示对手相关信息,点击确定,跳转到游戏房间页面:

​​​​​​​

至此,我们的匹配功能就基本实现完成了 

关键字:怎样在网上卖东西_如何开通企业邮箱_南京seo代理_seo最新快速排名

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: