如何做解析网站,做编程的 网站有哪些方面,昆明网站公司建设,市北建筑建网站哪家好目录
一、分析需求
二、约定前后端交互接口
匹配请求#xff1a;
匹配响应#xff1a;
三、实现游戏大厅页面#xff08;前端代码#xff09;
game_hall.html#xff1a;
common.css#xff1a;
game_hall.css#xff1a;
四、实现后端代码
WebSocketConfig
…目录
一、分析需求
二、约定前后端交互接口
匹配请求
匹配响应
三、实现游戏大厅页面前端代码
game_hall.html
common.css
game_hall.css
四、实现后端代码
WebSocketConfig
MatchAPI处理请求
MatchResponse响应
MatchRequest 请求
OnlineUserManager用户在线状态
Matcher匹配器
Room游戏房间
RoomManager房间管理器
五、线程安全问题
1、HashMap 和 多开
2、三个队列
六、忙等问题 一、分析需求 需求多个玩家在游戏大厅进行匹配系统会把实力相近的玩家匹配到一起。 要想实现上述效果就需要利用到消息推送机制即需要使用到 WebSocket 协议。如图 二、约定前后端交互接口 通过需求分析确认了要使用 WebSocket 协议来实现消息推送的效果因此约定前后端交互接口也是根据 WebSocket 展开的。 WebSocket 协议可以传输文本数据也可以传输二进制数据这里就采用传输 JSON 格式的文本数据。
匹配请求 这里并不需要传送用户信息因为在前面登录的时候就已经把当前用户信息保存到HttpSession中了在进行WebSocket连接时只需要把HttpSession中的Session拿过来就行了并保存在WebSocket连接中。 匹配响应 这里会有两个不同的匹配响应。
匹配响应1是指玩家点击开始匹配玩家的这个操作的请求发送成功后端返回回来的响应立即返回的响应。
匹配响应2指有两个玩家成功匹配到一起了服务器主动推送回来的响应多久返回这个响应服务器并不知道。匹配到的对手信息保存在服务器中 三、实现游戏大厅页面前端代码
game_hall.html JSON字符串 和 JS对象的转换 !DOCTYPE html
html langenheadmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1.0title游戏大厅/titlelink relstylesheet hrefcss/common.csslink relstylesheet hrefcss/game_hall.css
/headbodydiv classnav五子棋对战/div!-- 整个页面的容器元素‘ --div classcontainer!-- 这个div在container中是处于垂直水平居中这样的位置的 --div!-- 展示用户信息 --div idscreen/div!-- 匹配按钮 --div idmatch-button开始匹配/div/div/divscript src./js/jquery.min.js/scriptscript$.ajax({type: get,url: /userInfo,success: function (body) {let screenDiv document.querySelector(#screen);screenDiv.innerHTML 玩家: body.username 分数: body.score br 比赛场次: body.totalCount 获胜场数: body.winCount},error: function () {alert(获取用户信息失败);}});// 此处进行初始化 websocket并且实现前端的匹配逻辑// 此处的路径必须写作 /findMatchlet websocket new WebSocket(ws://127.0.0.1:8080/findMatch);websocket.onopen function () {console.log(onopen);}websocket.onclose function () {console.log(onclose);// alert(游戏大厅中接收到了失败响应! 请重新登录);// location.assign(/login.html);}websocket.onerror function () {console.log(onerror);}// 监听页面关闭事件在页面关闭之前手动调动这里的 websocket 的 close 方法window.onbeforeunload function () {websocket.close();}//一会重点来实现要处理服务器返回的响应websocket.onmessage function (e) {// 处理服务器返回的响应数据这个响应就是针对 开始匹配 / 结束匹配 来应对的//解析得到的响应对象返回的数据是一个 JSON 字符串解析成 js 对象let resp JSON.parse(e.data);let matchButton document.querySelector(#match-button);if (!resp.ok) {console.log(游戏大厅中接收到了失败响应! resp.reason);alert(游戏大厅中接收到了失败响应! resp.reason);location.assign(/login.html);return;}if (resp.message startMatch) {//开始匹配请求发起成功console.log(进入匹配队列成功);matchButton.innerHTML 匹配中...(点击停止);} else if (resp.message stopMatch) {//结束匹配请求发起成功console.log(离开匹配队列成功);matchButton.innerHTML 开始匹配;} else if (resp.message matchSuccess) {//已经匹配到对手了console.log(匹配到对手! 进入游戏房间);location.assign(/game_room.html);} else {console.log(收到了非法的响应! message resp.message);}}// 给匹配按钮添加一个点击事件let matchButton document.querySelector(#match-button);matchButton.onclick function () {//在触发 websocket 请求之前先确认下 websocket 连接是否好着if (websocket.readyState websocket.OPEN) {//如果当前 readyState 处在 OPPEN状态说明连接是好着的//这里发送的数据有两种可能开始匹配/停止匹配if (matchButton.innerHTML 开始匹配) {console.log(开始匹配);websocket.send(JSON.stringify({message: startMatch,}))} else if (matchButton.innerHTML 匹配中...(点击停止)) {console.log(停止匹配);websocket.send(JSON.stringify({message: stopMatch,}));}} else {//这是说明当前连接是异常的状态alert(当前您的连接已经断开! 请重新登录!);location.assign(/login.html);}}/script/body/html
common.css
* {margin: 0;padding: 0;box-sizing: border-box;
}html, body {height: 100%;background-image: url(../image/blackboard.jpg);background-repeat: no-repeat;background-position: center;background-size: cover;
}.nav {height: 50px;background: rgb(50, 50, 50);color: white;line-height: 50px;padding-left: 20px;
}.container {width: 100%;height: calc(100% - 50px);display: flex;align-items: center;justify-content: center;
}
game_hall.css
.container {width: 100%;height: calc(100% - 50px);display: flex;align-items: center;justify-content: center;
}#screen {width: 400px;height: 200px;font-size: 20px;background-color: gray;color: white;border-radius: 10px;text-align: center;line-height: 100px;
}#match-button {width: 400px;height: 50px;font-size: 20px;background-color: orange;color: white;border: none;outline: none;border-radius: 10px;text-align: center;line-height: 50px;margin-top: 20px;
}#match-button:active{background-color: gray;
} 四、实现后端代码 后端要想建立WebSocket连接需要创建一个专门的类MatchAPI来处理 WebSocket 的请求同时还要新建一个类WebSocketConfig进行WebSocket连接、配置 WebSocket 连接的路径以及拿到之前 HTTP 连接时的 Session。
WebSocketConfig
Configuration
EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {Autowiredprivate MatchAPI matchAPI;Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(matchAPI, /findMatch).addInterceptors(new HttpSessionHandshakeInterceptor());}
}
MatchAPI处理请求 JSON字符串 和 Java对象的转换 //通过这个类来处理匹配功能中的 websocket 请求
Slf4j
Component
public class MatchAPI extends TextWebSocketHandler {Autowiredprivate OnlineUserManager onlineUserManager;Autowiredprivate Matcher matcher;private ObjectMapper objectMapper new ObjectMapper();Overridepublic 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、拿到了身份信息之后进行判断当前用户是否已经登录过在线状态如果已经是在线就不该继续进行后续逻辑WebSocketSession tmpSession onlineUserManager.getFromGameHall(user.getUserId());if(tmpSession ! null) {// 说明该用户已经登录了// 针对这个情况要告知客户端你这里重复登录了MatchResponse response new MatchResponse();response.setOk(false);response.setReason(当前用户已经登录, 静止多开!);session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));session.close();return;}onlineUserManager.enterGameHall(user.getUserId(), session);
// System.out.println(玩家 user.getUsername() 进入游戏大厅);log.info(玩家 {},user.getUsername() 进入游戏大厅);} catch (NullPointerException e) {e.printStackTrace();// 出现空指针异常说明当前用户的身份信息为空也就是用户未登录// 就把当前用户尚未登录给返回回去MatchResponse response new MatchResponse();response.setOk(false);response.setReason(您尚未登录不能进行后续匹配);session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(response)));}}Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {// 实现处理开始匹配请求和停止匹配请求User user (User) session.getAttributes().get(user);// 拿到客户端发给服务器的数据String payload message.getPayload();// 当前传过来的数据是JSON格式的字符串就需要把它转成 Java 对象MatchRequestMatchRequest 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.setMessage(非法的匹配请求);}String jsonString objectMapper.writeValueAsString(response);session.sendMessage(new TextMessage(jsonString));}Overridepublic void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {// 玩家下线删除 OnlineUserManager 中的该用户的Sessiontry {User user (User) session.getAttributes().get(user);WebSocketSession tmpSession onlineUserManager.getFromGameHall(user.getUserId());if(tmpSession session) {onlineUserManager.exitGameHall(user.getUserId());}// 如果玩家正在匹配中但WebSocket断开了就应该把该玩家移除匹配队列log.info(Error玩家: {}, user.getUsername() 下线);matcher.remove(user);} catch (NullPointerException e) {e.printStackTrace();MatchResponse response new MatchResponse();response.setOk(false);response.setReason(您尚未登录不能进行后续匹配);session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(response)));}}Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {// 玩家下线删除 OnlineUserManager 中的该用户的Sessiontry {User user (User) session.getAttributes().get(user);WebSocketSession tmpSession onlineUserManager.getFromGameHall(user.getUserId());if(tmpSession session) {onlineUserManager.exitGameHall(user.getUserId());}// 如果玩家正在匹配中但WebSocket断开了就应该把该玩家移除匹配队列log.info(Closed玩家: {}, user.getUsername() 下线);matcher.remove(user);} catch (NullPointerException e) {e.printStackTrace();MatchResponse response new MatchResponse();response.setOk(false);response.setReason(您尚未登录不能进行后续匹配);session.sendMessage(new TextMessage(objectMapper.writeValueAsBytes(response)));}}
}对于 WebSocket 请求、返回的响应把传送的数据封装成这两个类
MatchResponse响应
// 这是表示一个 WebSocket响应
Data
public class MatchResponse {private boolean ok;private String reason;private String message;
}
MatchRequest 请求
// 这是表示一个 WebSocket 请求
Data
public class MatchRequest {private String message;
}
OnlineUserManager用户在线状态 之所以要维护用户的在线状态目的是为了能够在代码中比较方便的获取到某个用户当前的WebSocket 会话从而通过这个会话来对客户端发送消息。 同时也能感知到用户的 在线/离线 状态~。 此处使用 哈希表 来维护 userId 和 WebSocketSession 的映射关系。
Component
public class OnlineUserManager {//这个hash表就是用来表示当前用户在游戏大厅的在线状态private ConcurrentHashMapInteger, WebSocketSession gameHall 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);}
}
Matcher匹配器 通过这个匹配器来处理玩家的匹配功能。
// 这个类表示匹配器通过这个类来负责整个的匹配功能
Slf4j
Component
public class Matcher {// 创建三个匹配队列private QueueUser normalQueue new LinkedList();private QueueUser highQueue new LinkedList();private QueueUser veryHighQueue new LinkedList();Autowiredprivate OnlineUserManager onlineUserManager;Autowiredprivate RoomManager roomManager;private ObjectMapper objectMapper new ObjectMapper();// 操作匹配队列的方法// 把玩家放到匹配队列中public void add(User user) {if(user.getScore() 2000) {synchronized (normalQueue) {normalQueue.offer(user);normalQueue.notify();}log.info(把玩家 user.getUsername() 加入到了 normalQueue 中);} else if(user.getScore() 2000 user.getScore() 3000) {synchronized (highQueue) {highQueue.offer(user);highQueue.notify();}log.info(把玩家 user.getUsername() 加入到了 highQueue 中);} else {synchronized (veryHighQueue) {veryHighQueue.offer(user);veryHighQueue.notify();}log.info(把玩家 user.getUsername() 加入到了 veryHighQueue 中);}}// 当玩家点击停止匹配就需要把该玩家从匹配队列删除public void remove(User user) {if(user.getScore() 2000) {synchronized (normalQueue) {normalQueue.remove(user);}log.info(玩家: user.getUsername() 在 normalQueue 队列被删除);} else if(user.getScore() 2000 user.getScore() 3000) {synchronized (highQueue) {highQueue.remove(user);}log.info(把玩家: user.getUsername() 在 highQueue 队列被删除);} else {synchronized (veryHighQueue) {veryHighQueue.remove(user);}log.info(把玩家: user.getUsername() 在 veryHighQueue 队列被删除);}}public Matcher() {// 创建三个线程分别针对这三个匹配队列进行操作Thread t1 new Thread() {Overridepublic void run() {// 扫描normalQueuewhile(true) {handlerMatch(normalQueue);}}};t1.start();Thread t2 new Thread() {Overridepublic void run() {while (true) {handlerMatch(highQueue);}}};t2.start();Thread t3 new Thread() {Overridepublic void run() {while (true) {handlerMatch(veryHighQueue);}}};t3.start();}public void handlerMatch(QueueUser matchQueue) {synchronized (matchQueue) {try {// 1、检测队列中元素个数是否达到 2// 队列的初始情况可能是 空// 如果往队列中添加一个元素这个时候仍然是不能进行后续匹配操作的// 因此在这里使用 while 循环检查更合理while (matchQueue.size() 2) {matchQueue.wait();}// 2、尝试从队列中取出两个玩家User player1 matchQueue.poll();User player2 matchQueue.poll();log.info(匹配出两个玩家: 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) {// 如果玩家1不在线了就把玩家2放回匹配队列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();}}}
}
Room游戏房间
// 这个类表示一个游戏房间
Data
public class Room {// 使用字符串类型来表示方便生成唯一值private String roomId;private User user1;private User user2;public Room() {// 构造 Room 的时候生成一个唯一的字符串表示房间 id// 使用 UUID 来作为房间 idroomId UUID.randomUUID().toString();}
}RoomManager房间管理器
// 房间管理器类
// 这个类也希望有唯一实例
Component
public class RoomManager {private ConcurrentHashMapString, Room rooms new ConcurrentHashMap();private ConcurrentHashMapInteger, 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 映射关系不存在直接返回 nullreturn null;}return rooms.get(roomId);}
} 五、线程安全问题
1、HashMap 和 多开 如果多个线程访问同一个HashMap就容易出现线程安全问题。 如果同时多个用户和服务器 建立/断开 连接此时服务器就是并发的在针对 HashMap 进行修改。 所以要避免这种情况解决这个线程安全问题可以直接使用ConcurrentHashMap。 当多个浏览器通时对一个用户进行登录进入游戏大厅会引发下面这种问题 所以我们不仅要解决线程安全问题也要考虑用户多开的情况那么用户能进行多开操作吗显然是不能的所以上面的代码逻辑也是会处理这种多开的情况如果当前用户已经登录禁止其他地方再登录。
2、三个队列 在匹配模块Matcher类为了划分玩家水平实力使用了三个队列表示不同的实力分段 同时创建三个线程当用户进行匹配时就会不停的扫描这三个队列看是否能匹配对局成功。 但因为匹配时就会把玩家加入到对应段位的队列而停止匹配也会把玩家从对应的队列删除又有多个线程并发的去执行所以存在线程安全问题。 怎么办 针对这三个队列对象分别进行加锁如图 这三个线程都是调用同一个方法。如图 因此我们针对这一个方法加锁就好了 六、忙等问题 我们创建了三个线程 会不停的去扫描这三个队列元素个数是否达到2如果达到2就要吧这两个用户取出来放在同一个房间中进行对局。 但这里要一直扫描码显然是不用的所以可以在这里 wait 一下。 既然 wait了那就要有 notify来唤醒它继续锁竞争。那什么时候唤醒呢当然是这个队列有新的用户加进来了那再进行唤醒再重新判断用户个数是否达到2. 这样我们就能解决忙等的问题。