前奏
因为NIO
并不容易掌握,所以这注定会是一篇长文,而且即便篇幅很大,亦难以把很多细节解释清楚,只能侧重于从整体上进行把握,并实现一个简单的客户端服务端消息通信框架作为例子,以便有需要的开发人员参考之。借用淘宝伯岩给出的忠告就是
- 尽量不要尝试实现自己的
NIO
框架,除非有经验丰富的工程师 - 尽量使用经过广泛实践的开源
NIO
框架Mina/Netty/xSocket
- 尽量使用最新版稳定版
JDK
- 遇到问题的时候,可以先看下
Java
的Bug Database
Asynchronous I/O
是在JDK7
中提出的异步非阻塞I/O
,习惯上称之为NIO2
,也叫AIO
,AIO
是对JDK1.4
中提出的同步非阻塞I/O
的进一步增强,主要包括
- 更新的
Path
类,该类在NIO
里对文件系统进行了进一步的抽象,用来替换原来的java.io.File
,可以通过File.toPath()
和Path.toFile()
将File
和Path
进行相互转换 File Attributes
,java.nio.file.attribute
针对文件属性提供了各种用户所需的元数据,不同操作系统使用的类不太一样,支持的属性分类有- BasicFileAttributeView
- DosFileAttributeView
- PosixFileAttributeView
- FileOwnerAttributeView
- AclFileAttributeView
- UserDefinedFileAttributeView
Symbolic and Hard Links
,相当于用Java
程序实现Linux
中的ln
命令Watch Service API
,作为一个线程安全的服务用于监控对象的变化和事件,以前直接用Java
监控文件系统的变化是不可能的,只能通过JNI
的方式调用操作系统的API
,而在JDK7
中这部分被加入到了标准库里Random Access Files
主要提供了一个SeekableByteChannel
接口,配合ByteBuffer
使得随机访问文件更加方便Sockets API
主要是NIO1
中的Selector
模式实现同步非阻塞Asynchronous Channel API
由NIO1
中的Selector
模式变成方法回调模式,使用更加方便,主要是可以异步实现文件的读写了
AIO应用开发
Future方式
Future
是在JDK1.5
中加入Java
并发包的,该接口提供get()
方法用于获取任务完成之后的处理结果。在AIO
中,可以接受一个I/O
连接请求,返回一个Future
对象,然后可以基于该返回对象进行后续的操作,包括使其阻塞、查看是否完成、超时异常,使用方式如下。
服务端代码
|
|
客户端代码
|
|
Future方式实现为多客户端并发服务
如何让服务端同时可以接受多个客户端的连接呢?一个简单的处理方法就是使用ExecutorService
。每次新建一个连接,并且获得返回值之后,这个返回值就是一个AsynchronousSocketChannel
的通道,将其提交给线程池,由一个工作线程进行后续处理。然后一个新的线程准备好在等待接受下一个连接。代码示例如下。
|
|
Callback方式
方法回调模式,即提交一个I/O
操作请求,并且指定一个CompletionHandler
。当异步操作完成时,便会发一个通知,此时该CompletionHandler
对象覆写的方法将被调用,如果成功调用completed
方法,如果失败调用failed
方法,首先看下Java API
。
|
|
AIO
提供了四种类型的异步通道以及不同的I/O
操作可以接收一个CompletionHandler
对象,分别是:
- AsynchronousSocketChannel:connect,read,write
- AsynchronousFileChannel:lock,read,write
- AsynchronousServerSocketChannel:accept
- AsynchronousDatagramChannel:read,write,send,receive
服务端示例代码如下
|
|
客户端代码如下
|
|
Reader/Writer方式实现
其实除了使用匿名内部类的形式外,还有可以指定读写者的read
和write
方法,另外你还可以指定超时时间,这种实现方式相对来说比匿名内部类形式看起来代码解耦合更好,代码更简洁。
抽象的接口
|
|
|
|
|
|
读者
|
|
写者
|
|
服务端代码
|
|
客户端代码
|
|
Reader/Writer方式实现支持多客户端并发服务
想要使服务端支持多并发,必须要使用到AsynchronousChannelGroup
,有关细节在下一节详述,AsynchronousChannelGroup
用于管理异步通道资源,封装一个处理I/O
完成的机制。该组对象关联一个线程池,可以将处理任务提交到线程池,这个组对象相当于是一个Dispatcher
。
|
|
线程池和Group
四种异步通道的open
方法可以指定group
参数,或者不指定。每个异步通道都必须关联一个组,要么是系统默认组,要么是用户创建的组。如果不使用group
参数,java
使用一个默认的系统范围的组对象。系统默认的组对象的线程池参数可以使用两个属性进行配置:
- java.nio.channels.DefaultThreadPool.threadFactory 默认组对象不会将其关联的线程池中的线程进行额外的配置,因此,这些线程都是
daemon
线程。 - java.nio.channels.DefaultThreadPool.initialSize: 处理
I/O
事件的最大线程数量。
组与ExecutorService
类似,这意味着关闭过程通常是两步关闭方法。在多层次Client
结构(例如FTP
的控制通道需要衍生新的数据传输通道)中,如果要使用group
,很讨厌的一点就是group
参数传递。没有环境编程之类的工具进行辅助的话,使用者必须考虑如何有效传递group
参数。
不使用group
,最大的好处是不用传递group
参数。缺点是:必须注意处理非daemon
线程的完成和退出,不小心的话,将会导致异步通道的工作丢失;同时还需要处理线程工厂和最大线程数的配置。
PendingException 和 AsynchronousChannel
如果一个读写操作没有完成,程序又发送一个读写操作命令,则导致ReadPendingException
或者WritePendingException
。如果你的程序非要这样的话,只有一个解决办法,将读写操作的命令使用队列排队进行。通常应该不会出现这种需求,如果有的话,很有可能是设计上的缺陷。
读写超时。AsynchronousChannel
的读写操作可以指定超时参数,但是超时发生之后,传递给读写操作的ByteBuffer
参数不应该向正常读写完成一样进行处理。通常设计如果超时发生,一般应该丢弃当前期望数据结果。
ByteBuffer
AIO
鼓励使用DirectByteBuffer
。就算应用程序代码中不使用DirectByteBuffer
,AIO
内核实现也会使用DirectByteBuffer
来复制外部传入的HeadByteBuffer
内容。
ByteBuffer
主要有两个继承的类分别是:HeapByteBuffer
和MappedByteBuffer
。他们的不同之处在于HeapByteBuffer
会在JVM
的堆上分配内存资源,而MappedByteBuffer
的资源则会由JVM
之外的操作系统内核来分配。DirectByteBuffer
继承了MappedByteBuffer
,采用了直接内存映射的方式,将文件直接映射到虚拟内存,同时减少在内核缓冲区和用户缓冲区之间的调用,尤其在处理大文件方面有很大的性能优势。但是在使用内存映射的时候会造成文件句柄一直被占用而无法删除的情况,网上也有很多介绍。
Netty
中使用ChannelBuffer
来处理读写,之所以废弃ByteBuffer
,官方说法是ChannelBuffer
简单易用并且有性能方面的优势。在ChannelBuffer
中使用ByteBuffer
或者byte[]
来存储数据。同样的,ChannelBuffer
也提供了几个标记来控制读写并以此取代ByteBuffer
的position
和limit
,分别是:0 <= readerIndex <= writerIndex <= capacity
,同时也有类似于mark
的markedReaderIndex
和markedWriterIndex
。当写入buffer
时,writerIndex
增加,从buffer
中读取数据时readerIndex
增加,而不能超过writerIndex
。有了这两个变量后,就不用每次写入buffer
后调用flip()
方法,方便了很多。
参考文献
[1] https://www.ibm.com/developerworks/cn/java/j-lo-nio2/
[2] https://github.com/redkale/redkale
[3] http://colobu.com/2014/11/13/java-aio-introduction/
[4] http://zjumty.iteye.com/blog/1896350
[5] http://stevex.blog.51cto.com/4300375/1581701
[6] 《Pro Java 7 NIO2》