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条)

刀客源码 游客评论