Java的NIO工作机制

文章目录

    • 1. 问题引入
    • 2. NIO的工作方式
    • 3. Buffer的工作方式
    • 4. NIO数据访问方式

1. 问题引入

在网络通信中,当连接已经建立成功,服务端和客户端都会拥有一个Socket实例,每个Socket实例都有一个InputStream和OutputStream,并通过这两个对象来交换数据。同时我们也知道网络I/O都是以字节流传输的,当创建Socket对象时,操作系统会为InputStream和OutputStream分别分配一定大小的缓存区,数据的写入都是通过这个缓存区完成的。写入端将数据写到SendQ队列中,当队列填满时,数据将被转移到另一端的InputStream的RecvQ队列中,如果这时RecvQ已经满了,那么OutputStream的write方法将会阻塞直到RecvQ队列有足够的空间容纳SendQ发送的数据。值的注意的是,这个缓存区的大小及写入端的速度和读取端的速度非常影响这个连接的数据传输效率,由于可能发生阻塞,所以网络I/O与磁盘I/O不同的是数据的写入和读取还要有一个协调的过程,如果两边同时传送数据可能会产生死锁。使用NIO可以解决该问题。

死锁产生的根本原因是 客户端和服务端都需要对方释放资源,例如在缓存队列中写入数据。当缓存队列满时,写操作会被阻塞,但同时双方都可能需要对方的缓存队列空间,这样就形成了资源争用的情况

2. NIO的工作方式

  • BIO带来的挑战

BIO即阻塞I/O,不管是磁盘I/O还是网络I/O,数据载写入OutputStream或者从InputStream读取时都可能被阻塞,一旦被阻塞,线程将会失去CPU的使用权,这在当前大规模访问量和有性能要求的情况下时不能被接受的。虽然当前的网络I/O有一些解决方案,如一个客户端一个处理线程,出现阻塞时只能是一个线程阻塞而不会影响其他线程工作,还有为了减少系统线程的开销,采用线程池的办法来减少线程创建和回收的成本。单如果当前需要大量的HTTP长连接的情况,线程池可能无法创建那么多线程来保持连接。所以此时我们需要一种新的I/O操作方式。

磁盘IO场景
在磁盘 I/O 中,阻塞通常发生在数据的读取和写入过程中。假设一个应用程序需要从磁盘读取大量数据:

  1. 读取阻塞: 当应用程序发起读取请求时,系统会将请求发送给磁盘驱动器,然后等待磁盘驱动器将数据加载到内存中。在这个过程中,应用程序的线程会被阻塞,直到读取操作完成。如果读取的数据量较大,阻塞时间可能会显著增加。
  2. 写入阻塞: 类似地,当应用程序发起写入请求时,系统会将数据传输到磁盘,然后等待写入操作完成。写入操作的阻塞时间取决于数据的大小和磁盘的性能。如果写入的数据量很大,应用程序可能会长时间地被阻塞。

网络IO场景
在网络 I/O 中,BIO 模型同样存在阻塞问题。考虑一个基于阻塞 I/O 的服务器应用程序,它接受客户端连接并处理数据:

  1. 接受连接阻塞: 当服务器调用accept函数等待客户端连接时,如果没有客户端连接进来,该调用会一直阻塞。在这段时间内,服务器的线程无法执行其他任务,造成资源浪费。
  2. 读取数据阻塞: 在已经建立连接的情况下,当服务器调用read函数等待接收客户端发送的数据时,如果没有数据到达,该调用会一直阻塞。服务器线程被迫等待,可能会导致性能下降。
  3. 写入数据阻塞: 类似地,当服务器调用write函数将数据发送到客户端时,如果客户端接收缓冲区已满,写入操作也会阻塞。这可能使得服务器线程长时间处于等待状态。
  • NIO工作机制
    Java IO和NIO的主要区别在于两者的处理方式不同。Java IO是面向流(Stream)的,它将输入输出数据直接传输到目标设备或文件中,以流的形式进行读写;而NIO则是面向缓冲区(Buffer)的,它将会使用缓存去管理数据,使得读写操作更加快速和灵活。
    在这里插入图片描述

Buffer类

在 Java NIO 中,Buffer 类是一个抽象类,表示一个数据缓冲区,用于在通道(Channel)和原始数据之间进行数据传输。主要的 Buffer 子类包括 ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer 和 DoubleBuffer,它们分别对应不同的基本数据类型。

public abstract class Buffer { 
    //mark 属性和 reset() 方法一起使用,用于在缓冲区中设置一个标记(mark)位置,并在之后通过 reset() 方法将当前位置重置为这个标记位置。mark 属性的作用是用于记录一个位置,以便后续能够回到该位置,方便重新处理或采取其他措施。
    private int mark = -1;
    //当前位置的索引
    private int position = 0;
    //限制位置的索引,即缓冲区中数据的有效长度
    private int limit;
    //缓存区的容量
    private int capacity;
    //内存段的代理对象
    final MemorySegmentProxy segment;
    //该方法用于设置新的索引位置
      public Buffer position(int newPosition) {
        //如果新的索引位置大于数据的有效长度,或小于0,抛出异常
        if (newPosition > limit | newPosition < 0)
            throw createPositionException(newPosition);
        //如果新的索引位置在mark标记之后,那么标记置为-1
        if (mark > newPosition) mark = -1;
        //然后将当前索引位置设置为新的索引位置
        position = newPosition;
        return this;
    }
	//设置新的有效数据长度
	   public Buffer limit(int newLimit) {
	   //如果新的有效数据长度大于缓冲区最大容量或小于0,抛出异常
        if (newLimit > capacity | newLimit < 0)
            throw createLimitException(newLimit);
        //将有效数据长度设置为新的有效数据长度
        limit = newLimit;
        //如果当前位置所以大于新的有效数据长度,那么当前位置索引同样设置为有效数据长度
        if (position > newLimit) position = newLimit;
        //mark如果大于新的有效数据长度,则直接设置为无效
        if (mark > newLimit) mark = -1;
        return this;
    }
    //用于标记当前索引位置
    public Buffer mark() {
        mark = position;
        return this;
    }
    //将当前索引位置重置为mark标记所在的位置
    public Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }
    //清空缓存区数据,实际上就是重置了mark、position和limit
    public Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }
    //flip() 方法可以将缓冲区从写模式切换到读模式。调用 flip() 后,位置被设置为0,限制被设置为之前的位置,用于准备读取缓冲区中的数据。
      public Buffer flip() {
      //limit有效数据长度为当前索引所在位置
        limit = position;
        //postion设置为0,表示从0开始读取
        position = 0;
        mark = -1;
        return this;
    }
    //将缓冲区的位置设置为 0,限制保持不变,用于重新读取缓冲区中的数据,类似于 flip() 但不改变限制。
     public Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }
    //获取buffer中剩余的有效容量
    public final int remaining() {
        int rem = limit - position;
        return rem > 0 ? rem : 0;
    }
    
}

Channel类
Java NIO 中,Channel 是一个接口,它提供了用于读取和写入数据的统一的 API。Channel 接口是 NIO 用于与 I/O 设备(如文件、套接字、选择器等)交互的核心部分。Channel 接口的实现类可以包括文件通道、套接字通道等。

public interface Channel extends Closeable {
//判断通道是否打开
 public boolean isOpen();
 //关闭通道
   public void close() throws IOException;
}

Seletionkey类
SelectionKey 类是 Java NIO 中的关键类,它用于表示注册到 Selector 上的通道和对应的事件。SelectionKey 对象是 Selector 与通道之间的桥梁,它包含了与通道相关的一些信息,以及通道所感兴趣的事件,以便在选择器上进行有效的事件选择。

选择键则是一种将通道和选择器进行关联的机制。

public abstract class SelectionKey {
//SelectableChannel 是 Java NIO 中表示支持非阻塞模式的通道的抽象类,该返回返回一个通道
 public abstract SelectableChannel channel();
 //该方法返回一个选择器
 public abstract Selector selector();
 //获取通道所感兴趣的操作集合(事件集合),返回一个位掩码,用于表示关注的事件
 public abstract int interestOps();
 //读操作的位掩码,用于表示通道已经准备好进行读操作。
 public static final int OP_READ = 1 << 0;
 //写操作的位掩码,用于表示通道已经准备好进行写操作。
 public static final int OP_WRITE = 1 << 2;
 //连接操作的位掩码, 用于表示连接已经建立
 public static final int OP_CONNECT = 1 << 3;
 //accept操作的位掩码, 用于表示通道已经准备好接受新的连接。
 public static final int OP_ACCEPT = 1 << 4;
 //返回通道现在是否可读
 public final boolean isReadable() {
        return (readyOps() & OP_READ) != 0;
    }
    //通道现在是否可写
  public final boolean isWritable() {
        return (readyOps() & OP_WRITE) != 0;
    }
  //通道是否已经连接就绪
  public final boolean isConnectable() {
        return (readyOps() & OP_CONNECT) != 0;
    }
    //通道是否可以接受新的连接
 public final boolean isAcceptable() {
        return (readyOps() & OP_ACCEPT) != 0;
    }
  //将指定的对象附加到此键
  public final Object attach(Object ob) {
        return ATTACHMENT.getAndSet(this, ob);
    }
  
}

Selector类

Selector 是 Java NIO 中的一个关键类,用于实现非阻塞 I/O 操作的多路复用。通过 Selector,可以在单个线程上同时监控多个通道的事件,从而实现高效的事件驱动编程模型。

public abstract class Selector implements Closeable {
     //返回新创建的选择器实例
	 public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }
    //判断selector是否已经打开
     public abstract boolean isOpen();
    //SelectorProvider 是一个抽象类,用于提供 Selector 和 Channel 的创建。每个 Selector 都与一个 SelectorProvider 实例关联,而SelectorProvider 实例的具体实现是由具体的操作系统提供的。
	 public abstract SelectorProvider provider();
	//获取选择器上所有的键集
	public abstract Set<SelectionKey> keys();
	//关闭选择器
	public abstract void close() throws IOException;
	// 阻塞,直到至少有一个通道在选择器上准备好进行 I/O 操作,或者调用线程被中断。返回已经准备就绪的通道的数量。
	public abstract int select() throws IOException;
	//非阻塞地检查是否有通道准备好进行 I/O 操作。返回已经准备就绪的通道的数量。
	public abstract int selectNow() throws IOException;
    //唤醒因为调用 select 或 selectNow 方法而处于阻塞状态的线程。返回调用 wakeup 方法的选择器。
	public abstract Selector wakeup();
}

关键类就是Channel和Selector,它们是NIO中的两个核心概念。Channel 通过 register 方法可以注册到 Selector 中,以实现非阻塞 I/O 操作。Channel 和 Buffer 之间通过 read 和 write 方法进行数据的传输。数据首先被写入到缓冲区,然后从缓冲区读取到通道或从通道读取到缓冲区。Channel 可以通过 register 方法注册到 Selector 上,注册时需要指定感兴趣的事件,例如读、写等。SelectionKey 对象表示了一个通道在一个选择器上的注册信息,它与通道和选择器之间建立了关联。SelectionKey 包含了通道、选择器、感兴趣的操作集合(事件集合)、附件等信息。
在这里插入图片描述
下面看看NIO是如何工作的:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class NioServer {

    public static void main(String[] args) {
        try (
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open() //创建一个通道
        ) {
            //将通道绑定到一个指定的端口,ServerSocketChannel使用选择器(Selector)来管理多个通道,可以在一个线程中处理多个通道的连接请求。
            serverSocketChannel.bind(new InetSocketAddress(8080));
//将通道设置为非阻塞模式
         serverSocketChannel.configureBlocking(false);
            //创建一个选择器
            Selector selector = Selector.open();
            //将通道注册到选择器中,状态是等待接受客户端连接
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            System.out.println("NIO Server started on port 8080");

            while (true) {
                //自旋获取准备好的通道
                int readyChannels = selector.select();
                if (readyChannels == 0) {
                    continue;
                }
                //获取所有的键集
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                //迭代键集
                Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    //处理相应事件
                    if (key.isAcceptable()) {
                        handleAcceptEvent(key, selector);
                    } else if (key.isReadable()) {
                        handleReadEvent(key);
                    }

                    keyIterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void handleAcceptEvent(SelectionKey key, Selector selector) throws IOException {
        //获取键所关联的通道
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
        //准备接受连接
        SocketChannel clientChannel = serverSocketChannel.accept();
        //设置通道模式为非阻塞
        clientChannel.configureBlocking(false);
        //通道注册到选择器上,状态为可以读取数据
        clientChannel.register(selector, SelectionKey.OP_READ);
        System.out.println("Accepted connection from " + clientChannel);
    }

    private static void handleReadEvent(SelectionKey key) throws IOException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        //分配缓存区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //通道读取
        int bytesRead = clientChannel.read(buffer);

        if (bytesRead == -1) {
            System.out.println("Connection closed by client: " + clientChannel);
            clientChannel.close();
        } else if (bytesRead > 0) {
            buffer.flip();
            clientChannel.write(buffer);
        }
    }
}

上面代码用NIO实现了一个简单的服务器

调用Selector的静态工厂方法创建一个选择器(这个静态工厂是操作系统底层实现的)。然后创建一个服务端Channel,绑定到一个Socket对象,并把这个通道注册到选择器上,把这个通道设置为非阻塞模式,然后就可以调用Selector的selectedKeys方法检查已经注册在这个选择器上的所有通信信道是否有需要事件发生,从而可以读取通信的数据,而这里读取的数据是Buffer,这个Buffer是我们可以控制的缓冲区。在上面这段程序中,将server的监听连接请求和事件吹了放在一个线程中,但是在事件应用中,我们通常会放在两个线程中,一个线程专门监听客户端的连接请求,而且是以阻塞的方式执行的;另一个线程专门负责处理请求,这个专门处理请求的线程才会真正采用NIO的方式。

下图展示量NIO的工作方式,Selector可以监听一组通信信道上的I/O状态,前提是这个Selector已经注册到这些通信通道中,选择器Selector可以调用select()方法检查已经注册的通信信道上I/O是否已经准备好,如果没有至少一个信道I/O状态发生变化,那么select方法会阻塞等待或在超时时间后返回0。如果有多个信道有数据,那么将会把这些数据分配到对应的数据Buffer中。所以关键的地方是,有一个线程来处理所有连接的数据交互,每个链接的数据交互不是阻塞方式的,所以可以同时处理大量的连接请求。
在这里插入图片描述

  • 总结

NIO模式下客户端和服务端通信流程对比如下:

1.服务端初始化:

服务端创建一个 ServerSocketChannel,并绑定到一个特定的端口。将 ServerSocketChannel 设置为非阻塞模式,并注册到一个 Selector 上,以监听连接事件。

2.客户端初始化

客户端创建一个 SocketChannel,连接到服务端的地址。将 SocketChannel 设置为非阻塞模式,并注册到一个 Selector 上,以监听连接事件。

3.事件循环

服务端和客户端都进入一个事件循环,不断地检查发生的事件。在服务端,可能会检查 OP_ACCEPT 事件,表示有新的连接请求。在客户端,可能会检查 OP_CONNECT 事件,表示连接已建立。(这里如果一直没有客户端访问程序陷入空转并不会阻塞,这样也就避免了内核切换)

4.处理连接事件

在服务端,当有新的连接请求到达时,通过 ServerSocketChannel.accept() 接受连接,并将新的 SocketChannel 注册到 Selector 上监听读事件。

5.客户端处理连接事件

在客户端,当连接建立完成时,通过 SocketChannel.finishConnect() 完成连接,然后注册到 Selector 上监听读事件

6.处理读事件

在服务端和客户端,当通道可读时,从通道中读取数据

这样,通过 NIO,服务端和客户端可以通过非阻塞的方式处理多个连接,并在一个事件循环中实现数据的读取和写入。在实际应用中,可能需要结合多线程、线程池等机制,以更好地处理多个连接的并发处理。

3. Buffer的工作方式

通过前面分析源码,我们也大致知道了Buffer的工作方式,Buffer可以简单理解为一组基本数据类型的元素列表,它通过介个变量来保持这个数据当前的位置状态,也就是有四个索引:

在这里插入图片描述

我们通过ByteBuffer.allocate(11)方法创建了一个11个byte的数组缓冲区,初始状态如下所示,position为0,capacity和limit都是数组默认长度。
在这里插入图片描述
当我们写入五个数据后位置如下所示。

在这里插入图片描述
调用flip方法,数组切换状态为读状态:
在这里插入图片描述
此时底层操作系统可以从缓存区中正确读取这5个字节数据并发送出去了,在下一次写数据之前我们再调用一下clear方法,缓冲区又回回到默认位置。mark标记就是记录当前position的前一个位置,我们调用reset时,position会恢复到mark位置。

通过Channel获取的I/O数据首先要经过操作系统的Socket缓冲区缓冲区再讲数据复制到Buffer中,从操作系统缓冲区到用户缓冲区比较消耗性能,Buffer提供了另外一种直接操作操作系统缓冲区的方法,即ByteBuffer.allocateDirector(size);,该方法返回与底层存储空间关联的缓冲区,它通过Native代码操作非JVM堆的内存空间,每次创建和释放的时候都会调用一次System.gc()。

4. NIO数据访问方式

我们知道当我们通过IO读取磁盘数据时,需要使用操作系统的系统调用方法,而这会涉及用户态到内核态的切换,操作系统需要把数据读取到内核态空间,然后将数据送到用户态,这是十分重的一个操作,NIO提供了两个优化方法:

  • FIleChannel.transferXXX

减少数据从内核到用户空间中的复制,数据直接在哪和空间中移动,下图首先是传统数据访问方式,然后是tansferXXX方式:

在这里插入图片描述
在这里插入图片描述

  • FileChannel.map

它将文件按照一定大小块映射为内存区域,当程序访问这个内存区域时,直接操作这个文件数据,这种方式就直接跳过了数据从内核空间向用户空间复制。

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

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

相关文章

云基础软件深化合作,云轴科技ZStack与麒麟软件战略签约

12月8日&#xff0c;云轴科技ZStack与麒麟软件战略合作签约仪式在北京举行&#xff0c;双方对过往紧密合作表达了充分肯定&#xff0c;并就进一步联合技术创新、打造重点行业标杆和持续赋能客户达成高度共识。云轴科技创始人&CEO张鑫和麒麟软件高级副总经理谢文征共同见证双…

高通平台开发系列讲解(USB篇)Composite USB gadget framework

文章目录 一、Gadget framework二、Composite driver and gadget driver interaction沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇章主要图解高通平台PCIe EP软件架构 一、Gadget framework Composite USB gadget framework 架构如下所示: The composite fram…

<软考>软件设计师-4知识产权与标准化(总结)

(一)知识产权概述 1 知识产权 是指公民、法人、非法人单位对自己的创造性智力成果和其他科技成果依法享有的民事权。是智力成果的创造人依法享有的权利和在生产经营活动中标记所有人依法所享有的权利的总称。包含著作权、专利权、商标权、商业秘密权、植物新品种权、集成电路布…

Network 灰鸽宝典【目录】

目前已有文章 21 篇 Network 灰鸽宝典专栏主要关注服务器的配置&#xff0c;前后端开发环境的配置&#xff0c;编辑器的配置&#xff0c;网络服务的配置&#xff0c;网络命令的应用与配置&#xff0c;windows常见问题的解决等。 文章目录 服务配置环境部署GitNPM 浏览器编辑器系…

玻色量子袁为出席中国移动第四届科技周量子计算算法与应用分论坛

9月12日&#xff0c;中国移动第四届科技周“量子计算算法与应用”分论坛在北京成功举办&#xff0c;中国移动研究院院长黄宇红发表致辞&#xff0c;中国移动未来研究院院长崔春风全程主持。玻色量子作为光量子计算领域真机测试与场景应用的标杆企业应邀出席&#xff0c;玻色量子…

Kubernetes(k8s)集群部署----->超详细

Kubernetes&#xff08;k8s&#xff09;集群部署-----&#xff1e;超详细 一、资源准备二、安装准备2.1 主机环境设置2.1.1 关闭操作系统防火墙、selinux2.1.2 关闭swap交换分区2.1.3 允许iptables检测桥接流量&#xff08;可选&#xff09; 2.2 安装Docker环境2.3 安装Kubeadm…

自动化补丁管理软件

什么是自动化补丁管理 自动补丁管理&#xff08;或自动补丁&#xff09;是指整个补丁管理过程的自动化&#xff0c;从扫描网络中的所有系统到检测缺失的补丁&#xff0c;在一组测试系统上测试补丁&#xff0c;将它们部署到所需的系统&#xff0c;并提供定期更新和补丁部署状态…

不再兼容“安卓“,鸿蒙开发与android对比

首先&#xff0c;鸿蒙系统采用了分布式技术&#xff0c;其设计理念是“能用分布式解决的问题就不用单机解决”。这意味着鸿蒙旨在构建一个统一的分布式操作系统&#xff0c;可以支持不同设备之间的交互和通信。 而安卓系统基于Linux内核和Java编程语言构建&#xff0c;属于单机…

代理ip一般适用于什么行业,什么场景

代理IP适用于多个行业和场景&#xff0c;以下是其中一些主要的应用领域&#xff1a; 互联网营销&#xff1a; 数据抓取&#xff1a;用于收集竞争对手的价格、产品信息等。社交媒体管理&#xff1a;在不同账户之间切换&#xff0c;提高账号安全性或进行市场调研。广告投放优化&a…

探索SSL证书的应用场景,远不止网站,还有小程序、App Store等

说到SSL证书&#xff0c;我们都知道其是用于实现HTTPS加密保障数据安全的重要工具&#xff0c;在建设网站的时候经常会部署SSL证书。但实际上&#xff0c;SSL证书的应用场景远不止网站&#xff0c;它还被广泛地应用到小程序、App Store、抖音广告、邮件服务器以及各种物联网设备…

西南交通大学【数电实验6---可控分频器设计】

一、实验电路图、状态图、程序代码、仿真代码、仿真波形图&#xff08;可以只写出核心功能代码&#xff0c;代码要有注释&#xff09; 不管sel为0或者1&#xff0c;clk_out[0]的频率都是不变的&#xff0c;故在always块当中&#xff0c;可优先对clk_out[0]进行处理&#xff0c;…

文件操作及函数

什么是文件&#xff1f; 在程序设计中&#xff0c;文件有两种&#xff1a;程序文件和数据文件。 程序文件 包括源程序文件&#xff08;.c&#xff09;&#xff0c;目标文件&#xff08;.obj&#xff09;&#xff0c;可执行程序(.exe)。 数据文件 文件的内容不一定是程序&…

超过 1450 个 pfSense 服务器因错误链而遭受 RCE 攻击

在线暴露的大约 1450 个 pfSense 实例容易受到命令注入和跨站点脚本漏洞的攻击&#xff0c;这些漏洞如果链接起来&#xff0c;可能使攻击者能够在设备上执行远程代码。 pfSense 是一款流行的开源防火墙和路由器软件&#xff0c;允许广泛的定制和部署灵活性。 它是一种经济高效…

MYSQL练题笔记-子查询-电影评分

一、题目相关内容 1&#xff09;相关的表 2&#xff09;题目 3&#xff09;帮助理解题目的示例&#xff0c;提供返回结果的格式 二、自己初步的理解 1.字典序是指从前到后比较两个字符串大小的方法。 首先比较第1个字符&#xff0c;如果不同则第1个字符较小的字符串更小&…

在idea中使用maven创建dynamic web project

0、先正确安装MAVEN, TOMCAT &#xff0c;并集成到idea 1、new 一个 project&#xff0c; 使用maven的archetype-webapp创建 2、等待创建&#xff0c;会提示build success 3、给project 添加tomcat配置&#xff0c;并部署project到 tomcat 4、运行 5、OK 6、再次引入时&…

CentOS 7 源码部署 Nginx

文章目录 1. 概述2. 部署示例2.1 下载和解压 Nginx 源码2.2 安装编译依赖包2.3 编译和安装2.4 启动 Nginx2.5 配置防火墙2.6 设置 Nginx 为系统服务2.7 配置访问 3. 扩展知识 1. 概述 Nginx 是一款高性能的开源 Web 服务器软件&#xff0c;广泛应用于互联网领域。本篇博客将介…

go学习之反射知识

反射 文章目录 反射1、反射的使用场景1&#xff09;结构体标签的应用2&#xff09;使用反射机制编写函数的适配器&#xff08;桥连接&#xff09; 2、反射的基本介绍-1.基本介绍-2.反射的图解-3.反射重要的函数和概念 3.反射快速入门-1.请编写一个函数&#xff0c;演示对&#…

设计模式-享元模式

设计模式专栏 模式介绍模式特点应用场景工厂模式和享元模式的区别代码示例Java实现享元模式python实现享元模式 享元模式在spring中的应用 模式介绍 享元模式是一种软件设计模式&#xff0c;它使用共享对象来减少内存使用量&#xff0c;并分享信息给尽可能多的相似对象。这种模…

【C语言程序设计】循环结构程序设计

目录 前言 一、程序设计第一题 二、程序设计第二题 三、程序设计第三题 总结 &#x1f308;嗨&#xff01;我是Filotimo__&#x1f308;。很高兴与大家相识&#xff0c;希望我的博客能对你有所帮助。 &#x1f4a1;本文由Filotimo__✍️原创&#xff0c;首发于CSDN&#x1f4da…

C语言—每日选择题—Day45

第一题 1. 以下选项中&#xff0c;对基本类型相同的指针变量不能进行运算的运算符是&#xff08;&#xff09; A&#xff1a; B&#xff1a;- C&#xff1a; D&#xff1a; 答案及解析 A A&#xff1a;错误&#xff0c;指针不可以相加&#xff0c;因为指针相加可能发生越界&…