Nettyの参数优化简单RPC框架实现

news2024/11/17 18:25:38

本篇介绍Netty调优,在上篇聊天室的案例中进行改造,手写一个简单的RPC实现。

1、超时时间参数

        CONNECT_TIMEOUT_MILLIS 是Netty的超时时间参数,属于客户端SocketChannel的参数,客户端连接时如果一定时间没有连接上,就会抛出 timeout 异常

        如何在代码中添加参数?在new Bootstrap()时使用

.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 300)

        启动客户端,不启动服务器,发现连接超时

        打上断点(选择多线程模式):

        这一行获取到的值是创建BootStrap时添加CONNECT_TIMEOUT_MILLIS 的值(300)

int connectTimeoutMillis = config().getConnectTimeoutMillis();

         满足条件,进入If块:

         这是一个定时任务,延迟CONNECT_TIMEOUT_MILLIS 的值(300)后触发,执行Runnable中的逻辑,抛出超时异常。

connectTimeoutFuture = eventLoop().schedule(new Runnable() {
  @Override
  public void run() {
     ChannelPromise = AbstractNioChannel.this.connectPromise;
     ConnectTimeoutException cause = new ConnectTimeoutException("connection timed out: " + remoteAddress);
          if (connectPromise != null && connectPromise.tryFailure(cause)) {
                 close(voidPromise());
          }
     }
}, connectTimeoutMillis, TimeUnit.MILLISECONDS);

        主线程和NIO线程也是通过connectPromise进行异步通信的,两个线程持有的是同一个connectPromise对象。

2、SO_BACKLOG

        SO_BACKLOG是一个与服务器套接字相关的参数,主要用于配置服务器套接字的接受队列大小,属于ServerSocketChannel的参数

什么是套接字?

套接字是计算机网络中的一种通信端点,用于在两个节点之间建立连接并进行数据传输,它包含了IP地址端口号,通过这两个标识符,网络上的设备可以相互定位和通信。

套接字在客户端和服务器的工作顺序:

服务器端:

  1. 创建套接字。
  2. 绑定到指定的IP地址和端口号。
  3. 监听连接请求。
  4. 接受连接,创建一个新的套接字用于与客户端通信。
  5. 读取或写入数据。

客户端

  1. 创建套接字。
  2. 连接到服务器的IP地址和端口号。
  3. 读取或写入数据。

         而SO_BACKLOG参数决定了服务器套接字在操作系统内核中维护的一个挂起连接队列的最大长度。

        当一个服务器应用程序启动并监听某个端口时,它会创建一个服务器套接字,用于等待客户端的连接请求。

        当客户端尝试连接服务器时,连接请求会首先进入服务器端的一个等待队列,称为挂起连接队列。这个队列中的连接请求还没有被服务器应用程序正式接受处理。

        在大多数操作系统中,套接字操作是由操作系统内核负责管理的。内核会为每个监听中的服务器套接字维护一个挂起连接队列。

        如果参数设置的如果队列已满,新的连接请求将被拒绝或被操作系统忽略。

        假设SO_BACKLOG设置为50,这意味着挂起连接队列的最大长度是50。当第51个连接请求到达时,如果前面的请求还没有被处理,新的请求将被拒绝。

3、ulimit -n

ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.childOption(ChannelOption.TCP_NODELAY, true);

       ulimit -n控制了操作系统中一个进程可以打开的最大文件描述符(file descriptor)数量。

什么是文件描述符?

文件描述符是操作系统内核用于管理打开文件的一个抽象概念,包括普通文件、套接字、管道等。每个打开的文件、网络连接都会占用一个文件描述符。

        为什么要设置最大文件描述符

        Netty 是一个高性能的网络框架,设计用于处理大量并发连接。如果文件描述符的限制太低,当连接数超过此限制时,服务器将无法接受新的连接,这将导致连接失败。 

4、TCP_NODELAY

        TCP_NODELAY 是 TCP 协议中的一个选项,用于控制 Nagle 算法的启用或禁用。

        Nagle 算法 在前篇中有所提及,简单的说,当发送方有小数据包要发送时,如果前一个数据包的确认(ACK)尚未收到,Nagle 算法会将这些小数据包暂时存储起来,直到收到前一个数据包的确认或足够多的数据可以组成一个较大的数据包。

        可以通过以下的代码设置是否开启Nagle 算法 ,同样地,这个参数属于ServerSocketChannel

ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.childOption(ChannelOption.TCP_NODELAY, true);

        什么场景下应该禁用Nagle 算法

  • 实时应用:在需要低延迟的实时应用中(例如在线游戏、实时通信应用)。
  • 小数据包频繁发送:如果应用程序频繁发送小数据包,并且对每个数据包的发送延迟敏感。

5、SO_SNDBUF & SO_RCVBUF

        SO_SNDBUF & SO_RCVBUFSO_BACKLOG类似,也是与网络套接字相关的两个重要参数,用于配置发送和接收缓冲区的大小。

        发送缓冲区用于临时存储应用程序要发送到网络的数据。

        接收缓冲区用于临时存储从网络接收到的数据,直到应用程序读取它们。

        缓冲区过大可能增加延迟,因为数据在缓冲区中停留的时间更长;缓冲区过小可能导致频繁的缓冲区溢出和数据包丢失。

        可以通过以下的代码进行设置:

ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.childOption(ChannelOption.SO_SNDBUF, 32 * 1024); // 发送缓冲区大小32KB
bootstrap.childOption(ChannelOption.SO_RCVBUF, 32 * 1024); // 接收缓冲区大小32KB

        如何选择合适的缓冲区大小

  1. 根据网络带宽和延迟:在高带宽和高延迟的网络环境中,需要更大的缓冲区来充分利用带宽。例如,宽带网络和跨国连接可能需要更大的缓冲区。
  2. 根据应用需求:不同的应用有不同的需求。实时应用(如视频流和在线游戏)通常需要较小的缓冲区以减少延迟,而大数据传输(如文件下载和大数据处理)可能需要较大的缓冲区以提高吞吐量。
  3. 测试和调优:最佳的缓冲区大小通常需要通过测试和调优来确定。可以通过逐步调整缓冲区大小并监测网络性能来找到最佳配置。

6、ALLOCATOR

        ALLOCATOR 参数用于配置 ByteBuf 分配器,ByteBuf的相关概念在前篇中也提到过,大致可以分为池化和非池化:

  • PooledByteBufAllocator:池化分配器,重复使用内存以减少分配和释放内存的开销,适用于高并发和性能敏感的应用。
  • UnpooledByteBufAllocator:非池化分配器,每次都进行新的内存分配,适用于内存使用模式不可预测的应用。

        可以通过以下代码进行设置:

ServerBootstrap bootstrap = new ServerBootstrap();

// 使用池化分配器
bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

// 使用非池化分配器
// bootstrap.option(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT);

7、RCVBUF_ALLOCATOR

        RCVBUF_ALLOCATOR 是一个用于管理接收缓冲区大小的机制,用于确定和管理网络连接上每次读取操作时分配的字节缓冲区的大小。

        它是一个接口,常用的实现类有:

  • FixedRecvByteBufAllocator:每次读操作分配固定大小的缓冲区。

   

  • DefaultMaxBytesRecvByteBufAllocator:一个可以限制每次读取消息数量的实现。

  •  AdaptiveRecvByteBufAllocator:根据流量动态调整缓冲区大小,这是最常用的实现之一。  

8、RPC简单实现

        接下来会通过一个案例实现简单的RPC框架。

什么是RPC框架?

RPC(Remote Procedure Call,远程过程调用)框架是一种使程序能够通过网络调用远程服务器上的函数或方法的技术。

在表面上这种调用方式对用户是透明的,就像调用本地函数一样简单,但实际上底层会通过网络协议进行通信。

        8.1、1.0版      

        首先需要新增RPC的请求和响应消息:

@Data
public abstract class Message implements Serializable {

    // 省略旧的代码

    public static final int RPC_MESSAGE_TYPE_REQUEST = 101;
    public static final int  RPC_MESSAGE_TYPE_RESPONSE = 102;

    static {
        // ...
        messageClasses.put(RPC_MESSAGE_TYPE_REQUEST, RpcRequestMessage.class);
        messageClasses.put(RPC_MESSAGE_TYPE_RESPONSE, RpcResponseMessage.class);
    }

}

         然后定义一个RPC请求消息类,在请求消息类中,包括了调用接口及接口中方法的信息:

@Getter
@ToString(callSuper = true)
public class RpcRequestMessage extends Message {

    /**
     * 调用的接口全限定名,服务端根据它找到实现
     */
    private String interfaceName;
    /**
     * 调用接口中的方法名
     */
    private String methodName;
    /**
     * 方法返回类型
     */
    private Class<?> returnType;
    /**
     * 方法参数类型数组
     */
    private Class[] parameterTypes;
    /**
     * 方法参数值数组
     */
    private Object[] parameterValue;

    public RpcRequestMessage(int sequenceId, String interfaceName, String methodName, Class<?> returnType, Class[] parameterTypes, Object[] parameterValue) {
        super.setSequenceId(sequenceId);
        this.interfaceName = interfaceName;
        this.methodName = methodName;
        this.returnType = returnType;
        this.parameterTypes = parameterTypes;
        this.parameterValue = parameterValue;
    }

    @Override
    public int getMessageType() {
        return RPC_MESSAGE_TYPE_REQUEST;
    }
}

        再定义一个响应消息类,包括正常返回的值以及发生异常时的返回值。

@Data
@ToString(callSuper = true)
public class RpcResponseMessage extends Message {
    /**
     * 返回值
     */
    private Object returnValue;
    /**
     * 异常值
     */
    private Exception exceptionValue;

    @Override
    public int getMessageType() {
        return RPC_MESSAGE_TYPE_RESPONSE;
    }
}

        定义一个获取配置文件中接口实现类的工厂类:

public class ServicesFactory {

    static Properties properties;
    static Map<Class<?>, Object> map = new ConcurrentHashMap<>();

    static {
        try (InputStream in = SerializedConfig.class.getResourceAsStream("/application.properties")) {
            properties = new Properties();
            properties.load(in);
            Set<String> names = properties.stringPropertyNames();
            for (String name : names) {
                if (name.endsWith("Service")) {
                    Class<?> interfaceClass = Class.forName(name);
                    Class<?> instanceClass = Class.forName(properties.getProperty(name));
                    map.put(interfaceClass, instanceClass.newInstance());
                }
            }
        } catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    public static <T> T getService(Class<T> interfaceClass) {
        return (T) map.get(interfaceClass);
    }
}

        application.properties 

cn.itcast.server.service.HelloService=cn.itcast.server.service.HelloServiceImpl

       HelloService接口及实现类:

public interface HelloService {
    String sayHello(String name);
}
public class HelloServiceImpl implements HelloService {
    @Override
    public String sayHello(String msg) {
//        int i = 1 / 0;
        return "你好, " + msg;
    }
}

        准备RPC服务器端和客户端的代码,和聊天室案例类似,但是加上了对应的RPC请求消息和响应消息的处理器:

        服务器端:

/**
*
* RPC服务器端
**/
@Slf4j
public class RpcServer {
    public static void main(String[] args) {
        NioEventLoopGroup boss = new NioEventLoopGroup();
        NioEventLoopGroup worker = new NioEventLoopGroup();
        LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);
        MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();

        RpcRequestMessageHandler RPC_HANDLER = new RpcRequestMessageHandler();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.group(boss, worker);
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ProcotolFrameDecoder());
                    ch.pipeline().addLast(LOGGING_HANDLER);
                    ch.pipeline().addLast(MESSAGE_CODEC);
                    ch.pipeline().addLast(RPC_HANDLER);
                }
            });
            Channel channel = serverBootstrap.bind(8080).sync().channel();
            channel.closeFuture().sync();
        } catch (InterruptedException e) {
            log.error("server error", e);
        } finally {
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }
    }
}

        客户端:

/**
*
*RPC 客户端
**/
public class RpcClient {
    public static void main(String[] args) {
        NioEventLoopGroup group = new NioEventLoopGroup();
        LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);
        MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();
        
        // rpc 响应消息处理器,待实现
        RpcResponseMessageHandler RPC_HANDLER = new RpcResponseMessageHandler();
        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.group(group);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new ProcotolFrameDecoder());
                    ch.pipeline().addLast(LOGGING_HANDLER);
                    ch.pipeline().addLast(MESSAGE_CODEC);
                    ch.pipeline().addLast(RPC_HANDLER);
                }
            });
            Channel channel = bootstrap.connect("localhost", 8080).sync().channel();
            channel.closeFuture().sync();
        } catch (Exception e) {
            log.error("client error", e);
        } finally {
            group.shutdownGracefully();
        }
    }
}

        先编写服务器端的自定义RPC消息处理器RpcRequestMessageHandler

@Slf4j
@ChannelHandler.Sharable
public class RpcRequestMessageHandler extends SimpleChannelInboundHandler<RpcRequestMessage> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RpcRequestMessage message) {
        RpcResponseMessage responseMessage = new RpcResponseMessage();
        int sequenceId = message.getSequenceId();
        responseMessage.setSequenceId(sequenceId);
        try {
            //获取RPC消息对象中将要调用的接口的实现类 写在配置文件中
            HelloService service = (HelloService) ServicesFactory.getService(Class.forName(message.getInterfaceName()));
            //获取实现类中的方法
            Method method = service.getClass().getMethod(message.getMethodName(), message.getParameterTypes());
            //通过反射调用方法
            Object result = method.invoke(service, message.getParameterValue());
            responseMessage.setReturnValue(result);
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
            responseMessage.setExceptionValue(e);
        }
        //触发出站事件
        ctx.writeAndFlush(responseMessage);
    }
}

        通过main方法测试一下:

/**
     * 测试代码
     * @param args
     * @throws ClassNotFoundException
     * @throws NoSuchMethodException
     * @throws InvocationTargetException
     * @throws IllegalAccessException
     */
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        //封装RPC消息对象
        RpcRequestMessage message = new RpcRequestMessage(
                1,
                "cn.itcast.server.service.HelloService",
                "sayHello",
                String.class,
                new Class[]{String.class},
                new Object[]{"张三"});
        //获取RPC消息对象中将要调用的接口的实现类
        HelloService service = (HelloService) ServicesFactory.getService(Class.forName(message.getInterfaceName()));
        //获取实现类中的方法
        Method method = service.getClass().getMethod(message.getMethodName(), message.getParameterTypes());
        //调用方法
        Object result = method.invoke(service, message.getParameterValue());
        System.out.println(result);

    }

        编写客户端的代码以及自定义RPC消息返回处理器RpcResponseMessageHandler 暂时只将接收到的消息返回出去:

@Slf4j
@ChannelHandler.Sharable
public class RpcResponseMessageHandler extends SimpleChannelInboundHandler<RpcResponseMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RpcResponseMessage msg) throws Exception {
        log.debug("{}", msg);
    }
}

        改造客户端的代码,发送调用方法请求:

   ChannelFuture future = channel.writeAndFlush(new RpcRequestMessage(
                    1,
                    "cn.itcast.server.service.HelloService",
                    "sayHello",
                    String.class,
                    new Class[]{String.class},
                    new Object[]{"张三"}
            )).addListener(promise -> {
                if (!promise.isSuccess()) {
                    Throwable cause = promise.cause();
                    log.error("error", cause);
                }
            });

        它的执行顺序是:

        客户端发送消息,触发所有出站处理器:

        然后到服务器:

        在RpcRequestMessageHandler 中无论消息处理是否报错,都会触发出站处理器将返回值传递给客户端:

        最后再回到客户端:

        注意:LOGGING_HANDLER和MESSAGE_CODEC是双向处理,既可以是入站,也可以是出站!


        这样一个简单的RPC通信案例就已经实现了。

        8.2、2.0版

        但是在第一版中,用户在客户端发送调用请求时,需要自己封装RpcRequestMessage 请求对象,参数复杂,换做是我是绝对不愿意这样做的。那么我们对其进行优化。

        改造客户端,首先定义一个成员变量channel:

private static volatile Channel channel = null;

        然后将原有客户端的代码抽取成一个初始化channel的方法:

 private static void initChannel() {
        NioEventLoopGroup group = new NioEventLoopGroup();
        LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);
        MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();
        RpcResponseMessageHandler RPC_HANDLER = new RpcResponseMessageHandler();

        Bootstrap bootstrap = new Bootstrap();
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.group(group);
        bootstrap.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ch.pipeline().addLast(new ProcotolFrameDecoder());
                //双向事件
                ch.pipeline().addLast(LOGGING_HANDLER);
                ch.pipeline().addLast(MESSAGE_CODEC);
                //入站事件
                ch.pipeline().addLast(RPC_HANDLER);
            }
        });
        try {
            channel = bootstrap.connect("localhost", 8080).sync().channel();
            channel.closeFuture().addListener(future -> {
                group.shutdownGracefully();
            });
        } catch (Exception e) {
            log.error("client error", e);
        }
    }

        这个channel只应该存在一个实例,采用双检锁单例的方式获取:

  /**
     * 初始化单例channel
     * @return
     */
    private static Channel getChannel(){
        if (channel != null){
            return channel;
        }
        synchronized (LOCK){
            if (channel!=null){
                return channel;
            }
            initChannel();
            return channel;
        }
    }

        复习一下,为什么要使用双检锁模式?

        (成员位置的channel可以不用volatile关键字?此时的channel对象不是走构造方法new出来的)

         然后创建一个代理对象代理对象负责将请求参数打包并发送给远程服务器:

 public static <T> T getProxyService(Class<T> serviceClass){

        ClassLoader classLoader = serviceClass.getClassLoader();
        Class<?>[] interfaces = new Class[]{serviceClass};
        Object o = Proxy.newProxyInstance(classLoader, interfaces, (proxy, method, args) -> {
            //将方法调用转换成消息对象
            int sequenceId = SequenceIdGenerator.nextId();
            RpcRequestMessage message = new RpcRequestMessage(
                    sequenceId,
                    serviceClass.getName(),
                    method.getName(),
                    method.getReturnType(),
                    method.getParameterTypes(),
                    args);

            //发送消息
            Channel channel = getChannel();
            channel.writeAndFlush(message);

            //异步通信获取结果
            DefaultPromise<Object> objectDefaultPromise = new DefaultPromise<>(channel.eventLoop());
            //向PROMISE中注册ID和DefaultPromise
            RpcResponseMessageHandler.PROMISES.put(sequenceId,objectDefaultPromise);
            //等待结果
            objectDefaultPromise.await();

            if (objectDefaultPromise.isSuccess()){
                return objectDefaultPromise.getNow();
            }else {
                throw new RuntimeException(objectDefaultPromise.cause());
            }
        });
        return (T) o;

     
    }

        重点在于向客户端接收服务器响应的RpcResponseMessageHandler  中注册自己的消息ID和promise对象。

//向PROMISE中注册ID和DefaultPromise
RpcResponseMessageHandler.PROMISES.put(sequenceId,objectDefaultPromise);

        这样用户只需要调用代理对象的方法就可以了:

public static void main(String[] args) {
   HelloService helloService = getProxyService(HelloService.class);
   System.out.println(helloService.sayHello("张三"));
}

        同时需要修改客户端接受服务器响应的RpcResponseMessageHandler ,去找到对应消息ID的promise对象,并且移除,然后根据服务器返回的结果写入成功或异常情况,这时客户端的

//等待结果
objectDefaultPromise.await();

        获取到了结果,进行最后的处理。

/**
 * 接受服务器的响应
 */
@Slf4j
@ChannelHandler.Sharable
public class RpcResponseMessageHandler extends SimpleChannelInboundHandler<RpcResponseMessage> {

    /**
     * k:消息id
     * v:消息ID对应的promise对象
     */
    public static final ConcurrentHashMap<Integer, Promise<Object>> PROMISES = new ConcurrentHashMap<>();

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RpcResponseMessage msg) throws Exception {
        int sequenceId = msg.getSequenceId();
        Promise<Object> promise = PROMISES.remove(sequenceId);
        Exception exceptionValue = msg.getExceptionValue();
        Object returnValue = msg.getReturnValue();
        if (exceptionValue == null) {
            promise.setSuccess(returnValue);
        } else {
            promise.setFailure(exceptionValue);
        }

    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构】常见四类排序算法

1. 插入排序 1.1基本思想&#xff1a; 直接插入排序是一种简单的插入排序法&#xff0c;其基本思想是&#xff1a;把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中&#xff0c;直到所有的记录插入完为止&#xff0c;得到一个新的有序序列 。实际中我们…

Windows Server 2016 搭建 网络负载平衡 服务

网络负载平衡功能的安装 添加角色 默认不动————功能 勾选上 < 网络负载平衡 > 在工具中————打开 < 网络负载平衡管理器 > 网络负载平衡群集创建 注意 : 提前 将两台 web 站点服务器 都安装好 < 网络负载平衡功能 > 右键 选择 ————新建群集 ——…

麦蕊智数,,另外一个提供免费的股票数据API,可以通过其提供的接口获取实时和历史的股票数据。

麦蕊智数&#xff0c;&#xff0c;提供免费的股票数据API&#xff0c;可以通过其提供的接口获取实时和历史的股票数据。 API接口&#xff1a;http://api.mairui.club/hslt/new/您的licence 备用接口&#xff1a;http://api1.mairui.club/hslt/new/您的licence 请求频率&#x…

yolov5 json 和 txt数据格式关系

训练阶段 和 推理阶段数据格式转换说明 关于yolov5 数据格式一直以来都傻傻分不清楚&#xff0c;这下进行了一个梳理&#xff0c;做了笔记&#xff0c;也希望可帮助到有需要的有缘人~ 转换部分代码

使用JAR命令打包JAR文件使用Maven打包使用Gradle打包打包Spring Boot应用

本人详解 作者:王文峰,参加过 CSDN 2020年度博客之星,《Java王大师王天师》 公众号:JAVA开发王大师,专注于天道酬勤的 Java 开发问题中国国学、传统文化和代码爱好者的程序人生,期待你的关注和支持!本人外号:神秘小峯 山峯 转载说明:务必注明来源(注明:作者:王文峰…

跨平台书签管理器 - Raindrop

传统的浏览器书签功能虽然方便&#xff0c;但在管理和分类上存在诸多局限。今天&#xff0c;我要向大家推荐一款功能强大的跨平台书签管理-Raindrop https://raindrop.io/ &#x1f4e2; 主要功能&#xff1a; 智能分类和标签管理 强大的搜索功能 跨平台支持 分享与协作 卡片式…

电脑f盘的数据回收站清空了能恢复吗

随着信息技术的飞速发展&#xff0c;电脑已成为我们日常生活和工作中不可或缺的设备。然而&#xff0c;数据的丢失或误删往往会给人们带来极大的困扰。尤其是当F盘的数据在回收站被清空后&#xff0c;许多人会陷入绝望&#xff0c;认为这些数据已无法挽回。但事实真的如此吗&am…

芯片基识 | 掰开揉碎讲 FIFO(同步FIFO和异步FIFO)

文章目录 一、什么是FIFO二、为什么要用FIFO三、什么时候用FIFO四、FIFO分类五、同步FIFO1. 同步FIFO电路框图2. 同步FIFO空满判断3. 同步FIFO设计代码4. 同步FIFO仿真结果 六、异步FIFO1、异步FIFO的电路框图2 、亚稳态3、打两拍4、格雷码5、如何判断异步FIFO的空满&#xff0…

Selenium的自动化测试技巧有多少?【建议收藏】

Selenium是一个用于自动化Web应用程序测试的工具。它提供了一组API&#xff0c;允许用户与Web浏览器进行交互&#xff0c;来执行各种自动化测试任务。本文将从零开始&#xff0c;详细介绍Selenium的自动化测试技巧。 第一步&#xff1a;安装Selenium 首先&#xff0c;您需要安…

一站式天气预报解决方案,API接口轻松接入

天气对我们的日常生活有着重要的影响&#xff0c;无论是出门旅行还是安排工作&#xff0c;都需要提前了解天气情况。WAPI平台提供了一站式天气预报解决方案&#xff0c;通过简单的API接口&#xff0c;轻松获取各类天气预报数据。 这个API接口提供了丰富的天气预报信息&#xf…

mac怎么压缩pdf文件,mac压缩pdf文件大小不改变清晰度

在数字化时代&#xff0c;pdf文件因其良好的兼容性和稳定性&#xff0c;已经成为我们日常办公和学习中不可或缺的文件格式。然而&#xff0c;随着文件内容的增多&#xff0c;pdf文件的体积也往往会变得越来越大&#xff0c;给文件的传输和存储带来不便。本文将为你介绍几种简单…

WACV2023论文速览域迁移Domain相关

Paper1 CellTranspose: Few-Shot Domain Adaptation for Cellular Instance Segmentation 摘要原文: Automated cellular instance segmentation is a process utilized for accelerating biological research for the past two decades, and recent advancements have produc…

vue中自定义设置多语言(包括使用vue-i18n),并且运行js脚本自动生成多语言文件

在项目中需要进行多个国家语言的切换时&#xff0c;可以用到下面方法其中一个 一、自定义设置多语言 方法一: 可以自己编写一个设置多语言文件 在项目新建js文件&#xff0c;命名为&#xff1a;language.js&#xff0c;代码如下 // language.js 文档 let languagePage {CN…

深入分析 Android BroadcastReceiver (八)

文章目录 深入分析 Android BroadcastReceiver (八)1. 系统与自定义实现1.1 系统广播机制1.1.1 系统广播的实现原理1.1.2 系统广播的源码分析 1.2 自定义广播机制1.2.1 自定义广播的实现步骤1.2.2 自定义广播的源码分析 2. 广播机制设计的初衷与优势2.1 设计初衷2.2 优势 3. 总…

(七)[重制]C++命名空间与标准模板库(STL)

​ 引言 在专栏C教程的第六篇C中的结构体与联合体中&#xff0c;介绍了C中的结构体和联合体&#xff0c;包括它们的定义、初始化、内存布局和对齐&#xff0c;以及作为函数参数和返回值的应用。在专栏C教程的第七篇中&#xff0c;我们将深入了解C中的命名空间&#xff08;nam…

leetcode判断二分图

判断二分图 图的问题肯定要用到深度优先遍历或者广度优先遍历&#xff0c;但又不是单纯的深度优先遍历算法和广度优先遍历算法&#xff0c;而是需要在遍历的过程中加入与解决题目相关的逻辑。 题干中说了&#xff0c;这个图可能不是连通图&#xff0c;这个提示有什么作用呢&a…

x.java => 字节码文件x.class => 运行

使用javac.exe对.java进行编译&#xff0c;编译成.class字节码文件 使用java.exe启动java虚拟机执行.class字节码 JVM是一个系统进程&#xff0c;这个进程运行会读取.class字节码文件&#xff0c;一旦他抢到了CPU的执行权&#xff0c;就会以那个类的main方法来执行程序逻辑。

JVM专题之垃圾收集算法

标记清除算法 第一步:标记 (找出内存中需要回收的对象,并且把它们标记出来) 第二步:清除 (清除掉被标记需要回收的对象,释放出对应的内存空间) 缺点: 标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需 要分配较大对象时,无法找到…

1119 胖达与盆盆奶

solution 递推&#xff1a;序列的每一位所需要计算的值都可以通过该位左右两侧的结果计算得到&#xff0c;就可以考虑所谓的“左右两侧的结果”是否能通过递推进行预处理来得到&#xff0c;以避免后续使用中的反复求解。 #include<iostream> using namespace std; cons…

Ubuntu 20版本安装Redis教程

第一步 切换到root用户&#xff0c;使用su命令&#xff0c;进行切换。 输入&#xff1a; su - 第二步 使用apt命令来搜索redis的软件包&#xff0c;输入命令&#xff1a;apt search redis 第三步 选择需要的redis版本进行安装&#xff0c;本次选择默认版本&#xff0c;redis5.…