1. 简介
Java NIO(New IO Non Blocking IO)是从Java 1.4版本引入的一个新的IO API,其究极目标就是“Speed”,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
2. NIO与IO的区别
IO | NIO |
---|---|
面向流(Stream Oriented) | 面向缓冲区(Buffer Oriented) |
阻塞IO(Blocking IO) | 非阻塞IO(Non Blocking IO) |
无 | 选择器(Selectors) |
3. Channel
基本上所有的IO和NIO都是基于Channel延伸的。数据可以从Channel通道中读取到Buffer缓冲区,同时也可以从缓冲区中写入到Channel通道中。
其中,ByteBuffer是唯一一个能直接与Channel信道通信的 Buffer缓冲区。
- Java为Channel接口提供的最主要的实现类如下:
- FileChannel:用于读取、写入、映射和操作文件的通道。
- DatagramChannel:通过UDP读写网络中的数据通道。
- SocketChannel:通过TCP读写网络中的数据。
- ServerSocketChannel:可以监听新进来的TCP连接,像 Web 服务器那样,对每一个新进来的连接都会创建一个SocketChannel。
正如所见,这些通道涵盖了UDP和TCP网络IO,以及文件IO。其中,NIO通过更新IO从而产生了三个新的类,即FileChannel中的FileInputStream,FileOutputSteam和RandomAccessFile。Java中的Reader和Writer等字符模式类并没有提供通道的实现或方法。相反的,java.nio.channels.Channels
类中提供了实用的获取Readers和Writers方法。
- FileChannel常用方法
方法 | 描述 |
---|---|
open(Path path, OpenOption… options) | 打开或创建一个文件,并返回一个接入该文件的Channel通道 |
read(ByteBuffer dst) | 从Channel中读取数据到ByteBuffer缓冲区中 |
write(ByteBuffer src) | 将ByteBuffer缓冲区中的数据写入到Channel通道中 |
long position() | 返回此通道的文件位置 |
FileChannel position(long newPosition) | 根据文件位置获取文件通道对象 |
long size() | 获取当前文件通道的大小 |
FileChannel truncate(long size) | 将当前通道的文件截取为指定大小 |
force(boolean metaData) | 强制将所有此通道内的文件更见写入到存储设备中 |
long transferTo(long position, long count, WritableByteChannel target) | 将此通道内的指定位置和大小形成的区域写入到目标通道内 |
long transferFrom(ReadableByteChannel src, long position, long count) | 将可读通道内的指定区域写入至当前通道内 |
- 基本的Channel示例
1 | public void ChannelTest() { |
注意
buf.flip()
的调用,首先读取数据到 Buffer,然后反转 Buffer, 接着再从 Buffer 中读取数据
4. Buffer
缓冲区本质上是一块可以写入区域,然后可以从中读取数据的内存,这块内存被包装为NIO Buffer对象,并提供了一组方法,用来方便地访问该块内存。
4.1 基本用法
使用Buffer读写数据,一般遵循以下四个步骤:
- 写入数据到Buffer缓冲区中;
- 调用
flip()
方法转换读写模式; - 从Buffer缓冲区中读取数据;
- 调用
clear()
方法或者compact()
方法清空Buffer缓冲区。
当向 buffer 写入数据时,buffer 会记录下写了多少数据。一旦要读取数据,需要通过
flip()
方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 buffer 的所有数据一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用
clear()
或compact()
方法。clear()
方法会清空整个缓冲区。compact()
方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面
4.2 capacity,position 和 limit
- Buffer中通常包含以下三个属性:
- Capacity —— 容量
- Position —— 所在位置
- Limit —— 最远到达位置
其中position和limit的含义取决于Buffer处于读模式还是写模式。而不管Buffer处于什么模式,capacity的含义都是一样的,表示缓冲区的容量大小。
作为一个内存块,Buffer有一个固定的大小值,称为“capacity”。只能往里边写capacity个byte、long或char等类型,一旦Buffer满了之后,需要将其清空(通过读数据或清除数据),才能继续往Buffer中写数据。
- position
当写数据至Buffer中时,position表示当前的位置。初始的position位置为0,当一个byte、long或char类型等数据写到Buffer后,position会向前移动到下一个可插入数据的Buffer单元,position最大可为capacity - 1,此处对应数组下标。
当从Buffer中读数据时,也是从某个特定位置开始读取,当将Buffer从写模式切换到读模式,position就会被置为0,当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
- limit
在写模式下,Buffer的limit表示最多能往Buffer里写多少数据。写模式下,limit就等于Buffer的capacity。
当切换至读模式时,limit则表示最多能读多少数据。因此**当切换Buffer至读模式时,limit就会被设置成写模式下的position值*(超过写模式下的position值都是空的)***。
4.3 Buffer的类型
Java NIO中有以下八种Buffer类型:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
由上述几种类型可知,不同的Buffer类型代表了不同的能读写的数据类型,除了MappedByteBuffer外,其他类型的Buffer相对应地可以通过char,short,long,int,byte,float,double等类型来操作缓冲区中的字节。
4.4 Buffer大小分配
必须通过分配大小来实现获取Buffer缓冲区,在allocate
方法初始化Buffer的时候,必须为其提供一个capacity参数,其单位通常为字节,具体实例如下所示:
1 | # 分配一个48字节的ByteBuffer |
4.5 写数据
写数据到Buffer有两种方式:
- 从Channel通道中读取数据至Buffer中;
- 从一个Buffer中使用
put()
方法写入到另一个Buffer中。
4.6 读数据
从Buffer中读数据有两种方式:
- 从Buffer中读取数据至Channel通道;
- 使用
get()
方法读取Buffer中的数据。
4.7 其他方法
rewind() 方法
rewind()
将 position 设回 0(数据仍然存在),所以可以重读 Buffer 中的所有数据。limit 保持不变,仍然表示能从 Buffer 中读取多少个元素(byte、char 等)。
clear() 与 compact() 方法
一旦读完 Buffer 中的数据,需要让 Buffer 准备好再次被写入。可以通过clear()
或 compact()
方法来完成。
如果调用的是clear()
方法,position 将被设回 0,limit 被设置成 capacity 的值。换句话说,Buffer 被清空了。Buffer 中的数据并未清除,只是这些标记告诉我们可以从哪里开始往 Buffer 里写数据(源码如下所示)。
1 | public final Buffer clear() { |
如果 Buffer 中有一些未读的数据,调用 clear()
方法,数据将 “被遗忘”,意味着不再有任何标记会告诉哪些数据被读过,哪些还没有。
如果 Buffer 中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用 compact()
方法。
compact()
方法将所有未读的数据拷贝到 Buffer 起始处,也就是整理碎片。然后将 position 设到最后一个未读元素正后面。limit 属性依然像 clear()
方法一样,设置成 capacity。现在 Buffer 准备好写数据了,但是不会覆盖未读的数据。
mark() 与 reset() 方法
通过调用 mark()
方法,可以标记 Buffer 中的一个特定 position。之后可以通过调用 reset()
方法恢复到这个 position。例如:
1 | buffer.mark(); |
equals() 与 compareTo() 方法
可以使用 equals()
和 compareTo()
方法两个 Buffer。
equals()
当满足下列条件时,表示两个 Buffer 相等:
- 有相同的类型(byte、char、int 等)。
- Buffer 中剩余的 byte、char 等的个数相等。
- Buffer 中所有剩余的 byte、char 等都相同。
equals 只是比较 Buffer 的一部分,不是每一个在它里面的元素都比较。实际上,它只比较 Buffer 中的剩余元素。
compareTo() 方法
compareTo()
方法比较两个 Buffer 的剩余元素 (byte、char 等), 如果满足下列条件,则认为一个 Buffer“小于” 另一个 Buffer:
- 第一个不相等的元素小于另一个 Buffer 中对应的元素 。
- 所有元素都相等,但第一个 Buffer 比另一个先耗尽 (第一个 Buffer 的元素个数比另一个少)。
Tips:剩余元素是从 position 到 limit 之间的元素。
5. Scatter / Gather
Java NIO开始支持Scatter / Gather,Scatter / Gather用于描述从Channel通道中读取和写入到Channel的操作。
分散(Scatter)从Channel中读取是指在读操作时将读取的数据写入多个Buffer中。因此Channel将从Channel中读取的数据“分散(Scatter)”到多个Buffer中。
聚集(Gather)是指从多个Buffer中将数据写入至Channel通道中,Channel将多个Buffer中的数据“聚集(Gather)”后发送到Channel。
Scatter / Gather 经常用于需要传输的数据分开处理的场合,例如传输一个由消息头到消息体组成的消息,你可能会将消息体和消息头分散到不同的Buffer中,这样可以方便地处理消息头和消息体。
6. Selector
Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样的话,一个单独的线程就能管理多个Channel通道,从而管理多个网络连接。
6.1 Selector的创建
通过调用Selector.open()
方法创建一个Selector,如下所示:
1 | Selector selector = Selector.open(); |
6.2 注册Channel通道
为了使Channel与Selector配合工作,需要在Selector选择器中注册Channel通道,通过Channel.register(Selector sel,int ops)方法实现***(由于Channel必须是非阻塞的,所以FileChannel不适用Selector,因为FileChannel不能切换为非阻塞模式,更准确的来说是因为FileChannel没有继承SeletableChannel,Socket Channel可以正常使用)***SelectableChannel抽象类有一个configureBlocking()
用于使通道处于阻塞模式或非阻塞模式,如下所示:
1 | // 开启非阻塞模式 |
其中Channel.register(Selector sel,int ops)方法中的第一个参数指定了注册的目标选择器,而第二个参数指定的是选择器需要查询的通道操作。
可以供选择器查询的通道操作,从类型来分,包括以下四种:
(1)可读 : SelectionKey.OP_READ
(2)可写 : SelectionKey.OP_WRITE
(3)连接 : SelectionKey.OP_CONNECT
(4)接收 : SelectionKey.OP_ACCEPT
如果Selector对通道的多操作类型感兴趣,可以用“位或”操作符来实现:int ops = SelectionKey.OP_READ | SelectionKey.OP_WRITE
;
6.3 Selection Key
在每个Channel通道向Selector进行注册后,都将获得一个Selection Key
,类似通常使用的Token,该Selection Key将在Channel通道调用的close()
方法或主动调用cancel()
方法取消后才失效,而再取消该Selection Key时,并不会立即生效,而是将其添加进Selector中的 cancelled-key
Set集合中,待下次selection operation操作时再清除。
在上述注册Channel通道时,register()
方法都会返回一个SelectionKey对象,这个对象包含了一些属性:
- interest集合
- ready集合
- Channel
- Selector
- Others
interest集合
interest集合是所选择的感兴趣的事件集合,可以通过Selection Key读写interest集合,如下所示:
1 | int interestSet = selectionKey.interestOps(); |
用”位与”操作interest集合和给定的Selection Key变量,可以确定某个事件是否在interest集合中。
ready集合
ready集合是通道以及准备就绪的操作集合。在一次Selection之后,会首先访问这个ready集合,可以通过以下方式读取ready集合。
1 | int readyOps = selectionKey.readyOps(); |
可以用类似判断interest集合的方法,检测Channel中有什么事件已经就绪,也可以使用以下方法进行判断。
1 | // 是否可接受 |
获取Channel / Selector
可以通过Selection Key获取Channel通道或Selector选择器,如下所示:
1 | // 获取对应的通道 |
还可以在用 register() 方法向 Selector 注册 Channel 的时候附加对象。如:
1 | SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject); |
示例
这里有一个完整的示例,打开一个 Selector,注册一个通道注册到这个 Selector 上 (通道的初始化过程略去), 然后持续监控这个 Selector 的四种事件(接受,连接,读,写)是否就绪。
1 | Selector selector = Selector.open(); |