摘要
用SpringBoot WebSocket实现对决配对,让客户自由选择敌人,进行答题PK。客户有四种在线状态,包括待配对、配对中、游戏中和比赛结束。流程表规范客户步骤,让游戏更加有序。
正文
SpringBoot WebSocket 完成解题对决配对体制
详细设计
相近比赛问答游戏:客户任意配对一名敌人,彼此另外开始答题,直至双方都进行解题,pk完毕。基本上的逻辑性就这样,如果有别的要求,能够在其基本上开展拓展
确立了这一点,下边详细介绍开发设计构思。为每一个客户拟订四种在线状态,分别是:待配对、配对中、游戏里面、比赛终止。下边是流程表,客户的步骤是被标准管束的,情况也随步骤而转变
对步骤再填补以下:
- 客户进到配对服务厅(实际实际效果怎样由手机客户端反映),将客户的情况设定为待配对
- 客户逐渐配对,将客户的情况设定为配对中,系统软件检索别的一样处在配对中的客户,在这个全过程中,客户能够撤销配对,回到配对服务厅,这时客户情况再次设定为待配对。配对取得成功,储存配对信息内容,将客户情况设定为游戏里面
- 依据已储存的配对信息内容,客户能够得到敌人的信息内容。解题是时,每一次客户成绩升级,也会向敌人消息推送升级后的成绩
- 客户进行解题,则等候敌人也进行解题。双方都进行解题,客户情况设定为比赛终止,展现pk結果
总体设计
对于详细设计明确提出的构思,大家必须思索下列好多个难题:
- 怎样维持手机客户端与网络服务器的联接?
- 怎样设计方案手机客户端与服务器端的信息互动?
- 如何保存及其更改客户情况?
- 怎样配对客户?
下边大家一个一个来处理
1. 怎样维持客户与网络服务器的联接?
过去大家应用 Http 要求网络服务器,并获得回应信息内容。殊不知 Http 有一个缺点,便是通讯只有由手机客户端进行,没法保证服务器端积极向手机客户端消息推送信息内容。依据详细设计我们知道,服务器端必须向手机客户端消息推送敌人的即时成绩,因而这儿不宜应用 Http,而挑选了 WebSocket。WebSocket 较大 的特性便是服务器端能够积极向手机客户端消息推送信息内容,手机客户端还可以积极向服务器端发送短信,是真真正正的双重公平会话
相关 SpringBoot 集成化 WebSocket 可参照这篇blog:https://blog.csdn.net/qq_35387940/article/details/93483678
2. 怎样设计方案手机客户端与服务器端的信息互动?
依照配对体制规定,把信息区划为 ADD_USER(客户添加)、MATCH_USER(配对敌人)、CANCEL_MATCH(撤销配对)、PLAY_GAME(开始游戏)、GAME_OVER(比赛终止)
public enum MessageTypeEnum {
/**
* 客户添加
*/
ADD_USER,
/**
* 配对敌人
*/
MATCH_USER,
/**
* 撤销配对
*/
CANCEL_MATCH,
/**
* 开始游戏
*/
PLAY_GAME,
/**
* 比赛终止
*/
GAME_OVER,
}
应用 WebSocket 手机客户端能够向服务器端推送信息,服务器端也可以向手机客户端推送信息。把信息依照要求区划成不一样的种类,手机客户端推送某一种类的信息,服务器端接受后分辨,并依照种类各自解决,最终回到向手机客户端消息推送事件处理。差别手机客户端 WebSocket 联接的是以手机客户端传出的 userId,用 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 有新联接添加 userId: {}", userId);
this.userId = userId;
this.session = session;
matchCacheUtil.addClient(userId, this);
log.info("ChatWebsocket open 联接创建进行 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 联接断掉进行 userId: {}", userId);
}
@OnClose
public void onClose()
{
log.info("ChatWebsocket onClose 联接断掉 userId: {}", userId);
matchCacheUtil.removeClinet(userId);
matchCacheUtil.removeUserOnlineStatus(userId);
matchCacheUtil.removeUserFromRoom(userId);
matchCacheUtil.removeUserMatchInfo(userId);
log.info("ChatWebsocket onClose 联接断掉进行 userId: {}", userId);
}
@OnMessage
public void onMessage(String message, Session session) {
log.info("ChatWebsocket onMessage userId: {}, 来源于手机客户端的信息 message: {}", userId, message);
JSONObject jsonObject = JSON.parseObject(message);
MessageTypeEnum type = jsonObject.getObject("type", MessageTypeEnum.class);
log.info("ChatWebsocket onMessage userId: {}, 来源于手机客户端的信息种类 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: {} 信息接受完毕", userId);
}
/**
* 消息群发
*/
private void sendMessageAll(MessageReply<?> messageReply) {
log.info("ChatWebsocket sendMessageAll 群发消息逐渐 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 群发消息完毕 userId: {}", userId);
}
// 出自于降低篇数的目地,业务流程解决方式 暂未贴出来...
}
3. 如何保存及其更改客户情况?
建立一个枚举类,界定客户的情况
/**
* 客户情况
* @author yeeq
*/
public enum StatusEnum {
/**
* 待配对
*/
IDLE,
/**
* 配对中
*/
IN_MATCH,
/**
* 游戏里面
*/
IN_GAME,
/**
* 比赛终止
*/
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 new GameServerException(GameServerError.MESSAGE_TYPE_ERROR);
}
}
public String getValue() {
return this.name();
}
}
挑选 Redis 储存客户情况,或是建立一个枚举类,Redis 中储存数据信息都是有唯一的 Key 做标志,因而在这儿界定 Redis 中的 Key,各自详细介绍以下:
- USER_STATUS:加密存储情况的 Key,储存种类是 Map<String, String>,在其中客户 userId 为 key,客户在线状态 为 value
- USER_MATCH_INFO:当客户处在手机游戏里时,大家必须纪录客户的信息内容,例如成绩等。这种信息内容不用纪录到数据库查询,并且随时随地会升级,放进缓存文件便捷获得
- ROOM:能够了解为配对的两位客户建立一个屋子,实际完成是以键值对方法储存,例如客户 A 和客户 B 配对,客户 A 的 userId 是 A,客户 B 的 userId 是 B,则在 Redis 中纪录为 {A — B},{B — A}
public enum EnumRedisKey {
/**
* userOnline 在线状态
*/
USER_STATUS,
/**
* userOnline pk信息内容
*/
USER_IN_PLAY,
/**
* userOnline 配对信息内容
*/
USER_MATCH_INFO,
/**
* 屋子
*/
ROOM;
public String getKey() {
return this.name();
}
}
建立一个java工具,用以实际操作 Redis 中的数据信息。
@Component
public class MatchCacheUtil {
/**
* 客户 userId 为 key,ChatWebsocket 为 value
*/
private static final Map<String, ChatWebsocket> CLIENTS = new HashMap<>();
/**
* key 是标志加密存储在线状态的 EnumRedisKey,value 为 map 种类,在其中客户 userId 为 key,客户在线状态 为 value
*/
@Resource
private RedisTemplate<String, Map<String, String>> redisTemplate;
/**
* 加上手机客户端
*/
public void addClient(String userId, ChatWebsocket websocket) {
CLIENTS.put(userId, websocket);
}
/**
* 清除手机客户端
*/
public void removeClinet(String userId) {
CLIENTS.remove(userId);
}
/**
* 获得手机客户端
*/
public ChatWebsocket getClient(String userId) {
return CLIENTS.get(userId);
}
/**
* 清除客户在线状态
*/
public void removeUserOnlineStatus(String userId) {
redisTemplate.opsForHash().delete(EnumRedisKey.USER_STATUS.getKey(), userId);
}
/**
* 获得客户在线状态
*/
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());
}
/**
* 设定客户为 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());
}
/**
* 任意获得处在配对情况的客户(除开特定客户外)
*/
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);
}
/**
* 设定客户为 IN_GAME 情况
*/
public void setUserInGame(String userId) {
removeUserOnlineStatus(userId);
redisTemplate.opsForHash().put(EnumRedisKey.USER_STATUS.getKey(), userId, StatusEnum.IN_GAME.getValue());
}
/**
* 设定处在游戏里面的客户在同一屋子
*/
public void setUserInRoom(String userId1, String userId2) {
redisTemplate.opsForHash().put(EnumRedisKey.ROOM.getKey(), userId1, userId2);
redisTemplate.opsForHash().put(EnumRedisKey.ROOM.getKey(), userId2, userId1);
}
/**
* 从屋子中清除客户
*/
public void removeUserFromRoom(String userId) {
redisTemplate.opsForHash().delete(EnumRedisKey.ROOM.getKey(), userId);
}
/**
* 从屋子中获得客户
*/
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);
}
/**
* 清除处在游戏里面的客户的对决信息内容
*/
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();
}
/**
* 设定客户为比赛终止情况
*/
public synchronized void setUserGameover(String userId) {
removeUserOnlineStatus(userId);
redisTemplate.opsForHash().put(EnumRedisKey.USER_STATUS.getKey(), userId, StatusEnum.GAME_OVER.getValue());
}
}
4. 怎样配对客户?
配对客户的构思以前早已提及过,为了更好地不堵塞手机客户端与服务器端的 WebSocket 联接,建立一个进程专业用于配对客户,假如配对取得成功就向手机客户端消息推送信息
客户配对敌人时遵照那么一个标准:客户 A 寻找客户 B,由客户 A 承担一切工作中,既由客户 A 进行建立配对数据信息并储存到缓存文件的所有实际操作。特别注意的一点是,在配对时要留意确保情况的转变 :
- 当今客户在配对敌人的另外,被普通用户配对,那麼当今客户理应终止配对实际操作
- 当今客户配对到敌人,但敌人被普通用户配对了,那麼当今客户应当再次找寻新的敌人
客户配对敌人的全过程应当确保原子性,应用 Java 锁来确保
/**
* 客户任意配对敌人
*/
@SneakyThrows
private void matchUser(JSONObject jsonObject) {
log.info("ChatWebsocket matchUser 客户任意配对敌人逐渐 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 {
// 设定客户情况为配对中
matchCacheUtil.setUserInMatch(userId);
matchCond.signal();
} finally {
lock.unlock();
}
// 建立一个多线程进程每日任务,承担配对别的一样处在配对情况的普通用户
Thread matchThread = new Thread(() -> {
boolean flag = true;
String receiver = null;
while (flag) {
// 获得除自身之外的别的待配对客户
lock.lock();
try {
// 当今客户不处在待配对情况
if (matchCacheUtil.getUserOnlineStatus(userId).compareTo(StatusEnum.IN_GAME) == 0
|| matchCacheUtil.getUserOnlineStatus(userId).compareTo(StatusEnum.GAME_OVER) == 0) {
log.info("ChatWebsocket matchUser 当今客户 {} 已撤出配对", userId);
return;
}
// 当今客户撤销配对情况
if (matchCacheUtil.getUserOnlineStatus(userId).compareTo(StatusEnum.IDLE) == 0) {
// 当今客户撤销配对
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 当今客户 {} 已撤出配对", userId);
sendMessageAll(messageReply);
return;
}
receiver = matchCacheUtil.getUserInMatchRandom(userId);
if (receiver != null) {
// 敌人不处在待配对情况
if (matchCacheUtil.getUserOnlineStatus(receiver).compareTo(StatusEnum.IN_MATCH) != 0) {
log.info("ChatWebsocket matchUser 当今客户 {}, 配对敌人 {} 已撤出配对情况", userId, receiver);
} else {
matchCacheUtil.setUserInGame(userId);
matchCacheUtil.setUserInGame(receiver);
matchCacheUtil.setUserInRoom(userId, receiver);
flag = false;
}
} else {
// 假如当今沒有待配对客户,进到等候序列
try {
log.info("ChatWebsocket matchUser 当今客户 {} 无敌人可配对", userId);
matchCond.await();
} catch (InterruptedException e) {
log.error("ChatWebsocket matchUser 配对进程 {} 产生出现异常: {}",
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 客户任意配对敌人完毕 messageReply: {}", JSON.toJSONString(messageReply));
}, CommonField.MATCH_TASK_NAME_PREFIX userId);
matchThread.start();
}
新项目展现
新项目编码以下:https://GitHub.com/Yee-Q/match-project
跑起来后,应用 websocket-client 能够开展检测。在打开浏览器,在控制面板查询信息。
在联接文本框随意键入一个数据做为 userId,点一下联接,这时手机客户端就和服务器端创建 WebSocket 联接了
点一下添加客户按键,客户“进到配对服务厅”
点一下任意配对按键,逐渐配对,再撤销配对
依照以前的流程再创建一个客户联接,都点一下任意配对按键,配对取得成功,服务器端回到回应信息内容
客户成绩升级时,在文本框键入新的成绩,例如 6,点一下自动更新按键,敌人将遭受全新的成绩信息
当双方都点击游戏完毕按键,则比赛终止
关注不迷路
扫码下方二维码,关注宇凡盒子公众号,免费获取最新技术内幕!
评论0