Netty Review - 深入探讨Netty的心跳检测机制:原理、实战、IdleStateHandler源码分析

文章目录

  • 概述
  • 心跳检测
  • Code
    • 模拟心跳超时
    • 正常情况
  • IdleStateHandler源码分析
    • channelRead
    • channelActive
      • initialize

在这里插入图片描述


概述

在这里插入图片描述


心跳检测

Netty 的心跳检测机制是一种用于保持网络连接活跃的机制,它通过定期发送和接收特定的消息(心跳包)来确保客户端和服务器之间的连接仍然有效。这种机制对于需要长时间保持连接的应用程序(如实时通信、监控、推送服务等)非常重要,因为它可以帮助检测连接是否因网络问题或客户端崩溃而断开。

Netty 提供了心跳检测机制,用于检测连接是否仍然处于活动状态。在 TCP 连接中,如果连接断开了,服务端和客户端不会立即知道它已经断开。因此,通过发送心跳消息并等待对方的响应,可以检测连接是否仍然处于活动状态。

Netty 提供了两种方式来实现心跳检测:

  1. 使用 TCP 层的 KeepAlive 机制。该机制默认的心跳时间是 2 小时,依赖操作系统实现,不够灵活。

  2. 使用 Netty 的 IdleStateHandler。IdleStateHandler 是 Netty 提供的空闲状态处理器,可以自定义检测间隔时间。通过设置 IdleStateHandler 的构造函数中的参数,可以指定读空闲检测的时间、写空闲检测的时间和读写空闲检测的时间。将它们设置为 0 表示禁用该类型的空闲检测。

IdleStateHandler 的构造函数如下:

public IdleStateHandler(long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit)
  • readerIdleTime:读的空闲时间,超出此时间就会发送一个心跳检测包,检测是否连接。
  • writerIdleTime:写的空闲时间,超出此时间就会发送一个心跳检测包,检测是否连接。
  • allIdleTime:读写的空闲时间,超出此时间就会发送一个心跳检测包,检测是否连接。
  • unit:空闲时间单位,默认为秒(TimeUnit.SECONDS)。

当满足上述其中一个条件后,就会自动触发 IdleStateEvent,会传递给管道中的下一个 handler 的 user在这里插入代码片EventTriggered 事件去处理。

在服务端,可以添加 IdleStateHandler 心跳检测处理器,并添加自定义处理 handler 类实现 userEventTriggered() 方法作为超时事件的逻辑处理。

例如,可以设置每 5 秒进行一次读检测,如果 5 秒内 ChannelRead() 方法未被调用则触发一次 userEventTrigger() 方法。

在客户端,可以启动客户端后,先发送一个 “hello” 消息,然后等候服务端心跳信息 “ping”,收到心跳后,回复心跳响应 “ok”。心跳消息可以根据需要进行定义。

通过 Netty 的心跳检测机制,可以有效地维护长连接,保证连接的有效性,避免浪费服务端资源。


Code

package com.artisan.heartbeat;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;
import java.util.concurrent.TimeUnit;
/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
public class HeartBeatArtisanServer {
    public static void main(String[] args) throws Exception {
        // 创建主从事件循环组
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        try {
            // 创建服务器启动类
            ServerBootstrap bootstrap = new ServerBootstrap();
            // 配置事件循环组
            bootstrap.group(boss, worker)
                    .channel(NioServerSocketChannel.class) // 使用NioServerSocketChannel作为通道
                    .childHandler(new ChannelInitializer<SocketChannel>() { // 初始化子通道
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline(); // 获取管道
                            pipeline.addLast("decoder", new StringDecoder()); // 添加解码器
                            pipeline.addLast("encoder", new StringEncoder()); // 添加编码器
                            // 添加空闲状态处理器,设置3秒的读空闲时间
                            pipeline.addLast(new IdleStateHandler(3, 0, 0, TimeUnit.SECONDS));
                            pipeline.addLast(new HeartBeatArtisanServerHandler()); // 添加自定义处理器
                        }
                    });
            System.out.println("netty server start。。"); // 打印启动信息
            // 绑定端口并同步等待成功
            ChannelFuture future = bootstrap.bind(9000).sync();
            // 等待服务器socket关闭
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace(); // 打印异常信息
        } finally {
            // 释放资源,优雅地关闭事件循环组
            worker.shutdownGracefully();
            boss.shutdownGracefully();
        }
    }
}

一个简单的 Netty 服务器示例,它使用 NioServerSocketChannel 作为服务器通道,并设置了一个 IdleStateHandler 来处理空闲状态。如果客户端在 3 秒内没有发送任何消息,服务器将触发一个 IdleStateEvent 事件,并传递给管道中的下一个处理器,即 HeartBeatArtisanServerHandler。这个自定义处理器需要实现 userEventTriggered 方法来处理这个事件,例如发送一个心跳包以保持连接活跃。

main 方法中,我们创建了一个 ServerBootstrap 实例,并配置了事件循环组、通道类型、空闲状态处理器和自定义处理器。然后,我们绑定了一个端口,并等待服务器启动和关闭。

注意:在实际应用中,HeartBeatArtisanServerHandler 类需要实现 userEventTriggered 方法来处理 IdleStateEvent,请继续看


这段代码是一个Netty的ChannelHandler,用于处理服务器端的心跳包。

package com.artisan.heartbeat;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.timeout.IdleStateEvent;
/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
public class HeartBeatArtisanServerHandler extends SimpleChannelInboundHandler<String> {
    // 定义一个计数器,用于记录读空闲的次数
    int readIdleTimes = 0;
    // 当从通道读取到消息时,该方法将被调用
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
        System.out.println(" ====== > [server] message received : " + s);
        // 如果接收到的消息是"Heartbeat Packet",则回复"ok"
        if ("Heartbeat Packet".equals(s)) {
            ctx.channel().writeAndFlush("ok");
        } else {
            // 如果是其他信息,打印其他信息处理 ...
            System.out.println(" 其他信息处理 ... ");
        }
    }
    // 当Netty触发超时事件时,该方法将被调用
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        IdleStateEvent event = (IdleStateEvent) evt;
        // 定义一个字符串,用于存储超时事件的类型
        String eventType = null;
        // 根据事件的状态进行切换
        switch (event.state()) {
            case READER_IDLE:
                eventType = "读空闲";
                // 读空闲的计数加1
                readIdleTimes++; 
                break;
            case WRITER_IDLE:
                eventType = "写空闲";
                // 不处理
                break;
            case ALL_IDLE:
                eventType = "读写空闲";
                // 不处理
                break;
        }
        
        // 打印出超时事件的信息
        System.out.println(ctx.channel().remoteAddress() + "超时事件:" + eventType);
        // 如果读空闲的次数超过3次,则关闭连接,释放更多资源
        if (readIdleTimes > 3) {
            System.out.println(" [server]读空闲超过3次,关闭连接,释放更多资源");
            ctx.channel().writeAndFlush("idle close");
            ctx.channel().close();
        }
    }
    // 当通道激活时,即连接成功建立时,该方法将被调用
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.err.println("=== " + ctx.channel().remoteAddress() + " is active ===");
    }
}

这段代码定义了一个Netty的ChannelHandler,用于处理网络连接中的心跳包。这个处理器继承自SimpleChannelInboundHandler<String>,意味着它主要用于处理字符串类型的消息。

  • channelRead0方法:当从通道读取到消息时,该方法将被调用。在这里,它检查接收到的消息是否是"Heartbeat Packet",如果是,则回复"ok",否则打印其他信息处理。
  • userEventTriggered方法:该方法用于处理Netty的超时事件。Netty会定期检查通道是否处于空闲状态,这里的空闲指的是没有读写操作发生。如果有超时事件,Netty将触发此方法。在这个方法中,它统计读空闲的次数,如果超过3次,则发送"idle close"消息并关闭连接。
  • channelActive方法:当通道激活时,即连接成功建立时,该方法将被调用。在这里,它打印出连接的远程地址。

简而言之: 这个处理器主要处理三种类型的超时事件:读空闲、写空闲和读写空闲。当接收到心跳包时,会回复"ok",如果读空闲的次数超过3次,则会关闭连接。


【Client】

这段代码是一个简单的Netty客户端示例,用于发送心跳包到服务器。以下是详细的中文注释:

package com.artisan.heartbeat;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import java.util.Random;
/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
public class HeartBeatArtisanClient {
    public static void main(String[] args) throws Exception {
        // 创建一个NioEventLoopGroup,用于处理Netty的事件循环
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            // 创建一个Bootstrap对象,用于配置客户端
            Bootstrap bootstrap = new Bootstrap();
            // 设置EventLoopGroup和Channel类型
            bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class)
                    // 添加ChannelInitializer,用于初始化ChannelPipeline
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            // 获取ChannelPipeline并添加解码器、编码器和心跳处理handler
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            pipeline.addLast(new HeartBeatArtisanClientHandler());
                        }
                    });
            // 打印出Netty客户端启动信息
            System.out.println("netty client start。。");
            // 连接到服务器,并获取Channel对象
            Channel channel = bootstrap.connect("127.0.0.1", 9000).sync().channel();
            // 定义要发送的心跳包文本
            String text = "Heartbeat Packet";
            // 创建一个Random对象,用于生成随机数
            Random random = new Random();
            // 在循环中随机发送心跳包
            while (channel.isActive()) {
                int num = random.nextInt(8);
                Thread.sleep(num * 1000);
                channel.writeAndFlush(text);
            }
        } catch (Exception e) {
            // 打印异常信息
            e.printStackTrace();
        } finally {
            // 优雅地关闭EventLoopGroup,释放资源
            eventLoopGroup.shutdownGracefully();
        }
    }
    
}

HeartBeatArtisanClientHandler,它继承自SimpleChannelInboundHandler<String>。这个处理器用于处理从服务器接收到的字符串消息。

package com.artisan.heartbeat;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
/**
 * @author 小工匠
 * @version 1.0
 * @mark: show me the code , change the world
 */
public class HeartBeatArtisanClientHandler extends SimpleChannelInboundHandler<String> {
    // 重写channelRead0方法以处理接收到的消息
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        // 打印出接收到的消息
        System.out.println(" client received :" + msg);
        // 如果接收到的消息是"idle close",则关闭连接
        if (msg != null && msg.equals("idle close")) {
            // 打印出服务端关闭连接的信息
            System.out.println(" 服务端关闭连接,客户端也关闭");
            // 关闭客户端连接
            ctx.channel().closeFuture();
        }
    }
}

在这个处理器中,当接收到消息时,channelRead0方法会被调用。如果接收到的是"idle close"消息,处理器会打印一条信息表明服务端已经关闭了连接,并立即关闭客户端的连接。这种情况下,客户端不会继续发送心跳包,因为服务端已经不再接受连接。


请看 客户端中 HeartBeatArtisanClient

在这里插入图片描述

再看下 HeartBeatArtisanServer端的设置

在这里插入图片描述

模拟心跳超时

按照如上的设计, 随机 0 - 7

我们先后启动Server和Client观察

在这里插入图片描述

在这里插入图片描述

正常情况

在这里插入图片描述

重新测试一下

在这里插入图片描述

在这里插入图片描述

消息逐一对应 。


IdleStateHandler源码分析

在这里插入图片描述

channelRead

我们先看下IdleStateHandler中的channelRead方法

在这里插入图片描述
只是进行了透传,不做任何业务逻辑处理,让channelPipe中的下一个handler处理channelRead方法.

channelActive

在这里插入图片描述

initialize

initialize的方法,这是IdleStateHandler的核心源码

private void initialize(ChannelHandlerContext ctx) {
        // Avoid the case where destroy() is called before scheduling timeouts.
        // See: https://github.com/netty/netty/issues/143
        switch (state) {
        case 1:
        case 2:
            return;
        }

        state = 1;
        initOutputChanged(ctx);

        lastReadTime = lastWriteTime = ticksInNanos();
        if (readerIdleTimeNanos > 0) {
            readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
                    readerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
        if (writerIdleTimeNanos > 0) {
            writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
                    writerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
        if (allIdleTimeNanos > 0) {
            allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
                    allIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
    }

在这个方法中,首先通过switch语句检查当前的状态。如果状态已经是1或2,则不需要进行初始化。状态1通常表示初始化完成,状态2表示通道已经关闭。

然后将状态设置为1,并调用initOutputChanged方法来处理输出变化,这通常是为了确保通道的输出缓冲区大小与初始化时设置的大小相匹配。
接下来,设置最后读取和写入的时间为当前时间,这些时间戳将用于后续的空闲状态检测。
然后,根据设置的读空闲超时时间(readerIdleTimeNanos)、写空闲超时时间(writerIdleTimeNanos)和所有空闲超时时间(allIdleTimeNanos),分别调度对应的超时任务。这些任务会在指定的超时时间后被执行,以处理通道的空闲状态。

  • ReaderIdleTimeoutTask:如果通道在readerIdleTimeNanos内没有读取操作,这个任务将被触发。
  • WriterIdleTimeoutTask:如果通道在writerIdleTimeNanos内没有写入操作,这个任务将被触发。
  • AllIdleTimeoutTask:如果通道在allIdleTimeNanos内既没有读取操作也没有写入操作,这个任务将被触发。

一般我们都是使用 readerIdleTimeout ,所以我们需要重点关注

 if (readerIdleTimeNanos > 0) {
            readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
                    readerIdleTimeNanos, TimeUnit.NANOSECONDS);
        }

继续 ReaderIdleTimeoutTask
在这里插入图片描述

直接看run方法

@Override
        protected void run(ChannelHandlerContext ctx) {
            // 定义下一次触发空闲状态事件前的延迟时间
            long nextDelay = readerIdleTimeNanos;
            // 如果当前不在读取状态
            if (!reading) {
                // 减去自上次读取时间以来的纳秒数,以调整下一次的延迟
                nextDelay -= ticksInNanos() - lastReadTime;
            }

            // 如果延迟时间小于等于0,说明读已经空闲了足够长的时间
            if (nextDelay <= 0) {
                // 设置一个新的超时时间,并通过回调通知
                readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);

                // 是否是第一次触发读空闲事件
                boolean first = firstReaderIdleEvent;
                firstReaderIdleEvent = false; // 将标志设置为false,以避免后续的重复触发

                try {
                    // 创建一个空闲状态事件
                    IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
                    // 在通道上下文中触发这个事件
                    channelIdle(ctx, event);
                } catch (Throwable t) {
                    // 如果发生异常,传播这个异常到通道上下文
                    ctx.fireExceptionCaught(t);
                }
            } else {
                // 如果在超时之前发生了读取操作,安排一个较短延迟的超时
                readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
            }
        }
    }

  1. run方法首先定义了一个nextDelay变量,这个变量表示下一次触发空闲状态事件前的延迟时间。这个延迟时间由readerIdleTimeNanos决定,它是在IdleStateHandler构造函数中设置的。
  2. 如果reading标志为false,意味着通道当前不在读取状态,那么会从nextDelay中减去自上次读取时间以来的纳秒数,以调整下一次的延迟。
  3. 如果nextDelay小于或等于0,这意味着读者已经空闲了足够长的时间,需要设置一个新的超时时间,并通过回调通知。这里使用了schedule方法在ctx通道上下文中安排一个IdleStateHandler的执行,执行时间为readerIdleTimeNanos
  4. firstReaderIdleEvent标志用于标识是否是第一次触发读者空闲事件。如果是第一次,这个标志会被设置为false,以避免后续的重复触发。
  5. 代码尝试创建一个IdleStateEvent事件,并使用channelIdle方法在通道上下文中触发这个事件。这个事件会被传递给在ChannelPipeline中注册的IdleStateHandler的回调。
  6. 如果在上述过程中发生异常,使用ctx.fireExceptionCaught(t)方法在通道上下文中传播这个异常。
  7. 如果nextDelay大于0,说明在超时之前发生了读取操作,因此会安排一个较短延迟的超时。

我们举个例子来理解下哈

nextDelay -= ticksInNanos() - lastReadTime;

用当前时间减去最后一次channelRead方法调用的时间,假如这个结果是6s ,说明最后一次调用channelRead已经是6s
之前的事情了,你设置的是5s,那么nextDelay则为-1,说明超时了

那就会触发 channelIdle(ctx, event);

源码如下

    /**
     * Is called when an {@link IdleStateEvent} should be fired. This implementation calls
     * {@link ChannelHandlerContext#fireUserEventTriggered(Object)}.
     */
    protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
        ctx.fireUserEventTriggered(evt);
    }

如果没有超时则不触发userEventTriggered方法。

没有超时则走

// 如果在超时之前发生了读取操作,安排一个较短延迟的超时
readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);

这个run方法是Netty处理通道空闲状态的关键部分,它确保了在通道长时间未进行读取操作时能够触发相应的处理逻辑,从而避免资源浪费和潜在的连接问题

在这里插入图片描述

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

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

相关文章

Java复习二—CH3-Exception 、CH4-I/O

CH3 Java Exception (异常) 3.1 Exception&#xff08;异常&#xff09;和 Error&#xff08;错误&#xff09; 异常能被程序本身可以处理&#xff0c;错误是无法处理 大多数错误与代码编写者执行的操作无关&#xff0c;而表示代码运行时 JVM 出现的问题 异常发生的原因有很…

电子科大软件系统架构设计——软件建模详细设计

文章目录 软件建模详细设计概述软件建模详细设计目标软件建模详细设计原则开闭原则里氏 (Liskov) 替换原则依赖倒置原则接口分离原则单一职责原则最少知识原则&#xff08;迪米特法则&#xff09;高内聚原则松耦合原则可重用原则 软件建模详细设计内容 UML 软件静态结构视图建模…

现代控制理论-李雅普诺夫

现代控制理论-李雅普诺夫 单输入单输出系统&#xff08;BIBO&#xff09;的系统函数如下&#xff1a; 则&#xff0c;该系统的能控标准型&#xff08;能空性&#xff09;为&#xff1a; 能观性&#xff1a; 李雅普诺夫下的稳定性&#xff1a; 李雅普诺夫下的渐进稳定性&a…

Ubuntu20.04纯命令配置PCL(点云库)

Ubuntu20.04纯命令配置PCL&#xff08;点云库&#xff09; 最近在学习点云库&#xff08;PCL&#xff09;的使用&#xff0c;第一步就是在自己的电脑安装配置PCL。 首先&#xff0c;对于ubuntu 16.04以上版本&#xff0c;可以直接使用命令进行安装&#xff0c;新建好一个文件夹…

小程序radio单选框回显

话不多说&#xff0c;效果图如下&#xff1a; 具体代码如下&#xff1a; <radio-group name"radio" bindchange"getSex"><label><radio value"1" checked"{{xingbie1}}" />男</label><label><radio…

Python序列之元组

系列文章目录 Python序列之列表 Python序列之元组 系列文章目录前言一、元组是什么&#xff1f;二、元组操作1.元组的创建&#xff08;1&#xff09;通过()创建。小括号可以省略。&#xff08;2&#xff09;通过tuple()函数创建。&#xff08;3&#xff09;通过生成器推导式创…

HBase 集群搭建

文章目录 安装前准备兼容性官方网址 集群搭建搭建 Hadoop 集群搭建 Zookeeper 集群解压缩安装配置文件高可用配置分发 HBase 文件 服务的启停启动顺序停止顺序 验证进程查看 Web 端页面 安装前准备 兼容性 1&#xff09;与 Zookeeper 的兼容性问题&#xff0c;越新越好&#…

ubuntu docker 进入容器内使用ping 指令,提示bash: ping: command not found问题

解决方法 #更新软件列表 apt update #安装 ping 工具iputils-ping apt install -y iputils-ping #最后使用ping一下容器ping 172.18.0.3欢迎关注我的公众号“点滴分享技术猿”&#xff0c;原创技术文章第一时间推送。

WT2605C音频蓝牙语音芯片:单芯片实现蓝牙+MP3+BLE+电话本多功能应用

在当今的电子产品领域&#xff0c;多功能、高集成度成为了一种趋势。各种产品都需要具备多种功能&#xff0c;以满足用户多样化的需求。针对这一市场趋势&#xff0c;唯创知音推出了一款集成了蓝牙、MP3播放、BLE和电话本功能的音频蓝牙语音芯片——WT2605C&#xff0c;实现了单…

超维空间S2无人机使用说明书——21、VINS视觉定位仿真

引言&#xff1a;为了实现室内无人机的定位功能&#xff0c;S系列无人机配置了VINS-FUSION定位环境&#xff0c;主要包含了仿真跑数据集和实际操作部分。为了提前熟悉使用原理&#xff0c;可以先使用仿真环境跑数据集进行学习和理解 硬件&#xff1a;1080P显示器、Jetson orin…

博弈论:理解决策背后的复杂动态

1.基本概念 博弈论是一门研究具有冲突和合作元素决策制定的数学理论。它不仅适用于经济学&#xff0c;还广泛应用于政治学、心理学、生物学等领域。博弈论的核心在于分析参与者&#xff08;称为“玩家”&#xff09;在特定情境下的策略选择&#xff0c;以及这些选择如何影响最…

真实工作中,做接口测试的流程一般是怎么样的?一定要学透彻

在讲接口流程测试之前&#xff0c;首先需要给大家申明下&#xff1a;接口测试对于测试人员而言&#xff0c;非常非常重要&#xff0c;懂功能测试接口测试&#xff0c;就能在企业中拿到一份非常不错的薪资。 这么重要的接口测试&#xff0c;一般也是面试笔试必问。为方便大家更…

界面控件DevExpress WinForms PDF Viewer,让应用轻松显示PDF文档(一)

DevExpress WinForms的PDF Viewer&#xff08;查看器&#xff09;控件使用户可以轻松地在Windows应用程序中直接显示PDF文档&#xff0c;而无需在最终用户的机器上安装外部PDF阅读器。 DevExpress WinForms有180组件和UI库&#xff0c;能为Windows Forms平台创建具有影响力的业…

【贪心】最小生成树Kruskal算法Python实现

文章目录 [toc]问题描述最小生成树的性质证明 Kruskal算法时间复杂性Python实现 个人主页&#xff1a;丷从心 系列专栏&#xff1a;贪心算法 问题描述 设 G ( V , E ) G (V , E) G(V,E)是无向连通带权图&#xff0c; E E E中每条边 ( v , w ) (v , w) (v,w)的权为 c [ v ] …

听GPT 讲Rust源代码--src/tools(24)

File: rust/src/tools/clippy/clippy_lints/src/types/borrowed_box.rs 在Rust源代码中的rust/src/tools/clippy/clippy_lints/src/types/borrowed_box.rs文件是Clippy项目的一个规则&#xff0c;用于检查可能是误用或错误的Box引用情况。 Rust中的Box是一个堆分配的值的所有权…

京东员工:35岁,我只是年龄大了,又不是傻了残疾了,为啥找不到与3-5年经验者平等的面试机会?...

* 你好&#xff0c;我是前端队长&#xff0c;在职场&#xff0c;玩副业&#xff0c;文末有福利! 年龄大了&#xff0c;并不代表我们已经过时了。超过35岁的我们&#xff0c;面对着职场的冷风&#xff0c;经验丰富却常被误解。为什么年轻人能轻松得到的面试机会&#xff0c;到我…

【C语言刷题每日一题#牛客网BC68】——X形图案

问题描述 思路分析 首先根据输入的描述&#xff0c;多组输入需要将scanf放在循环中来实现 #include<stdio.h> int main() {int a 0;while (scanf("%d", &a) ! EOF){} } 完成了输入之后&#xff0c;再来分析输出——输出的是一个由“*”组成的对称的X形…

vue3项目 - 目录调整

省流 删除默认文件&#xff0c;修改代码 ---> 调整目录结构 ---> 添加全局样式和图片&#xff0c;安装预处理器 具体步骤&#xff1a; 1. 删除初始化的默认文件&#xff0c;修改剩余代码 清空 assets、components、stores、views文件夹里的文件&#xff0c;仅留核心文件…

【数据结构】布隆过滤器原理详解及其代码实现

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能AI、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推荐--…

神经网络:深度学习优化方法

1.有哪些方法能提升CNN模型的泛化能力 采集更多数据&#xff1a;数据决定算法的上限。 优化数据分布&#xff1a;数据类别均衡。 选用合适的目标函数。 设计合适的网络结构。 数据增强。 权值正则化。 使用合适的优化器等。 2.BN层面试高频问题大汇总 BN层解决了什么问…
最新文章