基于Netty构建WebSocket服务并实现项目群组聊天和实时消息通知推送

文章目录

  • 前言
    • 需求分析
    • 技术预研
      • Web端方案
      • 服务端技术
  • 技术方案
    • 设计思路
    • 功能实现
      • 添加依赖
      • 自定义NettyServer
      • 自定义webSocketHandler
      • 使用NettyServer向在线用户发送消息
  • 需要完善的地方

前言

我们的项目有个基于项目的在线文档编制模块,可以邀请多人项目组成员在线协同编制项目文档,现在的需求是要实现项目组成员在线实时协作沟通交流功能以及消息实时推送功能。

需求分析

根据需求分析,首先我们要基于项目组成员构建在线聊天群组并支持在线聊天,同时成员在线时支持实时推送消息。
在这里插入图片描述

技术预研

Web端方案

实现Web消息实时推送的方案比较多,包括轮询、长轮询、SSE、AJAX、WebSocket等。根据对比我们最终选择使用WebSocket来实现Web消息实时推送。

  • WebSocket: WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
    WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

服务端技术

  • spring-boot-starter-websocket
    SpringBoot框架提供了WebSockets自动配置,通过spring-boot-starter-websocket模块轻松访问。
 <!-- 引入 WebSocket 模块依赖 -->
  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-websocket</artifactId>
  </dependency>
  
// 创建一个配置类来配置 WebSocket 服务器
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyWebSocketHandler(), "/ws").setAllowedOrigins("*");
    }
}
// 自定义消息处理
public class MyWebSocketHandler extends TextWebSocketHandler {

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        log.debug("Connection established: " + session.getId());
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        log.debug("Received message: " + payload);
        
        // 回复消息
        session.sendMessage(new TextMessage("Echo: " + payload));
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        log.debug("Connection closed: " + session.getId());
    }
}

如果不考虑吞吐和并发,spring-boot-starter-websocket非常适合构建WebSocket Server端。

  • Netty
    Netty 是一个基于NIO的客户、服务器端的编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。

技术方案

设计思路

在这里插入图片描述

  1. 自定义NettyServer 基于项目构建用户群组
  2. 用户在指定群组发送消息,NettyServer向群组所有用户推送消息
  3. 业务系统向指定用户发送通知消息到kafka
  4. 消费者消费消息通过暴露出的NettyServerHanlder向所有在线用户实时推送消息

功能实现

添加依赖

<dependency>
     <groupId>io.netty</groupId>
     <artifactId>netty-all</artifactId>
     <version>4.1.112.Final</version>
 </dependency>

 <dependency>
     <groupId>org.springframework.kafka</groupId>
     <artifactId>spring-kafka</artifactId>
 </dependency>

自定义NettyServer

因为我们需要暴露NettyServer 的webSocketHandler,所以将NettyServer实例交由Spring管理,并暴露广播消息和系统消息接口

@Slf4j
@Component
public class CusNettyServer implements InitializingBean, DisposableBean {

    @Value("${netty.port:9000}")
    Integer nettyPort;

    EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 通常只需要一个线程即可
    EventLoopGroup workerGroup = new NioEventLoopGroup(); // 根据实际情况调整线程数 默认创建与 CPU 核心数相等的线程数
    private ChannelFuture channelFuture;
    private NettyWebSocketHandler webSocketHandler;

    @Override
    public void destroy() throws Exception {
        if (channelFuture != null && channelFuture.channel().isOpen()) {
            channelFuture.channel().closeFuture().sync();
        }
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        ServerBootstrap sb = new ServerBootstrap();
        sb.option(ChannelOption.SO_BACKLOG, 128); // 考虑调整这个值
        sb.option(ChannelOption.SO_REUSEADDR, true); // 避免地址重用问题
        sb.childOption(ChannelOption.TCP_NODELAY, true); // 减少延迟
        sb.childOption(ChannelOption.SO_KEEPALIVE, true); // 保持连接

        webSocketHandler = new NettyWebSocketHandler();

        // 绑定线程池
        sb.group(bossGroup, workerGroup)
                // 指定使用的channel
                .channel(NioServerSocketChannel.class)
                // 绑定监听端口
                .localAddress(this.nettyPort)
                // 绑定客户端连接时候触发操作
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        log.debug("收到新连接: {}", ch.remoteAddress());
                        //websocket协议本身是基于http协议的,所以这边也要使用http解编码器
                        ch.pipeline().addLast(new HttpServerCodec());
                        //以块的方式来写的处理器
                        ch.pipeline().addLast(new ChunkedWriteHandler());
                        ch.pipeline().addLast(new HttpObjectAggregator(8192));
                        ch.pipeline().addLast(webSocketHandler);//添加聊天消息处理类
                        ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws", null, true, 65536 * 10));
                    }
                });
        // 服务器异步创建绑定
        channelFuture = sb.bind().sync();
        log.debug("{} 启动正在监听: {}", NettyServer.class, channelFuture.channel().localAddress());
    }
	// 广播消息接口
    public void broadcastMessage(SocketMessage socketMessage) {
        webSocketHandler.broadcastMessage(socketMessage);
    }

	// 系统通知接口
    public void sendSystemMessage(SocketMessage socketMessage, String toUserId) {
        webSocketHandler.sendSystemMessage(socketMessage, toUserId);
    }
}

自定义webSocketHandler

前面自定义CusNettyServer 过程我们是将NettyWebSocketHandler 放在外层初始化的,为了避免一个Handler被多个channel传递抛io.netty.channel.ChannelPipelineException异常,我们需要将NettyWebSocketHandler 标记为 @ChannelHandler.Sharable

@ChannelHandler.Sharable
@Slf4j
public class NettyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    /**
     * 存储已经登录用户的channel对象
     */
    public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    /**
     * 存储用户id和用户的channelId绑定
     */
    public static ConcurrentHashMap<String, ChannelId> userMap = new ConcurrentHashMap<>();

    /**
     * 存储广播消息的channel对象
     */
    private static final ConcurrentHashMap<String, Channel> broadcastClients = new ConcurrentHashMap<>();

    /**
     * 用于存储群聊房间号和群聊成员的channel信息
     */
    public static ConcurrentHashMap<String, ChannelGroup> groupMap = new ConcurrentHashMap<>();

    private static final ExecutorService executor = Executors.newFixedThreadPool(10); // 创建线程池

    private final TokenStore tokenStore = SpringUtil.getBean(TokenStore.class);
    private final StringRedisTemplate redisTemplate = SpringUtil.getBean(StringRedisTemplate.class);

    /**
     * 获取用户拥有的群聊id号
     */
    private final UserGroupRepository userGroupRepository = SpringUtil.getBean(UserGroupRepository.class);
    private final MessageDataAssembler messageDataAssembler = SpringUtil.getBean(MessageDataAssembler.class);
    private final MessageManagerService messageService = SpringUtil.getBean(MessageManagerService.class);

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {

        log.info("与客户端建立连接,通道开启!");
        //添加到channelGroup通道组
        channelGroup.add(ctx.channel());
        ctx.channel().id();
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.info("与客户端断开连接,通道关闭!");
        //添加到channelGroup 通道组
        channelGroup.remove(ctx.channel());
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //首次连接是FullHttpRequest,把用户id和对应的channel对象存储起来
        if (msg instanceof FullHttpRequest) {
            FullHttpRequest request = (FullHttpRequest) msg;
            // 首次握手进行登录验证
            String uri = request.uri();
            String token = getUrlParams(uri);
            String userId = chkLogin(token);
            userMap.put(userId, ctx.channel().id());
            broadcastClients.put(userId, ctx.channel());
            log.info("登录的用户id是:{}", userId);
            //第1次登录,需要查询下当前用户是否加入项目组,没有拒绝连接,有将群聊管理对象放入groupMap中
            List<UserGroup> groups = userGroupRepository.findGroupIdByUserId(userId);
            if (CollUtil.isNotEmpty(groups)) {
                groups.stream().map(UserGroup::getProjectId).forEach(groupId -> {
                    ChannelGroup cGroup = Optional.ofNullable(groupMap.get(groupId)).orElseGet(() -> {
                        ChannelGroup newGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
                        groupMap.put(groupId, newGroup);
                        return newGroup;
                    });
                    //把用户放到群聊管理对象里去
                    cGroup.add(ctx.channel());
                });
            }

            //如果url包含参数,需要处理
            if (uri.contains("?")) {
                String newUri = uri.substring(0, uri.indexOf("?"));
                request.setUri(newUri);
            }

        } else if (msg instanceof TextWebSocketFrame) {
            //正常的TEXT消息类型
            TextWebSocketFrame frame = (TextWebSocketFrame) msg;
            log.info("客户端收到服务器数据:{}", frame.text());
            SocketMessage socketMessage = JSON.parseObject(frame.text(), SocketMessage.class);
            socketMessage.setSendTime(new Date());
            socketMessage.setId(IdUtil.getSnowflakeNextIdStr());
            // 如果群聊不存在,则不处理消息
            if (!groupMap.containsKey(socketMessage.getProjectId())) {
                log.info("无效消息,对应群聊不存在 {}", socketMessage.getProjectId());
                return;
            }

            // 将消息存储到 Redis
            String projectId = socketMessage.getProjectId();
            String messageKey = String.join(":", "message", projectId, socketMessage.getId());
            String messageJson = JSON.toJSONString(socketMessage);
            redisTemplate.opsForValue().set(messageKey, messageJson, 10, TimeUnit.MINUTES);

            // 异步处理消息
            executor.submit(() -> {
                // 从 Redis 中获取消息
                String storedMessageJson = redisTemplate.opsForValue().get(messageKey);
                if (storedMessageJson != null) {
                    SocketMessage storedMessage = JSON.parseObject(storedMessageJson, SocketMessage.class);

                    // 持久化消息
                    Message message = messageDataAssembler.toEntity(storedMessage);
                    message.setBizType(MsgBizType.PROJECT.getCode());
                    Message saved = messageService.saveMessage(message);

                    storedMessage.setId(saved.getId());
                    // 推送群聊信息
                    // 这里假设 groupMap 已经定义并且是线程安全的
                    ChannelGroup group = groupMap.get(projectId);
                    if (group != null) {
                        group.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(storedMessage)));
                    }

                    // 处理完成移除Redis
                    redisTemplate.delete(messageKey);
                }
            });
        }

        super.channelRead(ctx, msg);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {

    }

    public void broadcastMessage(SocketMessage socketMessage) {

        // 异步处理消息
        executor.submit(() -> {

            // 持久化消息
            Message message = messageDataAssembler.toEntity(socketMessage);
            message.setSendTime(new Date());
            message.setBizType(MsgBizType.BROADCAST.getCode());
            message.setMessageType(MessageType.TEXT.getCode());
            messageService.broadcastMessage(message);

            channelGroup.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(socketMessage)));
        });
    }

    public void sendSystemMessage(SocketMessage socketMessage, String toUserId) {

        // 持久化消息
        Message message = messageDataAssembler.toEntity(socketMessage);
        messageService.sendUserMessage(message, toUserId);

        // 如何用户在线则推送websocket消息
        Optional.ofNullable(userMap.get(toUserId)).map(channelId -> channelGroup.find(channelId))
                .ifPresent(channel -> channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(socketMessage))));
    }

    private static String getUrlParams(String url) {
        if (!url.contains("=")) {
            throw new BusinessException(CusBusinessExceptionEnum.BUSINESS_ERROR_NETTY_SERVER_PATH_MUST_HAS_USER_ID_ERROR);
        }
        return url.substring(url.indexOf("=") + 1);
    }

    private String chkLogin(String token) {

        OAuth2AccessToken accessToken =  tokenStore.readAccessToken(token);
        if (accessToken == null) {
            throw new BusinessException(401, "Invalid access token:" + token);
        }

        if (accessToken.isExpired()) {
            throw new BusinessException(401, "Expired access token:" + token);
        }
        OAuth2Authentication oauth2Authentication = tokenStore.readAuthentication(accessToken);
        if (tokenStore.readAuthentication(accessToken) == null) {
            throw new BusinessException(401, "access token Authentication error:" + token);
        }
        LoginAppUser loginAppUser = (LoginAppUser) oauth2Authentication.getPrincipal();
        return loginAppUser.getUserId();
    }
}

这里有个问题是我们始终未解决的,那就是首次握手token传递的问题,最开始后端是FullHttpRequest request中获取token,且通过apifox也验证通过,但是前端在实现过程始终无法传递token(这块有大佬实现可以在评论下留言指点下)
这是我最开始的实现

 FullHttpRequest request = (FullHttpRequest) msg;
 // 首次握手进行登录验证
 // String userId = chkLogin(request);

private String chkLogin(FullHttpRequest request) {

        String token = Optional.ofNullable(request.headers())
                .map(headers -> headers.get(HttpHeaderNames.AUTHORIZATION))
                .map(authHeader -> authHeader.replace("Bearer ", "")).orElseThrow(() -> new BusinessException(CusBusinessExceptionEnum.BUSINESS_ERROR_NETTY_SERVER_NOT_LOGIN_ERROR));

        OAuth2AccessToken accessToken =  tokenStore.readAccessToken(token);
        if (accessToken == null) {
            throw new BusinessException(401, "Invalid access token:" + token);
        }

        if (accessToken.isExpired()) {
            throw new BusinessException(401, "Expired access token:" + token);
        }

        OAuth2Authentication oauth2Authentication = tokenStore.readAuthentication(accessToken);
        if (tokenStore.readAuthentication(accessToken) == null) {
            throw new BusinessException(401, "access token Authentication error:" + token);
        }
        LoginAppUser loginAppUser = (LoginAppUser) oauth2Authentication.getPrincipal();
        return loginAppUser.getUserId();
    }

下图是我通过apifox使用header传递token验证成功
在这里插入图片描述

使用NettyServer向在线用户发送消息

消费kafka消息并使用NettyServer向在线用户发送消息

@Component
public class NotifyMsgConsumer {

    private final MessageManagerApplication messageManagerApplication;


    @KafkaListener(topics = "system_message_notify")
    public void processMessage(ConsumerRecord<Long, String> record, Acknowledgment acknowledgment) {
        log.info("system_message_notify 通知: {} {} ", record.key(), record.value());
        if (StringUtils.isEmpty(record.value())) {
            log.debug("system_message_notify 消息为空 {} 消息直接丢弃", record.key());
            acknowledgment.acknowledge();
            return;
        }

        NotifyMsg notifyMsg = null;
        try {
            notifyMsg = JSONObject.parseObject(record.value(), NotifyMsg.class);
        } catch (Exception e) {
            log.debug("system_message_notify 消息格式异常 {} {} 消息直接丢弃", record.key(), record.value());
            acknowledgment.acknowledge();
            return;
        }
        messageManagerApplication.sendNotify(notifyMsg);

        acknowledgment.acknowledge();
    }

}

@Override
public void sendNotify(NotifyMsg notifyMsg) {
     SocketMessage socketMessage = SocketMessage.buildNotifyMessage(messageDataAssembler, notifyMsg);
     cusNettyServer.sendSystemMessage(socketMessage, notifyMsg.getTo());
 }

下图是我们实现的一个前端效果图:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

需要完善的地方

该方案目前是我们单机部署的方案,集群下还需要扩展,包括:

  1. 在线用户同步的问题:群组新消息处理如何实时同步到所有NettyServer节点连接下的客户端
  2. 通知消息处理问题:也是实时同步的问题,要考虑到所有NettyServer节点连接下的客户端

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/900759.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2024mathorcup大数据竞赛B题【电商品类货量预测及品类分仓规划】思路详解

问题 1&#xff1a;建立货量预测模型&#xff0c;对该仓储网络 350 个品类未来 3 个月&#xff08;7-9月&#xff09;每个月的库存量及销量进行预测&#xff0c;其中库存量根据历史每月数据预测月均库存量即可&#xff0c;填写表 1 的预测结果并放在正文中&#xff0c;并将完整…

Discuz发布原创AI帖子内容生成:起尔 | AI原创帖子内容生成插件开发定制

Discuz发布原创AI帖子内容生成&#xff1a;起尔 | AI原创帖子内容生成插件开发定制 在当今互联网快速发展的时代&#xff0c;内容创作成为了网站运营、社交媒体管理和个人博客维护不可或缺的一部分。然而&#xff0c;高质量内容的创作往往耗时耗力&#xff0c;特别是对于需要频…

实现prometheus+grafana的监控部署

直接贴部署用的文件信息了 kubectl label node xxx monitoringtrue 创建命名空间 kubectl create ns monitoring 部署operator kubectl apply -f operator-rbac.yml kubectl apply -f operator-dp.yml kubectl apply -f operator-crd.yml # 定义node-export kubectl app…

Qt 支持打包成安卓

1. 打开维护Qt&#xff0c;双击MaintenanceTool.exe 2.登陆进去,默认是添加或移除组件&#xff0c;点击下一步&#xff0c; 勾选Android, 点击下一步 3.更新安装中 4.进度100%&#xff0c;完成安装&#xff0c;重启。 5.打开 Qt Creator&#xff0c;编辑-》Preferences... 6.进…

self-supervised learning(BERT和GPT)

1芝麻街与NLP模型 我們接下來要講的主題呢叫做Self-Supervised Learning&#xff0c;在講self-supervised learning之前呢&#xff0c;就不能不介紹一下芝麻街&#xff0c;為什麼呢因為不知道為什麼self-supervised learning的模型都是以芝麻街的人物命名。 因為Bert是一個非常…

maven下载依赖报错Blocked mirror for repositories

原因&#xff1a;Maven版本过高 解决办法 setting文件添加 或者降低maven版本 <mirrors><mirror><id>maven-default-http-blocker</id><mirrorOf>external:dummy:*</mirrorOf><name>Pseudo repository to mirror external reposit…

表格切割效果,“两个”表格实现列对应、变化一致

如何让两个表格的部分列对应且缩放一致 先看效果 使用一个原生table的即可实现 “两个”表格的视觉效果让“两个”表格的对应列缩放保持一致 废话不多说&#xff0c;直接上代码 html: <html><div><table><caption class"table-name">表格…

模拟信号采集显示器+GPS同步信号发生器制作全过程(焊接、问题、代码、电路)

1、制作最小系统板 在制作最小系统板的时候&#xff0c;要用USB转TTL给板子供电&#xff0c;留了一个电源输入的四个接口&#xff0c;同时又用排针引出来VCC和GND用于后续其他外设的电源供应&#xff0c;电源配有电源指示灯和保护电容&#xff0c; 当时在焊接的时候把接口处的…

设计模式(二)工厂模式详解

设计模式&#xff08;二&#xff09;工厂模式详解 简单工厂模式指由一个工厂对象来创建实例,适用于工厂类负责创建对象较少的情况。例子&#xff1a;Spring 中的 BeanFactory 使用简单工厂模式&#xff0c;产生 Bean 对象。 工厂模式简介 定义&#xff1a;工厂模式是一种创建…

机房巡检机器人有哪些功能和作用

随着数据量的爆炸式增长和业务的不断拓展&#xff0c;数据中心面临诸多挑战。一方面&#xff0c;设备数量庞大且复杂&#xff0c;数据中心内服务器、存储设备、网络设备等遍布&#xff0c;这些设备需时刻保持良好运行状态&#xff0c;因为任何一个环节出现问题都可能带来严重后…

java项目之电影评论网站(springboot)

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的电影评论网站。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 电影评论网站的主要使用者管…

如何在 Ubuntu 24.04 上安装多PHP版本 (从8.3到5.6) ?

PHP 代表超文本预处理器&#xff0c;它仍然是网络的基石&#xff0c;为互联网上很大一部分网站和网络应用程序提供动力。大多数顶级网站和博客工具仍然使用 PHP&#xff0c;如 WordPress, Facebook, Wikipedia 等。如果你在 Ubuntu 24.04 上为 web 开发&#xff0c;安装 PHP 可…

算法的学习笔记—数组中只出现一次的数字(牛客JZ56)

&#x1f600;前言 在数组中寻找只出现一次的两个数字是一道经典的问题&#xff0c;通常可以通过位运算来有效解决。本文将详细介绍这一问题的解法&#xff0c;深入解析其背后的思路。 &#x1f3e0;个人主页&#xff1a;尘觉主页 文章目录 &#x1f970;数组中只出现一次的数字…

rtsp的2种收流模式

rtsp协商成功以后就是rtp收流&#xff0c;又分为两种模式:rtp over rtsp(tcp)和rtp over udp。 1.rtsp over rtsp 这个现在一般都叫TCP&#xff0c;它的特点是rtsp服务端和客户端是共用一个tcp链接&#xff0c;也就是说rtsp协议报文、rtp包、rtcp数据都是通过这一个链接来交互…

合约门合同全生命周期管理系统:企业合同管理的数字化转型之道

合约门合同全生命周期管理系统&#xff1a;企业合同管理的数字化转型之道 1. 引言 在现代企业中&#xff0c;合同管理已经不再是简单的文件存储和审批流程&#xff0c;而是企业合规性、风险管理和业务流程的关键环节之一。随着企业规模的扩大和合同数量的增加&#xff0c;传统…

第二单元历年真题整理

1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 参考答案 1. A 2. A 3. A 4. D 5. D 6. D 解析&#xff1a; 栈和队列是两个不一样的结构&#xff0c;不能放在一起表示 7. B 8. C 解析&#xff1a; S --> A0 | B1 --> (S1 | 1) 0 | (S0 | 0)1 --> S10 | 10 | S…

51单片机快速入门之 模拟 I2C 用精准中断来控制

51单片机快速入门之 模拟 I2C 用精准中断来控制 首先复习一下51单片机快速入门之定时器和计数器(含中断基础) 再看看之前的I2C操作 51单片机快速入门之 IIC I2C通信 定时器/计数器是51单片机中用于实现精确延时的硬件资源。通过配置定时器的初始值和工作模式&#xff0c;可以…

Unable to open nested entry ‘********.jar‘ 问题解决

今天把现网版本的task的jar拖回来然后用7-zip打开拖了一个jar进去替换mysql-connector-java-5.1.47.jar 为 mysql-connector-java-5.1.27.jar 启动微服务的时候就报错下面的 Exception in thread "main" java.lang.IllegalStateException: Failed to get nested ar…

《Python游戏编程入门》注-第2章2

《Python游戏编程入门》的“2.2.5 绘制线条”中提到了通过pygame库绘制线条的方法。 1 相关函数介绍 通过pygame.draw模块中的line()函数来绘制线条&#xff0c;该函数的格式如下所示。 line(surface, color, start_pos, end_pos, width1) -> Rect 其中&#xff0c;第一…

开源限流组件分析(二):uber-go/ratelimit

文章目录 本系列漏桶限流算法uber的漏桶算法使用mutex版本数据结构获取令牌松弛量 atomic版本数据结构获取令牌测试漏桶的松弛量 总结 本系列 开源限流组件分析&#xff08;一&#xff09;&#xff1a;juju/ratelimit开源限流组件分析&#xff08;二&#xff09;&#xff1a;u…