【编程导航】WebSocket-毕设

WebSocket

简介

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。

​ WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

⭐️说白了就是服务器向客户端传输信息的手段。一般情况下只能是客户端向服务器发送消息,服务器无法发送消息给客户端,使用WebSocket则可以实现双向通信。🌟

后端Springboot整合

前端我暂时还没写,哈哈哈

依赖引入

    <dependencies>
         <!--websocket-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
         <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

启动类

@SpringBootApplication
public class WebSocketApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebSocketApplication.class, args);
    }

    @Bean
    public ServerEndpointExporter serverEndpointExporter(){
        return new ServerEndpointExporter();
    }
}

当然,你也可以另写一个配置文件

这个bean的注册,用于扫描带有@ServerEndpoint的注解成为websocket

Socket编写

@Component
@Slf4j
@ServerEndpoint(value = "/socket/{userId}")
public class TestSocket {
    @OnOpen
    public void onOpen(@PathParam("userId") String userId) {
        log.info("ChatWebsocket open 有新连接加入 userId: {}", userId);
        log.info("ChatWebsocket open 连接建立完成 userId: {}", userId);
    }
}

测试网址:

https://www.lddgo.net/network/websocket

image-20221216204032673

image-20221216204136077

几个重要注解

  • @OnOpen

    连接成功时调用

  • @OnClose

    连接断开时调用

  • @OnError

    发生错误时调用

  • @OnMessage

    收到消息时调用

简单实现

@Component
@Slf4j
@ServerEndpoint(value = "/socket/{userId}")
public class TestSocket {
    @OnOpen
    public void onOpen(@PathParam("userId") String userId) {
        log.info("有新连接加入 userId: {}", userId);
        log.info("连接建立完成 userId: {}", userId);
    }

    @OnClose
    public void OnClose() {
        log.info("连接断开");
    }

    @OnError
    public void OnError(Throwable error) {
        log.error("发生了错误  errorMessage: {}", error.getMessage());
    }

    @OnMessage
    public void OnMessage(String message) {
        log.info("收到消息:{}", message);
    }
}

image-20221216205213595

image-20221216205231332

聊天室实现

@Component
@Slf4j
@ServerEndpoint(value = "/chatRoom/{userId}")
public class ChatRoom {
    /**
     * 存储每一个连接的用户和其对应的ChatRoom
     * 由于webSocket是多例的,采用static把userMap变成一个
     */
    private static Map<String, ChatRoom> userMap = new HashMap<>();
    /**
     * 会话    import javax.websocket.Session;
     */
    private Session session;
    /**
     * userId
     */
    private String userId;


    @OnOpen
    public void onOpen(@PathParam("userId") String userId, Session session) {
        log.info("有新连接加入 userId: {}", userId);
        log.info("连接建立完成 userId: {}", userId);
        this.session = session;
        this.userId = userId;
        userMap.put(userId, this);
    }

    @OnClose
    public void OnClose() {
        log.info("连接断开userId: {}", userId);
    }

    @OnError
    public void OnError(Throwable error) {
        log.error("发生了错误  errorMessage: {}", error.getMessage());
    }

    @OnMessage
    public void OnMessage(String message) {
        log.info("收到消息:{}", message);
        sendMessage(message);
    }
	//发送消息
    public void sendMessage(String message) {
        userMap.keySet().forEach((userId) -> {
            if (!userId.equals(this.userId))     //不给自己发
                userMap.get(userId).session.getAsyncRemote().sendText(message);
        });
    }
}
  • 由于websocket是多例的,即每次连接都会产生一个新的类。所以使用static保证userMap只有一个,存储所有的userId和ChatRoom
  • session和userid没有使用static,所以对于每个连接的用户都是唯一的,都是自己的。

❗️session为websocket包下的​

  • 收到消息时向map中的所有user发送消息(除开自己)

image-20221216212451103

连接3个用户,id分别为1,2,3

用户1发送消息,除了用户1,用户2,用户3都收到

用户1:

image-20221216212528418

用户2:

image-20221216212610056

用户3:

image-20221216212620607

匹配实现

🚀🚀✈️✈️由于毕设需要实现一个在线匹配对战功能,因此有了这个。

参考:(20条消息) SpringBoot + WebSocket 实现答题对战匹配机制_低吟不作语的博客-CSDN博客

图示:

  1. 进入匹配大厅,设置为待匹配
  2. 点击匹配,设置用用户的状态为匹配中,搜索其他状态为匹配中的用户。在这个过程中,用户可以取消匹配,回到匹配大厅,设置状态为待匹配。
  3. 开始游戏,实时推送对手答题信息。
  4. 游戏结束,设置状态为游戏结束。

大致是这个样子,目前完成了匹配机制这一部分。还有很多不足。🤣

依赖引入
  </properties>
    <dependencies>
        <!--引入Knife4j的官方start包,Swagger2基于Springfox2.10.5项目-->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <!--使用Swagger2-->
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>2.0.9</version>
        </dependency>
        <!--使用lombook-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--mysql连接-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--使用mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--json解析-->
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
        </dependency>
        <!--使用redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--使用webSocket-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
    </dependencies>
yml配置
spring:
  datasource:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/learningPlat
      username: root
      password: root
  redis:
    port: 6379
    host: 127.0.0.1
    database: 0
    #knife4j配置
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
      #mybatis-plus日志打印
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: false
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      logic-delete-field: isDelete # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
实体类
@TableName(value ="questions")
@Data
public class Questions implements Serializable {
    /**
     * id
     */
    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 题目内容
     */
    @ExcelProperty("题目内容")
    private String content;

    /**
     * 选项A
     */
    @ExcelProperty("选项A")
    private String optionA;

    /**
     * 选项B
     */
    @ExcelProperty("选项B")
    private String optionB;

    /**
     * 选项C
     */
    @ExcelProperty("选项C")
    private String optionC;

    /**
     * 选项D
     */
    @ExcelProperty("选项D")
    private String optionD;

    /**
     * 正确选项
     */
    @ExcelProperty("正确选项")
    private String correct;

    /**
     * 解析
     */
    @ExcelProperty("解析")
    private String analyzation;

    /**
     * 创建时间
     */
    private Date createTime;

    /**
     * 更新时间
     */
    private Date updateTime;

    /**
     * 是否删除
     */
    @TableLogic
    private Integer isDelete;
    /**
     * 题型  0-单选 1-多选 2-判断
     */
    private int type;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}

@ExcelProperty这个注解是写文件上传时用的,不用太在意哈😘

相关sql:

CREATE TABLE `questions` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `content` text NOT NULL COMMENT '题目内容',
  `optionA` text NOT NULL COMMENT '选项A',
  `optionB` text NOT NULL COMMENT '选项B',
  `optionC` text COMMENT '选项C',
  `optionD` text COMMENT '选项D',
  `correct` varchar(512) NOT NULL COMMENT '正确选项',
  `analyzation` text COMMENT '解析',
  `type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '题型  0-单选 1-多选 2-判断',
  `createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `isDelete` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=20077 DEFAULT CHARSET=utf8mb4 COMMENT='题目';
枚举类

用户状态枚举:

@Slf4j
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 BusinessException(ErrorCode.SYSTEM_ERROR);
        }
    }

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

Redis中key的枚举类:

选择 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 对局信息
     */
    USER_IN_PLAY,
    /**
     * userOnline 匹配信息
     */
    USER_MATCH_INFO,
    /**
     * 房间
     */
    ROOM;

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

消息类型的枚举:

public enum MessageTypeEnum {

    /**
     * 用户加入
     */
    ADD_USER,
    /**
     * 匹配对手
     */
    MATCH_USER,
    /**
     * 取消匹配
     */
    CANCEL_MATCH,
    /**
     * 游戏开始
     */
    PLAY_GAME,
    /**
     * 游戏结束
     */
    GAME_OVER,
}

工具类:操作redis

@Component
public class MatchCacheUtil {

    /**
     * 用户 userId 为 key,MatchSocket 为 value
     */
    private static final Map<String, MatchSocket> CLIENTS = new HashMap<>();

    /**
     * key 是标识存储用户在线状态的 EnumRedisKey,value 为 map 类型,其中用户 userId 为 key,用户在线状态 为 value
     */
    @Resource
    private RedisTemplate<String, Map<String, String>> redisTemplate;

    /**
     * 添加客户端
     */
    public void addClient(String userId, MatchSocket websocket) {
        CLIENTS.put(userId, websocket);
    }

    /**
     * 移除客户端
     */
    public void removeClient(String userId) {
        CLIENTS.remove(userId);
    }

    /**
     * 获取客户端
     */
    public MatchSocket 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());
    }
}
redis配置

来源:黑马🐴

@Configuration
public class RedisTemplateConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        // 创建RedisTemplate对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 设置连接工厂
        template.setConnectionFactory(connectionFactory);
        // 创建JSON序列化工具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer =
                new GenericJackson2JsonRedisSerializer();
        // 设置Key的序列化
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        // 设置Value的序列化
        template.setValueSerializer(jsonRedisSerializer);
        template.setHashValueSerializer(jsonRedisSerializer);
        // 返回
        return template;
    }
}
匹配实现

​ 创建一个线程专门用来匹配用户,如果匹配成功就向客户端推送消息

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

  • 当前用户在匹配对手的同时,被其他用户匹配,那么当前用户应当停止匹配操作
  • 当前用户匹配到对手,但对手被其他用户匹配了,那么当前用户应该重新寻找新的对手
@Component
@Slf4j
@ServerEndpoint(value = "/game/match/{userId}")
@Data
public class MatchSocket {

    private Session session;

    private String userId;

    private static MatchCacheUtil matchCacheUtil;
    private static MatchImpl matchImpl;

    @Autowired
    public void setMatchImpl(MatchImpl matchImpl) {
        MatchSocket.matchImpl = matchImpl;
    }

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


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


    @OnClose
    public void onClose() {
        log.info(" onClose 连接断开 userId: {}", userId);

        matchCacheUtil.removeClient(userId);
        matchCacheUtil.removeUserOnlineStatus(userId);
        matchCacheUtil.removeUserFromRoom(userId);
        matchCacheUtil.removeUserMatchInfo(userId);

        log.info("onClose 连接断开完成 userId: {}", userId);
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("onMessage userId: {}, 来自客户端的消息 message: {}", userId, message);
        Gson gson = new Gson();
        Type gsonType = new TypeToken<Map<String, String>>() {
        }.getType();
        Map<String, String> map = gson.fromJson(message, gsonType);
        MessageTypeEnum type = gson.fromJson(map.get("type"), MessageTypeEnum.class);
        log.info("onMessage userId: {}, 来自客户端的消息类型 type: {}", userId, type);
        if (type == MessageTypeEnum.ADD_USER) {
//            addUser();
        } else if (type == MessageTypeEnum.MATCH_USER) {
            matchImpl.match(userId);
        } else if (type == MessageTypeEnum.CANCEL_MATCH) {
//            cancelMatch();
        } else if (type == MessageTypeEnum.PLAY_GAME) {
//            toPlay();
        } else if (type == MessageTypeEnum.GAME_OVER) {
//            gameover();
        } else {
            throw new BusinessException(ErrorCode.SYSTEM_ERROR);
        }

        log.info("onMessage userId: {} 消息接收结束", userId);
    }

}

😭前文提到,由于webSocket是多例模式的,而spring默认是单例的,所以,这了我就踩了个坑。

🐛🐛按照以往的编程习惯,我通过@Autowired自动注入MatchImpl与MatchCacheUtil的值,结果报空指针。结合球友解释以及百度了解原因以及解决方式。上文是第一种,还有一种未实验。🤪

实现:

@Service
@Slf4j
public class MatchImpl implements MatchService {

    @Autowired
    private QuestionsService questionsService;
    @Autowired
    private MatchCacheUtil matchCacheUtil;
    private Lock lock = new ReentrantLock();

    private Condition matchCond = lock.newCondition();

    /**
     * 向成员发送题目
     * @param ids 房间内成员id
     * @param data 题目数据
     * @param <T>
     */
    public <T> void sendMessage(Set<String> ids, T data) {
        ids.forEach(userId -> {
            try {
                MatchSocket client = matchCacheUtil.getClient(userId);
                Gson gson = new Gson();
                String json = gson.toJson(ResultUtils.success(data));
                client.getSession().getBasicRemote().sendText(json);
            } catch (IOException e) {
                throw new BusinessException(ErrorCode.SYSTEM_ERROR);
            }
        });


    }

    /**
     * 获取题目,随机从数据库抽取20道题目,发送给房间内的成员
     * @param ids 房间内的成员id
     */
    public void sendQues(Set<String> ids) {
        Map<Integer, List<Questions>> ques = questionsService.getQuesRandom(20);
        Gson gson = new Gson();
        sendMessage(ids, gson.toJson(ques));
    }

    @Override
    /**
     * 匹配
     */
    public void match(String userId) {
        log.info("matchUser 用户随机匹配对手开始 , userId: {}", userId);
        //保证原子性
        lock.lock();
        try {
            // 设置用户状态为匹配中
            matchCacheUtil.setUserInMatch(userId);
            //唤醒等待线程
            matchCond.signal();
        } finally {
            lock.unlock();
        }
        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("matchUser 当前用户 {} 已退出匹配", userId);
                        return;
                    }
                    //当前用户取消匹配
                    if (matchCacheUtil.getUserOnlineStatus(userId).compareTo(StatusEnum.IDLE) == 0) {
                        log.info("matchUser 当前用户 {} 已退出匹配", userId);
                        HashSet<String> idSet = new HashSet<>();
                        idSet.add(userId);
                        sendMessage(idSet, Constant.CANCEL);
                    }
                    receiver = matchCacheUtil.getUserInMatchRandom(userId);
                    if (receiver != null) {
                        //对手取消匹配
                        if (matchCacheUtil.getUserOnlineStatus(receiver).compareTo(StatusEnum.IN_MATCH) != 0) {
                            log.info("matchUser 当前用户 {}, 匹配对手 {} 已退出匹配状态", userId, receiver);
                            HashSet<String> idSet = new HashSet<>();
                            idSet.add(userId);
                            sendMessage(idSet, Constant.CANCEL);
                        } else {
                            //加入对战房间,设置状态
                            matchCacheUtil.setUserInGame(userId);
                            matchCacheUtil.setUserInGame(receiver);
                            matchCacheUtil.setUserInRoom(userId, receiver);
                            //结束循环
                            flag = false;
                            log.info("匹配成功{}---->{}", userId, receiver);
                            HashSet<String> ids = new HashSet<>();
                            ids.add(userId);
                            ids.add(receiver);
                            //抽取题目,发送
                            sendQues(ids);
                        }
                    } else {
                        try {
                            log.info("matchUser 当前用户 {} 无对手可匹配", userId);
                            //等待
                            matchCond.await();
                        } catch (InterruptedException e) {
                            log.error("matchUser 匹配线程 {} 发生异常: {}",
                                    Thread.currentThread().getName(), e.getMessage());
                        }
                    }
                } finally {
                    lock.unlock();
                }
            }

        }).start();
    }
}

大致就这样啦,可能还有bug未发现。。。。。
想学习编程的小伙伴,十分推荐编程导航学习圈子 https://yupi.icu💎

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值