一、概述

JDK 1.4 之前,Java 对 I/O 的支持并不完善,开发人员在开发高性能 I/O 程序时,会面临一些巨大的挑战和困难。

  • 没有数据缓冲区,I/O 性能存在问题。
  • 没有 C 或 C++ 中的 Channel 概念,只有输入和输出流。
  • 同步阻塞式 I/O 通信(BIO),通常会导致通信线程被长时间阻塞。
  • 支持的字符集有限,硬件可移植性不好。

JDK 1.4 版本提供了新的 NIO,开始支持非阻塞 I/O。

  • 进行异步 I/O 操作的缓冲区,Buffer等。
  • 进行异步 I/O 操作的管道,Pipe。
  • 进行各种 I/O 操作(异步或同步),Channel。
  • 多种字符集的编码能力和解码能力。
  • 实现非阻塞 I/O 操作的多路复用器,selector。
  • 基于流行的Perl实现的正则表达式类库。
  • 文件通道,FileChannel。

JDK 1.7 将原来的 NIO 类库进行升级,称为 NIO 2.0,由 JSR-203演进而来。

  • 提供能够指获取文件属性的 API,与平台无关,不与特性的文件系统相耦合,提供了标准文件系统 SPI。
  • 提供 AIO 功能,支持基于文件的异步 I/O 操作和针对网络套接字的异步操作。
  • 完成 JSR-51定义的通道功能,包括对配置和多播数据报的支持。

NIO 的创建目的是为了让 Java 程序员可以实现高速 I/O 而无需编写自定义的本机代码,将最耗时的操作(即填充和提取缓冲区)转移回操作系统内核,因而可以极大地提高速度。
原来的 I/O 是以流的方式处理数据,效率很低。而 NIO 以块的方式处理数据,效率很高。因此,旧有的 java.io.* 以 NIO 为基础重新实现,它可以利用 NIO 的一些特性提高性能。

二、通道和缓冲区

通道缓冲区 是 NIO 中的核心对象,几乎在每一个 I/O 操作中都要使用它们。

缓冲区

缓冲区 Buffer 是一个对象,本质是一个数组。它包含一些要写入或刚读出的数据。在 NIO 库中,所有数据都是用 Buffer 处理。写入数据是写到 Buffer 中,读取也是从 Buffer 对象中读入。
ByteBuffer.png
Buffer 继承体系
基本数据类型除了 Boolean 外,都有对应的 Buffer。而最常用的是 ByteBuffer,因为大多数标准 I/O 操作都使用 ByteBuffer ,所以它喝的所有共享的缓冲区操作以及一些特有操作。

缓冲区内部细节

状态变量

  • **capacity**
    • 可以储存在缓冲区中的最大数据容量,实际上是底层数组的大小。
  • **position**
    • 写模式,当前写位置序号,初始序号为 0,根据数据类型移动对应步长。最大值为 capacity-1
    • 读模式,通过 flip() 将 position 置为0,并根据数据类型移动对应步长,指向下一个可读序号。
  • **limit**
    • 写模式,表示你能往 Buffer 中写入多少数据,其值等于 capacity。
    • 读模式,表示你能从 Buffer 中读取多少数据,当切换为读模式时,其值为写模式下的 position 的值。

      flip

      这是一个重要的方法,很多人在使用 Buffer 时都会忘记调用该方法复原指针位置,而导致程序数据出错。它做了两件重要的事情:
  1. _limit_ 设置为当前的 **_position_**
  2. position 设置为 0。

image.png
Buffer 相关操作示意图
Buffer_API.png
Buffer 相关 API

高级知识

分配&包装

  1. ByteBuffer buffer = ByteBuffer.allocate( 1024 );
  2. byte array[] = new byte[1024];
  3. ByteBuffer buffer = ByteBuffer.wrap( array );

分片

  1. buffer.position( 3 );
  2. buffer.limit( 7 );
  3. ByteBuffer slice = buffer.slice();

分片对于促进抽象非常有帮助,你编写的接口只需要对传入的 ByteBuffer操作即可,不需要考虑边界情况。

直接缓冲区 & 间接缓冲区

直接缓冲区 是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区。
实际上,直接缓冲区的准确定义是与实现相关的。

给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。 也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。

  1. // 大端,
  2. ByteBuffer buffer = ByteBuffer.allocateDirect( 1024 );

分散 & 聚集

分散 & 聚集 I/O 对于将数据划分为几个部分很有用。

通道

通道 是对原 I/O 包中的流的模拟,到任何目的地(或来自任何地方)的所有数据都 必须 经过一个 Channel 对象。通道不提供存储空间,所有的 I/O 读取/写入数据目的地都是 Buffer 对象。你可以通过 Channel 来读取/写入 Buffer 对象内的数据。
通道的不同之处在于通道是双向的。而流只能在一个方向上移动。而 通道 可用于读/写/同时读写操作。因此,使用 通道 能更好的反映底层操作系统的真实情况。特别是在 UNIX 模型中,底层操作系统通道是双向的。
Channel.png
Channel 通道体系结构

FileChannel

  1. package network;
  2. import java.io.*;
  3. import java.nio.*;
  4. import java.nio.channels.*;
  5. public class CopyFile {
  6. static public void main(String args[]) throws Exception {
  7. String infile = "src/main/resources/hello.txt";
  8. String outfile = "src/main/resources/hello_copy.txt";
  9. FileInputStream fin = new FileInputStream(infile);
  10. FileOutputStream fout = new FileOutputStream(outfile);
  11. FileChannel fcin = fin.getChannel();
  12. FileChannel fcout = fout.getChannel();
  13. ByteBuffer buffer = ByteBuffer.allocate(1024);
  14. // 对于文件通道,-1 表示已讲到文件末尾
  15. while (fcin.read(buffer) != -1) {
  16. buffer.flip();
  17. fcout.write(buffer);
  18. buffer.clear();
  19. }
  20. fcout.close();
  21. fcin.close();
  22. fin.close();
  23. fout.close();
  24. }
  25. }

SocketChannel

  • Java NIO 中的 SocketChannel 是一个连接到 TCP 网络套接字的通道。可以通过以下两种方式创建 SocketChannel:
    • 手动创建 ```java // 创建 SocketChannel socketChannel = SocketChannel.open(); socketChannel.connect(new InetSocketAddress(“http://helloworld.com“, 80));

// 配置非阻塞模式 socketChannel.configureBlocking(false); // 使用非阻塞模式,connect()方法会立即返回,甚至在建立连接之前返回,在等待操作系统结束连接时, // 程序可以进行其他操作,但是,在程序实际使用连接之前,必须调用 finishConnect(); finishConnect();

// 如果想检查连接是否完成,可以调用下面两个方法 public abstract boolean isConnected(); public abstract boolean isConnectionPending();

// 关闭(关闭动作很重要,对于套接字而言,关闭会完成 TCP 三次握手动作,所以别忘记关闭了) socketChannel.close();

  1. - **一个新连接到达 `ServerSocketChannel` 时会自动创建一个 `SocketChannel`。**
  2. - 一般是在客户端主动创建,当连接到达服务端,服务端的 `ServerSocketChannel#accept()` 方法会创建一个 `SocketChannel` 表示新连接通道。
  3. <a name="obihe"></a>
  4. ### ServerSocketChannel
  5. - Java NIO 中的 `ServerSocketChannel` 是一个可以监听新进来的 TCP 连接的通道,它只有一个目的: **接受入站连接。**
  6. - 当一个新连接到达时,也由它创建一个新的 `SocketChannel`
  7. ```java
  8. // 创建通道,这个方法名具有一定欺骗性,并非打开一个新的报备器 Socket,而是只创建该对象
  9. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
  10. // 通过 socket() 方法获得对应的对等端 ServerSocket,使用 ServerSocket的各种设置方法配置选项
  11. ServerSocket socket = serverSocketChannel.socket();
  12. SocketAddress address = new InetSocketAddress(80);
  13. socket.bind(address);
  14. // 配置非阻塞模式
  15. serverSocketChannel.configureBlocking(false);
  16. // 监听新到达的连接
  17. while(true){
  18. SocketChannel socketChannel = serverSocketChannel.accept();
  19. //do something with socketChannel...
  20. }
  21. // 关闭
  22. serverSocketChannel.close();
  • 使用步骤
    • 创建服务器 Socket 通道。

    • DatagramChannel

      Java NIO 中的 DatagramChannel 是一个能收发UDP包的通道。因为 UDP 是无连接的网络协议,所以不能像其它通道那样读取和写入。它发送和接收的是数据包。

小结

三、管道

Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
image.png
相关 API

// 创建管道
Pipe pipe = Pipe.open();

// 向管道写数据
// 获取Sink通道
Pipe.SinkChannel sinkChannel = pipe.sink();
// 调用 SinkChannel#write() 方法,将数据写入 SinkChannel
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put("hello".getBytes());
buf.flip();
sinkChannel.write(buf);

// 从管道中读取数据
Pipe.SourceChannel sourceChannel = pipe.source();
ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = sourceChannel.read(buf);

四、Selector

  • 对于 ServerSocketChannel 而言,唯一关心的操作是 OP_ACCEPT,向 Selector

    五、总结

  • NIO 允许你仅用一个(或几个)线程管理多个通道(SocketChannel 或 FileChannel),但成本是解析数据可能比阻塞 I/O 复杂一些。

  • 如果你需要同时管理上千个连接,且每个连接只发送一点点数据,比如聊天服务器,那么使用 NIO 则可能是一个优势。
  • 如果你的连接较少,带宽非常高,一次发送大量数据,也许典型的 I/O 服务器实现可能是最适合的。