Java NIO 上手教程

程序读取数据模型

Java NIO 上手教程_第1张图片

传统 IO 和 NIO 的不同

传统 IO 特点

  • 面向流。
  • 单向(只能读或只能写)。
  • 程序读写直接和操作系统交互。
  • 读写线程阻塞。

NIO 特点

  • 面向缓冲区。
  • 双向(既能读,又能写)。
  • 程序读写通过 Channel,程序和 Channel 之间的交互通过缓冲区。
  • 读写线程不阻塞。(但是 selector 的 select() 会阻塞)。

NIO 读取数据模型

Java NIO 上手教程_第2张图片

NIO 在读取数据时,是数据通过 Channel 写入缓冲区,程序从缓冲区读取数据。写数据也是同理。

NIO 四大核心组件

NIO 有 4 个核心组件,它们是:

  • Buffer:为了读或写,存储数据的容器。
  • Channel:与某些组件建立连接,类似 Java IO 中的流,进行读或写操作,但是和 IO 流不同的是,Channel 是双向的。
  • Charsets:包含字符集(charsets)、解码器(decoders)、编码器(encoders),可以用来进行 byte 与 unicode 的转换。
  • Selector:通过 Selector,可以与多个 Channel 一起工作。

Buffer

Java NIO 上手教程_第3张图片

Buffer 是一个存储固定数据大小、存储 Java 基本数据类型数据的容器。一个 Buffer 由下面这些组成:

• Capacity :容器容量
• Limit: 不能读或写的索引
• Position: 下一个读或写的元素索引
• Flip: 切换 IO 操作(读写操作)
• Rewind: 设置 position = 0,limit 保持不变
• Mark: 在 Buffer 标记一个位置
• Reset: 将 position 重置为 mark 的位置

直接字节缓冲区

Java 可以使用 ByteBuffer 的 allocateDirect 方法创建直接缓冲区:

ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);

相比较于非直接缓冲区。IO 效率会更高。因为我们在使用非直接缓冲区时,操作系统会创建一个临时直接缓冲区,将非直接缓冲区中的内容读入创建的临时直接缓冲区再进行操作。如果单从这点来看,使用直接缓冲区是 IO 最好的选择。

但是在 Java 中创建直接缓冲区,是直接调用操作系统的 API 方法来创建的,会绕过 JVM 的内存管理,而且 JVM 针对缓冲区也有优化,所以可能使用直接缓冲区不是最好的方式。

往 ByteBuffer 写入数据

public class ByteBufferReadDemo {
    /**
     * 读取文件路径
     */
    private static final String INPUT_FILE = "./resources/input.txt";
    /**
     * 缓冲区大小
     */
    private static final int BYTE_BUFFER_LENGTH = 1024;

    public static void main(String[] args) {
        // 通过 IO 流获取 Channel
        try (FileChannel fileChannel = new FileInputStream(INPUT_FILE).getChannel()) {
            // 创建 ByteBuffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(BYTE_BUFFER_LENGTH);
            StringBuilder content = new StringBuilder();
            int read = 0;
            // 数据从通道读出(写入到 ByteBuffer)
            while ((read = fileChannel.read(byteBuffer)) != -1) {
                content.append(new String(byteBuffer.array(), 0, read));
                // 让 ByteBuffer 恢复如初:position = 0, limit = capacity, 丢掉 mark
                byteBuffer.clear();
            }
            System.out.println(content);
        } catch (IOException e) {
            throw new RuntimeException("Unable to read file", e);
        }
    }
}

Buffer 读写原理

ByteBuffer 继承 Buffer 抽象类,很重要的三个属性是:

    # 读或者写的开始位置
    private int position = 0;
    # 限制读或写的位置
    private int limit;
    # 容量
    private int capacity;

当新创建一个容量为 10 的 ByteBuffer 对象时,它们三个的位置分别为:

Java NIO 上手教程_第4张图片

此时 ByteBuffer 为写模式,可以从 position 开始写, limit 是最多能写到的位置,此时 limit = capacity。

随后我们往 buffer 写入 1234,buffer 内部结构变为:

Java NIO 上手教程_第5张图片

position 移动到最小的一个可写位置 5(position = 5)。

此时我们想要将 buffer 数据读出,就要转成读模式(调用 flip() 方法):

Java NIO 上手教程_第6张图片

position 移动到 buffer 中第一个可读的位置,limit 移动到 buffer 中最后一个可读的位置。

我们将数据全部读出,此时 buffer 内部变为:

Java NIO 上手教程_第7张图片

此时 position = limit,证明已经没有可读。

此时我们使用 clear(),将 ByteBuffer 恢复,此时 buffer 回到写模式(默认是写模式):

Java NIO 上手教程_第8张图片

compact()和 clear()的区别就是,clear() 会直接让 buffer 回到刚创建的状态,而 compact() 会压缩还没被读的数据。比如我们只读了 1 和 2:
Java NIO 上手教程_第9张图片

此时调用 compact(),buffer 内部结构会变为:
Java NIO 上手教程_第10张图片

Channel

Java NIO 上手教程_第11张图片

可以通过 Channel 对 Buffer 读写。

创建一个 FileChannel:

        // 创建一个 File 文件对象
        final File file = new File(FileChannelReadExample.class.getClassLoader().getResource(path).getFile());  
        // 根据需要读或者需要写创建不同的 FileChannel              
        return fileOperation == FileOperation.READ ? new FileInputStream(file).getChannel() : new FileOutputStream(file).getChannel();

Charsets

包含 charsets、decoders、encoders。通过 Charsets 可以进行 unicode 与 byte 的转换。

编码与解码

编码(Encoding):一序列字符 -> 字节。

解码(Decoding):字节 -> 字符。

Charset 使用

        // 拿到 UTF-8 字符集    
        Charset utf8Charset = Charset.forName("UTF-8");
        // 编码
        ByteBuffer byteBuffer = utf8Charset.encode("zhangsan");
        // 解码
        System.out.println(utf8Charset.decode(byteBuffer));

Selector

Selector 可以多路复用 SelectableChannel,当 SelectableChannel 有 IO 事件发生时,Selector 就会通知我们的程序。

这里的 IO 事件(SelectableChannel 连接 Selectors 的事件)包括:

  1. Connect
  2. Accept
  3. Read
  4. Write

SelectableChannel:可以多路复用的连接 Selecotr 的通道。

Server/Client Demo

使用 Java NIO 实现一个简单的 Server、Client 示例:

Server

public class Server {
    protected static final String HOST = "127.0.0.1";

    protected static final int PORT = 8899;

    public static void main(String[] args) {
        try {
            ServerSocketChannel serverSocket = ServerSocketChannel.open();
            InetSocketAddress hostAddress = new InetSocketAddress(HOST, PORT);
            // 绑定端口
            serverSocket.bind(hostAddress);
            // 设置为非阻塞
            serverSocket.configureBlocking(false);
            // 创建 selector
            Selector selector = Selector.open();
            // Channel 注册到 selector(ACCEPT 事件)
            serverSocket.register(selector, serverSocket.validOps(), null);
            while (true) {
                // select 是阻塞的
                int selectKeyNums = selector.select();
                if (selectKeyNums > 0) {
                    Set selectionKeys = selector.selectedKeys();
                    Iterator selectionKeyIterator = selectionKeys.iterator();
                    while (selectionKeyIterator.hasNext()) {
                        SelectionKey selectionKey = selectionKeyIterator.next();
                        if (selectionKey.isAcceptable()) {
                            // 注册读事件
                            registerRead(serverSocket, selector);
                        } else if (selectionKey.isReadable()) {
                            // 处理写事件
                            dealRead(selectionKey);
                        } else {
                            System.out.println("Invalid selection key");
                        }
                        // 每次事件操作做完要删除,否则会继续读,报错
                        selectionKeyIterator.remove();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void registerRead(ServerSocketChannel channel, Selector selector) throws IOException {
        SocketChannel client = channel.accept();
        client.configureBlocking(false);
        client.register(selector, SelectionKey.OP_READ, null);
        System.out.println(client.getRemoteAddress() + " connected!");
    }

    public static void dealRead(SelectionKey selectionKey) {
        try {
            SocketChannel channel = (SocketChannel) selectionKey.channel();
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            int read = channel.read(byteBuffer);
            if (read == -1) {
                selectionKey.channel();
                channel.close();
            } else {
                byteBuffer.flip();
                System.out.println("from " + channel.getRemoteAddress() + " reve a message: " + StandardCharsets.UTF_8.decode(byteBuffer));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Client

public class Client {
    public static void main(String[] args) {
        try {
            InetSocketAddress hostAddress = new InetSocketAddress(Server.HOST, Server.PORT);
            SocketChannel client = SocketChannel.open(hostAddress);
            Scanner sc = new Scanner(System.in);
            while (sc.hasNextLine()) {
                client.write(StandardCharsets.UTF_8.encode(sc.next()));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

总结

IO 与 NIO 对比:

Java NIO 上手教程_第12张图片

NIO 四大核心组件:

  1. ByteBuffer (存储数据的容器)
  2. Charsets (方便字符和字节的转化)
  3. Channels (真正 IO 的地方)
  4. Selectors (多路复用 SelectableChannel)

它们四个互相配合,构成 NIO 的流程。

你可能感兴趣的:(java)