项目功能
项目实现了用户的注册、登录、匹配、匹配时的双方信息、对战功能、结算时的分数加成。
注册 : 注册的时候通过 BCrypt 加密,防止用户的信息被黑客盗取。
大厅页面 :
对战页面 : 对战的时候,可以看到双方的信息,看到比赛场次和胜场,还有对方的积分情况。
对战结束的页面 : 对战结束回到大厅的时候,会刷新玩家的游戏信息。
具体功能如下视频 :
WebSocket
是一种建立在客户端和服务器当中的链接。
- HTTP 协议是无状态、无连接、单向的应用层协议。采用了请求-响应模式,由客户端发送一个请求,由服务端返回一个响应。
- HTTP 协议的弊端是服务器无法主动向客户端发起消息。所以就导致客户端想要获取服务端连续的状态变化很困难。
- 大多是 web 程序是通过频繁的异步JavaScript和XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源。所以就有了 WebSocket。
- 举例:就像我们在餐馆买了饭,如果不用 WebSocket 的话,就需要我们几分钟就跑过去问问老板熟没熟。如果用了 WebSocket 的话,就相当于饭熟了之后,老板直接给你端过来了。
请求头 : 返回头 :
Spring 内置的 WebSocket
通过 Spring,我们就可以直接使用 WebSocket 了。创建一个 Test 类,要继承自 TextWebSocketHandler:
public class Test extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
super.afterConnectionEstablished(session);
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
super.handleMessage(session, message);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
super.handleTransportError(session, exception);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
super.afterConnectionClosed(session, status);
}
}
再创建一个类,原来让 Spring 知道这是一个 WebSocket 配置类:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private Test test;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(test,"/test");
}
}
客户端代码 :
<body>
<input type="text" id = "message">
<input type="button" id = "submit" value="提交">
<script>
let url = "ws://127.0.0.1:8080/test"
let websocket = new WebSocket(url)
websocket.onopen = function() {
console.log("建立连接");
}
websocket.onmessage = function(e) {
console.log("收到消息" + e.date);
}
websocket.onerror = function() {
console.log("连接异常");
}
websocket.onclose = function() {
console.log("连接关闭");
}
let input = document.querySelector('#message');
let button = document.querySelector('#submit')
button.onclick = function() {
console.log("发送消息" + input.value);
websocket.send(input.value);
}
</script>
</body>
运行结果如下:
实现匹配模块
实现匹配的时候,约定一个前后端的交互接口,然后通过匹配来把玩家放进一个房间。匹配的时候,也是通过消息推送机制的。就是通过 WebSocket 传输 JSON 格式的文本数据。
请求和响应
设置的请求和响应如下:
服务器逻辑
服务器当中要处理的事情有:对玩家进行匹配、对用户进行管理、处理连接对象、处理匹配请求、实现匹配器、实现对战的房间以及房间管理、对异常的处理。
在线玩家类处理
通过哈希表来进行记录用户是否在线,如果不在线,那么就需要对玩家的相关功能进行关闭。并且通过哈希表来判断房间是否在线,不在线就处理房间:
@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 session) {
gameRoom.put(userId, session);
}
public void exitGameRoom(int userId) {
gameRoom.remove(userId);
}
public WebSocketSession getFromGameRoom(int userId) {
return gameRoom.get(userId);
}
}
匹配类
在匹配类当中,通过对玩家分数进行区分,来放入不同的匹配队列。如果匹配队列当中够了两个玩家,那么就把玩家拿出来,然后放到同一个房间。
@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() < 1500) {
synchronized (normalQueue) {
normalQueue.offer(user);
normalQueue.notify();
}
System.out.println("玩家:" + user.getUsername() + " 加入到了 normalQueue 中!");
} else if (user.getScore() >= 1500 && user.getScore() < 2000) {
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() < 1500) {
synchronized (normalQueue) {
normalQueue.remove(user);
}
System.out.println("玩家:" + user.getUsername() + " 退出了 normalQueue 中!");
} else if (user.getScore() >= 1500 && user.getScore() < 2000) {
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() {
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 {
while (matchQueue.size() < 2) {
matchQueue.wait();
}
User player1 = matchQueue.poll();
User player2 = matchQueue.poll();
System.out.println("匹配到的两个玩家:" + player1.getUsername() + " " + player2.getUsername());
WebSocketSession session1 = onlineUserManager.getFromGameHall(player1.getUserid());
WebSocketSession session2 = onlineUserManager.getFromGameHall(player2.getUserid());
if (session1 == null) {
matchQueue.offer(player2);
return;
}
if (session2 == null) {
matchQueue.offer(player1);
return;
}
if (session1 == session2) {
matchQueue.offer(player1);
return;
}
Room room = new Room();
roomManager.add(room, player1.getUserid(), player2.getUserid());
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 e) {
e.printStackTrace();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
创建一个类来进行匹配
我们通过一个 MatchAPI 类来进行对玩家的匹配,然后重写里面的 afterConnectionEstablished 、handleTextMessage 、handleTransportError 、afterConnectionClosed 方法。用来处理 WebSocket 请求。当然也需要在 WebSocketConfig 当中把这个类注册进来。
通过 afterConnectionEstablished 来设置玩家上线 :
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
try {
User user = (User) session.getAttributes().get("user");
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)));
return;
}
onlineUserManager.enterGameHall(user.getUserid(), session);
System.out.println("玩家:" + user.getUsername() + " 进入游戏大厅!");
} catch (NullPointerException e) {
System.out.println("MatchAPI afterConnectionEstablished 当前用户未登录!");
}
}
通过 handleTextMessage 来处理匹配请求 :
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
User user = (User) session.getAttributes().get("user");
String payload = message.getPayload();
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.setMessage("非法匹配请求");
}
String jsonString = objectMapper.writeValueAsString(response);
session.sendMessage(new TextMessage(jsonString));
}
通过 handleTransportError 来处理玩家下线 :
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
try {
User user = (User) session.getAttributes().get("user");
WebSocketSession tempSession = onlineUserManager.getFromGameHall(user.getUserid());
if (tempSession == session) {
onlineUserManager.exitGameHall(user.getUserid());
}
matcher.remove(user);
} catch (NullPointerException e) {
System.out.println("MatchAPI handleTransportError 当前用户未登录!");
}
}
通过 afterConnectionClosed 来删除玩家的 WebSocket 通信 :
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
try {
User user = (User) session.getAttributes().get("user");
WebSocketSession tmpSession = onlineUserManager.getFromGameHall(user.getUserid());
if (tmpSession == session) {
onlineUserManager.exitGameHall(user.getUserid());
}
matcher.remove(user);
} catch (NullPointerException e) {
System.out.println("MatchAPI afterConnectionClosed 当前用户未登录!");
}
}
游戏类目设置
在这个类里面,要判断玩家登录的时候,是否已经匹配到房间,是否是账号多开:
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
GameReadyResponse response = new GameReadyResponse();
User user = (User) session.getAttributes().get("user");
if (user == null) {
response.setOk(false);
response.setReason("用户未登录");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
return;
}
Room room = roomManager.getRoomByUserId(user.getUserid());
if (room == null) {
response.setOk(false);
response.setReason("玩家没有匹配到房间");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
return;
}
if (onlineUserManager.getFromGameHall(user.getUserid()) != null
|| onlineUserManager.getFromGameRoom(user.getUserid()) != null) {
response.setOk(true);
response.setReason("不能多开账号!");
response.setMessage("repeatConnection");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
return;
}
onlineUserManager.enterGameRoom(user.getUserid(), session);
synchronized (room) {
if (room.getUser1() == null) {
room.setUser1(user);
room.setWhiteUser(user.getUserid());
System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家1");
return;
}
if (room.getUser2() == null) {
room.setUser2(user);
System.out.println("玩家 " + user.getUsername() + " 已经准备就绪! 作为玩家2");
noticeGameReady(room, room.getUser1(), room.getUser2());
noticeGameReady(room, room.getUser2(), room.getUser1());
return;
}
}
response.setOk(false);
response.setReason("当前房间已满,您不能加入!");
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(response)));
}
如果玩家中途退出之后,就视为放弃比赛,然后另外一方获胜。
|