Java NIO 学习

缓冲区和通道: Java 中 NIO 看这一篇就够了 - 知乎 (zhihu.com)

案例代码: java之NIO简介_爱上口袋的天空的博客-CSDN博客_java nio

IO 和 NIO区别:java NIO_菜b一枚的博客-CSDN博客_java nio

Selector选择器:nio学习之Selector选择器_大忽悠爱忽悠的博客-CSDN博客_selector选择器

Selector选择器程序案例理解图:简书 (smartapps.cn)

Selector选择器程序案例理解图:【NIO】Selector 详解 - 在下右转,有何贵干 - 博客园 (cnblogs.com)

Selector选择器:Java NIO 的前生今世 之四 NIO Selector 详解_zhaoweiwei369的博客-CSDN博客

Selector选择器:nio学习之Selector选择器_大忽悠爱忽悠的博客-CSDN博客_selector选择器

Selector理解:四、Java NIO Selector_骑士梦的博客-CSDN博客

1、概念

JAVA NIO (New IO 也称之为 Non-Blocking IO) 是从 Java 1.4 版本开始引入的一个新的 IO API,可以代替标准的 Java IO API。

NIO 与 原来的 IO 有同样的作用和目的,但是工作方式完全不一样:标准的 IO 基于字节流和字符流进行操作的;NIO 支持面向缓冲区(Buffer),基于通道(Channel)的 IO 操作,数据总是向通道读取到缓冲区中,或者从缓冲区中写入通道中。

NIO 以更加高效的方式进行文件的读写操作。

Java NIO 学习_第1张图片

2、NIO 和 IO 的区别

NIO 和传统 IO 之间第一个最大的区别是: IO 是面向流的, NIO 是面向缓冲区的。

IO NIO
面向流(Stream Oriented) 面向缓冲区(Buffer Oriented)
阻塞 IO(Blocking) 非阻塞 IO(Non Blocking IO)
选择器(Selectors)

2.1、传统的 IO

1、类图

Java NIO 学习_第2张图片

2、理解

相当于装水的管道,只能一端向另一端单向传输(如分为输入流用来读和输出流用来写)

Java NIO 学习_第3张图片

把文件(网络文件或者磁盘文件)读取到程序来,需要创建一个用于传输数据的管道,传输的数据(一个 Byte 数组,来回进行数据传递)直接在管道中一个一个字节的流动。所以说传统的 IO 就是管道里面的一个数据流动,即传统的 IO 是面向流的。

传统的 IO 流是单向的:把目标数据读取到程序中来,需要建立一个管道,称为输入流。把程序中的数据写到目标地点去,需要再建一个管道,称为输出流。

2.2、NIO

1、类图

Java NIO 学习_第4张图片

2、理解

缓冲区相当于一个可以携带数据双向走动的小车(可以用通道来读写,不分输入流和输出流)

Java NIO 学习_第5张图片

只要是 IO ,就是为了完成数据传输的。NIO 也需要建立用于传输数据的通道,可以理解为火车轨道,通道就是为了连接目标地点和源地点。通道本身不能传输数据,要想传输数据必须要有缓冲区。 这个缓冲区可以理解为火车。(通道只负责连接,缓冲区才负责存储数据

把文件中的数据写到程序中:把数据写到缓冲区 ==> 缓冲区通过通道进行传输 ==> 程序从缓冲区读取数据。

把程序中的数据写到文件中:把数据写到缓冲区 ==> 缓冲区通过通道进行传输 ==> 从缓冲区读取数据写入文件。

3、NIO 三大核心

NIO 主要有三大核心部分: Channel(通道), Buffer(缓冲区),Selector(选择器)。

3.1、Buffer(缓冲区)

1、介绍

一个用于特定基本数据类型(Boolean除外)的容器,由 java.nio 包定义,所有缓冲区都是 Buffer 抽象类的子类。缓冲区就是用于存储不同数据类型的数组。

Java NIO 学习_第6张图片

Buffer(缓冲区)主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道。

Java NIO 学习_第7张图片

2、缓冲区中的四个核心属性
  • capacity(容量值) : 表示缓冲区的存储数据容量,一旦声明不能更改。
  • limit(限制) :表示缓冲区可以操作的数据大小,limit 后的数据不能进行读写。
  • position(位置):表示缓冲区中正在操作数据的位置。
  • mark(标记):表示记录当前 position 的位置,可以通过 reset() 方法恢复到标记的 position 位置。

这几个属性的关系:
0 < = m a r k < = p o s i t i o n < = l i m i t < = c a p a c i t y 0<=mark<=position<=limit<=capacity 0<=mark<=position<=limit<=capacity

3、常用方法
方法 描述
xxxBuffer.allocate(capacity) 分配一个容量为 capacity 的缓冲区。
xxxBuffer.allocateDirect(capacity) 分配一个容量为 capacity 的直接缓冲区。
put() 存入数据到缓冲区中。
get() 获取缓冲区中的数据。
limit() 返回缓冲区限制的位置。
mark() 对缓冲区设置标志。
flip() 设置界限(limit)的当前位置,然后设置位置(position)为零。(读模式)
clear() 设置限制(limit)的能力和位置(position)为零。(数据依旧存在)
rewind() 限制(limit)不变,设置位置(position)为零。(重读读)
reset() 重置此缓冲区的位置(position)到之前标记(mark)的位置。
isReadOnly() 判断该缓冲区是否是只读的。
isDirect() 判断是否是直接缓冲区。
hasRemaining() 判断当前位置(position)和限制(limit)之间是否有任何元素。(return position < limit
remaining() 返回当前位置和限制之间的元素的数目。(return limit - position;
4、直接缓冲区和非直接缓冲区

1)直接缓冲区(非堆内存)

通过 allocateDirect() 方法分配缓冲区,将缓冲区建立在内存物理内存之中。直接作用于本地系统的 IO 操作。直接使用物理内存作为缓冲区,读写数据直接通过物理内存。

使用场景:有很大的数据需要存储,它的生命周期又很长。适合频繁的 IO 操作,如网络并发场景,直接内存具有更高的效率。

如果不是能带来明显的性能提升,推荐使用非直接缓冲区。

Java NIO 学习_第8张图片

2)非直接缓冲区(堆内存)

通过 allocate() 方法分配缓冲区,将缓冲区建立在 JVM 的内存之中。

读数据会先通过系统 IO 读取到内核地址空间中,再将数据拷贝到用户地址空间中(即 JVM 内存中),最后在把数据读到应用程序中。写操作与此类似。

Java NIO 学习_第9张图片

5、代码案例
/**
 * @desc
 * @auth llp
 * @date 2022/7/27 17:03
 */
public class BufferTest {
    public static void main(String[] args) {
        final String str = "abcde";
        // 非直接缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 直接缓冲区
        ByteBuffer bufferDirect = ByteBuffer.allocateDirect(1024);
        System.out.println("========== 初始化 ==============");
        System.out.println("capacity: " + buffer.capacity());
        System.out.println("limit: " + buffer.limit());
        System.out.println("position: " + buffer.position());
        // 写入缓冲区
        buffer.put(str.getBytes());
        System.out.println("========== put ==============");
        System.out.println("limit: " + buffer.limit());
        System.out.println("position: " + buffer.position());
        // 切换到读模式
        buffer.flip();
        System.out.println("========== flip ==============");
        System.out.println("limit: " + buffer.limit());
        System.out.println("position: " + buffer.position());
        // 从缓冲区读取数据
        byte[] bytes = new byte[buffer.limit()];
        buffer.get(bytes, 0, 2);
        System.out.println("========== get ==============");
        System.out.println("limit: " + buffer.limit());
        System.out.println("position: " + buffer.position());
        System.out.println(new String(bytes,0, 2));
        // 标记当前 position 位置
        buffer.mark();
        System.out.println("========== mark ==============");
        System.out.println("limit: " + buffer.limit());
        System.out.println("mark position: " + buffer.position());
        // get
        buffer.get(bytes, buffer.position(), 2);
        System.out.println("========== get ==============");
        System.out.println("limit: " + buffer.limit());
        System.out.println("position: " + buffer.position());
        System.out.println(new String(bytes, 2, 2));
        // reset position 恢复到 mark 位置
        buffer.reset();
        System.out.println("========== reset ==============");
        System.out.println("reset position: " + buffer.position());
        // rewind
        buffer.rewind();
        System.out.println("========== rewind ==============");
        System.out.println("limit: " + buffer.limit());
        System.out.println("position: " + buffer.position());
        // 判断是否有剩余数据
        if(buffer.hasRemaining()){
            System.out.println("========== hasRemaining ==============");
            // 剩余量
            System.out.println("remaining: " + buffer.remaining());
        }
        // 直接与非直接缓冲区判断
        System.out.println("========== isDirect ==============");
        System.out.println("isDirect: " + buffer.isDirect());
        System.out.println("isDirect: " + bufferDirect.isDirect());
        // 清空缓冲区
        buffer.clear();
        System.out.println("========== clear ==============");
        System.out.println("capacity: " + buffer.capacity());
        System.out.println("limit: " + buffer.limit());
        System.out.println("position: " + buffer.position());
    }
}

3.2、Channel(通道)

1、介绍

java.nio.channels 包定义,通道表示 IO 源与目标打开的连接。通道类似于传统的流,但通道本身不能直接访问数据,通道只能与缓冲区进行机交互。通道用于源节点和目标节点的连接,在 Java NIO 中负责缓冲区中数据的传输,通道本身不存储数据,需要配合缓冲区进行传输。

Java NIO 学习_第10张图片

Channel 是一个接口:

public interface Channel extends Closeable {
    /** channel 是否是开启的 */
    public boolean isOpen();
    /** 关闭 channel。在一个通道关闭,任何试图调用I/O操作后,它会造成ClosedChannelException被。
    	如果这个通道已经关闭,则调用该方法没有效果。
		这种方法可能在任何时候被调用。如果一些其他线程已经调用它,那么另一个调用将被阻止,直到第一次调用完成,之后它将返回没有效果。
 	*/
    public void close() throws IOException;
}
2、主要实现类
描述
FileChannel 用于读、写、映射和操作文件的通道。
DatagramChannel 通过 UDP 读写网络中的数据通道。
SocketChannel 通过 TCP 读写网络中的数据通道。(SocketChannel 类似 Socket
ServerSocketChannel 可以监听新进来的 TCP 连接,对每个新进来的连接都会创建一个 SocketChannelServerSocketChannel 类似 ServerSocket

Java 针对支持通道的类的对象提供了 getChannel() 方法调用来获取通道。支持通道的类:

  • 本地 IO : FileInputStreamFileOutputStreamRandomAccessFile
  • 网络 IO :DatagramChannelSocketChannelServerSocketChannelAsynchronousSocketChannelAsynchronousServerSocketChannel

获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回制定通道。

3、FileChannel 类常用方法
方法 描述
read(ByteBuffer dst) 从 Channel 读取数据到给定的缓冲区中。返回已读取的字节数,可能是零,-1表示已经读取完。
read(ByteBuffer[] dsts) 将 Channel 中的数据 “分散” 读取到 ByteBuffer[] 中。
write(ByteBuffer src) 从缓冲区中的数据写入 Channel 中。
write(ByteBuffer[] srcs) 将 ByteBuffer[] 中的数据 “聚集” 写入到 Channel 中。
force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中。
position() 返回此通道的文件位置。
position(long newPosition) 设置此通道的文件位置。
size() 返回此通道文件的当前大小。
truncate(long size) 将此通道的文件截取为给定大小。
transferTo(long position, long count, WritableByteChannel target) 将字节从这通道的文件给出的可写字节通道。
transferFrom(ReadableByteChannel src, long position, long count) 将字节从给定的可读字节通道传输到这个通道的文件中。
open(Path path, OpenOption… options) 打开或创建一个文件,返回一个文件通道来访问该文件。
path: 打开或者创建文件的位置
options: 选项指定如何打开文件
open(Path path, Set options, FileAttribute… attrs) 打开或创建一个文件,返回一个文件通道来访问该文件。
path: 打开或者创建文件的位置
options: 选项指定如何打开文件
attrs : 自动创建文件时文件的可选列表属性设置
4、通道数据传输和内存映射文件

1)使用通道实现文件复制(非直接缓冲区)

public static void test1() throws IOException {
    // 利用通道完成文件的复制(非直缓冲区)
    FileInputStream fis = new FileInputStream("G:\\Typora\\Netty\\test\\input.txt");
    FileOutputStream fos = new FileOutputStream("G:\\Typora\\Netty\\test\\output.txt");
    // 获取通道
    FileChannel fisChannel = fis.getChannel();
    FileChannel fosChannel = fos.getChannel();
    // 通道没有办法传输数据,必须依赖缓冲区
    // 分配指定大小的缓冲区
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    // 将通道的数据存入缓冲区
    while (fisChannel.read(byteBuffer) != -1){
        // 切换成读模式
        byteBuffer.flip();
        // 将缓冲区的数据写入通道
        fosChannel.write(byteBuffer);
        // 清空缓冲区
        byteBuffer.clear();
    }
    // 关闭
    fis.close();
    fos.close();
    fisChannel.close();
    fosChannel.close();
}

2)使用直接缓冲区实现文件复制(内存映射文件)

private static void test2() throws IOException {
    /**
      * 使用 open() 方法来获取通道
      * 参数1:Path 是 JDK1.7 提供的类,代表文件路径
      * 参数2:Option 设置对这个文件的操作模式
      */
    FileChannel inChannel = FileChannel.open(Paths.get("G:\\Typora\\Netty\\test\\input.txt"), StandardOpenOption.READ);
    FileChannel outChannel = FileChannel.open(Paths.get("G:\\Typora\\Netty\\test\\output.txt"), StandardOpenOption.READ,
                                              StandardOpenOption.WRITE, StandardOpenOption.CREATE);
    /**
      * 内存映射文件
      * 这种方式缓冲区是直接建立在物理内存之上
      */
    MappedByteBuffer inMappedByteBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
    MappedByteBuffer outMappedByteBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
    // 直接对缓冲区进行数据读写操作
    byte[] dst = new byte[inMappedByteBuffer.limit()];
    // 把数据读取到 dst 这个字节数组中去
    inMappedByteBuffer.get(dst);
    // 把直接数组的数据写出去
    outMappedByteBuffer.put(dst);
	// 关闭信道
    inChannel.close();
    outChannel.close();
}

通道之间的数据传输:

private static void test3() throws IOException {
    // 通道之间的数据传输 (直接缓冲区)
    FileChannel inChannel = FileChannel.open(Paths.get("G:\\Typora\\Netty\\test\\input.txt"), StandardOpenOption.READ);
    FileChannel outChannel = FileChannel.open(Paths.get("G:\\Typora\\Netty\\test\\output.txt"), StandardOpenOption.READ,
                                              StandardOpenOption.WRITE, StandardOpenOption.CREATE);

    inChannel.transferTo(0, inChannel.size(), outChannel);
    // 或者
    // outChannel.transferFrom(inChannel, 0, inChannel.size());
    // 关闭
    inChannel.close();
    outChannel.close();
}
5、分散读取和聚集写入

5.1、分散读取

分散读取(Scattering Reads)是指从 Channel 中读取数据 ”分散“ 到多个 Buffer 中。

Java NIO 学习_第11张图片

5.2、聚集写入

聚集写入(Gathering Writes)是指将多个 Buffer 中的数据 “聚集” 到 Channel 中。

Java NIO 学习_第12张图片

5.3、代码案例

private static void test4() throws IOException {
    RandomAccessFile raf = new RandomAccessFile("G:\\Typora\\Netty\\test\\input.txt", "rw");
    // 获取通道
    FileChannel channel = raf.getChannel();
    // 分配指定大小的缓冲区
    ByteBuffer buffer1 = ByteBuffer.allocate(16);
    ByteBuffer buffer2 = ByteBuffer.allocate(1024);
    // 分散读取
    ByteBuffer[] buffers = {buffer1, buffer2};
    channel.read(buffers);
    // 切换到读模式
    for (ByteBuffer buffer : buffers) {
        buffer.flip();
        System.out.println(new String(buffer.array(), 0, buffer.limit()));
    }
    // 聚集写入
    RandomAccessFile raf2 = new RandomAccessFile("G:\\Typora\\Netty\\test\\output.txt", "rw");
    // 获取通道
    FileChannel channel2 = raf2.getChannel();
    channel2.write(buffers);
    // 关闭
    raf.close();
    raf2.close();
    channel.close();
    channel2.close();
}

3.3、Selector(选择器)

1、介绍

选择器(Selector)是 SelectableChannel 对象的多路复用器。 选择器可以同时监控多个 SelectableChannel 的 IO 状况。

选择器可以通过调用 Selector 类的 open() 方法创建的,它将使用系统默认的 SelectorProvider 类创建一个新的选择器。

Java NIO 学习_第13张图片

Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程处理多个客户端连接,就会使用到Selector(选择器)。**Selector(选择器)能够检测多个注册的通道上是否有事件发生,如果有事件发生,则获取事件然后针对每个事件进行相应的处理。**这样就可以只用一个单线程去管理多个通道(多个连接和请求,如文件、套接字等的打开到 IO 设备的连接)。只有在 连接/通道 真正有读写事件发生时,才会进行读写,这就大大减少系统的开销,不必为每个连接都创建一个线程去处理,避免多个线程之间的上下文切换导致的开销,不用去维护多线程等。

Java NIO 学习_第14张图片

Selector 选择器维护着注册过的通道集合,并且这些注册关系都封装在 SelectionKey 对象中。

Java NIO 学习_第15张图片

2、方法
  • Selector 类 open() :创建一个Selector。

  • Selector 类 select() : 这个方法会阻塞,直到注册在 Selector 中的 Channel 发送可读写事件。(如:ServerSocketChannel 是否有新的连接)。返回 I/O 操作准备就绪的 Channel。

  • Selector 类 keys() : 返回此选择器所注册的键集。

  • Selector 类 selectedKeys() : 返回此选择器的所注册且有事件发生的键集。

  • SelectableChannel 类 configureBlocking(boolean block) : 设置这个通道的阻塞模式。block: true阻塞模式;false非阻塞模式。

  • SelectableChannel 类 SelectionKey register(Selector sel, int ops) :注册通道到选择器上,返回一个 SelectionKey 。(注意:注册的通道必须是非阻塞的,即调用 configureBlocking()方法设置。FileChannel 不能使用选择器,因为其都是阻塞的。同一通道注册两次相当于更新 ops 参数即SelectorKey).
    参数1: 选择器
    参数2: 选择器对通道的监听事件(指定选择器需要查询的通道操作)。
    可以监听和事件的类型:

ops 参数 描述
SelectionKey.OP_READ 读状态。读事件。
SelectionKey.OP_WRITE 写状态。写事件。
SelectionKey.OP_CONNECT socket 连接状态。连接事件(TCP连接)。
SelectionKey.OP_ACCEPT socket 接收状态。确认事件。

注意: 若注册时不止一个监听事件,则可以使用 “位或” 操作符连接。如

int ops = SelectionKey.OP_READ | SelectionKey.OP_WRITE

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
3、案例

NIO 非阻塞式网络通信入门案例

需求:服务端接收客户端的连接请求,并接收多个客户端发送过来的事件。

服务端:

/**
 * @desc
 * @auth llp
 * @date 2022/7/28 16:37
 */
public class NioServer {
    public static void main(String[] args) {
        try {
            // 1、获取通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // 2、设置非阻塞模式
            serverSocketChannel.configureBlocking(false);
            // 3、绑定端口
            serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 6666));
            // 4、获取选择器
            Selector selector = Selector.open();
            // 5、将通道注册到选择器上,并且指定 "监听接收事件"
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            // 6、轮询的获取选择器上已经”准备就绪“的事件
            while (selector.select() > 0){
                System.out.println("=============开始事件处理==============");
                System.out.println("已注册键的集合中的个数:==> " + selector.keys().size());
                // 7、获取选择器中所有注册的通道中已准备好的事件 (选择键:已经准备就绪的监听事件)
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                // 8、遍历事件
                while (it.hasNext()){
                    // 9、获取准备就绪的事件
                    SelectionKey selectionKey = it.next();
                    System.out.println("selectionKey ----> " + selectionKey);
                    // 10、判断这个事件具体是什么状态的“准备就绪”的事件
                    if (selectionKey.isAcceptable()){
                        // 10.1、获取当前接入事件的客户端通道  若是“接收就绪”,获取客户端连接(通道)
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        // 10.2、切换成非阻塞模式
                        socketChannel.configureBlocking(false);
                        // 10.3、将本客户端通道注册到选择器上
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    }else if (selectionKey.isReadable()){
                        // 10.1、获取当前选择器上的读 通道
                        SocketChannel socketChannel = (SocketChannel)selectionKey.channel();
                        // 10.2、读取
                        // 设置缓冲区
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int len;
                        while ((len = socketChannel.read(buffer)) > 0){
                            // 切换读模式
                            buffer.flip();
                            System.out.println(new String(buffer.array(), 0, len));
                            // 清空缓冲区
                            buffer.clear();
                        }
                    }
                    // 10、处理结果后移除当前事件
                    it.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端:

/**
 * @desc
 * @auth llp
 * @date 2022/7/28 17:11
 */
public class NioClient {
    public static void main(String[] args) {
        try {
            SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
            socketChannel.configureBlocking(false);
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNext()){
                System.out.println("=======请输入=======");
                String msg = scanner.nextLine();
                buffer.put(msg.getBytes(StandardCharsets.UTF_8));
                buffer.flip();
                socketChannel.write(buffer);
                buffer.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

NIO 网络编程应用实例 – 群聊系统

需求:进一步理解NIO 非阻塞式网络通信入门案例,实现多人群聊

  • 编写一个 NIO 群聊系统,实现用户端与用户端的通信要求(非阻塞)
  • 服务器端:可以监测用户上线、离线、实现消息转发功能
  • 客户端:通过 Channel 可以无阻塞发送消息给其它客户端用户,同时可以接收其它客户端用户通过服务器转发的消息

服务端代码:

/**
 * @desc
 * @auth llp
 * @date 2022/7/28 17:38
 */
public class MulNioServer {
    private static final int PORT = 8888;
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;

    // 初始化构造器
    public MulNioServer() {
        try {
            // 1、获取通道
            serverSocketChannel = ServerSocketChannel.open();
            // 2、设置非阻塞模式
            serverSocketChannel.configureBlocking(false);
            // 3、绑定端口
            serverSocketChannel.bind(new InetSocketAddress(PORT));
            // 4、获取选择器
            selector = Selector.open();
            // 5、将通道注册到选择器上,并且指定监听接收事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 监听
    public void listen(){
        System.out.println("监听线程 ==> " + Thread.currentThread().getName());
        try {
            // 6、轮询已经就绪的事件
            while (selector.select() > 0) {
                System.out.println("=============开始事件处理==============");
                // 7、获取选择器中所有注册的通道中已准备好的事件
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                // 8、遍历事件
                while (it.hasNext()) {
                    SelectionKey selectionKey = it.next();
                    System.out.println("selectionKey ----> " + selectionKey);
                    // 9、判断这个事件具体是什么事件
                    if (selectionKey.isAcceptable()) {
                        // 9.1、 获取当前接入事件的客户端通道
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        // 9.2、切换成非阻塞模式
                        socketChannel.configureBlocking(false);
                        // 9.3、将本客户端通道注册到选择器上
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    } else if (selectionKey.isReadable()) {
                        // 处理读
                        readData(selectionKey);
                    }
                    // 10、处理结果后移除当前事件
                    it.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 读取客户端消息
    private void readData(SelectionKey selectionKey) {
        // 获取关联的通道
        SocketChannel channel = null;
        try {
            channel = (SocketChannel)selectionKey.channel();
            // 设置缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int len = channel.read(buffer);
            if (len > 0){
                String msg = new String(buffer.array(), 0, len);
                System.out.println("来自客户端的消息 ==> " + msg);
                // 向其他客户端发送消息(去掉自己)
               sendMsgToOtherClients(msg, channel);
            }
        } catch (IOException e) {
            System.out.println("异常 ====> " + e.getMessage());
            try {
                System.out.println(channel.getRemoteAddress() + " 离线了......");
                // 取消注册
                selectionKey.cancel();
                channel.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }

    // 转发消息给其他客户(通道)
    private void sendMsgToOtherClients(String msg, SocketChannel self) throws IOException {
        System.out.println("服务器转发消息中...");
        System.out.println("服务器转发数据给客户端线程 ==> " + Thread.currentThread().getName());
        // 遍历 所有注册到 selector 上的 SocketChannel, 并排除自己
        for (SelectionKey key : selector.keys()) {
            // 通过 key 取出对应的 SocketChannel
            Channel targetChannel = key.channel();
            // 排除自己
            if (targetChannel instanceof SocketChannel && targetChannel != self){
                // 转换类型
                SocketChannel dstSocketChannel = (SocketChannel) targetChannel;
                // 将消息存到 缓冲区
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes(StandardCharsets.UTF_8));
                System.out.println("服务器转发给 ==> " + key);
                // 将缓冲区写入 通道
                dstSocketChannel.write(buffer);
            }
        }
    }

    public static void main(String[] args) {
        // 创建服务器对象
        MulNioServer mulNioServer = new MulNioServer();
        mulNioServer.listen();
    }
}

客户端代码:

/**
 * @desc
 * @auth llp
 * @date 2022/7/28 18:02
 */
public class MulNioClient {
    private static final String HOST = "127.0.0.1";
    private static final int PORT = 8888;
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    // 初始化客户端
    public MulNioClient(){
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);

            username = socketChannel.getLocalAddress().toString().substring(1);
            System.out.println(username + " is ok.....");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 向服务器发送消息
    public void sendMessage(String message){
        String msg = username + " 说: ==> " + message;
        try {
            socketChannel.write(ByteBuffer.wrap(msg.getBytes(StandardCharsets.UTF_8)));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    // 读取从服务器回复的消息
    public void readMessage(){
        try {
            while (selector.select() > 0) {
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                while (it.hasNext()) {
                    SelectionKey selectionKey = it.next();
                    if (selectionKey.isReadable()) {
                        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        socketChannel.read(byteBuffer);
                        byteBuffer.flip();
                        // 打印消息
                        System.out.println(new String(byteBuffer.array(), 0, byteBuffer.limit()));
                        byteBuffer.clear();
                    }
                    it.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //
    public static void main(String[] args) {
        MulNioClient mulNioClient = new MulNioClient();
        new Thread(()->{
            while (true){
                mulNioClient.readMessage();

                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        // 发送数据给服务器
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()){
            String msg = scanner.nextLine();
            // System.out.println("发送的消息 ==> " + msg);
            mulNioClient.sendMessage(msg);
        }
    }
}

看图好理解:

Java NIO 学习_第16张图片

Java NIO 学习_第17张图片

4、理解

使用 Selector ,首先需要将 Channel 注册到 Selector 中,随后 Selector 调用 select() 方法,这个方法会阻塞,直到注册在 Selector 中的 Channel 发送可读写事件。当这个方法返回后,这个线程就可以处理 Channel 事件了。

向 Selector 注册的任何 Channel 都是 SelectableChannel 的之类。

在注册的时候,需要指定哪些操作是 Selector 感兴趣的。

Selector 查询的不是通道的操作,而是通道的某个操作的一种就绪状态

什么是就绪状态?

一旦通道具备完成某个操作的条件,就表示某个操作已经就绪,就可以被 Selector 查询到,程序可以对通道进行操作。

一个 SocketChannel 通道可以连接到一个服务器,则处于 连接就绪(OP_CONNECT);

一个 ServerSocketChannel 服务器通道准备好接收新进入的连接,则处于 接收就绪(OP_ACCEPT);

一个 有数据可读的 Channel ,处于 读就绪(OP_READ);

一个 等待写数据的 Channel ,处于 写就绪(OP_WRITE);

一个通道并不是支持所有的四种操作,如 服务器通道 ServerSocketChannel 支持 OP_ACCEPT,而 客户端通道 SocketChannel 并不支持。可以通过 validOps() 方法,来获取特定通道下所支持的操作集合。

总结: Selector 只对注册的 Channel 指定的操作感兴趣。注册多少个 Channel ,Selector 下就有多少个 Channel

4、AIO

java中IO模型-AIO模型_西财彭于晏的博客-CSDN博客_java中aio

【Java】Java AIO使用 - SegmentFault 思否

深入分析 Java I/O (四)AIO_keep_trying_gogo的博客-CSDN博客

BIO,NIO,多路复用,AIO_Aaron_涛的博客-CSDN博客_bio、nio、aio

Java NIO 学习_第18张图片

BIO(IO) NIO AIO
Socket SocketChannel AsynchronousSocketChannel
ServerSocket ServerSocketChannel AsynchronousServerSocketChannel

异步非阻塞
服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。

Java NIO 学习_第19张图片

Java NIO 学习_第20张图片

4.1、AsynchronousFileChannel

1、通过 Future 读写文件数据

// 异步 FileChannel 读数据
public static void readAsyncFileChannelFuture() throws IOException {
    // 1、创建 AsynchronousFileChannel
    Path path = Paths.get("D:\\IDEA\\Netty\\nio\\input.txt");
    AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
    // 2、创建 Buffer
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    // 3、调用 channel 的 read 方法得到 Future
    Future<Integer> future = asynchronousFileChannel.read(buffer, 0);

    // 处理其他事情

    // 4、判断是否完成 isDone返回true完成
    while (!future.isDone());
    // 5、读取数据到 buffer 中
    buffer.flip();
    System.out.println(new String(buffer.array(), 0, buffer.limit()));
}

// 异步 FileChannel 写数据
public static void writeAsyncFileChannelFuture() throws IOException {
    // 1、创建 AsynchronousFileChannel
    Path path = Paths.get("D:\\IDEA\\Netty\\nio\\input.txt");
    AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);
    // 2、创建 Buffer
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    // 3、写数据
    buffer.put("hello World".getBytes());
    buffer.flip();
    // 3、调用 channel 的 write 方法得到 Future
    Future<Integer> future = asynchronousFileChannel.write(buffer, 0);

    // 处理其他事情

    // 4、判断是否完成 isDone返回true完成
    while (!future.isDone());
    // 5、读取数据到 buffer 中
    buffer.clear();
    System.out.println("write over!");
}

2、通过 CompletionHandler 读取文件数据

// 异步 FileChannel 读数据
public static void readAsyncFileChannelCompletionHandler() throws IOException {
    // 1、创建 AsynchronousFileChannel
    Path path = Paths.get("D:\\IDEA\\Netty\\nio\\input.txt");
    AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
    // 2、创建 Buffer
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    // 3、调用 channel 的 read 方法
    /**
         * 从给定的文件位置开始,从此通道将字节序列读取到给定的缓冲区中。
         * 此方法启动从给定文件位置开始从该通道读取字节序列到给定缓冲区中。 读取的结果是读取的字节数,如果给定位置大于或等于尝试读取时文件的大小,则为 -1。
         * 此方法的工作方式与 AsynchronousByteChannel.read(ByteBuffer, Object, CompletionHandler) 方法相同,只是从给定文件位置开始读取字节。
         * 如果在尝试读取时给定的文件位置大于文件的大小,则不读取任何字节。
         * 参数:
         * dst – 字节要传输到的缓冲区
         * position – 开始传输的文件位置; 必须是非负数
         * attach -- 附加到 I/O 操作的对象; 可以为空
         * handler - 使用结果的处理程序
         */
    asynchronousFileChannel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            System.out.println("result: " + result);
            // 5、读取数据到 buffer 中
            attachment.flip();
            System.out.println(new String(attachment.array(), 0, attachment.limit()));
            attachment.clear();
        }

        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {
            exc.printStackTrace();
        }
    });
    // 处理其他事情
}

你可能感兴趣的:(Netty,java,nio,aio)