IO系列(八) -浅析NIO工作原理

news2024/12/25 9:15:45

一、简介

现在使用 NIO 的场景越来越多,很多网上的技术框架或多或少的使用 NIO 技术,譬如 Tomcat、Jetty、Netty,学习和掌握 NIO 技术已经不是一个 Java 攻城狮的加分技能,而是一个必备技能。

那什么是 NIO 呢?

NIO,英文全称 Non-blocking I/O,在 Java 领域,也称为 New I/O,是一种同步非阻塞的 I/O 模型,是解决高并发、I/O 高性能的有效方式,已经被越来越多地应用到大型应用服务器中。

既然 NIO 如此的受欢迎,那么它的本质是什么?又是如何实现高性能的呢?

带着这个问题,我们先从传统的阻塞 I/O 说起,在一步一步的分析 NIO 是如何利用非阻塞模式来解决大量请求带来的性能瓶颈问题。

二、传统 BIO 的瓶颈

在介绍 NIO 之前, 让我们先回顾一下传统的服务器端同步阻塞 I/O (也称为 BIO,英文全称 blocking I/O)的经典编程模型。

采用 BIO 通信模型的服务端,通常由一个独立的 Acceptor 线程负责监听所有客户端的连接,当服务端接受到多个客户端的链接请求时,通常所有的客户端请求需要排队等待服务端一个一个的处理。

BIO 简易通信模型图如下!

一般在服务端通过while(true)循环中会调用accept() 方法监听客户端的连接,一旦接收到一个连接请求,就可以建立通信套接字进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成再处理下一个连接请求。

下面是一个简易版的 BIO 服务端,示例程序

public class BioServer {

    public static void main(String[] args) throws IOException {
        // 初始化服务端socket并且绑定 8080 端口
        ServerSocket serverSocket = new ServerSocket(8080);
        // 循环监听所有客户端的请求接入
        while (true) {
            // 监听客户端请求
            Socket socket = serverSocket.accept();
            // 读取客户端发送的请求数据
            InputStream in = socket.getInputStream();
            byte[] buffer = new byte[1024];
            int len;
            while ((len = in.read(buffer))!=-1){
                System.out.println(new String(buffer,0,len));
            }
            System.out.println("接收完毕");
            // 关闭流
            in.close();
        }
    }
}

下面是一个简易版的 BIO 客户端,示例程序

public class BioClient {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1",8080);
        OutputStream out = socket.getOutputStream();
        out.write("Hello,this is Client".getBytes());
        out.close();
        socket.close();
    }
}

随着客户端的请求次数增多,可能需要排队的时间会越来越长,用户等待的时间越久,体验感就越差。

因此有的大仙就将服务端的编程模型进行了适当的改造,引入多线程方式来处理客户端的请求,从而实现了 N (客户端请求数量)大于 M (服务端处理客户端请求的线程数量)的 I/O 模型

改造后的 IO 模型图,如下图:

采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,当有新的客户端接入时,将客户端的 Socket 封装成一个 Task 投递到线程池中进行处理。

下面是一个简易版的改造后 BIO 服务端,示例程序

public class BioServerTest {

    public static void main(String[] args) throws IOException {
        //在线程池中创建5个固定大小线程,来处理客户端的请求
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        //初始化服务端socket并且绑定 8080 端口
        ServerSocket serverSocket = new ServerSocket(8080);
        //循环监听客户端请求
        while (true) {
            // 监听客户端请求
            Socket socket = serverSocket.accept();
            //使用线程池处理多个任务
            executorService.execute(new Runnable() {

                @Override
                public void run() {
                    // 读取客户端发送的请求数据
                    InputStream in = socket.getInputStream();
                    byte[] buffer = new byte[1024];
                    int len;
                    while ((len = in.read(buffer))!=-1){
                        System.out.println(new String(buffer,0,len));
                    }
                    System.out.println("接收完毕");
                    // 关闭流
                    in.close();
                }
            });
        }
    }
}

服务端使用线程池来处理客户端的请求,线程数量为 5 个,由于线程池可以设置消息队列的大小和最大线程数,因此它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

服务端的编程模型进行改造后,处理客户端的请求速度确实提升了不少。

之所以使用多线程,主要原因在于socket.accept()socket.read()socket.write()三个主要函数都是同步阻塞的,当一个连接在处理 I/O 的时候,系统是阻塞的,如果是单线程的话,必然所有的任务等都挂在它名下,处理效率低下;引入多线程之后,等待的资源就可以释放出来,充分发挥 CPU 多任务的并发处理能力。

在活动连接数不是特别高的情况下,这种编程模型还是不错的,不用过多考虑系统的过载、限流等问题。

但是呢,这个模型也有弊端,底层还是 BIO 模型,严重依赖于线程,在操作系统中,我们知道线程是很"昂贵"的资源,主要表现在以下几点:

  • 1.线程的创建和销毁成本很高,在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁都是重量级的系统函数
  • 2.线程本身占用较大内存,像 Java 的线程栈,一般至少分配 512K~1M 的空间,如果系统中的线程数过千,恐怕整个 JVM 的内存都会被吃掉一半
  • 3.线程的切换成本也很高,操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用,如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统 load 偏高、CPU 系统使用率特别高,导致系统几乎陷入不可用的状态

最重要的是,当面对十万甚至百万级请求接入的时候,传统的 BIO 模型真的无能为力,随着移动端应用的兴起和各种网络游戏的盛行,百万级的请求接入非常普遍,因此我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

三、NIO 解析

在上文中我们也提到,BIO 是同步阻塞的 IO 模型NIO 是同步非阻塞的 IO 模型,仅仅一个字的差别,它们到底有何不同呢?

首先我们来看看几种常见 I/O 模型!

在 Linux 操作系统上,以发起读取数据为例:

  • 在传统的 BIO 中,也就是同步阻塞 IO 模型,当系统调用recvfrom()函数时,如果里面没有数据,函数会一直阻塞,直到收到数据,最后返回读到的数据。
  • 而在 NIO 中,也就是同步非阻塞 IO 模型,当系统调用recvfrom()函数时,如果里面有数据,就把数据读取出来并返回;如果没有就返回一个EWOULDLOCK标记,永远不会阻塞。
  • 同时还有最新的 AIO,它是一种异步非阻塞的 IO 模型,主要基于事件回调机制来实现,当系统调用recvfrom()函数时,不管里面有没有数据,直接返回结果;当后台处理完成后,操作系统会通知相应的线程进行后续的操作。

通俗的说,在读取数据的过程中,BIO 只关注“我要读”,NIO 只关注“我可以读了”,而 AIO 只关注“我读完了”。

其中 NIO 一个很重要的特点就是:采用单线程非阻塞的编程方式来处理客户端所有的连接请求,虽然执行过程比较消耗 CPU,但性能非常的高

那 NIO 具体是如何实现的呢?下面我们一起来看看!

3.1、NIO 工作原理介绍

与传统的 BIO 不同,NIO 新增了 Channel、Selector、Buffer 等抽象概念,支持面向缓冲、基于通道的 I/O 数据传输方法

NIO 简易模型图,如下:

与此同时,NIO 还提供了与传统 BIO 模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的套接字通道实现。

NIO 这两种通道都支持阻塞和非阻塞两种模式,阻塞模式使用就像传统中的 BIO 一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反

对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发效率和更好的维护性;对于高负载、高并发的网络应用,可以使用 NIO 的非阻塞模式来开发,可以显著的提升数据传输效率。

NIO 在 Java 1.4 中引入,对应的代码实现在java.nio包下,涉及到的核心类关联关系,如下图:

上图中有三个关键类:Channel 、Selector 和 Buffer,它们是 NIO 中的核心概念。

  • Channel:可以理解为通道
  • Selector:可以理解为选择器
  • Buffer:可以理解为数据缓冲区

刚接触 NIO 的同学,当第一眼看到 Channel、Selector、Buffer 等抽象概念,可能感觉难以理解,下面我们还是用之前介绍的城市交通工具来继续形容 一下 NIO 的工作方式。

这里的 Channel 要比 Socket 更加具体,它可以比作为某种具体的交通工具,如汽车、高铁或者飞机等,而 Selector 可以比作为一个车站的车辆运行调度系统,它将负责监控每辆车的当前运行状态,是已经出站还是在路上等等,也就是说它可以轮询每个 Channel 的状态。

还有一个 Buffer 类,你可以将它看作为 IO 中 Stream,但是它比 IO 中的 Stream 更加具体化,我们可以将它比作为车上的座位,Channel 如果是汽车的话,那么 Buffer 就是汽车上的座位,Channel 如果是高铁上,那么 Buffer 就是高铁上的座位,它始终是一个具体的概念,这一点与 Stream 不同。

NIO 引入了 Channel、Buffer 和 Selector 就是想把 IO 传输过程中涉及到的信息具体化,让程序员有机会去控制它们。

当我们进行传统的网络 IO 操作时,比如调用write()往 Socket 中的SendQ队列写数据时,当一次写的数据超过SendQ长度时,操作系统会按照SendQ 的长度进行分割的,这个过程中需要将用户空间数据和内核地址空间进行切换,而这个切换不是程序员可以控制的,由底层操作系统来帮我们处理。

而在Buffer中,我们可以控制Buffercapacity(容量),并且是否扩容以及如何扩容都可以控制。

讲了这么多,可能有的同学感觉,依然很难理解,下面我们直接来看看具体的代码实现。

下面是一个简易版的 NIO 服务端,示例程序

/**
 * NIO 服务端
 */
public class NioServerTest {

    public static void main(String[] args) throws IOException {
        // 打开服务器套接字通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        // 服务器配置为非阻塞
        ssc.configureBlocking(false);
        // 进行服务的绑定,监听8080端口
        ssc.socket().bind(new InetSocketAddress(8080));

        // 构建一个Selector选择器,并且将channel注册上去
        Selector selector = Selector.open();
        // 将serverSocketChannel注册到selector,并对accept事件感兴趣(serverSocketChannel只能支持accept操作)
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        while (true){
            // 查询指定事件已经就绪的通道数量,select方法有阻塞效果,直到有事件通知才会有返回,如果为0就跳过
            int readyChannels = selector.select();
            if(readyChannels == 0) {
                continue;
            };
            //通过选择器取得所有key集合
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectedKeys.iterator();
            while (iterator.hasNext()){
                SelectionKey key = iterator.next();
                //判断状态是否有效
                if (!key.isValid()) {
                    continue;
                }
                if (key.isAcceptable()) {
                    // 处理通道中的连接事件
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel sc = server.accept();
                    sc.configureBlocking(false);
                    System.out.println("接收到新的客户端连接,地址:" + sc.getRemoteAddress());

                    // 将通道注册到选择器并处理通道中可读事件
                    sc.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    // 处理通道中的可读事件
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    while (channel.isOpen() && channel.read(byteBuffer) != -1) {
                        // 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)
                        if (byteBuffer.position() > 0) {
                            break;
                        };
                    }
                    byteBuffer.flip();

                    //获取缓冲中的数据
                    String result = new String(byteBuffer.array(), 0, byteBuffer.limit());
                    System.out.println("收到客户端发送的信息,内容:" + result);

                    // 将通道注册到选择器并处理通道中可写事件
                    channel.register(selector, SelectionKey.OP_WRITE);
                } else if (key.isWritable()) {
                    // 处理通道中的可写事件
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    byteBuffer.put("server send".getBytes());
                    byteBuffer.flip();
                    channel.write(byteBuffer);

                    // 将通道注册到选择器并处理通道中可读事件
                    channel.register(selector, SelectionKey.OP_READ);
                    //写完之后关闭通道
                    channel.close();
                }
                //当前事件已经处理完毕,可以丢弃
                iterator.remove();
            }
        }
    }
}

下面是一个简易版的 NIO 客户端,示例程序

/**
 * NIO 客户端
 */
public class NioClientTest {

    public static void main(String[] args) throws IOException {
        // 打开socket通道
        SocketChannel sc = SocketChannel.open();
        //设置为非阻塞
        sc.configureBlocking(false);
        //连接服务器地址和端口
        sc.connect(new InetSocketAddress("127.0.0.1", 8080));
        while (!sc.finishConnect()) {
            // 没连接上,则一直等待
            System.out.println("客户端正在连接中,请耐心等待");
        }

        // 发送内容
        ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
        writeBuffer.put("Hello,我是客户端".getBytes());
        writeBuffer.flip();
        sc.write(writeBuffer);

        // 读取响应
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        while (sc.isOpen() && sc.read(readBuffer) != -1) {
            // 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)
            if (readBuffer.position() > 0) {
                break;
            };
        }
        readBuffer.flip();

        String result = new String(readBuffer.array(), 0, readBuffer.limit());
        System.out.println("客户端收到服务端:" + sc.socket().getRemoteSocketAddress() + ",返回的信息:" + result);

        // 关闭通道
        sc.close();
    }
}

最后,依次启动服务端、客户端,看看控制台输出情况如何。

服务端控制台结果如下:

接收到新的客户端连接,地址:/127.0.0.1:57644
收到客户端发送的信息,内容:Hello,我是客户端

客户端控制台结果如下:

客户端收到服务端:/127.0.0.1:8080,返回的信息:server send

从编程上可以看到,NIO 的操作比传统的 IO 操作要复杂的多

Selector 被称为选择器 ,当然你也可以翻译为多路复用器 。它是 Java NIO 核心组件中的一个,用于检查一个或多个 Channel(通道)的状态是否处于连接就绪接受就绪可读就绪可写就绪

如此可以实现单线程管理多个 channel 的目的,也就是可以管理多个网络连接。

使用 Selector 的好处在于 :相比传统方式使用多个线程来管理 IO,Selector 只使用了一个线程就可以处理所有的通道,从而实现网络高效传输!

同时,Java 对Selector还做了优化处理,当调用Selector.select()方法时有阻塞的效果,当没有Channel的时候,不会一直不停的去空循环,避免消耗 CPU 资源,这个功能需要操作系统来支持。

针对不同的操作系统,Java 会执行对应的系统调用(Linux 2.6 之前是 select、poll,2.6 之后是 epoll,Windows 是 IOCP),当有新事件到来的时候才会返回。

所以,你可以放心大胆地在一个while(true)里面调用这个函数而不用担心 CPU 空转。

3.2、Buffer 类详解

接着我们再来说说 Buffer,也称为缓冲区,在 Java NIO 中负责数据的存取。

Buffer 其实是一个数组,可以用于存储不同类型的数据,根据数据类型的不同(boolean 除外),提供了相应类型的缓冲区类,类关系图如下:

上述的子类,管理方式几乎一致,都可以通过allocate()静态方法来获取堆内缓冲区对象。

Buffer 有 2 个核心方法和 4 个核心属性,下面我们一起来看看。

核心方法,内容如下:

  • put():存入数据到缓冲区中
  • get():从缓冲区中的读取数据

核心属性,内容如下:

  • capacity:容量,表示缓冲区中最大存储数据的容量,一旦声明不能更改
  • limit:界限,表示缓冲区中可以操作数据的最大界限
  • position:位置,表示缓冲区中正在操作数据的位置
  • mark:标记,可以使用mark()方法记录当前position的位置,后续可以通过reset()方法恢复到mark标记的位置

以分配一个大小为 10 个字节容量的缓冲区,向里面写入abcde并读取数据为例,Buffer 处理过程如下图:

有几个地方,需要特别注意:

  • 0 <= mark <= position <= limit <= capacity
  • limit后的数据不能进行读写

下面是一些关于 Buffer 类相关操作示例介绍!

  • put 和 get 方法基本操作,示例程序
public static void main(String[] args) {
        String str = "abcde";

        //分配一个指定大小的缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        System.out.println("---------allocate-----------");
        System.out.println(byteBuffer.capacity());   //1024
        System.out.println(byteBuffer.limit());      //1024
        System.out.println(byteBuffer.position());   //0

        //利用 put() 存入数据到缓冲区中
        byteBuffer.put(str.getBytes());
        System.out.println("---------put-----------");
        System.out.println(byteBuffer.capacity());   //1024
        System.out.println(byteBuffer.limit());      //1024
        System.out.println(byteBuffer.position());   //5

        //切换到读数据模式
        byteBuffer.flip();
        System.out.println("---------flip-----------");
        System.out.println(byteBuffer.capacity());   //1024
        System.out.println(byteBuffer.limit());      //5,limit 表示可以操作数据的大小,只有 5 个字节的数据给你读,所以可操作数据大小是 5
        System.out.println(byteBuffer.position());   //0,读数据要从第 0 个位置开始读

        //利用 get() 读取缓冲区中的数据
        byte[] dst = new byte[byteBuffer.limit()];
        byteBuffer.get(dst);
        System.out.println(new String(dst,0,dst.length));
        System.out.println("---------get-----------");
        System.out.println(byteBuffer.capacity());   //1024
        System.out.println(byteBuffer.limit());      //5,可以读取数据的大小依然是 5 个
        System.out.println(byteBuffer.position());   //5,读完之后位置变到了第 5 个

        //rewind() 可重复读
        byteBuffer.rewind();         //这个方法调用完后,又变成了读模式
        System.out.println("---------rewind-----------");
        System.out.println(byteBuffer.capacity());   //1024
        System.out.println(byteBuffer.limit());      //5
        System.out.println(byteBuffer.position());  //0

        //clear() 清空缓冲区,虽然缓冲区被清空了,但是缓冲区中的数据依然存在,只是出于"被遗忘"状态。意思其实是,缓冲区中的界限、位置等信息都被置为最初的状态了,所以你无法再根据这些信息找到原来的数据了,原来数据就出于"被遗忘"状态
        byteBuffer.clear();
        System.out.println("---------clear-----------");
        System.out.println(byteBuffer.capacity());   //1024
        System.out.println(byteBuffer.limit());      //1024
        System.out.println(byteBuffer.position());  //0
    }

输出结果如下:

---------allocate-----------
1024
1024
0
---------put-----------
1024
1024
5
---------flip-----------
1024
5
0
abcde
---------get-----------
1024
5
5
---------rewind-----------
1024
5
0
---------clear-----------
1024
1024
0
  • mark 方法基本操作,示例程序
public static void main(String[] args) {
    String str = "abcde";

    // 写数据
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    byteBuffer.put(str.getBytes());

    // 读取指定几个数据
    byteBuffer.flip();
    byte[] byteArray = new byte[byteBuffer.limit()];
    byteBuffer.get(byteArray,0,2);
    System.out.println(new String(byteArray,0,2));  //结果是 ab
    System.out.println(byteBuffer.position());   //结果是 2

    // 利用mark()标记一下当前 position 的位置
    byteBuffer.mark();
    byteBuffer.get(byteArray,2,2);
    System.out.println(new String(byteArray,2,2));
    System.out.println(byteBuffer.position());   //结果是 4

    // 利用 reset() 恢复到 mark 的位置
    byteBuffer.reset();
    System.out.println(byteBuffer.position());   //结果是 2

    //判断缓冲区中是否还有剩余数据
    if (byteBuffer.hasRemaining()) {
        //获取缓冲区中可以操作的数量
        System.out.println(byteBuffer.remaining());  //结果是 3,上面 position 是从 2 开始的
    }
}

输出结果如下:

ab
2
cd
4
2
3
  • allocate 和 allocateDirect 的区别

在 Buffer 类中,除了有allocate()静态方法可以创建对象外,还有allocateDirect()也可以创建对象。

它们直接最明显的区别是:allocate()创建的对象,是建立在 JVM 的内存之中的,也称为堆内内存;allocateDirect()创建的对象,是建立在 JVM 的内存之外的,不会占用 JVM 的内存空间,也称为堆外内存,更具体的说是建立到物理内存之中,由操作系统来代管。

示例程序如下:

public static void main(String[] args) {
    // 分配直接缓冲区
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
    // 判断是直接缓冲区还是非直接缓冲区
    System.out.println(byteBuffer.isDirect());
}

输出结果:

true

JVM 对堆外对象的管理,主要是通过操作系统与物理内存之间建立一份映射文件,以此来扩充或者回收物理内存空间。

此外,JVM 堆内内存和堆外内存的区别,我们可以用如下的例子来解释。

以 Java 程序读取磁盘文件为例,操作系统出于安全的考虑,当应用程序向操作系统发起读取磁盘数据的操作时,首先是操作系统会将从磁盘读取的数据存放到内核空间,然后 CPU 再将内核空间的数据复制到 JVM 内存中,以供 Java 程序使用,在 JVM 中的数据,都属于堆内内存。

还有另一种方式,就是省掉 CPU 将内核空间的数据复制到 JVM 内存的过程,当操作系统将从磁盘读取的数据存放到内核空间后,Java 程序通过映射文件直接操作物理内存的数据,效率上会有所提高,在 JVM 中的数据,都属于堆外内存。

那是不是所有的对象都可以直接采用堆外内存呢?

通过allocateDirect()方式分配的内存,比起 JVM 内存的分配要耗时得多,所以并非不论什么时候使用allocateDirect()的操作效率都是最高的。

以拷贝文件为例,从实际的使用情况看,当操作的数据量很小时,两种操作使用时间基本是同样的,第一种方式有时可能会更快些;当操作的数据量大,比如大文件,堆外内存有优势。

四、小结

最后总结一些,NIO 相比传统的 BIO 模型,最大的不同点在于:采用单线程非阻塞的编程方式来处理客户端所有的连接请求,虽然执行过程比较消耗 CPU,但性能非常的高。

不过 java NIO 并没有完全屏蔽平台的差异,它仍然是基于各个操作系统的 I/O 系统实现的,差异仍然存在。

其次 JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%!

最后如果自己使用 NIO 做网络编程并不容易,自行实现的 NIO 很容易出现各类 bug,陷阱重重,维护成本较高。

推荐大家使用成熟的 NIO 框架,比如 Netty,MINA 等。解决了很多 NIO 的陷阱,并屏蔽了操作系统的差异,有较好的性能和编程模型。

五、参考

1、美团技术团队 - Java NIO浅析

1、hepingfly - Java 中 NIO 看这一篇就够了

六、写到最后

最近无意间获得一份阿里大佬写的技术笔记,内容涵盖 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多线程、JPA、MyBatis、MySQL 等技术知识。需要的小伙伴可以点击如下链接获取,资源地址:技术资料笔记。

不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激!

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

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

相关文章

民国漫画杂志《时代漫画》第24期.PDF

时代漫画24.PDF: https://url03.ctfile.com/f/1779803-1248635000-177187?p9586 (访问密码: 9586) 《时代漫画》的杂志在1934年诞生了&#xff0c;截止1937年6月战争来临被迫停刊共发行了39期。 ps: 资源来源网络!

怎么理解直接程序控制和中断方式?

直接程序控制 看完之后是不是依然一头雾水&#xff1f;来看下面两个例子 无条件传送 假设你正在使用键盘打字。当你敲击键盘上的一个键时&#xff0c;键盘会立即产生一个信号&#xff08;即输入数据&#xff09;&#xff0c;并且这个信号会立即被电脑接收。在这个过程中&…

属于程序员的浪漫,一颗会跳动的心!!!

绘制一颗会跳动的心❤ 嘿嘿 可以说是程序员的专属浪漫了吧&#xff0c;就像点燃一颗LED灯一样&#xff1f;&#xff08;我瞎说的啊&#xff0c;大家别当真&#xff0c;我很菜的&#xff01;&#xff01;&#xff01;&#xff01;&#xff09; 程序就在下面啦&#xff0c;然…

Markdown魔法手册:解锁高效写作的新技能

边使用边更新0.0... 文章目录 一、如何在Markdown中插入表情&#xff1f;二、文字样式设置1.文本颜色设置2.文本字号设置3.文本字体设置4. 实战演练5.黄色高亮 一、如何在Markdown中插入表情&#xff1f; 在Markdown中插入表情&#xff08;emoji&#xff09;的方法取决于你使用…

C#--SVG矢量图画法示例

1.代码示例 <Viewbox Grid.Column"1" Grid.ColumnSpan"1" Grid.RowSpan"1" ><Path Name"ValveShape" Stroke"Black" Data"M 50,0 L 150,200 L 50,200 L 150,0 Z" Width"200" Height"…

网络安全等级保护:正确配置 Linux

正确配置 Linux 对Linux安全性的深入审查确实是一项漫长的任务。原因之一是Linux设置的多样性。用户可以使用Debian、Red Hat、Ubuntu或其他Linux发行版。有些可能通过shell工作&#xff0c;而另一些则通过某些图形用户界面&#xff08;例如 KDE 或 GNOME&#xff09;工作&…

uni-app微信小程序动态切换tabBar,根据不同用户角色展示不同的tabBar

前言 在UniApp的开发小程序过程中&#xff0c;为了针对不同角色用户登录后的个性化需求。通过动态权限配置机制&#xff0c;能够根据用户的角色展示不同的TabBar。此项目是通过Uni-App命令行的方式搭建的Vue3ViteTsPiniaUni-ui的小程序项目 最终效果 1、司机角色&#xff1a; …

Kubernetes(K8S) 集群环境搭建指南

Kubernetes&#xff08;简称K8s&#xff09;是一个开源的容器编排平台&#xff0c;旨在自动化部署、扩展和管理容器化应用。K8S环境搭建过程比较复杂&#xff0c;涉及到非常多组件安装和系统配置&#xff0c;本文将会详细介绍如何在服务器上搭建好Kubernetes集群环境。 在学习…

MybatisPlus中自定义sql

背景 在开发过程中&#xff0c;可能会出现除了where条件&#xff0c;其它sql比较复杂&#xff0c;这时候就需要用到自定义sql了。 问题 如&#xff1a;用户状态为正常的数据年龄加一&#xff08;所有用户年龄加一&#xff09; 数据库中sql&#xff1a; UPDATE USER SET…

Go GORM介绍

GORM 是一个功能强大的 Go 语言 ORM&#xff08;对象关系映射&#xff09;库&#xff0c;它提供了一种方便的方式来与 SQL 数据库进行交互&#xff0c;而不需要编写大量的 SQL 代码。 GORM的关键特性 全功能的ORM&#xff1a;支持几乎所有的ORM功能&#xff0c;包括模型定义、基…

【漏洞复现】英飞达医学影像存档与通信系统 WebJobUpload 任意文件上传漏洞

0x01 产品简介 英飞达医学影像存档与通信系统 Picture Archiving and Communicaton System&#xff0c;它是应用在医院影像科室的系统&#xff0c;主要的任务就是把日常产生的各种医学影像(包括核磁&#xff0c;CT&#xff0c;超声&#xff0c;各种X光机&#xff0c;各种红外仪…

如何编辑 PDF 中的文本?4个有效的编辑PDF方法

PDF 文件可以轻松打开和查看&#xff0c;但修改要复杂得多 - 尤其是在 PDF 中的文本编辑方面。 知道如何离线编辑 PDF 中的文本对于任何需要快速更改而无需在线加载文档或担心安全问题的人来说都非常有益。它使用户能够更好地控制他们的文档&#xff0c;并有更广泛的字体和图形…

详解 Scala 的集合类型

一、集合简介 1. 类型 序列 Seq&#xff1a;类似于 Java 中的 List 接口集 Set&#xff1a;类似于 Java 中的 Set 接口映射 Map&#xff1a;类似于 Java 中的 Map 接口所有的集合都扩展自 Iterable 特质 2. 不可变集合 位于 scala.collection.immutable 包&#xff0c;指该集…

【Linux】fork和exec中的信号继承探索

fork和exec中的信号继承探索 一、结论二、代码验证2.1 代码编写2.2 代码执行 三、linux源码验证四、APUE中的验证五、其他 一、结论 fork时子进程会继承父进程的信号处理方式&#xff0c;包括父进程设置信号为SIG_DFL或SIG_IGN或捕获后设置自定义处理函数。exce时子进程会继承…

有些错误,常犯常新、常新常犯:记录一个使用element-plus的tooltip组件的错误

使用element-plus的tooltip组件&#xff0c;最开始的写法是这样的&#xff1a; <el-tooltipclass"box-item"effect"dark"content"tooltip content" ><el-button v-if"isDisabled" :underline"false" type"pr…

一文扫尽Nas常用Docker软件

NAS&#xff08;Network Attached Storage&#xff0c;网络附加存储&#xff09;设备上的Docker软件选择取决于您的具体需求和用途。以下是一些NAS上常用的Docker软件推荐&#xff1a; Docker管理工具&#xff1a; Watchtower&#xff1a;它可以自动更新Docker容器中的镜像&…

从零开始学Vue3--环境搭建

1.搭建环境 下载nodejs nodejs下载地址 更新npm npm install -g npm 设置npm源&#xff0c;加快下载速度 npm config set registry https://registry.npmmirror.com 使用脚手架创建项目 npm create vuelatest 根据你的需要选择对应选项 进入新建的项目下载依赖 npm in…

kaggle竞赛实战3

接前文&#xff0c;本文主要做以下几件事&#xff1a; 1、把前面处理完的几个表拼成一个大表 2、做特征衍生&#xff08;把离散特征和连续特征两两组合得出&#xff09; # In[89]: #开始拼接表 transaction pd.concat([new_transaction, history_transaction], axis0, ignor…

STM32 USART的字符编码(发送器的实现逻辑)

目录 概述 1 字符编码 1.1 USART 字符说明 1.2 字长编程 2 发送器 2.1 字符发送 2.2 可配置的停止位 2.3 配置停止位方法 3 单字节通信 4 中断字符 5 空闲字符 概述 本文主要讲述STM32 USART的发送端功能实现的原理&#xff0c;包括字节编码长度&#xff0c;发送器…

Cyber Weekly #8

赛博新闻 1、微软召开年度发布会Microsoft Build 2024 本周&#xff08;5.22&#xff09;微软召开了年度发布会&#xff0c;Microsoft Build 2024&#xff0c;发布了包括大杀器 Copilot Studio 在内的 50 项更新。主要包括&#xff1a; 硬件层面&#xff1a;与英伟达 & A…