Java NIO 由以下几个核心部分组成:
1、Buffer
2、Channel
3、Selector
传统的IO操作面向数据流,面向流的 I/O 系统一次一个字节地处理数据,意味着每次从流中读一个或多个字节,直至完成,数据没有被缓存在任何地方。
NIO操作面向缓冲区( 面向块),数据从Channel读取到Buffer缓冲区,随后在Buffer中处理数据。
Buffer(缓冲区)的理解
Buffer是一个对象,它包含一些要写入或者刚读出的数据。==在面向流的 I/O 中,一般将数据直接写入或者将数据直接读到 Stream 对象中。
缓冲区实质上是一个数组。通常它是一个字节数组,内部维护几个状态变量,可以实现在同一块缓冲区上反复读写(不用清空数据再写)。
Buffer重要方法
- mark():把当前的position赋值给mark
1 | public final Buffer mark() { |
2 | mark = position; |
3 | return this; |
4 | } |
- flip():Buffer有两种模式,写模式和读模式,flip后Buffer从写模式变成读模式(设置状态变量值)。
1 | public final Buffer flip() { |
2 | limit = position; |
3 | position = 0; |
4 | mark = -1; |
5 | return this; |
6 | } |
- reset():把mark值还原给position
1 | public final Buffer reset() { |
2 | int m = mark; |
3 | if (m < 0) |
4 | throw new InvalidMarkException(); |
5 | position = m; |
6 | return this; |
7 | } |
- clear():读完Buffer中的数据,需要让Buffer准备好再次被写入,clear会恢复状态值,但不会擦除数据。
1 | public final Buffer clear() { |
2 | position = 0; |
3 | limit = capacity; |
4 | mark = -1; |
5 | return this; |
6 | } |
- rewind():重置position为0,从头读写数据。
1 | public final Buffer rewind() { |
2 | position = 0; |
3 | mark = -1; |
4 | return this; |
5 | } |
Channel(通道)的理解
Channel是一个对象,可以通过它读取和写入数据。
通常我们都是将数据写入包含一个或者多个字节的缓冲区,然后再将缓存区的数据写入到通道中,将数据从通道读入缓冲区,再从缓冲区获取数据。
Channel类似于原I/O中的流(Stream),但有所区别:
- 流是单向的,通道是双向的,可读可写。
- 流读写是阻塞的,通道可以异步读写。
目前Channel主要实现类有:
- FileChannel:是一个连接到文件的通道。可以通过文件通道读写文件。
- read:从文件中指定的位置开始读取数据到缓冲区
- write:将缓冲区的数据写入文件,从文件指定处开始。
- DatagramChannel: 一个能收发 UDP 包的通道。它发送和接收的是数据包。
- SocketChannel: 是一个连接到 TCP 网络套接字的通道
- ServerSocketChannel: 是一个可以监听新进来的 TCP 连接的通道。
Selector(选择器/多路复用器)的理解(核心)
Selector 一般称为选择器 (或多路复用器)。
它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。可以实现单线程管理多个channels,也就是说可以管理多个网络连接。
为了使用 Selector, 我们首先需要将 Channel 注册到 Selector 中, 随后调用 Selector 的 select()方法, 这个方法会阻塞, 直到注册在 Selector 中的 Channel 发送可读写事件(或其它注册事件). 当这个方法返回后, 当前的这个线程就可以处理 Channel 的事件了(已准备就绪的Channel)。
创建Selector
通过 Selector.open()方法, 我们可以创建一个选择器:
1
Selector selector = Selector.open();
将 Channel注册到Selector 中
需要将 Channel注册到Selector 中,这样才能通过 Selector 监控 Channel :
因为 channel 是非阻塞的,因此当没有数据的时候会理解返回,因此实际上 Selector 是不断的在轮询其注册的 channel 是否有数据就绪。
1
//非阻塞模式
2
channel.configureBlocking(false);
3
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
调用 Selector 的 select()方法, 这个方法会阻塞, 直到注册在 Selector 中的 Channel 发送可读写事件(或其它注册事件). 当这个方法返回后, 当前的这个线程就可以处理 Channel 的事件了(已准备就绪的Channel).
当我们使用 register 注册一个 Channel 时, 会返回一个 SelectionKey 对象, 这个对象包含了如下内容:
interest set, 即我们感兴趣的事件集
ready set
channel
selector
attached object, 可选的附加对象如果 select()方法返回值表示有多个 Channel 准备好了, 那么我们可以通过 Selected key set 访问这个Channel:
注意, 在每次迭代时, 我们都调用 “keyIterator.remove()” 将这个 key 从迭代器中删除, 因为 select() 方法仅仅是简单地将就绪的 IO 操作放到 selectedKeys 集合中, 因此如果我们从 selectedKeys 获取到一个 key, 但是没有将它删除, 那么下一次 select 时, 这个 key 所对应的 IO 事件还在 selectedKeys 中.1
Set<SelectionKey> selectedKeys = selector.selectedKeys();
2
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
3
while(keyIterator.hasNext()) {
4
SelectionKey key = keyIterator.next();
5
keyIterator.remove();
6
//可能有多个注册事件就绪
7
if(key.isAcceptable()) {
8
// a connection was accepted by a ServerSocketChannel.
9
}
10
if (key.isConnectable()) {
11
// a connection was established with a remote server.
12
}
13
if (key.isReadable()) {
14
// a channel is ready for reading
15
}
16
if (key.isWritable()) {
17
// a channel is ready for writing
18
}
19
}
选择器执行选择的过程,系统底层会依次询问每个通道是否已经就绪,这个过程可能会造成调用线程进入阻塞状态,wakeup方式可以唤醒在select()方法中阻塞的线程。
Selector使用流程
1、建立 ServerSocketChannel
2、通过 Selector.open() 打开一个 Selector.
3、将 Channel 注册到 Selector 中, 并设置需要监听的事件
4、循环:
1、调用 select() 方法
2、调用 selector.selectedKeys() 获取 就绪 Channel
3、迭代每个 selected key:
学习总结
Java NIO 由Buffer、Channel以及Selector这三个核心部分组成。
在面向流的 I/O 中,一般将数据直接写入或者将数据直接读到 Stream 对象中。NIO操作面向缓冲区( 面向块),数据从Channel读取到Buffer缓冲区,随后在Buffer中处理数据。
Buffer 是缓冲区, 它包含一些要写入或者刚读出的数据。Channel是通道,通过它读取和写入数据。Selector为选择器(或多路复用器),是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。可以实现单线程管理多个channels,也就是说可以管理多个网络连接。
为了使用 Selector, 我们首先需要将 Channel 注册到 Selector 中, 随后调用 Selector 的 select()方法, 这个方法会阻塞, 直到注册在 Selector 中的 Channel 发送可读写事件(或其它注册事件),当这个方法返回后( select()方法返回值表示有多个 Channel 准备好了, 那么我们可以通过 Selected key set 访问这个 Channel),当前的这个线程就可以处理 Channel 的事件了(已准备就绪的Channel)。