The profile design
Similar to competitive quiz game: the user randomly matches an opponent, both sides start answering questions at the same time, until both sides complete the questions, the end of the game. That’s the basic logic, and if there are other requirements, you can expand on them
Clear this point, the following development ideas. Draw up four online states for each user, namely: to be matched, matching, game, game over. The following is a flow chart. The user’s flow is governed by rules, and the state changes with the flow
Add the following to the process:
- The user enters the matching lobby (how is the specific effect reflected by the client) and sets the state of the user to be matched
- The user starts to match and the status of the user is set to Matching. The system searches for other users who are also matching. During this process, the user can cancel the match and return to the matching lobby. If the match is successful, save the matching information and set the user state as in the game
- Based on the saved match information, the user can get information about the opponent. When the answer is correct, each time the user score updates, it will also push the updated score to the opponent
- When the user finishes, he waits for his opponent to do the same. When both sides complete the questions, the user status is set to end of the game, and the result of the match is displayed
The detailed design
In view of the ideas proposed in the outline design, we need to consider the following questions:
- How do I keep the client connected to the server?
- How to design client-server message interaction?
- How to save and change user states?
- How to match users?
So let’s do it one by one
1. How do I keep users connected to the server?
In the past we used Http to request the server and get the response information. However, Http has a defect that the communication can only be initiated by the client, and the server cannot actively push information to the client. Based on the outline design, we know that the server needs to push real-time opponent scores to the client, so Http is not suitable here, and WebSocket is chosen instead. The biggest feature of WebSocket is that the server can take the initiative to push information to the client, and the client can also take the initiative to send information to the server, which is a real two-way equal dialogue
SpringBoot integration with WebSocket can be found in this blog: blog.csdn.net/qq_35387940…
2. How to design message interaction between the client and the server?
According to the matching mechanism requirements, the message is divided into ADD_USER (user joined), MATCH_USER (match opponent), CANCEL_MATCH (cancel match), PLAY_GAME (start of game), GAME_OVER (end of game).
public enum MessageTypeEnum {
/**
* 用户加入
*/
ADD_USER,
/** * matches the opponent */
MATCH_USER,
/** * cancel the match */
CANCEL_MATCH,
/** * The game starts */
PLAY_GAME,
/** * Game over */
GAME_OVER,
}
Copy the code
The WebSocket client can send messages to the server, and the server can send messages to the client. The message is divided into different types according to the requirements. The client sends a certain type of message, the server judges it after receiving it, processes it according to the type, and finally returns to push the processing result to the client. What distinguishes the WebSocket connection from the client is the userId passed from the client, which is stored in a HashMap
@Component
@Slf4j
@ServerEndpoint(value = "/game/match/{userId}")
public class ChatWebsocket {
private Session session;
private String userId;
static QuestionSev questionSev;
static MatchCacheUtil matchCacheUtil;
static Lock lock = new ReentrantLock();
static Condition matchCond = lock.newCondition();
@Autowired
public void setMatchCacheUtil(MatchCacheUtil matchCacheUtil) {
ChatWebsocket.matchCacheUtil = matchCacheUtil;
}
@Autowired
public void setQuestionSev(QuestionSev questionSev) {
ChatWebsocket.questionSev = questionSev;
}
@OnOpen
public void onOpen(@PathParam("userId") String userId, Session session) {
log.info("ChatWebsocket open new connection added userId: {}", userId);
this.userId = userId;
this.session = session;
matchCacheUtil.addClient(userId, this);
log.info("ChatWebsocket open Connection setup complete userId: {}", userId);
}
@OnError
public void onError(Session session, Throwable error) {
log.error("ChatWebsocket onError 发生了错误 userId: {}, errorMessage: {}", userId, error.getMessage());
matchCacheUtil.removeClinet(userId);
matchCacheUtil.removeUserOnlineStatus(userId);
matchCacheUtil.removeUserFromRoom(userId);
matchCacheUtil.removeUserMatchInfo(userId);
log.info("ChatWebsocket onError Connection disconnection completed userId: {}", userId);
}
@OnClose
public void onClose(a)
{
log.info("ChatWebsocket onClose Connection disconnection userId: {}", userId);
matchCacheUtil.removeClinet(userId);
matchCacheUtil.removeUserOnlineStatus(userId);
matchCacheUtil.removeUserFromRoom(userId);
matchCacheUtil.removeUserMatchInfo(userId);
log.info("ChatWebsocket onClose Connection disconnection completed userId: {}", userId);
}
@OnMessage
public void onMessage(String message, Session session) {
log.info("ChatWebsocket onMessage userId: {}, message from client: {}", userId, message);
JSONObject jsonObject = JSON.parseObject(message);
MessageTypeEnum type = jsonObject.getObject("type", MessageTypeEnum.class);
log.info("ChatWebsocket onMessage userId: {}, message type from the client type: {}", userId, type);
if (type == MessageTypeEnum.ADD_USER) {
addUser(jsonObject);
} else if (type == MessageTypeEnum.MATCH_USER) {
matchUser(jsonObject);
} else if (type == MessageTypeEnum.CANCEL_MATCH) {
cancelMatch(jsonObject);
} else if (type == MessageTypeEnum.PLAY_GAME) {
toPlay(jsonObject);
} else if (type == MessageTypeEnum.GAME_OVER) {
gameover(jsonObject);
} else {
throw new GameServerException(GameServerError.WEBSOCKET_ADD_USER_FAILED);
}
log.info("ChatWebsocket onMessage userId: {} Message receiving end", userId);
}
/** * group message */
private void sendMessageAll(MessageReply
messageReply) {
log.info("ChatWebsocket sendMessageAll Message group start userId: {}, messageReply: {}", userId, JSON.toJSONString(messageReply));
Set<String> receivers = messageReply.getChatMessage().getReceivers();
for (String receiver : receivers) {
ChatWebsocket client = matchCacheUtil.getClient(receiver);
client.session.getAsyncRemote().sendText(JSON.toJSONString(messageReply));
}
log.info("ChatWebsocket sendMessageAll Group sending end userId: {}", userId);
}
// For the purpose of reducing space, the business processing method is not posted at present...
}
Copy the code
3. How to save and change the user status?
Create an enumeration class that defines the state of the user
/** * User status *@author yeeq
*/
public enum StatusEnum {
/**
* 待匹配
*/
IDLE,
/** ** matching */
IN_MATCH,
/** ** in the game */
IN_GAME,
/** * Game over */
GAME_OVER,
;
public static StatusEnum getStatusEnum(String status) {
switch (status) {
case "IDLE":
return IDLE;
case "IN_MATCH":
return IN_MATCH;
case "IN_GAME":
return IN_GAME;
case "GAME_OVER":
return GAME_OVER;
default:
throw newGameServerException(GameServerError.MESSAGE_TYPE_ERROR); }}public String getValue(a) {
return this.name(); }}Copy the code
Select Redis to save user state, or create an enumeration class, Redis to store data is identified by a unique Key, so here define the Key in Redis, respectively described as follows:
- USER_STATUS: Key for storing user status. The storage type is Map
,>
, where userId is Key and online status is value
- USER_MATCH_INFO: When the user is in the game, we need to record the user’s information, such as score, etc. This information does not need to be recorded in the database, and is updated at any time
- ROOM: For example, if user A matches user B, user A’s userId is A, and user B’s userId is B, then it will be recorded in Redis as {A — B}, {B — A}.
public enum EnumRedisKey {
/** * userOnline Status */
USER_STATUS,
/** * userOnline Peer information */
USER_IN_PLAY,
/** * userOnline Matching information */
USER_MATCH_INFO,
/** * room */
ROOM;
public String getKey(a) {
return this.name(); }}Copy the code
Create a utility class for manipulating data in Redis.
@Component
public class MatchCacheUtil {
/** * The user userId is key and ChatWebsocket is value */
private static final Map<String, ChatWebsocket> CLIENTS = new HashMap<>();
/** * Key is EnumRedisKey that identifies the online status of a storage user. Value is of the map type. UserId is key and online status is value */
@Resource
private RedisTemplate<String, Map<String, String>> redisTemplate;
/** * Add client */
public void addClient(String userId, ChatWebsocket websocket) {
CLIENTS.put(userId, websocket);
}
/** * Remove client */
public void removeClinet(String userId) {
CLIENTS.remove(userId);
}
/** * Get client */
public ChatWebsocket getClient(String userId) {
return CLIENTS.get(userId);
}
/** * Remove the user status */
public void removeUserOnlineStatus(String userId) {
redisTemplate.opsForHash().delete(EnumRedisKey.USER_STATUS.getKey(), userId);
}
/** * Get user online status */
public StatusEnum getUserOnlineStatus(String userId) {
Object status = redisTemplate.opsForHash().get(EnumRedisKey.USER_STATUS.getKey(), userId);
if (status == null) {
return null;
}
return StatusEnum.getStatusEnum(status.toString());
}
/** * Set the user to IDLE */
public void setUserIDLE(String userId) {
removeUserOnlineStatus(userId);
redisTemplate.opsForHash().put(EnumRedisKey.USER_STATUS.getKey(), userId, StatusEnum.IDLE.getValue());
}
/**
* 设置用户为 IN_MATCH 状态
*/
public void setUserInMatch(String userId) {
removeUserOnlineStatus(userId);
redisTemplate.opsForHash().put(EnumRedisKey.USER_STATUS.getKey(), userId, StatusEnum.IN_MATCH.getValue());
}
/** * Randomly gets matched users (except specified users) */
public String getUserInMatchRandom(String userId) { Optional<Map.Entry<Object, Object>> any = redisTemplate.opsForHash().entries(EnumRedisKey.USER_STATUS.getKey()) .entrySet().stream().filter(entry -> entry.getValue().equals(StatusEnum.IN_MATCH.getValue()) && ! entry.getKey().equals(userId)) .findAny();return any.map(entry -> entry.getKey().toString()).orElse(null);
}
/** * Set user to IN_GAME state */
public void setUserInGame(String userId) {
removeUserOnlineStatus(userId);
redisTemplate.opsForHash().put(EnumRedisKey.USER_STATUS.getKey(), userId, StatusEnum.IN_GAME.getValue());
}
/** * Sets the user in the same room */
public void setUserInRoom(String userId1, String userId2) {
redisTemplate.opsForHash().put(EnumRedisKey.ROOM.getKey(), userId1, userId2);
redisTemplate.opsForHash().put(EnumRedisKey.ROOM.getKey(), userId2, userId1);
}
/** * Remove user */ from room
public void removeUserFromRoom(String userId) {
redisTemplate.opsForHash().delete(EnumRedisKey.ROOM.getKey(), userId);
}
/** * get the user */ from the room
public String getUserFromRoom(String userId) {
return redisTemplate.opsForHash().get(EnumRedisKey.ROOM.getKey(), userId).toString();
}
/**
* 设置处于游戏中的用户的对战信息
*/
public void setUserMatchInfo(String userId, String userMatchInfo) {
redisTemplate.opsForHash().put(EnumRedisKey.USER_MATCH_INFO.getKey(), userId, userMatchInfo);
}
/** * Remove the user's play info */
public void removeUserMatchInfo(String userId) {
redisTemplate.opsForHash().delete(EnumRedisKey.USER_MATCH_INFO.getKey(), userId);
}
/**
* 设置处于游戏中的用户的对战信息
*/
public String getUserMatchInfo(String userId) {
return redisTemplate.opsForHash().get(EnumRedisKey.USER_MATCH_INFO.getKey(), userId).toString();
}
/** * Set the user to the game over state */
public synchronized void setUserGameover(String userId) { removeUserOnlineStatus(userId); redisTemplate.opsForHash().put(EnumRedisKey.USER_STATUS.getKey(), userId, StatusEnum.GAME_OVER.getValue()); }}Copy the code
4. How to match users?
The idea of matching users was mentioned earlier. In order not to block the WebSocket connection between the client and the server, create a thread dedicated to matching users and push messages to the client if the match is successful
When A user matches an opponent, the principle is as follows: When user A finds user B, user A is responsible for all the operations of creating matching data and storing it in the cache. It is worth noting that when matching, care should be taken to ensure that the state changes:
- If the current user is matched by another user while matching an opponent, the current user should stop matching
- If the current user matches an opponent but the opponent is matched by another user, the current user should search for a new opponent
The user’s matching process should be atomic, using Java locks
/** * The user randomly matches the opponent */
@SneakyThrows
private void matchUser(JSONObject jsonObject) {
log.info("ChatWebsocket matchUser randomly matches opponent start message: {}, userId: {}", jsonObject.toJSONString(), userId);
MessageReply<GameMatchInfo> messageReply = new MessageReply<>();
ChatMessage<GameMatchInfo> result = new ChatMessage<>();
result.setSender(userId);
result.setType(MessageTypeEnum.MATCH_USER);
lock.lock();
try {
// Set the user status to matching
matchCacheUtil.setUserInMatch(userId);
matchCond.signal();
} finally {
lock.unlock();
}
// Create an asynchronous thread task that matches other users who are also in the matching state
Thread matchThread = new Thread(() -> {
boolean flag = true;
String receiver = null;
while (flag) {
// Get other users to be matched
lock.lock();
try {
// The current user is not in the state to be matched
if (matchCacheUtil.getUserOnlineStatus(userId).compareTo(StatusEnum.IN_GAME) == 0
|| matchCacheUtil.getUserOnlineStatus(userId).compareTo(StatusEnum.GAME_OVER) == 0) {
log.info("ChatWebsocket matchUser Current user {} is out of match", userId);
return;
}
// The current user is unmatched
if (matchCacheUtil.getUserOnlineStatus(userId).compareTo(StatusEnum.IDLE) == 0) {
// The current user cancels the match
messageReply.setCode(MessageCode.CANCEL_MATCH_ERROR.getCode());
messageReply.setDesc(MessageCode.CANCEL_MATCH_ERROR.getDesc());
Set<String> set = new HashSet<>();
set.add(userId);
result.setReceivers(set);
result.setType(MessageTypeEnum.CANCEL_MATCH);
messageReply.setChatMessage(result);
log.info("ChatWebsocket matchUser Current user {} is out of match", userId);
sendMessageAll(messageReply);
return;
}
receiver = matchCacheUtil.getUserInMatchRandom(userId);
if(receiver ! =null) {
// The opponent is not in the state to be matched
if(matchCacheUtil.getUserOnlineStatus(receiver).compareTo(StatusEnum.IN_MATCH) ! =0) {
log.info("ChatWebsocket matchUser Current user {}, matching opponent {} has exited the matching state", userId, receiver);
} else {
matchCacheUtil.setUserInGame(userId);
matchCacheUtil.setUserInGame(receiver);
matchCacheUtil.setUserInRoom(userId, receiver);
flag = false; }}else {
// If there is no user to match, the user enters the waiting queue
try {
log.info("ChatWebsocket matchUser Current user {} no match.", userId);
matchCond.await();
} catch (InterruptedException e) {
log.error("ChatWebsocket matchUser matching thread {} exception: {}", Thread.currentThread().getName(), e.getMessage()); }}}finally {
lock.unlock();
}
}
UserMatchInfo senderInfo = new UserMatchInfo();
UserMatchInfo receiverInfo = new UserMatchInfo();
senderInfo.setUserId(userId);
senderInfo.setScore(0);
receiverInfo.setUserId(receiver);
receiverInfo.setScore(0);
matchCacheUtil.setUserMatchInfo(userId, JSON.toJSONString(senderInfo));
matchCacheUtil.setUserMatchInfo(receiver, JSON.toJSONString(receiverInfo));
GameMatchInfo gameMatchInfo = new GameMatchInfo();
List<Question> questions = questionSev.getAllQuestion();
gameMatchInfo.setQuestions(questions);
gameMatchInfo.setSelfInfo(senderInfo);
gameMatchInfo.setOpponentInfo(receiverInfo);
messageReply.setCode(MessageCode.SUCCESS.getCode());
messageReply.setDesc(MessageCode.SUCCESS.getDesc());
result.setData(gameMatchInfo);
Set<String> set = new HashSet<>();
set.add(userId);
result.setReceivers(set);
result.setType(MessageTypeEnum.MATCH_USER);
messageReply.setChatMessage(result);
sendMessageAll(messageReply);
gameMatchInfo.setSelfInfo(receiverInfo);
gameMatchInfo.setOpponentInfo(senderInfo);
result.setData(gameMatchInfo);
set.clear();
set.add(receiver);
result.setReceivers(set);
messageReply.setChatMessage(result);
sendMessageAll(messageReply);
log.info("ChatWebsocket matchUser randomly matches the opponent end messageReply: {}", JSON.toJSONString(messageReply));
}, CommonField.MATCH_TASK_NAME_PREFIX + userId);
matchThread.start();
}
Copy the code
Projects show
The project code is as follows: github.com/Yee-Q/match…
Once running, you can use websocket-client to test. Open in your browser and view the message on the console.
Enter a random number in the connection input box as userId, click Connect, then the client and server will establish a WebSocket connection
Click the “Join user” button, and the user “enter the matching hall”
Click the random match button to start matching and then cancel matching
Follow the previous steps to establish a user connection, click the random match button, match success, the server returns a response message
When the user scores update, input a new score in the input box, such as 6, click the real-time update button, the opponent will receive the latest score message
When both sides click the game over button, the game ends