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对应的SelectionKey
                    Set<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接收客户端SocketChannel
                            SocketChannel 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进行处理了。

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

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

相关文章

#渗透测试#SRC漏洞挖掘# 信息收集-Shodan之搜索语法进阶

免责声明 本教程仅为合法的教学目的而准备&#xff0c;严禁用于任何形式的违法犯罪活动及其他商业行为&#xff0c;在使用本教程前&#xff0c;您应确保该行为符合当地的法律法规&#xff0c;继续阅读即表示您需自行承担所有操作的后果&#xff0c;如有异议&#xff0c;请立即停…

Python复习1:

一、数据类型 1.数字&#xff1a;int、float、bool 2.字符串&#xff1a;string 3.列表&#xff1a;list 4.集合&#xff1a;set 5.字典&#xff1a;dictionary 二、Test 1.print输出固定格式 num110 str1"hello world" #输出的固定格式 print("num1%d&…

【MyBatis源码】BoundSql分析

基础 BoundSql是对SQL语句及参数信息的封装&#xff0c;它是SqlSource解析后的结果。Executor组件并不是直接通过StaticSqlSource对象完成数据库操作的&#xff0c;而是与BoundSql交互。BoundSql是对Executor组件执行SQL信息的封装&#xff0c;具体实现代码如下&#xff1a; …

Python爬虫抓取三个网站上的英语每日一句

一、引言 大学英语学习需要巩固高中语法&#xff0c;补充四六级词汇&#xff0c;增加英语语感&#xff0c;提升英语的运用能力。学好英语有很多种方法&#xff0c;采用句子来突破英语语法、词汇、口语和听力的方法简单有效&#xff0c;值得提倡。李阳就是采用这种方法来教授英…

三相LCL并网逆变器—为什么采用LCL滤波器

1.为什么需要滤波器 当前并网逆变器大多采用脉冲宽度调制(PWM)技术&#xff0c;导致桥臂输出的电压中存在开关频率及倍数频率次的谐波电压&#xff0c;进而使得输出到电网的电流中含有谐波。从电网的角度来看&#xff0c;是不希望系统内含有高次谐波的&#xff0c;因为这会影响…

【STM32】按键控制LED 光敏传感器控制蜂鸣器

文章目录 前置知识按键介绍传感器模块硬件电路按键硬件电路传感器模块硬件电路 C语言数据类型在Keil中的对应写法C语言枚举 按键控制LED接线图Hardware文件夹&#xff08;模块化编程&#xff09;LED驱动程序封装Key(按键)驱动程序封装 main.c源文件 光敏传感器控制蜂鸣器接线图…

飞书API-获取tenant_access_token

1.在飞书工作台创建应用&#xff0c;跳到开发者后台&#xff0c;选创建企业自建应用 2.设置并发布应用 必须要发布应用才可以开始使用了&#xff01;&#xff01;&#xff01; 3.调用获取token的API 参考链接&#xff1a; 开发文档 - 飞书开放平台https://open.feishu.cn/do…

SpringBoot篇(自动装配原理)

目录 一、自动装配机制 1. 简介 2. 自动装配主要依靠三个核心的关键技术 3. run()方法加载启动类 4. 注解SpringBootApplication包含了多个注解 4.1 SpringBootConfiguration 4.2 ComponentScan 4.3 EnableAutoConfiguration 5. SpringBootApplication一共做了三件事 …

Rust 力扣 - 48. 旋转图像

文章目录 题目描述题解思路题解代码题目链接 题目描述 题解思路 我们可以将原矩阵进行水平翻转&#xff0c;然后在沿主对角线进行翻转&#xff0c;就能完成原矩阵沿顺时针方向旋转90o的变换 题解代码 impl Solution {pub fn rotate(matrix: &mut Vec<Vec<i32>&…

ima.copilot:智慧因你而生

在数字化时代&#xff0c;信息的获取、处理和创作已经成为我们日常工作和学习中不可或缺的一部分。腾讯公司推出的ima.copilot&#xff08;简称ima&#xff09;正是为了满足这一需求&#xff0c;它是一款由腾讯混元大模型提供技术支持的智能工作台产品&#xff0c;旨在通过智能…

使用 PyCharm 构建 FastAPI 项目:零基础入门 Web API 开发

使用 PyCharm 构建 FastAPI 项目&#xff1a;零基础入门 Web API 开发 本文提供了一份完整的 FastAPI 入门指南&#xff0c;涵盖从环境搭建、依赖安装到创建并运行一个简单的 FastAPI 应用的各个步骤。通过 FastAPI 和 Uvicorn&#xff0c;开发者可以快速构建现代化的 Web API…

练习LabVIEW第三十五题

学习目标&#xff1a; 刚学了LabVIEW&#xff0c;在网上找了些题&#xff0c;练习一下LabVIEW&#xff0c;有不对不好不足的地方欢迎指正&#xff01; 第三十五题&#xff1a; 使用labview模拟一个3-8译码器 开始编写&#xff1a; 用LabVIEW做3-8译码器首先要知道它是个啥…

【流量控制】之创建和管理前缀列表

前缀列表是包含一个或多个CIDR地址块的集合。您可以自主创建前缀列表并对其进行管理。在创建过程中&#xff0c;您可以将现有的前缀列表克隆&#xff08;复制&#xff09;到指定地域&#xff08;同地域或跨地域&#xff09;&#xff0c;让操作更加便捷。本文介绍如何使用专有网…

数据库作业5

1&#xff0c;建立触发器&#xff0c;订单表中增加订单数量后&#xff0c;商品表商品数量同步减少对应的商品订单出数量,并测试 测试&#xff1a; 2.建立触发器&#xff0c;实现功能:客户取消订单&#xff0c;恢复商品表对应商品的数量 测试 3.建立触发器&#xff0c;实现功能:…

Spring Boot 注解大全:全面解析 Spring Boot 常用注解及其应用场景

Spring Boot 注解大全:全面解析 Spring Boot 常用注解及其应用场景 简介 Spring Boot 是一个基于 Spring 框架的简化开发框架,它旨在简化 Spring 应用的初始搭建和开发过程。Spring Boot 提供了一系列的注解,使得开发者可以更加方便地进行应用开发和配置。本文将详细介绍 S…

11.3比赛总结

Bricks 1.题意&#xff1a;有一个由 B 和 W 组成的字符串&#xff0c;要将它划分成尽量多的区间&#xff0c;并使得每个区间中 B 和 W 的比例相等。 2.一道贪心题。首先特殊处理比值恒为 0 和不存在比值的情况&#xff0c;也就是全是 W 或者 B&#xff0c;明显这两种情况下每…

excel自定义导出实现(使用反射)

前言 项目中接到需求&#xff0c;需要对导出的字段进行自定义导出 &#xff0c;用户可在前端选择自定义导出的字段&#xff08;如图&#xff09;&#xff0c;实现过程做以下记录&#xff0c;仅供参考&#xff1b; 思路 跟前端约定好所有要导出的字段名称(headName)跟对应的…

“游戏人”也能拿诺贝尔奖!

“游戏人”也能拿诺贝尔奖&#xff01; 点击蓝链领取游戏开发教程 2024年度的诺贝尔奖&#xff0c;堪称AI深刻影响传统科学领域的一次生动展现。在这一年里&#xff0c;备受瞩目的诺贝尔物理学奖与诺贝尔化学奖两大核心奖项&#xff0c;均颁给了在AI领域做出杰出贡献的研究者…

OmniGen: Unified Image Generation(代码的复现)

文章目录 论文简介模型的部署需要下载的预训练权重 模型的生成效果图像编辑的效果风格迁移的效果 总结 论文简介 OmniGen的github项目地址 OmniGen: Unified Image Generation。OmniGen 在各种图像生成任务中都表现出了卓越的性能&#xff0c;并可能大大超过现有扩散模型的极…

docker-compose安装rabbitmq 并开启延迟队列和管理面板插件(rabbitmq_delayed_message_exchange)

问题&#xff1a; 解决rabbitmq-plugins enable rabbitmq_delayed_message_exchange &#xff1a;plugins_not_found 我是在docker-compose环境部署的 services:rabbitmq:image: rabbitmq:4.0-managementrestart: alwayscontainer_name: rabbitmqports:- 5672:5672- 15672:156…