返回顶部
首页 > 资讯 > 精选 >SpringBoot + WebSocket如何实现答题对战匹配机制案例详解
  • 181
分享到

SpringBoot + WebSocket如何实现答题对战匹配机制案例详解

2023-06-15 06:06:36 181人浏览 泡泡鱼
摘要

小编给大家分享一下SpringBoot + websocket如何实现答题对战匹配机制案例详解,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!概要设计类似竞技问答游戏:用户随机匹配一名对手,双方同时开始答题,直到双方都完成答

小编给大家分享一下SpringBoot + websocket如何实现答题对战匹配机制案例详解,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!

概要设计

类似竞技问答游戏:用户随机匹配一名对手,双方同时开始答题,直到双方都完成答题,对局结束。基本的逻辑就是这样,如果有其他需求,可以在其基础上进行扩展

明确了这一点,下面介绍开发思路。为每个用户拟定四种在线状态,分别是:待匹配、匹配中、游戏中、游戏结束。下面是流程图,用户的流程是被规则约束的,状态也随流程而变化

SpringBoot + WebSocket如何实现答题对战匹配机制案例详解

对流程再补充如下:

  • 用户进入匹配大厅(具体效果如何由客户端体现),将用户的状态设置为待匹配

  • 用户开始匹配,将用户的状态设置为匹配中,系统搜索其他同样处于匹配中的用户,在这个过程中,用户可以取消匹配,返回匹配大厅,此时用户状态重新设置为待匹配。匹配成功,保存匹配信息,将用户状态设置为游戏中

  • 根据已保存的匹配信息,用户可以获得对手的信息。答题是时,每次用户分数更新,也会向对手推送更新后的分数

  • 用户完成答题,则等待对手也完成答题。双方都完成答题,用户状态设置为游戏结束,展示对局结果

详细设计

针对概要设计提出的思路,我们需要思考以下几个问题:

  • 如何保持客户端与服务器的连接?

  • 如何设计客户端与服务端的消息交互?

  • 如何保存以及改变用户状态?

  • 如何匹配用户?

下面我们一个一个来解决

如何保持用户与服务器的连接?

以往我们使用 Http 请求服务器,并获取响应信息。然而 Http 有个缺陷,就是通信只能由客户端发起,无法做到服务端主动向客户端推送信息。根据概要设计我们知道,服务端需要向客户端推送对手的实时分数,因此这里不适合使用 Http,而选择了 WEBSocket。WebSocket 最大的特点就是服务端可以主动向客户端推送信息,客户端也可以主动向服务端发送信息,是真正的双向平等对话

有关 springBoot 集成 WebSocket 可参考这篇博客:https://www.yisu.com/article/208279.htm

如何设计客户端与服务端的消息交互?

按照匹配机制要求,把消息划分为 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);    }    // 出于减少篇幅的目的,业务处理方法暂不贴出...}

如何保存以及改变用户状态?

创建一个枚举类,定义用户的状态

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 {        USER_STATUS,        USER_IN_PLAY,        USER_MATCH_INFO,        ROOM;    public String geTKEy() {        return this.name();    }}

创建一个工具类,用于操作 Redis 中的数据。

@Componentpublic class MatchCacheUtil {        private static final Map<String, ChatWebsocket> CLIENTS = new HashMap<>();        @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());    }        public void setUserIDLE(String userId) {        removeUserOnlineStatus(userId);        redisTemplate.opsForHash().put(EnumRedisKey.USER_STATUS.getKey(), userId, StatusEnum.IDLE.getValue());    }        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);    }        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());    }}

如何匹配用户?

匹配用户的思路之前已经提到过,为了不阻塞客户端与服务端的 WebSocket 连接,创建一个线程专门用来匹配用户,如果匹配成功就向客户端推送消息

用户匹配对手时遵循这么一个原则:用户 A 找到用户 B,由用户 A 负责一切工作,既由用户 A 完成创建匹配数据并保存到缓存的全部操作。值得注意的一点是,在匹配时要注意保证状态的变化:

  • 当前用户在匹配对手的同时,被其他用户匹配,那么当前用户应当停止匹配操作

  • 当前用户匹配到对手,但对手被其他用户匹配了,那么当前用户应该重新寻找新的对手

用户匹配对手的过程应该保证原子性,使用 Java 来保证

@SneakyThrowsprivate 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 连接了

SpringBoot + WebSocket如何实现答题对战匹配机制案例详解

点击加入用户按钮,用户“进入匹配大厅”

SpringBoot + WebSocket如何实现答题对战匹配机制案例详解

点击随机匹配按钮,开始匹配,再取消匹配

SpringBoot + WebSocket如何实现答题对战匹配机制案例详解

按照之前的步骤再建立一个用户连接,都点击随机匹配按钮,匹配成功,服务端返回响应信息

SpringBoot + WebSocket如何实现答题对战匹配机制案例详解

用户分数更新时,在输入框输入新的分数,比如 6,点击实时更新按钮,对手将受到最新的分数消息

SpringBoot + WebSocket如何实现答题对战匹配机制案例详解

当双方都点击游戏结束按钮,则游戏结束

SpringBoot + WebSocket如何实现答题对战匹配机制案例详解

看完了这篇文章,相信你对“SpringBoot + WebSocket如何实现答题对战匹配机制案例详解”有了一定的了解,如果想了解更多相关知识,欢迎关注编程网精选频道,感谢各位的阅读!

--结束END--

本文标题: SpringBoot + WebSocket如何实现答题对战匹配机制案例详解

本文链接: https://lsjlt.com/news/278553.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • SpringBoot + WebSocket 实现答题对战匹配机制案例详解
    概要设计 类似竞技问答游戏:用户随机匹配一名对手,双方同时开始答题,直到双方都完成答题,对局结束。基本的逻辑就是这样,如果有其他需求,可以在其基础上进行扩展 明确了这一点,下面介绍开...
    99+
    2024-04-02
  • SpringBoot + WebSocket如何实现答题对战匹配机制案例详解
    小编给大家分享一下SpringBoot + WebSocket如何实现答题对战匹配机制案例详解,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧!概要设计类似竞技问答游戏:用户随机匹配一名对手,双方同时开始答题,直到双方都完成答...
    99+
    2023-06-15
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作