NIO编程

目录

1、什么是NIO编程?

为什么说Java NIO是非阻塞的?

2、Java NIO 通道(Channel)详解

如何获取Channel对象?

3、Java NIO 缓冲区(Buffer)详解

(1)获取缓冲区对象

(2)将数据写入Buffer以及从Buffer读取数据

(3)通道Channel和缓冲区Buffer的相互读写

(4)图解缓冲区Buffer

4、Java NIO 选择器/Selector详解

(1)将channel注册到Selector上

(2)为什么说 select() 方法是阻塞的?

(3)Selector 的 selectedKeys() 方法

5、为什么说Java NIO是事件驱动的?

6、NIO编程完整的代码示例


1、什么是NIO编程?

        NIO(New I/O)是Java中一种提供了非阻塞式I/O操作的编程模型。它引入了一组新的Java类,用于取代传统的Java I/O类(如InputStream和OutputStream),以提供更高效、更灵活的I/O操作。// 部分NIO API实际上是阻塞的,例如File API

        Java NIO允许执行非阻塞的IO。例如,线程可以请求通道将数据读入缓冲区。当通道将数据读入缓冲区时,线程可以做其他事情。一旦数据被读入缓冲区,线程就可以继续处理它。将数据写入通道也是如此。// 非阻塞的核心在于选择器

        NIO的核心组件包括以下几个方面:

  1. 通道(Channel):通道是数据传输的载体,类似于传统的流(Stream)。不同的是,通道可以同时支持读和写操作,而流一般只能单向传输数据。通道可以连接到文件、套接字等不同的I/O源。//数据总是从通道读入缓冲区,或从缓冲区写入通道
  2. 缓冲区(Buffer):缓冲区是NIO中用于存储数据的对象。数据从通道读入缓冲区,或者从缓冲区写入通道。缓冲区可以提供更高效的数据处理,因为它可以批量读写数据,而不需要逐个字节地操作。
  3. 选择器(Selector):选择器是NIO的一个关键概念,用于实现多路复用。它允许单个线程监视多个通道的I/O事件,从而提高系统资源的利用率。选择器会不断轮询注册在其上的通道,一旦某个通道就绪(有数据可读或可写),就会通知程序进行相应的处理。

        下面是一个Thread使用Selector处理3个Channel的示例:

NIO编程_第1张图片

        要使用Selector,首先需要注册Channel,然后调用它的select()方法。select()方法将阻塞,直到为其中一个已注册通道准备好事件(Event)为止。select()方法返回后,线程就可以处理这些事件。

为什么说Java NIO是非阻塞的?

        Java NIO提供了一种非阻塞的I/O操作模型,相比传统的阻塞式I/O模型,Java NIO允许应用程序在进行I/O操作时不需要等待数据的到达或发送完成,而可以继续执行其他任务。

        在传统的阻塞式I/O模型中,当一个线程执行一个I/O操作(如读取文件或网络数据)时,它将被阻塞,直到操作完成或发生错误。这意味着在阻塞模型下,一个线程只能处理一个I/O操作,如果有多个I/O操作需要处理,就需要使用多个线程,增加了线程的开销和管理复杂性。

        而Java NIO使用了非阻塞的I/O操作模型。它引入了Channel(通道)和Selector(选择器)的概念,使得应用程序可以注册多个Channel到一个Selector上,并通过Selector来监控这些Channel的状态。Selector可以轮询已注册的Channel,当某个Channel满足I/O事件时(如可读、可写等),就会通知应用程序进行相应的处理

        在非阻塞模型下,一个线程可以同时处理多个Channel的I/O操作,而不需要为每个Channel创建一个单独的线程。这种方式可以大大提高系统的并发性能和资源利用率。

        此外,Java NIO还提供了基于事件驱动的异步I/O操作模型(AIO,Asynchronous I/O),通过使用Future和回调机制,允许应用程序在进行I/O操作时不需要主动等待操作完成,而是在操作完成后由操作系统通知应用程序。// NIO2

2、Java NIO 通道(Channel)详解

        Java NIO通道类似于流,但有一些不同:

  1. 可以对通道进行读和写操作。流通常是单向的(读或写)。// 通道可以读和写
  2. 通道可以异步读写。
  3. 通道总是向缓冲区读或从缓冲区写。

        如上所述,数据总是从通道读入缓冲区,或者从缓冲区写入通道:

NIO编程_第2张图片

        下面是Java NIO中最重要的Channel实现:

  1. FileChannel:从文件中读取和写入数据。
  2. DatagramChannel:可以通过UDP在网络上读写数据。
  3. SocketChannel:可以通过TCP在网络上读写数据。
  4. ServerSocketChannel:允许侦听传入的TCP连接,如web服务器一样。该Channel会为每个传入连接创建一个SocketChannel。

如何获取Channel对象?

        在Java NIO中,可以通过以下几种方式来获取Channel对象:

        (1)文件通道(FileChannel):通过文件输入流(FileInputStream)或文件输出流(FileOutputStream)获取文件通道。示例代码如下:

FileInputStream fis = new FileInputStream("path/to/file");
FileChannel fileChannel = fis.getChannel();

FileOutputStream fos = new FileOutputStream("path/to/file");
FileChannel fileChannel = fos.getChannel();

        (2)网络通道(SocketChannel、ServerSocketChannel、DatagramChannel):通过Java NIO提供的网络编程类获取网络通道。示例代码如下:

SocketChannel socketChannel = SocketChannel.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
DatagramChannel datagramChannel = DatagramChannel.open();

        (3)管道(Pipe):通过管道获取通道。管道用于在两个线程之间进行通信,可以使用Pipe.open()方法打开一个管道,并通过source()和sink()方法获取通道。示例代码如下:

Pipe pipe = Pipe.open();
Pipe.SinkChannel sinkChannel = pipe.sink();
Pipe.SourceChannel sourceChannel = pipe.source();

        这些方法返回的Channel对象可以用于读取或写入数据,执行I/O操作。

        另外,需要注意的是,以上方法获取的Channel对象都是阻塞式的。如果想要使用非阻塞模式,可以通过调用configureBlocking(false)方法将通道设置为非阻塞模式,然后可以使用Selector来进行非阻塞I/O操作。// FileChannel无此方法,所以它总是阻塞的

SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);

        这样设置后,可以通过Selector来监控这些非阻塞通道的状态,并进行相应的I/O操作。

        下面是一个使用FileChannel复制文件的基本示例:// 通道+缓冲区+阻塞式

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.channels.FileChannel;
import java.nio.ByteBuffer;

public class FileCopyExample {
    public static void main(String[] args) {
        try {
            FileInputStream sourceFile = new FileInputStream("F:\\source.txt");
            FileOutputStream destinationFile = new FileOutputStream("F:\\destination.txt");

            FileChannel sourceChannel = sourceFile.getChannel();
            FileChannel destinationChannel = destinationFile.getChannel();

            ByteBuffer buffer = ByteBuffer.allocate(1024);

            while (sourceChannel.read(buffer) != -1) {
                // 翻转缓冲区,为通道写入序列做准备:limit设置为当前position的值,position设置为0,mark设置为-1(废弃)
                buffer.flip();
                destinationChannel.write(buffer);
                // 在放置数据之前清空缓存,为通道读写入序列做准备:limit设置为capacity的值,position设置为0,mark设置为-1(废弃):
                buffer.clear();
            }

            sourceChannel.close();
            destinationChannel.close();
            sourceFile.close();
            destinationFile.close();

            System.out.println("File copied successfully.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3、Java NIO 缓冲区(Buffer)详解

        缓冲区本质上是一块内存,你可以向其中写入数据,然后可以再次读取数据。该内存块封装在NIO Buffer对象中,该对象提供了一组方法,让使用内存块变得更容易

        Java NIO附带了以下缓冲区类型:

  1. ByteBuffer(字节缓冲区): ByteBuffer是最常用的缓冲区类型之一,它可以存储字节数据。ByteBuffer可以分为直接缓冲区和非直接缓冲区。直接缓冲区使用操作系统的本地内存,可以提高I/O操作的性能。
  2. CharBuffer(字符缓冲区): CharBuffer用于存储字符数据,它是基于16位Unicode字符的缓冲区。CharBuffer提供了一些方便的方法来处理字符数据。
  3. ShortBuffer、IntBuffer、LongBuffer(短整型、整型和长整型缓冲区): 这些缓冲区类型分别用于存储短整型、整型和长整型数据。它们提供了特定于数据类型的方法和属性。
  4. FloatBuffer、DoubleBuffer(浮点型和双精度浮点型缓冲区): 这些缓冲区类型用于存储浮点型和双精度浮点型数据。它们也提供了特定于数据类型的方法和属性。

(1)获取缓冲区对象

        在Java NIO中,可以通过以下方式来获取Buffer对象:

        1)使用分配方法(Allocation Methods):每个缓冲区类(如ByteBuffer、CharBuffer、IntBuffer等)都提供了allocate()方法来分配一个新的缓冲区对象。示例代码如下:

ByteBuffer buffer = ByteBuffer.allocate(1024);
CharBuffer buffer = CharBuffer.allocate(1024);

        这种方式会分配一个指定容量的缓冲区对象,可以根据需要进行读写操作。

        2)使用包装方法(Wrapping Methods):除了分配方法,缓冲区类还提供了wrap()方法,用于包装现有的数组来创建缓冲区对象。示例代码如下:

byte[] byteArray = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap(byteArray);

char[] charArray = new char[1024];
CharBuffer buffer = CharBuffer.wrap(charArray);

        这种方式会使用现有的数组作为缓冲区的数据存储。

        3)使用视图方法(View Buffer):缓冲区类还提供了一些视图方法,可以创建基于现有缓冲区的新缓冲区对象。这些视图缓冲区共享底层缓冲区的数据,并根据视图的不同提供不同类型的访问方式。常见的视图缓冲区有ByteBuffer的子类:CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer和DoubleBuffer等。示例代码如下:

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
CharBuffer charBuffer = byteBuffer.asCharBuffer();
IntBuffer intBuffer = byteBuffer.asIntBuffer();

        这种方式可以根据需要创建适合特定数据类型的缓冲区。

(2)将数据写入Buffer以及从Buffer读取数据

        1)将数据写入Buffer

        在Java NIO中,有多种方式可以将数据写入Buffer对象。以下是几种常用的方式:

        使用put()方法:Buffer类提供了多个put()方法的重载形式,可以根据不同数据类型来写入数据。例如:

  1. put(byte b):将单个字节写入Buffer。
  2. put(byte[] array):将字节数组的内容写入Buffer。
  3. put(ByteBuffer src):将另一个ByteBuffer的内容写入当前Buffer。
  4. putXxx()方法(如putChar(char value)、putInt(int value)等):根据数据类型将特定类型的数据写入Buffer。

        使用通道(Channel):可以使用通道的read()方法将数据直接写入Buffer。例如:

int bytesRead = inChannel.read(buf); //read into buffer.

        2)从Buffer读取数据

        从Buffer读取数据的两种方式:使用get()方法和通道(Channel)

        使用get()方法从缓冲区读取数据:

byte aByte = buf.get();  

        还有许多其他版本的get()方法,允许以许多不同的方式从Buffer读取数据。例如,在特定位置读取,或者从缓冲区读取一个字节数组的数据等。

        将数据从缓冲区读入通道(Channel):可以使用通道的write()方法将数据从Buffer读入通道。例如:

//read from buffer into channel.
int bytesWritten = inChannel.write(buf);

(3)通道Channel和缓冲区Buffer的相互读写

        1)把数据从Channel写入Buffer:channel.read(buffer)

// 创建一个FileChannel
FileChannel channel = new FileInputStream("path/to/file").getChannel();
// 创建一个ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将数据从Channel读取到Buffer中
int bytesRead = channel.read(buffer);

        2)把数据从Buffer写入Channel:channel.write(buffer)

// 创建一个FileChannel
FileChannel channel = new FileOutputStream("path/to/file").getChannel();
// 创建一个ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将数据写入Buffer
buffer.put("Hello, World!".getBytes());
buffer.flip(); // 切换到读模式
// 将Buffer中的数据写入Channel
while (buffer.hasRemaining()) {
    channel.write(buffer);
}
// 关闭Channel
channel.close();

(4)图解缓冲区Buffer

        1)缓冲区的创建

  1. 创建一个缓冲区:ByteBuffer allocate = ByteBuffer.allocate(10);
  2. 然后往缓冲区中添加数据:allocate.put("ABC".getBytes());

NIO编程_第3张图片

  • 容量(Capacity):Buffer的容量表示它所能够存储的数据元素的最大数量。一旦Buffer被分配,它的容量就是固定的,不能更改。//内存块
  • 位置(Position):Buffer的位置表示下一个要读取或写入的元素的索引。初始时,位置为0,并且在读取或写入操作后会自动更新。
  • 限制(Limit):Buffer的限制表示在读取或写入操作中,最多可以读取或写入的元素数量。初始时,限制与容量相等(limit = capacity),并且在读取或写入操作后可以手动设置。

        示例代码:

public static void main(String[] args) throws IOException {
        //1、创建一个缓冲区
        ByteBuffer allocate = ByteBuffer.allocate(10);
        System.out.println(allocate.position());//0 获取当前索引所在位置
        System.out.println(allocate.limit());//10 最多能操作到哪个索引位置
        System.out.println(allocate.capacity());//10 返回缓冲区总长度
        System.out.println(allocate.remaining());//10 还有多少个可以操作的个数:limit - position
        System.out.println("———————————————————");
        //2、添加3个字节
        allocate.put("ABC".getBytes());
        System.out.println(allocate.position());//3 获取当前索引所在位置
        System.out.println(allocate.limit());//10 最多能操作到哪个索引位置
        System.out.println(allocate.capacity());//10 返回缓冲区总长度
        System.out.println(allocate.remaining());//7 还有多少个可以操作的个数
    }

        2)缓冲区的读写模式转换

        写模式切换到读模式:flip()方法

        flip()方法:用于将Buffer从写模式切换到读模式。flip()方法的作用是设置limit为当前位置,然后将position重置为0,以便在读取数据之前将Buffer准备好。

        当将数据写入Buffer时,Buffer会跟踪写入了多少数据。一旦需要从Buffer读取数据,就需要调用flip()方法将Buffer从写入模式切换到读取模式。在读取模式下,缓冲区允许读取写入缓冲区的所有数据// 作用:将缓冲区从写入模式切换到读取模式,读取缓冲区中的数据

NIO编程_第4张图片

        读模式切换到写模式:clear()和compact()方法

        clear()和compact():一旦从Buffer中读取了所有数据,就需要清除缓冲区,以便为再次写入做好准备。调用clear()或compact()方法可以达到清除缓冲区的效果。clear()方法会清除整个缓冲区,而compact()方法只会清除已经读取的数据,未读的数据都会被移到缓冲区的开头,新进的数据将在未读数据之后进行写入。// clear()和compact() 并不会真正清除数据,只是修改了相关位置数据的指针

NIO编程_第5张图片

        切换到写模式:clear()方法将Buffer从读模式切换到写模式,重置位置position为0,限制limit设置为容量capacity,以便重新写入数据。

        清空数据:clear()方法不会清除缓冲区的数据,而是将缓冲区标记为可重写状态,之前写入的数据仍然存在,但是在写模式下可以覆盖它们。

4、Java NIO 选择器/Selector详解

        在Java NIO中,Selector是一个可用于多路复用(Multiplexing)I/O操作的关键组件。它允许单个线程同时管理多个通道(Channels),监控这些通道的I/O事件(如读就绪、写就绪等),并且在有就绪事件发生时进行响应。

        以下是关于Selector的一些重要概念和使用方式:

        1)创建和获取Selector对象:

  • 创建:可以通过Selector.open()方法创建一个新的Selector对象。
  • 获取:可以通过SelectableChannel对象的selector()方法获取与之关联的Selector对象。

        2)注册通道到Selector:

  • 通道必须是可选择的(SelectableChannel),如SocketChannel、ServerSocketChannel、DatagramChannel等。
  • 通过调用通道的register(Selector selector, int interestOps)方法将通道注册到Selector上。
  • interestOps参数表示感兴趣的事件类型,可使用SelectionKey类的常量来指定,如SelectionKey.OP_READ、SelectionKey.OP_WRITE等。

        3)选择操作:

  • 调用Selector的select()方法进行选择操作,它会阻塞直到至少有一个注册的通道就绪或被中断。
  • 返回值表示有多少个通道已经就绪,可以通过Selector的selectedKeys()方法获取就绪的SelectionKey集合。

        4)处理就绪事件:

  • 通过遍历selectedKeys()方法返回的就绪SelectionKey集合,可以获取每个就绪通道的相关信息。
  • 可以使用SelectionKey的channel()方法获取就绪通道,readyOps()方法获取就绪的操作类型。
  • 可以根据就绪的操作类型执行相应的处理逻辑。

        5)取消注册和关闭:

  • 调用SelectionKey的cancel()方法可以取消通道的注册。
  • 调用Selector的close()方法可以关闭Selector对象。

        使用Selector可以在单个线程中同时处理多个通道的I/O操作,避免了为每个通道分配一个线程的开销。这种方式常用于需要同时管理多个通道的高并发应用场景,如网络服务器。

        需要注意的是,Selector是基于事件驱动的,所以在处理就绪事件时需要注意及时响应并处理,否则可能会错过事件。

(1)将channel注册到Selector上

        使用Selector,必须首先向选择器注册通道。向选择器注册通道可以使用Channel.register()方法进行注册,如下所示:

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

        注意:Channel必须处于非阻塞模式才能与Selector一起使用。所以不能在选择器中使用FileChannel,因为FileChannel不能切换到非阻塞模式// 所以一般文件的读写还是使用的BIO

        向Selector注册channel,也是由操作系统完成的,也调用了底层操作系统的相关实现,这是因为Selector的实现机制基于操作系统提供的底层I/O多路复用机制实现的

        通过register()方法注册channel时,需要指定侦听的 I/O 事件类型(读、写等)。选择器可以侦听四种不同的事件:Connect、Accept、Read、Write,具体如下所示:

        SelectionKey中定义的4种事件: // Java NIO基于事件驱动

  1. SelectionKey.OP_ACCEPT:接收连接继续事件,表示服务器监听到了客户连接,服务器可以接收这个连接了
  2. SelectionKey.OP_CONNECT:连接就绪事件,表示客户端与服务器的连接已经建立成功
  3. SelectionKey.OP_READ:读就绪事件,表示通道中已经有了可读的数据,可以执行读操作了(通道目前有数据,可以进行读操作了)
  4. SelectionKey.OP_WRITE:写就绪事件,表示已经可以向通道写数据了(通道目前可以用于写操作)

(2)为什么说 select() 方法是阻塞的?

        select()方法是阻塞的主要原因是它的实现机制基于操作系统提供的底层I/O多路复用机制,例如Linux上的select()系统调用或Windows上的select()函数。这些底层机制都是阻塞的,因此select()方法在调用底层机制时也会阻塞。// select()方法是由操作系统实现的

        看下源码,点击Selector.select()方法,进入Selector的实现类SelectorImpl,它会调用一个lockAndDoSelect()方法:// 基于 java 8

    public int select(long var1) throws IOException {
        if (var1 < 0L) {
            throw new IllegalArgumentException("Negative timeout");
        } else {
            return this.lockAndDoSelect(var1 == 0L ? -1L : var1);
        }
    }

        进入lockAndDoSelect()方法后,在该方法中会调用SelectorImpl.doSelect()方法。

    protected abstract int doSelect(long var1) throws IOException;

    private int lockAndDoSelect(long var1) throws IOException {
        synchronized(this) {
            if (!this.isOpen()) {
                throw new ClosedSelectorException();
            } else {
                int var10000;
                synchronized(this.publicKeys) {
                    synchronized(this.publicSelectedKeys) {
                        var10000 = this.doSelect(var1);
                    }
                }

                return var10000;
            }
        }
    }

        SelectorImpl.doSelect()方法是一个抽象方法,既然是抽象方法就会有对应的实现。继续跟进到该方法的实现类WindowsSelectorImpl.doSelect()方法中,其中有一行代码:this.subSelector.poll(),这行代码就是去调用操作系统的底层实现机制了。

protected int doSelect(long var1) throws IOException {
        if (this.channelArray == null) {
            throw new ClosedSelectorException();
        } else {
            // 省略...
            if (this.interruptTriggered) {
                this.resetWakeupSocket();
                return 0;
            } else {
                // 省略...
                try {
                    this.begin();

                    try {
                        // 在这里调用系统的select()方法
                        this.subSelector.poll();
                    } catch (IOException var7) {
                        this.finishLock.setException(var7);
                    }
                    if (this.threads.size() > 0) {
                        this.finishLock.waitForHelperThreads();
                    }
                } finally {
                    this.end();
                }
                // 省略...
            }
        }
    }

        WindowsSelectorImpl是Java NIO库中用于Windows平台上的Selector的具体实现类。它实现了Selector接口,并提供了在Windows操作系统上使用I/O多路复用机制的功能。  

      继续跟进this.subSelector.poll()方法,可以看到它调用了一个本地方法:// 该方法阻塞

    private int poll() throws IOException {
            return this.poll0(WindowsSelectorImpl.this.pollWrapper.pollArrayAddress, Math.min(WindowsSelectorImpl.this.totalChannels, 1024), this.readFds, this.writeFds, this.exceptFds, WindowsSelectorImpl.this.timeout);
        }

    // 本地方法
    private native int poll0(long var1, int var3, int[] var4, int[] var5, int[] var6, long var7);

        this.subSelector对象

 private final WindowsSelectorImpl.SubSelector subSelector = new WindowsSelectorImpl.SubSelector();

        在WindowsSelectorImpl类中,SubSelector是一个内部类,用于管理特定事件类型的I/O事件。SubSelector继承自AbstractPollArrayWrapper类,用于封装I/O事件的轮询和处理。

        SubSelector类中的poll()方法是用于执行对已注册通道的轮询操作,并返回就绪的通道数量

(3)Selector 的 selectedKeys() 方法

        一旦调用了select()方法,并且它的返回值表明一个或多个通道已经准备就绪,接下来就可以调用选择器的selectedKeys()方法,获取已经准备就绪的通道集。// 这个步骤还挺难理解的,先执行select()方法,然后再执行selectedKeys()方法

        在Java NIO中,Selector的selectedKeys()方法用于获取当前已选择键集合(selected key set)。

    public Set selectedKeys() {
        if (!this.isOpen() && !Util.atBugLevel("1.4")) {
            throw new ClosedSelectorException();
        } else {
            return this.publicSelectedKeys;
        }
    }

        该方法返回一个Set对象,表示当前已选择的键集合。这个集合包含了在上一次选择操作中就绪的SelectionKey对象。可以通过遍历该集合来处理相应的就绪事件。

        需要注意的是,selectedKeys()方法返回的是一个可变的集合,即可以直接在返回的集合上进行增删操作。这意味着在处理完就绪事件后,需要手动从集合中移除相应的SelectionKey,以便下次的选择操作能正确更新就绪状态// 先select(),后执行selectedKeys()

        在使用selectedKeys()方法时,一般会结合Iterator来遍历集合,并逐个处理就绪的SelectionKey。示例如下:

Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
    
    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.
    } else if (key.isConnectable()) {
        // a connection was established with a remote server.
    } else if (key.isReadable()) {
        // a channel is ready for reading
    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
}

        在上述该循环中,会循环判断每一个SelectionKey引用的通道是否已经准备就绪。

        在每次迭代结束时都会调用keyIterator.remove(),这是因为选择器自己不会从键集中删除SelectionKey的实例。所以,当你完成对该通道的处理时,就需要手动去移除它。当通道再次准备就绪时,选择器会再次将这个键添加到所选的键集中。// 手动移除键

        另外,在处理具体的通道时,SelectionKey.channel()方法返回的通道应该被强制转换为需要使用的通道类型,例如ServerSocketChannel或SocketChannel等。

SocketChannel sc = (SocketChannel) key.channel();

5、为什么说Java NIO是事件驱动的?

        Java NIO(New I/O)是事件驱动的,主要基于以下两个核心组件:选择器(Selector)和选择键(SelectionKey)。

  1. 选择器(Selector):选择器是Java NIO中的核心组件之一。它允许单线程管理多个通道(如SocketChannel、ServerSocketChannel等),并根据通道的就绪状态进行事件驱动的操作。选择器可以在通道上注册想要侦听的I/O事件(如读、写等),并在事件就绪时通知应用程序
  2. 选择键(SelectionKey):选择键是通道在选择器上的注册信息。当通道注册到选择器上时,将创建一个选择键来表示该注册关系。选择键包含了通道、选择器以及想要侦听的I/O事件类型。通过选择键,可以获取和修改想要侦听的事件类型,并获取就绪状态的通道。

        Java NIO的事件驱动模型基于以下原理:

  • 注册:应用程序将通道注册到选择器上,并指定想要侦听的I/O事件类型(如读、写等)。
  • 选择:选择器通过调用select()方法进行阻塞,等待就绪的通道。当有通道就绪时,select()方法会返回就绪通道的数量,并可以通过selectedKeys()方法获取到就绪通道的选择键集合。
  • 就绪通知:选择器在某个通道就绪时,会将相应的选择键添加到选择键集合中。应用程序可以通过遍历选择键集合来获取就绪的通道和感兴趣的事件类型。
  • 事件处理:应用程序可以根据就绪的通道和感兴趣的事件类型,执行相应的操作,如读取数据、写入数据、接受连接等。处理完事件后,可以取消或修改通道的注册信息。

        Java NIO的事件驱动模型允许应用程序在单线程中同时处理多个通道的I/O操作,提高了系统的并发性和可扩展性。相比于传统的阻塞I/O模型,事件驱动模型减少了线程的创建和管理开销,提高了系统的效率和响应能力。

// 在Java NIO中,Selector并不会不断轮询注册在其上的所有通道。实际上,Selector使用的是操作系统提供的事件通知机制,如epoll、kqueue等,来监听和等待就绪的通道。

// 验证操作系统提供的事件通知机制,待续...

6、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.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.UUID;

public class NioClient {

    public static void main(String[] args) {

        String host = "127.0.0.1";
        int port = 8001;

        Selector selector = null;
        SocketChannel socketChannel = null;

        try {
            selector      = Selector.open();
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false); // 非阻塞

            // 如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答
            if (socketChannel.connect(new InetSocketAddress(host, port))) {
                socketChannel.register(selector, SelectionKey.OP_READ);
                doWrite(socketChannel);
            } else {
                socketChannel.register(selector, SelectionKey.OP_CONNECT);
            }

        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }

        while (true) {
            try {
                selector.select(1000);
                Set selectedKeys = selector.selectedKeys();
                Iterator it = selectedKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()) {
                    key = it.next();
                    it.remove();
                    try {
                        //处理每一个channel
                        handleInput(selector, key);
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null) {
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // 多路复用器关闭后,所有注册在上面的Channel资源都会被自动去注册并关闭
//		if (selector != null)
//			try {
//				selector.close();
//			} catch (IOException e) {
//				e.printStackTrace();
//			}
//
//		}
    }

    public static void doWrite(SocketChannel sc) throws IOException {
        byte[] str = UUID.randomUUID().toString().getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(str.length);
        writeBuffer.put(str);
        writeBuffer.flip();
        sc.write(writeBuffer);
    }

    public static void handleInput(Selector selector, SelectionKey key) throws Exception {

        if (key.isValid()) {
            // 判断是否连接成功
            SocketChannel sc = (SocketChannel) key.channel();
            if (key.isConnectable()) {
                if (sc.finishConnect()) {
                    sc.register(selector, SelectionKey.OP_READ);
                }
            }
            if (key.isReadable()) {
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                if (readBytes > 0) {
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("Server said : " + body);
                } else if (readBytes < 0) {
                    // 对端链路关闭
                    key.cancel();
                    sc.close();
                } else {
                    ; // 读到0字节,忽略
                }
            }
            Thread.sleep(3000);
            doWrite(sc);
        }
    }
}

        服务端代码:

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) throws IOException {
        int port = 8001;
        Selector selector = null;
        ServerSocketChannel servChannel = null;

        try {
            selector    = Selector.open();
            servChannel = ServerSocketChannel.open();
            servChannel.configureBlocking(false);
            servChannel.socket().bind(new InetSocketAddress(port), 1024);
            servChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("服务器在8001端口守候");
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }

        while (true) {
            try {
                selector.select(1000);
                Set selectedKeys = selector.selectedKeys();
                Iterator it = selectedKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()) {
                    key = it.next();
                    it.remove();
                    try {
                        handleInput(selector, key);
                    } catch (Exception e) {
                        if (key != null) {
                            key.cancel();
                            if (key.channel() != null) {
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }

            try {
                Thread.sleep(500);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }

    public static void handleInput(Selector selector, SelectionKey key) throws IOException {

        if (key.isValid()) {
            // 处理新接入的请求消息
            if (key.isAcceptable()) {
                // Accept the new connection
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                SocketChannel sc = ssc.accept();
                sc.configureBlocking(false);
                // Add the new connection to the selector
                sc.register(selector, SelectionKey.OP_READ);
            }
            if (key.isReadable()) {
                // Read the data
                SocketChannel sc = (SocketChannel) key.channel();
                ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                int readBytes = sc.read(readBuffer);
                if (readBytes > 0) {
                    readBuffer.flip();
                    byte[] bytes = new byte[readBuffer.remaining()];
                    readBuffer.get(bytes);
                    String request = new String(bytes, "UTF-8"); //接收到的输入
                    System.out.println("client said: " + request);

                    String response = request + " 666";
                    doWrite(sc, response);
                } else if (readBytes < 0) {
                    // 对端链路关闭
                    key.cancel();
                    sc.close();
                } else {
                    ; // 读到0字节,忽略
                }
            }
        }
    }

    public static void doWrite(SocketChannel channel, String response) throws IOException {
        if (response != null && response.trim().length() > 0) {
            byte[] bytes = response.getBytes();
            ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            channel.write(writeBuffer);
        }
    }
}

        至此,全文结束。

        附NIO编程的一些学习资料《Java NIO 教程》

你可能感兴趣的:(Java,进阶,java,NIO,NIO编程)