一、概述
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 对象中读入。
Buffer 继承体系
基本数据类型除了 Boolean
外,都有对应的 Buffer。而最常用的是 ByteBuffer
,因为大多数标准 I/O 操作都使用 ByteBuffer
,所以它喝的所有共享的缓冲区操作以及一些特有操作。
缓冲区内部细节
状态变量
**capacity**
- 可以储存在缓冲区中的最大数据容量,实际上是底层数组的大小。
**position**
- 写模式,当前写位置序号,初始序号为 0,根据数据类型移动对应步长。最大值为 capacity-1。
- 读模式,通过 flip() 将 position 置为0,并根据数据类型移动对应步长,指向下一个可读序号。
**limit**
- 将
_limit_
设置为当前的**_position_**
。 - 将
position
设置为 0。
高级知识
分配&包装
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
byte array[] = new byte[1024];
ByteBuffer buffer = ByteBuffer.wrap( array );
分片
buffer.position( 3 );
buffer.limit( 7 );
ByteBuffer slice = buffer.slice();
分片对于促进抽象非常有帮助,你编写的接口只需要对传入的 ByteBuffer操作即可,不需要考虑边界情况。
直接缓冲区 & 间接缓冲区
直接缓冲区 是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区。
实际上,直接缓冲区的准确定义是与实现相关的。
给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。 也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中(或者从一个中间缓冲区中拷贝数据)。
// 大端,
ByteBuffer buffer = ByteBuffer.allocateDirect( 1024 );
分散 & 聚集
通道
通道 是对原 I/O 包中的流的模拟,到任何目的地(或来自任何地方)的所有数据都 必须 经过一个 Channel
对象。通道不提供存储空间,所有的 I/O 读取/写入数据目的地都是 Buffer 对象。你可以通过 Channel
来读取/写入 Buffer
对象内的数据。
通道 与 流的不同之处在于通道是双向的。而流只能在一个方向上移动。而 通道 可用于读/写/同时读写操作。因此,使用 通道 能更好的反映底层操作系统的真实情况。特别是在 UNIX 模型中,底层操作系统通道是双向的。
Channel 通道体系结构
FileChannel
package network;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
public class CopyFile {
static public void main(String args[]) throws Exception {
String infile = "src/main/resources/hello.txt";
String outfile = "src/main/resources/hello_copy.txt";
FileInputStream fin = new FileInputStream(infile);
FileOutputStream fout = new FileOutputStream(outfile);
FileChannel fcin = fin.getChannel();
FileChannel fcout = fout.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 对于文件通道,-1 表示已讲到文件末尾
while (fcin.read(buffer) != -1) {
buffer.flip();
fcout.write(buffer);
buffer.clear();
}
fcout.close();
fcin.close();
fin.close();
fout.close();
}
}
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();
- **一个新连接到达 `ServerSocketChannel` 时会自动创建一个 `SocketChannel`。**
- 一般是在客户端主动创建,当连接到达服务端,服务端的 `ServerSocketChannel#accept()` 方法会创建一个 `SocketChannel` 表示新连接通道。
<a name="obihe"></a>
### ServerSocketChannel
- Java NIO 中的 `ServerSocketChannel` 是一个可以监听新进来的 TCP 连接的通道,它只有一个目的: **接受入站连接。**
- 当一个新连接到达时,也由它创建一个新的 `SocketChannel` 。
```java
// 创建通道,这个方法名具有一定欺骗性,并非打开一个新的报备器 Socket,而是只创建该对象
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 通过 socket() 方法获得对应的对等端 ServerSocket,使用 ServerSocket的各种设置方法配置选项
ServerSocket socket = serverSocketChannel.socket();
SocketAddress address = new InetSocketAddress(80);
socket.bind(address);
// 配置非阻塞模式
serverSocketChannel.configureBlocking(false);
// 监听新到达的连接
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
//do something with socketChannel...
}
// 关闭
serverSocketChannel.close();
- 使用步骤
小结
三、管道
Java NIO 管道是2个线程之间的单向数据连接。Pipe
有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
相关 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);