Java NIO

一、流与块

NIO 与 IO 主要区别: I/O 与 NIO 最重要的区别是 ** 数据打包 ** 和 ** 传输 ** 的方式。NIO 是 ** 非阻塞 ** 的。
I/O 以 ** 流 ** 的方式处理数据
NIO 以 ** 块 ** 的方式处理数据

1. IO 特点

  1. 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据
  2. 创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分
  3. 速度慢

2. NIO 特点

  1. 一次处理一个数据块
  2. 速度快
  3. 面向块的 I/O ** 缺少 ** 一些面向流的 I/O 所具有的 优雅性和简单性

二、通道与缓冲区

1. 通道

概念: 通道 Channel 是对原 I/O 包中的 流的模拟,可以通过它读取和写入数据。
通道 与 流 的区别:

  • 流只能在 ** 一个方向上 ** 移动(一个流必须是 InputStream 或者 OutputStream 的子类)
  • 通道是 双向的,可以用于读、写或者 同时用于读写

类型:

  • FileChannel: 从 ** 文件 ** 中读写数据
  • DatagramChannel: 通过 UDP 读写网络中数据
  • SocketChannel: 通过 TCP 读写网络中数据
  • ServerSocketChannel: 可以 监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel

2. 缓冲区

概念: 不会直接对通道进行读写数据,要 先经过缓冲区
发送给一个通道的所有数据都必须首先放到缓冲区中。
同样地,从通道中读取的任何数据都要先读到缓冲区中。
原理: 缓冲区实质上是一个 数组,但它不仅仅是一个数组。
作用: 缓冲区提供了 对数据的结构化访问,而且还可以 跟踪系统的读 / 写进程
类型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

2.1. 缓冲区状态变量

  • capacity: 最大容量
  • position: 当前已经读写的字节数
  • limit: 可读写的容量限制(写入时为最大容量大小,读取时为当前数据大小)

状态变量的改变过程例子:

  1. 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。

image.png

  1. 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。

image.png

  1. 在将缓冲区的数据写到输出通道之前,需要先调用 flip () 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。

image.png

  1. 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。

image.png

  1. 最后需要调用 clear () 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。

image.png

3. 文件 NIO 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static void fastCopy(String src, String dist) throws IOException {

/* 获得源文件的输入字节流 */
FileInputStream fin = new FileInputStream(src);

/* 获取输入字节流的文件通道 */
FileChannel fcin = fin.getChannel();

/* 获取目标文件的输出字节流 */
FileOutputStream fout = new FileOutputStream(dist);

/* 获取输出字节流的文件通道 */
FileChannel fcout = fout.getChannel();

/* 为缓冲区分配 1024 个字节 */
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

while (true) {

/* 从输入通道中读取数据到缓冲区中 */
int r = fcin.read(buffer);

/* read() 返回 -1 表示 EOF */
if (r == -1) {
break;
}

/* 切换读写 */
buffer.flip();

/* 把缓冲区的内容写入输出文件中 */
fcout.write(buffer);

/* 清空缓冲区 */
buffer.clear();
}
}

三、选择器

非阻塞 IO: NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
Selector 监听实现多路复用:

  1. NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去 监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
  2. 通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,Selector 就 ** 不会进入阻塞状态 ** 一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。

良好性能: 因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。
注意: 应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。
image.png

1. 创建选择器

Selector selector = Selector.open();

2. 将通道注册到选择器上

1
2
3
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰
指定要注册的具体事件类型:

1
2
3
4
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;

可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如:

1
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

3. 监听事件

int num = selector.select();
使用 select () 来监听到达的事件,它会一直阻塞直到有至少一个事件到达

4. 获取到达的事件

1
2
3
4
5
6
7
8
9
10
11
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// ...
} else if (key.isReadable()) {
// ...
}
keyIterator.remove();
}

5. 事件循环

因为一次 select () 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while (true) {
int num = selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// ...
} else if (key.isReadable()) {
// ...
}
keyIterator.remove();
}
}

6. 套接字 NIO 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class NIOServer {

public static void main(String[] args) throws IOException {

Selector selector = Selector.open();

ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

ServerSocket serverSocket = ssChannel.socket();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
serverSocket.bind(address);

while (true) {

selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();

while (keyIterator.hasNext()) {

SelectionKey key = keyIterator.next();

if (key.isAcceptable()) {

ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();

// 服务器会为每个新连接创建一个 SocketChannel
SocketChannel sChannel = ssChannel1.accept();
sChannel.configureBlocking(false);

// 这个新连接主要用于从客户端读取数据
sChannel.register(selector, SelectionKey.OP_READ);

} else if (key.isReadable()) {

SocketChannel sChannel = (SocketChannel) key.channel();
System.out.println(readDataFromSocketChannel(sChannel));
sChannel.close();
}

keyIterator.remove();
}
}
}

private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {

ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuilder data = new StringBuilder();

while (true) {

buffer.clear();
int n = sChannel.read(buffer);
if (n == -1) {
break;
}
buffer.flip();
int limit = buffer.limit();
char[] dst = new char[limit];
for (int i = 0; i < limit; i++) {
dst[i] = (char) buffer.get(i);
}
data.append(dst);
buffer.clear();
}
return data.toString();
}
}

public class NIOClient {

public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream out = socket.getOutputStream();
String s = "hello world";
out.write(s.getBytes());
out.close();
}
}

四、内存映射文件

概念: 内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
下面代码行将文件的前 1024 个字节映射到内存中,map () 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);