IO详解(BIO、NIO、实战案例、底层原理刨析)

文章目录

  • IO详解(BIO、NIO、实战案例、底层原理刨析)
    • 🌎 IO
    • 🪐 同步、异步、阻塞、非阻塞
    • ⚡ BIO
        • 👽 简介
        • 😎 案例
    • 🚀 NIO
        • ✈️ 介绍
        • 🚗 Buffer(缓冲)
        • 🛸 Channel(通道)
        • 🛥️ Selector(选择器)
        • 🚍 案例
        • 🚩 源码解析

源码地址: IO模型详解.md · 小Liu/IO模型学习 - Gitee.com;

IO详解(BIO、NIO、实战案例、底层原理刨析)

文章理解若有误,烦请指出

🌎 IO

​ 我们先来了解一下IO,我们知道冯诺依曼结构体系中,计算机分为:计算器、控制器、存储器、输入设备、输出设备。我们平常所说的IO其实就是输入设备将数据交给CPU和内存,CPU和内存把处理过后的数据交给输出设备,这个过程就叫做IO

​ 我们知道在操作系统中,一个进程的地址空间划分为用户空间内核空间,从应用程序的视角来看的话,我们的应用程序对操作系统的内核发起 IO 请求调用(系统调用),操作系统负责的内核执行具体的 IO 操作。也就是说,我们的应用程序实际上只是发起了 IO 操作的请求调用而已,具体 IO 的执行是由操作系统的内核来完成的。

🪐 同步、异步、阻塞、非阻塞

​ 同步、异步、阻塞、非阻塞的概念是很容易搞混的,同步和异步是对于被调用者来说的。而阻塞和非阻塞是对于调用者来说的。

  • 同步:比如A调用B,而B不会立马返回响应,B处理完成之后才会给A返回响应,这种情况就叫做同步。
  • 异步:比如A调用B,B立马给A返回响应,告诉A我正在处理了,等B处理完之后,B会再告诉A,我处理完了,这就叫异步。
  • 阻塞:比如A调用B,A会被挂起一直等待B处理完成,才进行别的工作任务,这就是阻塞
  • 非阻塞:比如A调用B,A不会被挂起一直等待B处理完成,而是去执行别的操作,A可以通过轮询、提供回调函数给B,或者监听一个消息队列或者时间,B完成工作后将结果放入到消息队列中,来看B是否完成工作,这就是非阻塞

​ 通过同步、异步、阻塞、非阻塞进行组合,我们可以得出以下三种经典的IO模型:BIO(同步阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞)、理论上来说异步阻塞是不存在的,但是其实在业务场景中异步阻塞是存在的,我们来个举例说明

异步阻塞【业务上的】:线程A先调用B,B立马返回响应给A后,B执行真正的任务,这时候A收到了响应,继续执行,A再调用了C,C也立马返回响应给A后,C执行真正的任务,这时候将A去循环阻塞的轮询B和C,假设这时候C完成了,A轮询到了之后,对C进行处理,然后继续轮询,知道B也完成后,A才结束阻塞。

总结:同步和异步是相对于两个线程是否在同一时间做不同的事。但是阻塞和非阻塞在理论上和业务上有点细微的差别

  • 理论上来说:阻塞和非阻塞是指一个线程是否被挂起,而同步和异步是指两个线程的调用顺序和交互方式,所以理论上来说异步阻塞不存在

  • 业务上来说:阻塞是指一个线程是否在等待某个事情的完成,而不一定是线程被挂起,所以说业务上来说异步阻塞是存在的

​ 接下来我们来分别了解一下IO模型。

⚡ BIO

👽 简介

​ 我们看一下BIO的一个模型图,在Java中,像我们平时使用的IO流就是一种同步阻塞的IO,在执行read操作的时候,线程会阻塞,等待内核把数据准备好了,线程才继续执行。

我们来了解一下什么是read:read是一种系统调用,用于读取文件描述符对应的数据【注意,读取socket读取数据的系统调用是recvfrom 】,如果描述符没有数据可读,read 调用会阻塞,直到有数据可用或文件被关闭,这也是为什么BIO会阻塞的原因

在这里插入图片描述

​ 下面我们通过网络通讯的角度来写一个同步阻塞的案例

😎 案例
  • BIO服务端

    /*** 同步阻塞IO服务端* @author Liu Hanlin* @create 2024-10-25 0:30*/
    public class BIOServer {public static void main(String[] args) {try (ServerSocket serverSocket = new ServerSocket(8888)) {System.out.println("【服务端】等待连接中...");while (true){// 阻塞等待连接,收到连接后继续执行Socket clientSocket = serverSocket.accept();System.out.printf("【服务端】收到【客户端:%s】的连接\n", clientSocket.getRemoteSocketAddress());handle(clientSocket);}} catch (IOException e) {throw new RuntimeException(e);}}public static void handle(Socket socket) throws IOException {try (InputStream inputStream = socket.getInputStream()) {System.out.println("开始处理===");byte[] bytes = new byte[1024];while ( (inputStream.read(bytes)) != -1){System.out.printf("收到【客户端】消息:%s\n", new String(bytes, StandardCharsets.UTF_8));}}catch (Exception e){System.out.println(e.getMessage());}}
    }
    
  • BIO客户端

    /*** 阻塞IO客户端* @author Liu Hanlin* @create 2024-10-25 0:59*/
    public class BIOClient {public static void main(String[] args) throws IOException {OutputStream outputStream = null;try (Socket socket = new Socket("127.0.0.1", 8888)) {Scanner scanner = new Scanner(System.in);System.out.println("输入发送消息(exit退出):");while (scanner.hasNext()){String msg = scanner.next();if("exit".equals(msg)){scanner.close();break;}outputStream = socket.getOutputStream();outputStream.write(msg.getBytes());System.out.printf("发送消息:%s\n", msg);}} catch (Exception e) {System.out.println(e.getMessage());}finally {if (outputStream != null){outputStream.close();}}}
    }
    

    我们运行上述可看到

    在这里插入图片描述

​ 启动一个客户端可以正常连接发送,我们再把客户端2启动,发现服务器端此时收不到客户端2的消息,因为此时是阻塞的,当我们把客户端1停止的时候,我们可以看到如下,客户端2 发送的消息瞬间就被接收到并且处理了。

在这里插入图片描述

​ 上述案例可以看到,我们使用传统的Socket进行网络连接请求发送的时候,一个线程只能处理一个客户端,需要完全处理完这个客户端后才能下一个客户端,这种情况如果在客户端数量非常多的时候,那么就会导致某些客户端响应速度过慢,接下来我们来介绍一些NIO

🚀 NIO

✈️ 介绍

​ 在Java中NIO可以看作同步非阻塞IO模型,也可以看做IO多路复用模型,我们先来看一下同步非阻塞的模型图,我们可以看到,线程会调用内核进行IO读取,但是在内核准备数据的时候,线程并没有阻塞,而是一直在反复调用read,那为什么同步非阻塞模型在资源还没有准备好的时候进行read系统调用不回阻塞呢,因为在次模型中,如果资源没有准备好,内核会快速返回一个erroy来表示资源还没有准备好,这样应用程序就不会被阻塞。

在这里插入图片描述

接下来我们再来看看IO多路复用技术的模型图

在这里插入图片描述

​ 在IO多路复用模型中我们可以看到,应用程序发起IO不再是使用read系统调用,而是使用selectpollepoll,我们来解释一下这三个系统调用。

  • select:将所有的连接(io连接或者socket连接)都放到一个描述符集合里,然后通过select系统调用将描述符集合拷贝的内核中,内核来遍历检查描述符资源是否准备好,如果有准备好的描述符,就将其拷贝回用户空间中,然后用户空间再将此描述符集合遍历,拿到准备好的描述符资源进行读写>
  • poll:poll实际上和select没有太大的本质差别,差别在于poll存储描述符采用动态数组链表来存储
  • epoll:在内核中用了一颗红黑树来存储来存储所有需要监听的描述符,像selectpoll新增监听的描述符时,需要把新增的描述符放到描述符集合里,然后再把整个集合传给内核,而epoll红黑树之后,只需要将新增监听的描述符传给内核,内核将新增的描述符添加到红黑树中。epoll在内核中还维护了一个链表用来存放就绪的描述符,当某个描述符有事件发生之后,通过回调函数的方式将其注册到就绪的描述符链表中,而用户态和内核之间传输就只用传输这个就绪的描述符链表,而且用户进程也不需要再遍历去查找就绪的描述符

​ 上述我们讲到了NIO的细节,我们在Java中提供了一个NIO的包,其实现原理也和上述IO多路复用类似。在Java的NIO中有三个核心的概念

  • Buffer(缓冲区):NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。

  • Channel(信道):Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。

  • Selector(选择器):允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到 Selector 上,由 Selector 来分配线程来处理事件。

我们来详细介绍一下

🚗 Buffer(缓冲)

​ Buffer可以理解成一个数组,用来存储数据,在读写数据的时候,都是对Buffer进行操作,Buffer有几个重要的概念,我们来看一下

public abstract class Buffer {// 属性满足的关系: mark <= position <= limit <= capacity// 允许讲位置直接定位到该标记出,可选项private int mark = -1;// 下一个可以被读写的数据的位置,读写模式切换时,会归零private int position = 0;// 读写的边界,写模式下代表最多能写入的数据,通常等于capatity,读模式下表示buffer中数据的实际长度private int limit;// Buffer可以存储的最大数据量,创建时设置且不可改变;private int capacity;
}

​ 我们来看一下读写模式分别的图解

在这里插入图片描述

Buffer 对象不能通过 new 调用构造方法创建对象 ,只能通过静态方法实例化 Buffer

这里以 ByteBuffer为例进行介绍:

// 分配堆内存
public static ByteBuffer allocate(int capacity);
// 分配直接内存
public static ByteBuffer allocateDirect(int capacity);

常用方法:

  1. get : 读取缓冲区的数据
  2. put :向缓冲区写入数据

除上述两个方法之外,其他的重要方法:

  • flip :将缓冲区从写模式切换到读模式,它会将 limit 的值设置为当前 position 的值,将 position 的值设置为 0。
  • clear: 清空缓冲区,将缓冲区从读模式切换到写模式,并将 position 的值设置为 0,将 limit 的值设置为 capacity 的值。
🛸 Channel(通道)

​ Channel是一种全双工的数据通道,不同于Java传统IO中的流只能读或者写,同一个Channel可以进行读也可以进行写,Channel写数据时将数据写入Buffer中,Channel读数据时从Buffer中进行读取。

我们介绍一下常用的几种Channel类型

  • FileChannel:文件访问通道;
  • SocketChannelServerSocketChannel:TCP 通信通道;
  • DatagramChannel:UDP 通信通道;

再来介绍一下两个核心方法:

  • read :读取数据并写入到 Buffer 中。
  • write :将 Buffer 中的数据写入到 Channel 中。
🛥️ Selector(选择器)

​ Selector是NIO中的一个核心组件,一个线程对应一个Selector,Selector可以注册多个Channel,Selector会不断的轮询Channel,然后将有监听事件发生的Channel轮询出来,比如某个Channel上有新的 TCP 连接接入、读和写事件【比如缓冲区存在】,发生事件后通过回调函数将Channel设置成就绪,这个 Channel 就处于就绪状态,会被 Selector 轮询出来。Selector 会将相关的 Channel 加入到就绪集合中。通过 SelectionKey 【类似于上述的描述符】可以获取就绪 Channel 的集合,然后对这些就绪的 Channel 进行相应的 I/O 操作。如下图所示:

在这里插入图片描述

​ 上述我们提到了监听的事件,我们可以通过SelectionKey枚举类来获取事件,Selector监听的事件分为如下几种:

  • SelectionKey.OP_ACCEPT:表示通道接收连接事件,用于ServerSocketChannel
  • SelectionKey.OP_CONNECT:表示完成通道完成连接事件,用于SocketChannel
  • SelectionKey.OP_READ:表示通道准备好进行读取的事件,即有数据可读,也就是说。
  • SelectionKey.OP_WRITE:表示通道准备好进行写入的事件,即可以写入数据。

我们可以通过所对应的Channel.register(selector, selectionKey)注册channel到选择器并绑定发生某个事件时,channel就绪

​ Selector在Java的NIO中是一个抽象的类,可以通过调用Selector.open()方法获取实例,获取到的实例有三个集合,我们分别来解释一下。

  • 所有的SelectionKey集合:表示所有被注册到Selector上的Channel,可以通过keys()方法获取
  • 所有就绪的SelectionKey集合:表示所有,有事件发生的Channel,需要进行IO处理的Channel,通过selectedKeys()获取
  • 被删除的SelectionKey集合:代表了所有被取消注册关系的 Channel,在下一次执行 select() 方法时,这些 Channel 对应的 SelectionKey 会被彻底删除,程序通常无须直接访问该集合,也没有暴露访问的方法。
🚍 案例

我们下面通过一个案例来说明

NIO服务端

public class NIOServer {public static void main(String[] args) throws Exception {try(Selector selector = Selector.open();ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();){// 绑定监听端口号serverSocketChannel.bind(new InetSocketAddress(8888));// 设置非阻塞serverSocketChannel.configureBlocking(false);// 注册接收连接事件serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("等待连接...");while (true) {// 【阻塞】监听是否有事件发生int count = selector.select();if (count > 0) {// 发生事件后获取就绪key集合,拿到有新连接后的Channel对应的SelectionKeySet<SelectionKey> selectionKeys = selector.selectedKeys();Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();// isAcceptable() 是用于判断通道是否可以接受新连接的状态方法【处理新连接事件】if (key.isAcceptable()) {//                        当key.isAcceptable为true时,调用key.channel(),返回的channel和我们上面定义的其实是同一个
//                        ServerSocketChannel server = (ServerSocketChannel) key.channel();// 服务端channel接收客户端SocketChannelSocketChannel clientSocketChannel = serverSocketChannel.accept();clientSocketChannel.configureBlocking(false);// 注册读事件,读事件发生时,会将对应的selectionKey加入到就绪集合中clientSocketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_CONNECT);System.out.printf("连接成功!接收【%s】的连接\n", clientSocketChannel.getRemoteAddress());}// 用于判断key的Channel是否已准备好读取。【处理读事件】if(key.isReadable()){// 处理读操作handleRead(key);SocketChannel socketChannel = (SocketChannel) key.channel();// 设置可写,接下来将进行写操作socketChannel.register(selector, SelectionKey.OP_WRITE);}// 用于判断key的Channel是否已准备好写。【处理写事件】if(key.isWritable()){SocketChannel socketChannel = (SocketChannel) key.channel();socketChannel.write(ByteBuffer.wrap("我已经处理你的消息".getBytes(StandardCharsets.UTF_8)));// 将客户端通道注册到 Selector 并重新监听读事件,不然这里会一直写,值得缓冲区满socketChannel.register(selector, SelectionKey.OP_READ);}iterator.remove();}}}}}/*** 处理读事件* @param key*/private static void handleRead(SelectionKey key) throws IOException {SocketChannel channel = (SocketChannel) key.channel();// 创建缓冲区对象【默认是写模式】ByteBuffer byteBuffer = ByteBuffer.allocate(1024);int byteSize = channel.read(byteBuffer);if(byteSize > 0){// 切换读模式byteBuffer.flip();String msg = new String(byteBuffer.array(), 0, byteSize);System.out.printf("处理线程:【%s】--处理来自【%s】消息:【", Thread.currentThread().getName(), channel.getRemoteAddress());System.out.println(msg + "】");}else if(byteSize == -1){System.out.printf("关闭【%s】的连接\n", channel.getRemoteAddress());// 关闭连接channel.close();// 取消注册key.channel();}}
}

NIO客户端

public class NIOClient {public static void main(String[] args) {try(SocketChannel socketChannel = SocketChannel.open();Scanner scanner = new Scanner(System.in)){// 连接到服务器socketChannel.connect(new InetSocketAddress("127.0.0.1", 8888));System.out.println("发送消息(退出请输入quit):");while (scanner.hasNext()){String msg = scanner.nextLine();if ("quit".equals(msg)){break;}socketChannel.write(ByteBuffer.wrap(msg.getBytes()));System.out.printf("发送成功!消息内容:【%s】\n",msg);handleRead(socketChannel);}}catch (Exception e){System.out.println(e.getMessage());}}private static void handleRead(SocketChannel channel) throws IOException {// 创建缓冲区对象【默认是写模式】ByteBuffer byteBuffer = ByteBuffer.allocate(1024);int byteSize = channel.read(byteBuffer);if(byteSize > 0){// 切换读模式byteBuffer.flip();String msg = new String(byteBuffer.array(), 0, byteSize);System.out.printf("收到了来自【%s】响应:【", channel.getRemoteAddress());System.out.println(msg + "】");}}
}

​ 通过上述我们可以看到运行结果,可以同时处理两个客户端的请求,不用等待一个客户端断开连接才能处理另外一个客户端。

在这里插入图片描述

​ 我们来梳理一下上述代码流程:

  1. 首先我们在服务端声明一个SelectorServerSocketChannel,将ServerSocketChannel设置成非阻塞,绑定端口号,注册监听事件
  2. 然后通过调用selector.select()方法,获取到所有就绪channel对应的selectionKey集合,然后遍历该集合,通过key判断是否可以进行读写操作,是否有新连接可以建立,针对事件进行处理,处理完之后将就绪Key在就绪集合中移除,避免重复处理
  3. 在客户端声明一个SocketChannel,然后通过SocketChannel.connect()方法与服务端建立连接,然后将数据写到缓冲区,再通过ByteBuffer.wrap()方法将缓冲区数据发送给服务端
  4. 服务端监听到SocketChannel的数据后,会将对应客户端的channel标记从可读状态,然后selector轮循到可读状态的channel后,会将其SelectionKey加入到集合中,然后对SelectionKey判断是否可读,然后进行读处理,处理完之后设置该Channel监听可写,然后轮询到可写后,进行写操作,写完之后再将其设置成监听可读。
🚩 源码解析

​ 当我们使用selector.select()方法时,我们点进去其源码查看这里以JDK17为准,如图所示:

在这里插入图片描述

​ 我们继续进入该方法:

在这里插入图片描述

​ 这里我们看到有两个实现类,分别解释一下

  • WEPollSelectorImpl:主要用于类Unix系统(如Linux),基于 epoll系统调用,这是Linux内核提供的高效IO多路复用机制。
  • WindowsSelectorImpl:基于 Windows Sockets API,使用 IOCP(I/O Completion Ports)进行高效的异步IO处理。

这里我们以WEPollSelectorImpl举例,进去查看,我们重点关注这几行

在这里插入图片描述

​ 进入processUpdateQueue()方法中

在这里插入图片描述

updateKeys属性就是更新后的需要监听的channel对应的所有key;从上述代码int fd = ski.getFDVal();中我们可以看出来,我们Java中一个selectionKey对应一个chennel对应一个文件描述符,我们得到对应的文件描述符后,代码WEPoll.ctl()方法实际上就是设置epoll系统调用请求的资源,这个方法processUpdateQueue()大概就是说将我们最新的需要监听的channel,也就是需要获取的IO资源,设置在一个epoll系统调用里,方便后续发起epoll系统调用。

​ 我们再进入processDeregisterQueue()方法中
在这里插入图片描述

​ 该方法就是将取消注册的key从监听的Key集合中删除

​ 我们再来看WEPoll.wait()这个方法,这个方法实际上就是对epoll_wait()【操作系统中发起epoll调用的方法】的封装,所以我们可以得出结论,Java的NIO本质上是使用的epoll系统调用来实现的。调用这个方法后,epoll_wait 系统调用会阻塞当前线程,直到有事件发生或超时时间到达。并且epoll_wait 会返回实际发生的事件数量。

​ 最后我们再来看processEvents()这个方法,如图所示

在这里插入图片描述

​ 这个方法我们会对返回的事件进行一个处理,将其加入到我们的一个publicSelectedKeys集合中,也就是我们通过selector.selectedKeys();返回加入到publicSelectedKeys集合中的个数。这时候我们就可以通过业务代码对我们对应的channel进行处理了。

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

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

相关文章

Mac 配置SourceTree集成云效

1、背景 工作使用的是自己的笔记本&#xff0c;一个是比较卡&#xff0c;在一个是敏感信息比较多还是使用公司的电脑&#xff0c;但是系统是Mac就很麻烦&#xff0c;在网上找了帖子记录一下 2、配置 打开终端 ssh-keygen -t rsa #一直回车就行 cd .ssh cat id_rsa.pub #查…

.NET 8 Web API 中的身份验证和授权

本次介绍分为3篇文章&#xff1a; 1&#xff1a;.Net 8 Web API CRUD 操作.Net 8 Web API CRUD 操作-CSDN博客 2&#xff1a;在 .Net 8 API 中实现 Entity Framework 的 Code First 方法https://blog.csdn.net/hefeng_aspnet/article/details/143229912 3&#xff1a;.NET …

【论文阅读】Associative Alignment for Few-shot Image Classification

用于小样本图像分类的关联对齐 引用&#xff1a;Afrasiyabi A, Lalonde J F, Gagn C. Associative alignment for few-shot image classification[C]//Computer Vision–ECCV 2020: 16th European Conference, Glasgow, UK, August 23–28, 2020, Proceedings, Part V 16. Spri…

第03章 MySQL的简单使用命令

一、MySQL的登录 1.1 服务的启动与停止 MySQL安装完毕之后&#xff0c;需要启动服务器进程&#xff0c;不然客户端无法连接数据库。 在前面的配置过程中&#xff0c;已经将MySQL安装为Windows服务&#xff0c;并且勾选当Windows启动、停止时&#xff0c;MySQL也 自动启动、停止…

【Fastjson反序列化漏洞:深入了解与防范】

一、Fastjson反序列化漏洞概述 Fastjson是一款高性能的Java语言JSON处理库&#xff0c;广泛应用于Web开发、数据交换等领域。然而&#xff0c;由于fastjson在解析JSON数据时存在安全漏洞&#xff0c;攻击者可以利用该漏洞执行任意代码&#xff0c;导致严重的安全威胁。 二、F…

Python自动化测试一文详解

Python 作为一种高效、易读的编程语言&#xff0c;凭借其丰富的库和框架&#xff0c;成为自动化测试领域的热门选择。无论是Web应用、API&#xff0c;还是移动应用&#xff0c;Python 都能提供强大的支持&#xff0c;使得测试人员能够快速编写和维护测试用例。 本文将深入探讨…

一个免费开源自托管的机器翻译项目,支持API接口

大家好&#xff0c;今天给大家分享一个免费且开源的机器翻译项目LibreTranslate&#xff0c;旨在为用户提供一个完全自由且安全的翻译解决方案。 项目介绍 LibreTranslate采用神经翻译技术&#xff0c;使用开源语言模型对文本进行翻译&#xff0c;无需依赖外部服务。该项目的主…

视觉目标检测标注xml格式文件解析可视化 - python 实现

视觉目标检测任务&#xff0c;通常用 labelimage标注&#xff0c;对应的标注文件为xml。 该示例来源于开源项目&#xff1a;https://gitcode.com/DataBall/DataBall-detections-100s/overview 读取 xml 标注文件&#xff0c;并进行可视化示例如下&#xff1a; #-*-coding:ut…

什么是目标检测?

首先计算机视觉能够解决哪些问题&#xff1f;&#xff1f; 分类、检测、分割 首先以下面这幅图为例&#xff1a; 分类就是输入一张图像&#xff0c;算法能够告诉我们图像中有什么类别&#xff0c;比如说猫或者狗&#xff0c;而并不知道这个类别在图像中的位置&#xff0c;如…

20221403郑骁恒实验2-2

1.在Ubuntu或openEuler中&#xff08;推荐openEuler&#xff09;中调试运行教材提供的源代码&#xff0c;至少运行SM2&#xff0c;SM3&#xff0c;SM4代码&#xff0c;使用GmSSL命令验证你代码的正确性&#xff0c;使用Markdown记录详细记录实践过程&#xff0c;每完成一项功能…

vite构建Vue3项目:封装公共组件,发布npm包,自定义组件库

文章目录 前言一、创建基础的vite 脚手架二、文件结构三、编写组件代码,本地测试四、配置项五、打包npm发布六、npm下载使用总结 前言 使用vue开发组件封装是一个很普遍的事情了&#xff0c;封装好一个组件可以在项目的任意地方去使用&#xff0c;我们还可以从npm仓库下载别人…

外包功能测试就干了4周,技术退步太明显了。。。。。

先说一下自己的情况&#xff0c;大专生&#xff0c;21年通过校招进入武汉某软件公司&#xff0c;干了差不多3个星期的功能测试&#xff0c;那年国庆&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我才在一个外包企业干了4周的功…

基于LORA的一主多从监测系统_实物设计

最近代码写的差不多了&#xff0c;基本一主一从已经定下&#xff0c;并且经过24小时测试还算比较稳定&#xff0c;所以打算把硬件实物定下&#xff0c;之前用的杜邦线&#xff0c;看着也比较杂乱不是很好看&#xff0c;于是打算使用pcb来替代&#xff0c;这样也比较整洁可靠&am…

qt QRadioButton详解

QRadioButton 是一个可以切换选中&#xff08;checked&#xff09;或未选中&#xff08;unchecked&#xff09;状态的选项按钮。单选按钮通常呈现给用户一个“多选一”的选择&#xff0c;即在一组单选按钮中&#xff0c;一次只能选中一个按钮。 重要方法 QRadioButton(QWidget…

三:LoadBalancer负载均衡服务调用

LoadBalancer负载均衡服务调用 1.LB负载均衡(Load Balance)是什么2.loadbalancer本地负载均衡客户端 与 Nginx服务端负载均衡区别3.实现loadbalancer负载均衡实例3-1.首先应模拟启动多个服务提供者应用实例&#xff1a;3-2.在服务消费项目引入LoadBalancer3-3&#xff1a;测试用…

“农田奇迹:如何用遥感技术实现作物分类与产量精准估算“

在科技飞速发展的时代&#xff0c;遥感数据的精准分析已经成为推动各行业智能决策的关键工具。从无人机监测农田到卫星数据支持气候研究&#xff0c;空天地遥感数据正以前所未有的方式为科研和商业带来深刻变革。然而&#xff0c;对于许多专业人士而言&#xff0c;如何高效地处…

LeetCode :21. 合并两个有序链表(Java)

目录 题目描述: 代码: 第一种: 第二种: 题目描述: 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4,4]示例 2&#xff1a; …

Spring Boot框架在信息学科平台建设中的实战技巧

3系统分析 3.1可行性分析 通过对本基于保密信息学科平台系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本基于保密信息学科平台系统采用Spring Boot框架&a…

「Mac畅玩鸿蒙与硬件26」UI互动应用篇3 - 倒计时和提醒功能实现

本篇将带领你实现一个倒计时和提醒功能的应用&#xff0c;用户可以设置倒计时时间并开始计时。当倒计时结束时&#xff0c;应用会显示提醒。该项目涉及时间控制、状态管理和用户交互&#xff0c;是学习鸿蒙应用开发的绝佳实践项目。 关键词 UI互动应用倒计时器状态管理用户交互…

【升华】自然语言处理架构

自然语言处理&#xff08;Natural Language Processing&#xff0c;NLP&#xff09;是指让计算机接受用户自然语言形式的输入&#xff0c;并在内部通过人类所定义的算法进行加工、计算等系列操作&#xff0c;以模拟人类对自然语言的理解&#xff0c;并返回用户所期望的结果。自…