欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java+WebSocket實現(xiàn)簡單實時雙人協(xié)同pk答題系統(tǒng)

 更新時間:2025年06月18日 08:51:31   作者:皮皮林551  
在實時互動應(yīng)用中,實現(xiàn)流暢的多人協(xié)同對戰(zhàn)功能是一大挑戰(zhàn),WebSocket技術(shù),以其全雙工通信能力,提供了解決方案,本文我們就來使用WebSocket實現(xiàn)簡單實時雙人協(xié)同pk答題系統(tǒng)吧

引入

引入與技術(shù)選型:

在實時互動應(yīng)用中,實現(xiàn)流暢的多人協(xié)同對戰(zhàn)功能是一大挑戰(zhàn)。WebSocket技術(shù),以其全雙工通信能力,提供了解決方案。不同于傳統(tǒng)HTTP請求的短連接,WebSocket建立持久連接,極大減少了通信延遲,為實時數(shù)據(jù)傳輸提供了理想的環(huán)境,極大減少了傳統(tǒng)HTTP輪詢的延遲,為實時游戲提供了必要的技術(shù)基礎(chǔ)。

架構(gòu)設(shè)計:

采用前后端分離,將WebSocket服務(wù)獨立部署。前端使用JavaScript建立與WebSocket服務(wù)器的連接,實現(xiàn)即時消息交換;后端則負(fù)責(zé)邏輯處理,包括玩家匹配、狀態(tài)同步等,使用Java語言,借助Spring框架的強大支持,構(gòu)建了穩(wěn)定的WebSocket服務(wù)。

技術(shù)細(xì)節(jié)

狀態(tài)同步:在對戰(zhàn)中,通過WebSocket實時同步玩家的操作和游戲狀態(tài),每個動作都通過服務(wù)器廣播給所有參與者,確保了游戲進(jìn)程的同步性和準(zhǔn)確性。

  • 異常處理與穩(wěn)定性:  對于WebSocket連接。一旦檢測到游戲中的某一方異常斷開,系統(tǒng)會自動結(jié)算對局。
  • 用戶狀態(tài)設(shè)置:  用戶的狀態(tài)可粗略分為匹配中,對局中等,不同的狀態(tài)
  • 用戶匹配機制:  項目中的匹配系統(tǒng)采用動態(tài)隊列,遵循先來后到,并可以根據(jù)用戶的段位等信息進(jìn)行分開匹配,一定程度上提升用戶體驗。
  • 并發(fā)錯誤避免:  在項目中,每一次對局的雙方用戶的狀態(tài)信息都將保存在一個hashmap當(dāng)中,實現(xiàn)一個房間的機制,標(biāo)記用戶的狀態(tài)為游戲中,防止第三用戶加入對局,引發(fā)未知錯誤。

實現(xiàn)過程

整個對戰(zhàn)大致可以分為三個部分:對戰(zhàn)前,對戰(zhàn)中,對戰(zhàn)后。

  • 對戰(zhàn)前:  匹配對手,匹配成功后,服務(wù)器獲取雙方的個人信息,以及單次對局中的題目列表,發(fā)送到雙方的客戶端中。
  • 對戰(zhàn)中:  雙方用戶答題,提交答案后??蛻舳藢⒋痤}信息發(fā)送到服務(wù)器中,服務(wù)器將其廣播到對方客戶端中。刷新雙方分?jǐn)?shù)信息。之后的每一次答題均重復(fù)其過程。
  • 對戰(zhàn)后:  答題完畢,刷新用戶為結(jié)算狀態(tài)。服務(wù)器根據(jù)雙方得分情況判斷勝負(fù),廣播到雙方客戶端中。

此篇先介紹后端相關(guān)

WebSocket 后端

位于ws包中

1、實體類

1). 通信信息類 ChatMessage<T>

@Data
public class ChatMessage<T> {
    /**
     * 消息類型
     */
    private MessageTypeEnum type;
    /**
     * 消息發(fā)送者
     */
    private String sender;
    /**
     * 消息接收者
     */
    private Set<String> receivers;
    
    private T data;
}
  • MessageTypeEnum :  指此通信信息的類型 即pk中每一個階段的不同信息類型 eg匹配中 詳見下文
  • sender :  指此通信信息的發(fā)出方 這些響應(yīng)信息是由誰來發(fā)出的
  • Set receivers :  指此通信信息的接收方 這些信息是由誰來接受 這個誰可以是單個人也可以是一群人
  • T data :  指此信息中包含的實際數(shù)據(jù) 由 MessageTypeEnum 來確認(rèn)信息的類型,data 表示信息的數(shù)據(jù)

2). 消息類型枚舉類 MessageTypeEnum

/**
 * 消息類型

 */
public enum MessageTypeEnum {
    /**
     * 匹配對手
     */
    MATCH_USER,
    /**
     * 游戲開始
     */
    PLAY_GAME,
    /**
     * 游戲結(jié)束
     */
    GAME_OVER,
}

此枚舉類的三個信息分別代表在游戲的不同階段中,三個不同的狀態(tài)(不同的狀態(tài)下發(fā)出不同的信息)

3). 回答情況 AnswerSituation

@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
public class AnswerSituation {
    private ArrayList<String> selfAnswerSituations;
}

此實體類代表著用戶pk答題的情況 每一個用戶 在每一局游戲中 每一個用戶 都會有一個自己的 AnswerSituation

4). 單局游戲回答情況 RoomAnswerSituation

@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoomAnswerSituation {
    private String userId;
    private String receiver;
    private AnswerSituation selfAnswerSituations;
    private AnswerSituation opponentAnswerSituations;
}

其中一種響應(yīng)信息的類型 (GAME_OVER) 所對應(yīng)的T data數(shù)據(jù),包含每一局游戲中雙方的回答情況,以及對方和自己的id標(biāo)識(便于前端判斷)

5). 用戶的信息 UserMatchInfo

@Data
public class UserMatchInfo {
    private String userId;
    private Integer score;
}

包含用戶的id及分?jǐn)?shù) 在響應(yīng)信息類型為 (TO_PLAY) 時 傳輸對方用戶以及自己的分?jǐn)?shù)信息

在響應(yīng)信息類型為 (MATCH_USER) 時 初始化雙方的分?jǐn)?shù)(總分)及id信息

6). 游戲?qū)中畔?GameMatchInfo

@Data
public class GameMatchInfo {
    // TODO UserMatchInfo 后期按需擴充dan等屬性
    private UserMatchInfo selfInfo;
    private UserMatchInfo opponentInfo;
    private List<Question> questions;
//    用戶名
    private String selfUsername;
    private String opponentUsername;
//    頭像
    private String selfPicAvatar;
    private String opponentPicAvatar;
}

在匹配到對手之后 發(fā)送的chatMessage中的 T data 數(shù)據(jù)類型

包含在這一輪游戲中 自己以及對方的數(shù)據(jù)信息

此處包含雙方 id 用戶名 頭像 問題 以及初始化后的用戶總分信息(0)

可進(jìn)一步優(yōu)化封裝為 雙方游戲信息類 + 問題信息類

7). 分?jǐn)?shù)選項的反饋信息 ScoreSelectedInfo

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ScoreSelectedInfo {
    private UserMatchInfo userMatchInfo;
    private String userSelectedAnswer;
}

包含用戶信息 UserMatchInfo 及 userSelectedAnswer

顧名思義 其為用戶每一題所選擇的答案 此程序中記錄的是問題的答案 (數(shù)組中的具體值)

可優(yōu)化改變換成記錄數(shù)組對應(yīng)索引etc

2、異常處理類

1). 錯誤碼定義的統(tǒng)一接口

public interface IServerError {

    /**
     * 返回錯誤碼
     *
     * @return 錯誤碼
     */
    Integer getErrorCode();

    /**
     * 返回錯誤詳細(xì)信息
     *
     * @return 錯誤詳細(xì)信息
     */
    String getErrorDesc();
}

2). 運行時錯誤

public enum GameServerError implements IServerError {

    /**
     * 枚舉型錯誤碼
     */
    WEBSOCKET_ADD_USER_FAILED(4018, "用戶進(jìn)入匹配模式失敗"),
    MESSAGE_TYPE_ERROR(4019, "websocket 消息類型錯誤"),
    ;

    private final Integer errorCode;
    private final String errorDesc;

    GameServerError(Integer errorCode, String errorDesc) {
        this.errorCode = errorCode;
        this.errorDesc = errorDesc;
    }

    @Override
    public Integer getErrorCode() {
        return errorCode;
    }

    @Override
    public String getErrorDesc() {
        return errorDesc;
    }
}

枚舉錯誤類型及錯誤碼

3). 運行時異常

public class GameServerException extends RuntimeException {

    private Integer code;

    private String message;

    public GameServerException(GameServerError error) {
        super(error.getErrorDesc());
        this.code = error.getErrorCode();
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }
}

統(tǒng)一規(guī)范拋出的異常及其錯誤類型

3、游戲狀態(tài)枚舉類

public enum EnumRedisKey {

    /**
     * userOnline 在線狀態(tài)
     */
    USER_STATUS,
    /**
     * userOnline 匹配信息
     */
    USER_MATCH_INFO,
    /**
     * 房間
     */
    ROOM;

    public String getKey() {
        return this.name();
    }
}

使用枚舉類規(guī)定游戲中玩家的狀態(tài) 并且使用redis進(jìn)行存儲

4、ws主類

此部分代碼分為多板塊

此類完整代碼見附

1). 注入的相關(guān)類

private Session session;

private String userId;

static MatchCacheUtil matchCacheUtil;
// 修改 查看用戶的在線狀態(tài) 客戶端存儲狀態(tài)工具類

static Lock lock = new ReentrantLock();
static Condition matchCond = lock.newCondition();
// 鎖 防止并發(fā)異常情況

static QuestionService questionService;
// 題目業(yè)務(wù)類 用于在題庫中隨機獲取題目

static CompetitionService competitionService;
// 比賽業(yè)務(wù)類 用于獲取用戶段位信息等 pk相關(guān)的功能信息

static UserService userService;
// 用戶業(yè)務(wù)類 用于獲取用戶個人信息

static AnswerSituationUtil answerSituationUtil;
// 存儲用戶單輪答案信息 工具類


@Autowired
public void setQuestionService(QuestionService questionService) {
    ChatWebsocket.questionService = questionService;
}

@Autowired
public void setQuestionService(CompetitionService competitionService) {
    ChatWebsocket.competitionService = competitionService;
}

@Autowired
public void setQuestionService(UserService userService) {
    ChatWebsocket.userService = userService;
}

@Autowired
public void setMatchCacheUtil(MatchCacheUtil matchCacheUtil) {
    ChatWebsocket.matchCacheUtil = matchCacheUtil;
}

@Autowired
public void setAnswerSituationUtil(AnswerSituationUtil answerSituationUtil) {
    ChatWebsocket.answerSituationUtil = answerSituationUtil;
}

// 類單例模式注入相關(guān)業(yè)務(wù)工具類

詳細(xì)工具類見下一部分

2). 主要架構(gòu)

@OnOpen
public void onOpen(@PathParam("userId") String userId, Session session) {
    System.out.println(session);
    log.info("ChatWebsocket open 有新連接加入 userId: {}", userId);
    this.userId = userId;
    this.session = session;
    matchCacheUtil.addClient(userId, this);

    log.info("ChatWebsocket open 連接建立完成 userId: {}", userId);
}

onOpen:  建立ws連接時調(diào)用 使用工具類存儲當(dāng)前客戶端的 id和webSocket對象 入redis中 便于后期調(diào)用

@OnError
public void onError(Session session, Throwable error) {

    log.error("ChatWebsocket onError 發(fā)生了錯誤 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);
}

OnError:  遇到異常時退出并調(diào)用 將用戶信息經(jīng)過工具類 從redis中移除

OnClose:  同理 斷開連接時調(diào)用 正常使用時機為 用戶pk結(jié)束

@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.MATCH_USER) {
        matchUser(jsonObject);
    } elseif (type == MessageTypeEnum.PLAY_GAME) {
        toPlay(jsonObject);
    } elseif (type == MessageTypeEnum.GAME_OVER) {
        gameover(jsonObject);
    } else {
        throw new GameServerException(GameServerError.WEBSOCKET_ADD_USER_FAILED);
    }

    log.info("ChatWebsocket onMessage userId: {} 消息接收結(jié)束", userId);
}

OnMessage:  收到客戶端信息時調(diào)用 ws實現(xiàn)功能關(guān)鍵部分!

此處將整個游戲過程分割為三部分 分別為 匹配玩家 游戲中 游戲結(jié)束

3). 實現(xiàn)邏輯方法

1). 群發(fā)消息 sendMessageAll

/**
 * 群發(fā)消息
 */
private void sendMessageAll(MessageReply<?> messageReply) {

    log.info("ChatWebsocket sendMessageAll 消息群發(fā)開始 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 消息群發(fā)結(jié)束 userId: {}", userId);
}

此方法根據(jù)形參 確定信息的發(fā)出者 并從工具類中 獲取對方的webSocket對象 異步發(fā)送到對方的客戶端中

2). 用戶隨機匹配對手 matchUser

    /**
     * 用戶隨機匹配對手
     */
    @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 {
            // 設(shè)置用戶狀態(tài)為匹配中
//            TODO 修改工具類的類型 使他不只能存儲id 也能存儲玩家的段位
            matchCacheUtil.setUserInMatch(userId);
            matchCond.signal();
        } finally {
            lock.unlock();
        }

        // 創(chuàng)建一個異步線程任務(wù),負(fù)責(zé)匹配其他同樣處于匹配狀態(tài)的其他用戶
        Thread matchThread = new Thread(() -> {
            boolean flag = true;
            String receiver = null;
            while (flag) {
                // 獲取除自己以外的其他待匹配用戶
                lock.lock();
                try {
                    // 當(dāng)前用戶不處于待匹配狀態(tài) 直接返回
                    if (matchCacheUtil.getUserOnlineStatus(userId).compareTo(StatusEnum.IN_GAME) == 0
//                            觀察當(dāng)前用戶是否游戲中狀態(tài)
                            || matchCacheUtil.getUserOnlineStatus(userId).compareTo(StatusEnum.GAME_OVER) == 0) {
//                        觀察當(dāng)前用戶是否游戲結(jié)束 結(jié)算的狀態(tài)
                        log.info("ChatWebsocket matchUser 當(dāng)前用戶 {} 已退出匹配", userId);
                        return;
                    }
                    // 當(dāng)前用戶取消匹配狀態(tài)
                    if (matchCacheUtil.getUserOnlineStatus(userId).compareTo(StatusEnum.IDLE) == 0) {
                        // 當(dāng)前用戶取消匹配
                        messageReply.setCode(MessageCode.CANCEL_MATCH_ERROR.getCode());
                        messageReply.setDesc(MessageCode.CANCEL_MATCH_ERROR.getDesc());
//                        設(shè)定返回信息 將從匹配中的狀態(tài)該為待匹配
                        Set<String> set = new HashSet<>();
                        set.add(userId);
                        result.setReceivers(set);
                        result.setType(MessageTypeEnum.CANCEL_MATCH);
                        messageReply.setChatMessage(result);
                        log.info("ChatWebsocket matchUser 當(dāng)前用戶 {} 已退出匹配", userId);
//                        發(fā)送返回信息
                        sendMessageAll(messageReply);
                        return;
                    }

//                    從room中獲取對手的對象 這個receiver是對手的id
//                    可以在這里下手腳 加一個while循環(huán) 判斷直至找到相同段位的用戶為止
//                    TODO 直接在setUserMatchRoom那塊 加入玩家的段位信息 需要改動工具類
                    String userDan = competitionService.showPlayersExtraDan(Integer.parseInt(userId));
                    while (true) {
                        receiver = matchCacheUtil.getUserInMatchRandom(userId);
//                        這里必須把判空放在上面 否則如果是隊列中沒有人在匹配 卻給receiver調(diào)用了Integer.parseInt(receiver)方法 會報空指針
                        if (Objects.isNull(receiver))
                            break;
                        elseif (userDan.equals(competitionService.showPlayersExtraDan(Integer.parseInt(receiver))))
                            break;
                    }
                    if (receiver != null) {
                        // 對手不處于待匹配狀態(tài)
                        if (matchCacheUtil.getUserOnlineStatus(receiver).compareTo(StatusEnum.IN_MATCH) != 0) {
                            log.info("ChatWebsocket matchUser 當(dāng)前用戶 {}, 匹配對手 {} 已退出匹配狀態(tài)", userId, receiver);
   ??                    } else {
//                            進(jìn)了這個else就表明用戶已經(jīng)匹配到狀態(tài)正常的對手了
//                            設(shè)定對手的基本信息
                            matchCacheUtil.setUserInGame(userId);
                            matchCacheUtil.setUserInGame(receiver);
//                            將對手放入房間中 (指定唯一user)
                            matchCacheUtil.setUserInRoom(userId, receiver);
                            flag = false;
//                            匹配到了 令flag為false 跳出while循環(huán)
//                            此次匹配結(jié)束 進(jìn)入開打狀態(tài)
                        }
                    } else {
                        // 如果當(dāng)前沒有待匹配用戶,進(jìn)入等待隊列
                        try {
                            log.info("ChatWebsocket matchUser 當(dāng)前用戶 {} 無對手可匹配", userId);
                            matchCond.await();
                        } catch (InterruptedException e) {
                            log.error("ChatWebsocket matchUser 匹配線程 {} 發(fā)生異常: {}",
                                    Thread.currentThread().getName(), e.getMessage());
                        }
                    }
                } finally {
                    lock.unlock();
                }
            }

            log.info("已找到玩家 雙方分別為" + userId + "和" + receiver);

            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));
//            初始化接下來pk中玩家的信息 (每一道題提交答案完成都會 刷新一次對應(yīng)的得分)

            GameMatchInfo gameMatchInfo = new GameMatchInfo();

//            獲取玩家段位 根據(jù)玩家段位來獲取題目
            String dan = competitionService.showPlayersDan(Integer.parseInt(userId));
            List<Question> questions = questionService.getCompetitionQuestionsByDan(dan);

            gameMatchInfo.setQuestions(questions);
            gameMatchInfo.setSelfInfo(senderInfo);
            gameMatchInfo.setOpponentInfo(receiverInfo);
//            一次性獲取所有題目
//            存入此次對戰(zhàn)中的當(dāng)前玩家 對方 題目
//            一個GameMatchInfo就代表一個玩家的對象

//            新增 存入此次pk中對方的用戶名 頭像
            UserWithValue userWithValue = userService.showUser(Integer.parseInt(userId));
            String username = userWithValue.getUser().getUsername();
            String userPic = userWithValue.getUserValue().getPic();
            gameMatchInfo.setSelfUsername(username);
            gameMatchInfo.setSelfPicAvatar(userPic);

            UserWithValue receiverValue = userService.showUser(Integer.parseInt(receiver));
            String receiverUsername = receiverValue.getUser().getUsername();
            String opponentPic = receiverValue.getUserValue().getPic();
            gameMatchInfo.setOpponentUsername(receiverUsername);
            gameMatchInfo.setOpponentPicAvatar(opponentPic);

            messageReply.setCode(MessageCode.SUCCESS.getCode());
            messageReply.setDesc(MessageCode.SUCCESS.getDesc());
//            確認(rèn)返回信息的類型以及數(shù)據(jù)

            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(senderInfo);
            gameMatchInfo.setOpponentInfo(receiverInfo);

            result.setData(gameMatchInfo);
            set.clear();
            set.add(receiver);
            result.setReceivers(set);
            messageReply.setChatMessage(result);

            sendMessageAll(messageReply);

            log.info("ChatWebsocket matchUser 用戶隨機匹配對手結(jié)束 messageReply: {}", JSON.toJSONString(messageReply));

        }, MATCH_TASK_NAME_PREFIX + userId);
        matchThread.start();
    }

基本思路為 改變玩家狀態(tài)為匹配中 并且匹配相同狀態(tài) 相同段位的對手 (段位可按需增刪)

匹配的過程是異步多線程匹配 若沒有相同狀態(tài)的對手 則線程沉睡 直至有對手匹配為止 (此處可以設(shè)置匹配超時時間進(jìn)行優(yōu)化)

確認(rèn)對手狀態(tài)無誤后 根據(jù)id獲取雙方信息 此輪pk中題目

最后通過 sendMessageAll 的方法將信息發(fā)送給自己與對方

執(zhí)行步驟見代碼注釋

3). 游戲中 toPlay

    /**
     * 游戲中
     */
    @SneakyThrows
    public void toPlay(JSONObject jsonObject) {
//        每一道題提交了都會重新執(zhí)行一次這個方法
//        (由答題方 執(zhí)行)
        log.info("ChatWebsocket toPlay 用戶更新對局信息開始 userId: {}, message: {}", userId, jsonObject.toJSONString());

        MessageReply<ScoreSelectedInfo> messageReply = new MessageReply<>();
//        返回的信息類型

//        下面的這個思路就是 從房間中找到對方 并且發(fā)送自己的分?jǐn)?shù)更新信息給他
        ChatMessage<ScoreSelectedInfo> result = new ChatMessage<>();
        result.setSender(userId);
        String receiver = matchCacheUtil.getUserFromRoom(userId);
//        從房間中找出對面的對手是誰 發(fā)信息給他
        Set<String> set = new HashSet<>();
        set.add(receiver);
        result.setReceivers(set);
        result.setType(MessageTypeEnum.PLAY_GAME);
//        設(shè)置消息的發(fā)送方 和 接收方 以及消息類型 (游戲中)

//        獲取新的得分 并且重新賦值給當(dāng)前的user   (當(dāng)前的user就是得分的那個)
        UserMatchChoice userMatchChoice = jsonObject.getObject("data", UserMatchChoice.class);
        Integer newScore = userMatchChoice.getUserScore();
        String userSelectedAnswer = userMatchChoice.getUserSelectedAnswer();

//        獲取answerSituation對象 此對象中是所有正在游戲中的用戶的回答信息 暫時存在這里
        answerSituationUtil.addAnswer(userId, userSelectedAnswer);

        UserMatchInfo userMatchInfo = new UserMatchInfo();
        userMatchInfo.setUserId(userId);
        userMatchInfo.setScore(newScore);

//        setUserMatchInfo所改變的數(shù)據(jù)是 同一時刻所有對戰(zhàn)的用戶的信息
//        在這里set是根據(jù)當(dāng)前用戶的id
//        重新設(shè)置一下對應(yīng)的用戶對戰(zhàn)信息
        matchCacheUtil.setUserMatchInfo(userId, JSON.toJSONString(userMatchInfo));

//        設(shè)置響應(yīng)數(shù)據(jù)的類型
//        更新 同時發(fā)送對面所選的選項
        result.setData(new ScoreSelectedInfo(userMatchInfo, userSelectedAnswer));
        messageReply.setCode(MessageCode.SUCCESS.getCode());
        messageReply.setDesc(MessageCode.SUCCESS.getDesc());
        messageReply.setChatMessage(result);

//        返回包含當(dāng)前用戶的新信息的響應(yīng)數(shù)據(jù)
        sendMessageAll(messageReply);

        log.info("ChatWebsocket toPlay 用戶更新對局信息結(jié)束 userId: {}, userMatchInfo: {}", userId, JSON.toJSONString(userMatchInfo));
    }

pk中 每一道題答完后 客戶端往服務(wù)器發(fā)起的請求類型

主要思路是 根據(jù)信息的發(fā)出方 從redis中的房間機制獲取他的對手 (在匹配的時候會將一輪對戰(zhàn)中雙方的id存入redis 可理解成放入房間 防止第三者客戶端進(jìn)入 對pk過程進(jìn)行干擾) 之后將答題情況發(fā)送給對方

4). 游戲結(jié)束 gameover

    /**
     * 游戲結(jié)束
     */
    public void gameover(JSONObject jsonObject) {

        log.info("ChatWebsocket gameover 用戶對局結(jié)束 userId: {}, message: {}", userId, jsonObject.toJSONString());

//        設(shè)置響應(yīng)數(shù)據(jù)類型
        MessageReply<RoomAnswerSituation> messageReply = new MessageReply<>();

//        設(shè)置響應(yīng)數(shù)據(jù) 改變玩家的狀態(tài)
        ChatMessage<RoomAnswerSituation> result = new ChatMessage<>();
        result.setSender(userId);
        String receiver = matchCacheUtil.getUserFromRoom(userId);
        result.setType(MessageTypeEnum.GAME_OVER);
        lock.lock();
        try {
//            設(shè)定用戶為游戲結(jié)束的狀態(tài)
            matchCacheUtil.setUserGameover(userId);
            if (matchCacheUtil.getUserOnlineStatus(receiver).compareTo(StatusEnum.GAME_OVER) == 0) {
                messageReply.setCode(MessageCode.SUCCESS.getCode());
                messageReply.setDesc(MessageCode.SUCCESS.getDesc());

                //        記錄贏了的玩家的ID
                Integer winnerId = jsonObject.getInteger("data");
                boolean isUpdate = competitionService.updateUserStar(winnerId);
                if (!isUpdate) {
                    messageReply.setCode(MessageCode.SYSTEM_ERROR.getCode());
                    messageReply.setDesc(MessageCode.SYSTEM_ERROR.getDesc());
                }

//                獲取對戰(zhàn)后的對戰(zhàn)信息
                AnswerSituation selfAnswer = answerSituationUtil.getAnswer(userId);
                AnswerSituation opponentAnswer = answerSituationUtil.getAnswer(receiver);
                RoomAnswerSituation roomAnswerSituation = new RoomAnswerSituation(userId, receiver, selfAnswer, opponentAnswer);
                result.setData(roomAnswerSituation);

//                設(shè)置完結(jié)后的返回信息
                messageReply.setChatMessage(result);
                Set<String> set = new HashSet<>();
                set.add(receiver);
                result.setReceivers(set);
                sendMessageAll(messageReply);
//                屎山會出手 兩邊全部發(fā)
                set.clear();
                set.add(userId);
                result.setReceivers(set);
                sendMessageAll(messageReply);

//                移除屬于游戲中的游戲信息
                matchCacheUtil.removeUserMatchInfo(userId);
                matchCacheUtil.removeUserFromRoom(userId);

//                移除屬于這一次的游戲選擇信息
                answerSituationUtil.removeAnswer(userId);
                answerSituationUtil.removeAnswer(receiver);
            }
        } finally {
            lock.unlock();
        }

        log.info("ChatWebsocket gameover 對局 [{} - {}] 結(jié)束", userId, receiver);
    }

代碼中通過前端判斷題目數(shù)組遍歷完成 判斷雙方分?jǐn)?shù) 發(fā)送gameover狀態(tài)信息

前端通過比較雙方總分 將勝利用戶的id發(fā)回給后端 即此狀態(tài)信息中的 T data

后端將其狀態(tài)轉(zhuǎn)換 并且將對應(yīng)的雙方此輪對戰(zhàn)信息 包括總分 獲勝者 對方的回答情況 發(fā)回給雙方客戶端

至此 一輪pk結(jié)束

5、配置類及工具類

1). WebSocket配置類

@Configuration
@EnableWebSocket
public class WebsocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

2). AnswerSituation 用戶答題情況存儲工具類

@Component
//用戶答題情況 工具類
public class AnswerSituationUtil {

    private static final Map<String, AnswerSituation> ANSWER_SITUATION = new HashMap<>();

//    新增答案
    public void addAnswer(String userId,String answer){
        boolean isScored = ANSWER_SITUATION.containsKey(userId);
        if (isScored)
            ANSWER_SITUATION.get(userId).getSelfAnswerSituations().add(answer);
        if (!isScored) {
            ArrayList<String> answers = new ArrayList<>();
            answers.add(answer);
            ANSWER_SITUATION.put(userId,new AnswerSituation(answers));
        }
    }

//    獲取用戶所有答案
    public AnswerSituation getAnswer(String userId){
        boolean isContain = ANSWER_SITUATION.containsKey(userId);
        if (!isContain)
            return null;
        return ANSWER_SITUATION.get(userId);
    }

//    移除答案
    public boolean removeAnswer(String userId){
        boolean isContain = ANSWER_SITUATION.containsKey(userId);
        if (!isContain)
            returnfalse;
        ANSWER_SITUATION.remove(userId);
        returntrue;
    }
}

主要存儲 記錄 清除 用戶每一輪答題的緩存

可優(yōu)化存進(jìn) redis 中

3). MatchCacheUtil 存儲用戶在線狀態(tài)及其客戶端工具類

@Component
public class MatchCacheUtil {

    /**
     * 用戶 userId 為 key,ChatWebsocket 為 value
     */
    private static final Map<String, ChatWebsocket> CLIENTS = new HashMap<>();

    /**
     * key 是標(biāo)識存儲用戶在線狀態(tài)的 EnumRedisKey,value 為 map 類型,其中用戶 userId 為 key,用戶在線狀態(tài) 為 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);
    }

    /**
     * 移除用戶在線狀態(tài)
     */
    public void removeUserOnlineStatus(String userId) {
        redisTemplate.opsForHash().delete(EnumRedisKey.USER_STATUS.getKey(), userId);
    }

    /**
     * 獲取用戶在線狀態(tài)
     */
    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());
    }

    /**
     * 設(shè)置用戶為 IDLE 狀態(tài)
     */
    public void setUserIDLE(String userId) {
        removeUserOnlineStatus(userId);
        redisTemplate.opsForHash().put(EnumRedisKey.USER_STATUS.getKey(), userId, StatusEnum.IDLE.getValue());
    }

    /**
     * 設(shè)置用戶為 IN_MATCH 狀態(tài)
     */
    public void setUserInMatch(String userId) {
        removeUserOnlineStatus(userId);
        redisTemplate.opsForHash().put(EnumRedisKey.USER_STATUS.getKey(), userId, StatusEnum.IN_MATCH.getValue());
    }

    /**
     * 隨機獲取處于匹配狀態(tài)的用戶(除了指定用戶外)
     */
    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);
    }

    /**
     * 設(shè)置用戶為 IN_GAME 狀態(tài)
     */
    public void setUserInGame(String userId) {
        removeUserOnlineStatus(userId);
        redisTemplate.opsForHash().put(EnumRedisKey.USER_STATUS.getKey(), userId, StatusEnum.IN_GAME.getValue());
    }

    /**
     * 設(shè)置處于游戲中的用戶在同一房間
     */
    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();
    }

    /**
     * 設(shè)置處于游戲中的用戶的對戰(zhàn)信息
     */
    public void setUserMatchInfo(String userId, String userMatchInfo) {
        redisTemplate.opsForHash().put(EnumRedisKey.USER_MATCH_INFO.getKey(), userId, userMatchInfo);
    }

    /**
     * 移除處于游戲中的用戶的對戰(zhàn)信息
     */
    public void removeUserMatchInfo(String userId) {
        redisTemplate.opsForHash().delete(EnumRedisKey.USER_MATCH_INFO.getKey(), userId);
    }

    /**
     * 設(shè)置處于游戲中的用戶的對戰(zhàn)信息
     */
    public String getUserMatchInfo(String userId) {
        return redisTemplate.opsForHash().get(EnumRedisKey.USER_MATCH_INFO.getKey(), userId).toString();
    }

    /**
     * 設(shè)置用戶為游戲結(jié)束狀態(tài)
     */
    public synchronized void setUserGameover(String userId) {
        removeUserOnlineStatus(userId);
        redisTemplate.opsForHash().put(EnumRedisKey.USER_STATUS.getKey(), userId, StatusEnum.GAME_OVER.getValue());
    }
}

設(shè)置并改變用戶的在線狀態(tài) 利用redis + 枚舉類

在每一輪游戲結(jié)束之后 將其移除緩存

4). MessageCode 響應(yīng)碼

@Getter
public enum MessageCode {

    /**
     * 響應(yīng)碼
     */
    SUCCESS(2000, "連接成功"),
    USER_IS_ONLINE(2001, "用戶已存在"),
    CURRENT_USER_IS_INGAME(2002, "當(dāng)前用戶已在游戲中"),
    MESSAGE_ERROR(2003, "消息錯誤"),
    CANCEL_MATCH_ERROR(2004, "用戶取消了匹配"),
    SYSTEM_ERROR(2005,"系統(tǒng)錯誤");

    private final Integer code;
    private final String desc;

    MessageCode(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }
}

枚舉類定義封裝ws使用中會出現(xiàn)的錯誤

5). MessageTypeEnum 用戶狀態(tài)枚舉類

public enum MessageTypeEnum {
    /**
     * 匹配對手
     */
    MATCH_USER,
    /**
     * 游戲開始
     */
    PLAY_GAME,
    /**
     * 游戲結(jié)束
     */
    GAME_OVER,
}

枚舉類統(tǒng)一定義ws連接中OnMessage中會出現(xiàn)的消息類型

前后端根據(jù)判斷消息類型 判斷執(zhí)行的邏輯 和方法執(zhí)行的階段

6). StatusEnum 用戶狀態(tài)枚舉類

public enum StatusEnum {

    /**
     * 待匹配
     */
    IDLE,
    /**
     * 匹配中
     */
    IN_MATCH,
    /**
     * 游戲中
     */
    IN_GAME,
    /**
     * 游戲結(jié)束
     */
    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();
    }
}

確認(rèn)用戶此時的狀態(tài) 為 匹配中 待匹配 游戲中 游戲結(jié)束

注:用戶的狀態(tài) ≠ 消息類

以上就是Java+WebSocket實現(xiàn)簡單實時雙人協(xié)同pk答題系統(tǒng)的詳細(xì)內(nèi)容,更多關(guān)于Java WebSocket答題系統(tǒng)的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Mybatis下劃線駝峰處理的幾種方法

    Mybatis下劃線駝峰處理的幾種方法

    這篇文章主要講述Mybatis下劃線駝峰處理的幾種方法,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-12-12
  • 大話Java混合運算規(guī)則

    大話Java混合運算規(guī)則

    這篇文章主要介紹了大話Java混合運算規(guī)則,小編覺得挺不錯的,在這里分享給大家,需要的朋友可以了解下。
    2017-10-10
  • Java之Spring簡單的讀取和存儲對象

    Java之Spring簡單的讀取和存儲對象

    這篇文章主要介紹了Spring的讀取和存儲對象,獲取 bean 對象也叫做對象裝配,是把對象取出來放到某個類中,有時候也叫對象注?,想進(jìn)一步了解的同學(xué)可以參考本文
    2023-04-04
  • Spring Boot詳細(xì)打印啟動時異常堆棧信息詳析

    Spring Boot詳細(xì)打印啟動時異常堆棧信息詳析

    這篇文章主要給大家介紹了關(guān)于Spring Boot詳細(xì)打印啟動時異常堆棧信息的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-10-10
  • Java的設(shè)計模式編程中迪米特法則的應(yīng)用示例

    Java的設(shè)計模式編程中迪米特法則的應(yīng)用示例

    這篇文章主要介紹了Java的設(shè)計模式編程中迪米特法則的應(yīng)用示例,迪米特法則中主張創(chuàng)建和使用弱耦合的類,需要的朋友可以參考下
    2016-02-02
  • springboot后端存儲富文本內(nèi)容的思路與步驟(含圖片內(nèi)容)

    springboot后端存儲富文本內(nèi)容的思路與步驟(含圖片內(nèi)容)

    在所有的編輯器中,大概最受歡迎的就是富文本編輯器和MarkDown編輯器了,下面這篇文章主要給大家介紹了關(guān)于springboot后端存儲富文本內(nèi)容的思路與步驟的相關(guān)資料,需要的朋友可以參考下
    2023-04-04
  • 一文帶你了解Java創(chuàng)建型設(shè)計模式之原型模式

    一文帶你了解Java創(chuàng)建型設(shè)計模式之原型模式

    原型模式其實就是從一個對象在創(chuàng)建另外一個可定制的對象,不需要知道任何創(chuàng)建的細(xì)節(jié)。本文就來通過示例為大家詳細(xì)聊聊原型模式,需要的可以參考一下
    2022-09-09
  • Java中Elasticsearch 實現(xiàn)分頁方式(三種方式)

    Java中Elasticsearch 實現(xiàn)分頁方式(三種方式)

    Elasticsearch是用Java語言開發(fā)的,并作為Apache許可條款下的開放源碼發(fā)布,是一種流行的企業(yè)級搜索引擎,這篇文章主要介紹了Elasticsearch實現(xiàn)分頁的3種方式,需要的朋友可以參考下
    2022-07-07
  • Retrofit+RxJava實現(xiàn)帶進(jìn)度下載文件

    Retrofit+RxJava實現(xiàn)帶進(jìn)度下載文件

    這篇文章主要為大家詳細(xì)介紹了Retrofit+RxJava實現(xiàn)帶進(jìn)度下載文件,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-05-05
  • Java模擬實現(xiàn)斗地主的洗牌和發(fā)牌

    Java模擬實現(xiàn)斗地主的洗牌和發(fā)牌

    這篇文章主要為大家詳細(xì)介紹了Java模擬實現(xiàn)斗地主的洗牌和發(fā)牌,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-04-04

最新評論