Java NIO 快速入门:Java 非阻塞 IO 编程详解

一、NIO 简介

1. 概述

  1. 介绍: Java NIO(New Input/Output)是从Java 1.4开始引入的一组新的IO库,旨在替代传统的阻塞式IO。NIO提供了更高效的IO操作,支持非阻塞模式多路复用,适用于高并发场景。
  2. 概述:NIO 中通过 Buffer 作为缓存区,Channel 作为数据通道来传输数据进行数据通讯, 通过Selector实现多路复用,一个线程可以管理多个Channel,提高并发性能。Buffer、Channel和Selector 为 NIO 的三个核心组件。

2. 阻塞 VS 非阻塞

  1. BIO:Blocking I/O(阻塞 IO)。当程序在执行 IO 操作的时候,例如调用一个读操作或一个写操作的时候,线程会进行阻塞直至 IO 操作的成功完成。
  2. NIO:Non-blocking I/O(非阻塞 IO)。在执行IO操作时,线程不会被阻塞,可以立即返回并继续执行其他任务。线程需要轮询或通过回调机制来检查操作是否完成。

二、 三大组件

1. Buffer

1.1 概念

  1. Buffer:用来缓冲读写数据
    • 读:从通道读取数据(Channel)时,首先将数据读取到Buffer中,一旦Buffer中有了数据,就可以通过 Buffer 中的方法来获取这些数据。
    • 写:在向通道(Channel)中写入数据之前,先将数据填充到 Buffer 当中,当 Buffer 填满,需要将其数据刷新 到通道中,然后才能继续填充数据。
  2. 常用的Buffer类型 :
    • ByteBuffer(常用) :存储字节数据(二进制数据)
      • DirectByteBuffer(存储在系统内存)
      • HeapByteBuffer(存储在 JVM 堆内存)
    • IntBuffer: 用于存储整数数据。
    • LongBuffer: 用于存储长整型数据。
    • FloatBuffer: 用于存储浮点型数据。
    • DoubleBuffer: 用于存储双精度浮点型数据。
    • CharBuffer: 用于存储字符数据。
  3. 使用流程:
    • 分配 Buffer:使用 Buffer中的 allocate方法分配一个指定容量的 Buffer。
    • 从 Channel 中写入数据:向 Buffer 中写入数据。例如 channel.read(buffer)
    • 切换为读模式:使用 Buffer 的flip()方法由写模式切换为读模式
    • 从 Buffer 读取数据:可以使用 Buffer 提供的 get()方法
    • 切换为写模式(清理 Buffer):使用clear()compact方法重置 Buffer,以便再次写入数据。
      • clear:重置 postion为 0,limit 为 capacity。相当于全部清除,无论 Buffer 中的数据是否全部读取完整。
      • compact:将所有未读的数据移到 Buffer 的开始处,然后将position设置为未读数据的下一个未知,limit 设置为 capacity

capacity 为 Buffer 的容量;position 为下一个要读或写的元素的索引;limit 为 Buffer 中第一个不能读或写的元素索引。

  1. 示例代码:
import java.nio.ByteBuffer;

public class BufferExample {
    public static void main(String[] args) {
        // 1. 分配Buffer
        ByteBuffer buffer = ByteBuffer.allocate(10);
        
        // 2. 写入数据到Buffer
        for (int i = 0; i < buffer.capacity(); i++) {
            buffer.put((byte) i);
        }
        
        // 3. 准备读操作(切换为读模式)
        buffer.flip();
        
        // 4. 读取数据从Buffer
        while (buffer.hasRemaining()) {
            System.out.println(buffer.get());
        }
        
        // 5. 清理Buffer(重置Buffer以便再次写入数据)
        buffer.clear();
        
        // 再次写入数据
        for (int i = 10; i < 20; i++) {
            buffer.put((byte) i);
        }
        
        // 准备读操作(切换为读模式)
        buffer.flip();
        
        // 读取数据从Buffer
        while (buffer.hasRemaining()) {
            System.out.println(buffer.get());
        }
    }
}

1.2 Buffer 结构

掌握 Buffer 的结构以及不同状态下的情况有助于从本质上理解 Buffer 具体的操作流程

  1. capacity:Buffer 的容量,即它能容纳的最大数据量。
  2. limit:Buffer 中第一个不能读或写的元素索引,可以动态改变。
  3. position:下一个要读或写的元素索引。
  4. mark:一个用于记录当前 position 的标记。 通过调用mark()方法设置,调用reset()方法恢复到该位置。

以下为 Buffer 不同状态下的结构:

  • 初始化状态:此时 Buffer 为写模式,position 代表第一个要写入的元素的索引,limit 代表的是一旦 position 等于 limit 无法再往 Buffer 中写入数据,表示此时 Buffer 已满。

  • 写入数据:往 Buffer 中写入数据 JAVA 四个元素,此时 position 所在的索引 4 为下一个写入数据的索引,(limit - position)代表还可以写入多少个元素。

  • 切换为读模式:此时 position 的下一个读取元素的索引,limit 为读取限制索引。

  • 读取元素:读取前两个元素,position 指针向前移动两个单位。

  • 切换为写模式:
    • 方式一: 采用 clear方法,重置 postion为 0,limit 为 capacity。相当于全部清除,无论 Buffer 中的数据是否全部读取完整。
    • 方式二:采用compact方法,将所有未读的数据移到 Buffer 的开始处,然后将position设置为未读数据的下一个未知,limit 设置为 capacity

方式一

方式二

1.3 Buffer 常见 API

1.3.1 基本API
  1. allocate(int capacity):分配一个新的Buffer。

  2. capacity():返回Buffer的容量。

  3. position():返回Buffer的当前位置。

  4. limit():返回Buffer的限制。

  5. mark():标记当前position。(配合reset()使用)

  6. reset():将position重置为先前标记的位置。

  7. clear():清除Buffer,准备重新写入数据。

  8. flip():将Buffer从写模式切换到读模式。

  9. rewind():重置position为0,准备重新读取数据。

  10. compact():将未读的数据移到Buffer的开始位置,然后将position设置为未读数据之后的位置,limit设置为capacity。

  11. hasRemaining():判断position和limit之间是否有元素。

1.3.2 数据操作API
  1. put(byte b):将一个字节写入Buffer。

  2. get():从Buffer读取一个字节。

  3. put(byte[] src):将一个字节数组写入Buffer。

  4. get(byte[] dst):从Buffer读取字节到一个数组。

  5. put(int index, byte b):将一个字节写入Buffer指定位置。

  6. get(int index):从Buffer的指定位置读取一个字节。

1.4 API 示例代码

import java.nio.ByteBuffer;

public class BufferAPIExample {
    public static void main(String[] args) {
        // 分配一个新的ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(10);

        // 写入数据到Buffer
        buffer.put((byte) 1);
        buffer.put((byte) 2);
        buffer.put((byte) 3);

        // 使用flip()准备读操作
        buffer.flip();

        // 读取数据从Buffer
        while (buffer.hasRemaining()) {
            System.out.println(buffer.get());
        }

        // 清除Buffer,准备重新写入数据
        buffer.clear();

        // 再次写入数据到Buffer
        buffer.put(new byte[]{4, 5, 6});

        // 使用flip()准备读操作
        buffer.flip();

        // 读取数据从Buffer
        byte[] dst = new byte[buffer.remaining()];
        buffer.get(dst);
        for (byte b : dst) {
            System.out.println(b);
        }

        // 重新标记和重置
        buffer.clear();
        buffer.put((byte) 7);
        buffer.put((byte) 8);
        buffer.mark();   // 标记当前位置
        buffer.put((byte) 9);
        buffer.reset();  // 重置到标记位置
        buffer.flip();
        System.out.println("After reset: " + buffer.get());
    }
}

2. Channel

2.1 概念

  1. Channel:Channel 是一个可以进行读写操作的通道,类似于流(Stream),但它们的工作方式有很大的不同。Channel 可以同时支持读和写操作(双向),而流通常是单向的(输入流或输出流)。
  2. Channel 的种类:
    • FileChannel:用于文件的读写。
    • SocketChannel:用于 TCP 连接读写网络数据。
    • ServerSocketChannel:用于监听新进来的 TCP 连接,就像传统的服务器套接字。
    • DatagramChannel:用于通过 UDP 读写网络数据。
  3. 工作原理: Channel 工作在缓冲区(Buffer)上,所有与 Channel 相关的 I/O 操作都通过缓冲区进行。Channel 读取数据时,会将数据放入缓冲区,而写入数据时,会从缓冲区中取出数据。Channel 的非阻塞模式使得它们可以在数据未准备好时立即返回,而不是等待数据准备好。

2.2 文件编程

  1. 概述:FileChannel 是一个用于文件读写操作的通道,不能直接创建,而是通过FileInputStreamFileOutputStreamRandomAccessFileFileChannel.open()来获取。

  2. 注意事项:

    • FileChannel 只存在阻塞模式,不存在非阻塞模式。
    • 网络编程所用到的 Channel 才存在非阻塞模式。
  3. 常用 API:

    • open:打开一个FileChannel。

    • read:从FileChannel读取数据到Buffer。

    • write:将Buffer中的数据写入FileChannel。

    • position:获取或设置FileChannel的当前位置。

    • size:获取文件的大小。

    • truncate:截取文件到指定大小。

    • force:强制将所有修改写入磁盘。

  4. 示例代码:

import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class FileChannelExample {
    public static void main(String[] args) {
        try (RandomAccessFile file = new RandomAccessFile("example.txt", "rw");
             FileChannel fileChannel = file.getChannel()) {
             
            // 创建Buffer并写入数据
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            String data = "Hello, FileChannel!";
            buffer.put(data.getBytes());

            // 准备Buffer进行读操作
            buffer.flip();
            fileChannel.write(buffer);

            // 清空Buffer以便再次写入
            buffer.clear();

            // 设置Channel位置并读取数据
            fileChannel.position(0);
            int bytesRead = fileChannel.read(buffer);
            System.out.println("读取 " + bytesRead + " 字节");

            // 准备Buffer进行读操作
            buffer.flip();
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2.3 网络编程

  1. 概述:ServerSocketChannel作用于服务器,用于监听客户端的连接请求,SocketChannel用于客户端和服务器之间的数据传输。

  2. ServerSocketChannel 常见 API:

    • open:打开一个ServerSocketChannel

    • bind:绑定到指定的端口。

    • configureBlocking:设置是否为阻塞模式。

    • accept:接受客户端连接。

  3. SocketChannel 常见 API:

    • open:打开一个SocketChannel并连接到服务器。
    • configureBlocking:设置是否为阻塞模式。
    • read:从SocketChannel读取数据到ByteBuffer
    • write:将ByteBuffer中的数据写入SocketChannel
  4. 示例代码:以下通过实现一个简单的服务器-客户端程序来进行测试,服务器接收客户端发送的数据,并将其原样返回给客户端。

  • 服务器程序:其中用到 Selector 组件的内容,在后续内容中会提到。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.SelectionKey;
import java.util.Iterator;

public class EchoServer {
    public static void main(String[] args) {
        try {
            // 打开ServerSocketChannel
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(8080));
            serverSocketChannel.configureBlocking(false);

            // 打开Selector
            Selector selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {
                // 选择准备好的通道
                selector.select();
                Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();

                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    keyIterator.remove();

                    if (key.isAcceptable()) {
                        // 接受连接
                        SocketChannel clientChannel = serverSocketChannel.accept();
                        clientChannel.configureBlocking(false);
                        clientChannel.register(selector, SelectionKey.OP_READ);
                    } else if (key.isReadable()) {
                        // 读取数据
                        SocketChannel clientChannel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(256);
                        int bytesRead = clientChannel.read(buffer);

                        if (bytesRead == -1) {
                            clientChannel.close();
                        } else {
                            buffer.flip();
                            clientChannel.write(buffer);
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 客户端程序:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class EchoClient {
    public static void main(String[] args) {
        try {
            // 打开SocketChannel
            SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080));
            socketChannel.configureBlocking(false);

            // 发送消息
            ByteBuffer buffer = ByteBuffer.allocate(256);
            buffer.put("Hello, Server!".getBytes());
            buffer.flip();
            socketChannel.write(buffer);

            // 读取服务器的回显
            buffer.clear();
            int bytesRead = socketChannel.read(buffer);
            if (bytesRead > 0) {
                buffer.flip();
                System.out.println("Received from server: " + new String(buffer.array(), 0, bytesRead));
            }

            // 关闭连接
            socketChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3. Selector

3.1 概念

  1. Selector:Selector是 Java NIO 的一个重要组件,它允许单个线程监控多个通道(Channel)的 IO 事件,如连接请求、数据到达等。使用Selector,可以实现高效的多路复用,避免每个连接都创建一个线程,从而提高应用的并发性能。
  2. 多路复用:多路复用允许多个输入/输出通道(例如 Socket)共享一个同一个线程。通过这种方式,可以在一个线程中同时处理多个连接,提高资源利用率和应用的并发性能。在 Java NIO 中,Selector实现了多路复用机制。一个Selector可以同时监控多个Channel的 IO 事件,当某个Channel有事件准备好时,Selector会通知应用程序。
  3. IO 事件种类:
    • OP_ACCEPT:有连接可以接受,服务端成功接受连接时触发该事件(ServerSocketChannel)。
    • OP_CONNECT:连接已经建立,客户端成功连接上服务端时触发该事件(SocketChannel)。
    • OP_READ:有数据可以读取(SocketChannel)。
    • OP_WRITE:可以写数据(SocketChannel)。
  4. 使用流程:
  • 注册通道: 注册 Channel 到 Selector,并指定感兴趣的事件,事件通过SelectionKey对象进行指定(包含了关于通道和 Selector 的信息以及通道感兴趣的操作)。 当有事件发生时,Selector 会将准备好进行 I/O 操作的 SelectionKey 返回给应用程序,然后由应用程序遍历这些 SelectionKey 并处理相应的事件。
 SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
// 指定感兴趣的事件类型为OP_READ
socketChannel.register(selector, SelectionKey.OP_READ);
  • 选择就绪通道: 调用select()方法,阻塞直到至少有一个通道准备好进行I/O操作。
// 返回的是已经就绪的通道的个数
int readyChannels = selector.select();
  • 处理就绪事件:遍历返回的SelectionKey集合,确定哪些通道准备好进行I/O操作,并执行相应的操作。
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if (key.isReadable()) {
        // 处理读事件
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int bytesRead = channel.read(buffer);
        if (bytesRead > 0) {
            buffer.flip();
            // 处理读取的数据
        }
    } else if (key.isAcceptable()) {
        // 处理连接接受事件
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        SocketChannel socketChannel = serverChannel.accept();
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
    }
    keyIterator.remove();
}

3.2 常见API

  1. open:打开一个Selector。
  2. register:将一个Channel注册到Selector上,并指定感兴趣的操作。
  3. select:阻塞等待直到有至少一个通道准备好进行IO操作。
  4. selectedKeys:返回一组SelectionKey,对应于准备好的通道。

SelectionKey:代表一个通道在Selector上的注册关系,包含了通道和事件的信息。

3.3 示例代码

知道了 Selector 之后,我们就可以来重写理解上面的那个例子了,以下是关于这个例子的解释。

  1. 服务器代码:
    • 当将 ServerSocketChannel注册到 Selector后,每当有事件发生的时候,Selector获取其中的SelectionKey对象,根据SelectionKey对象可以获取其中的通道对象和具体的事件类型。
    • 根据具体的事件类型来执行不同的操作,如果是连接事件,则获取的通道对象为ServerSocketChannel对象,利用该对象可以创建一个新的SocketChannel用来处理服务器和客户端的连接,并将其注册到Selector,并指定感兴趣的事件类型;如果是读事件,此时获取的通道对象为处理服务器和客户端的连接SocketChannel,由于该示例代码是将接受到的数据发送给客户端,所以将从通道中读取的数据重写写入通道给客户端。
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 EchoServer {

    public static void main(String[] args) throws IOException {
        // 创建 selector 对象
        Selector selector = Selector.open();
        // 创建 serverSocketChannel 对象并进行相应的配置
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8080));
        // 将 serverSocketChannel 注册到 selector 并绑定 OP_ACCEPT 事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while(true){
            // 阻塞直到有至少一个通道准备好进行I/O操作
            selector.select();

            // 获取所有准备好I/O操作的SelectionKey
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            while(iterator.hasNext()){
                SelectionKey key = iterator.next();
                iterator.remove();

                if(key.isAcceptable()){
                    // 有新的连接请求
                    ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
                    SocketChannel socketChannel = serverChannel.accept();
                    socketChannel.configureBlocking(false);
                    // 注册新连接到Selector,监听读事件
                    socketChannel.register(selector,SelectionKey.OP_READ);
                }else if(key.isReadable()){
                    // 有数据可以读取
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int byteRead = socketChannel.read(buffer);
                    if (byteRead == -1) {
                        socketChannel.close();
                    }else {
                        buffer.flip();
                        socketChannel.write(buffer);
                        buffer.clear();
                    }
                }
            }
        }
    }
}
  1. 客户端代码:
    • 客户端连接上服务器之后,发送"Hello, Server!"字符串给服务器。
    • 发送完毕之后,通过SocketChannel的read方法监听服务器发送过来的数据,最后进行输出。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class EchoClient {

    public static void main(String[] args) {
        try {
            // 打开SocketChannel
            SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080));
            socketChannel.configureBlocking(false);

            // 发送消息
            ByteBuffer buffer = ByteBuffer.allocate(256);
            buffer.put("Hello, Server!".getBytes());
            buffer.flip();
            socketChannel.write(buffer);

            // 读取服务器的回显
            buffer.clear();
            int bytesRead = socketChannel.read(buffer);
            if (bytesRead > 0) {
                buffer.flip();
                System.out.println("Received from server: " + new String(buffer.array(), 0, bytesRead));
            }

            // 关闭连接
            socketChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

如果本文对你有帮助的话,希望可以点一个赞,嘻嘻🥰🥰🥰。

  • 56
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值