什么是NIO
NIO是 New I/O的简称,与旧式的基于流的I/O方法相对,也被称为非阻塞IO,它是一套新的I/O标准,在jdk1.4中被发布
NIO的特性
原始的I/O是基于的流的方式,而NIO是基于块(Block)的,它以块为单位来对数据进行处理,因为本身硬盘上的文件就是使用块进行存储的,NIO对此进行了对应,所以NIO的性能上比原始I/O要好的多。 为所有的原始类型提供(buffer)缓存支持 增加通道(Channel)对象,作为新的原始I/O抽象(可以说是代替以往的流) 支持锁和内存映射文件的文件访问接口 提供了基于selector的异步网络I/O
NIO的核心组成部分
Buffer
Buffer的本质实际是一块内存区域,通过native函数库分配堆外内存,然后通过存储在对堆中的DirectByteBuffer对象作为该内存的引用对象,这一实现将该内存区域与java运行时数据区进行分离,并且又通过堆内对象进行调用。
Buffer的子类实现: Buffer中的3个重要的参数:
position(位置):
写模式:当前缓冲区的位置,将从position的下一个位置写数据。 读模式:当前缓冲区的读取的位置,将从此位置后读取数据 capactiy(容量)
limit(上限)
写模式:缓冲区的实际上限,它总是小于等于容量,通常情况下与容量是相等的 读模式:代表可读取的容量,和上次写入的数据量相等。 Buffer中的重要方法:
rewind():将position置为零,并清除标志位(mark),用于重新读取buffer中的数据。 clear():将position置零,同时将limit的值设置与容量capactiy相等,并清除标志mark。 flip():用于读写转化是使用,先将limit设置位position所在的位置,再将position置为零,并清除标记位mark mark():可以标记Buffer中的一个特定position。 reset():跟mark()联合使用,调用该方法后可以恢复到mark标记的position位置上。
Channels
Channels的主要实现:
FileChannel:从文件中读取数据 DatagramChannel:通过UDP协议读取网络中的数据 SocketChannel:通过TCP协议读取网络中的数据 ServerSocketChannel:可以监听新建来的TCP连接,像web服务器一样,对每一个新进来的连接创建一个SocketChannel Channels与流形式对比:
Channels跟流还是比较类似的,但是流的读写一般时单向的,而 Channels则可以读也可以写。 Channels支持异步的读取和写入,而流必须同步进行 Channels中的数据读取或写入都需要buffer来进行周转。 API解析应用:
Channel的分散和聚集:
scatter(分散):Channels允许在读取通道数据的时,将数据读取到多个Buffer中。private static void scatterRead ( File source) throws Exception{
RandomAccessFile raf= null;
try {
raf = new RandomAccessFile ( source, "rw" ) ;
FileChannel channel = raf. getChannel ( ) ;
ByteBuffer buffer1 = ByteBuffer. allocate ( 1024 ) ;
ByteBuffer buffer2 = ByteBuffer. allocate ( 8192 ) ;
ByteBuffer[ ] array = new ByteBuffer [ ] { buffer1, buffer2} ;
channel. read ( array) ;
} finally { raf. close ( ) ; }
}
Gather(聚集):Channels允许在往通道中写入数据时,写入多个Buffer中的数据。private static void aggregateWrite ( File source) throws Exception{
RandomAccessFile raf= null;
try {
raf = new RandomAccessFile ( source, "rw" ) ;
FileChannel channel = raf. getChannel ( ) ;
ByteBuffer buffer1 = ByteBuffer. allocate ( 1024 ) ;
ByteBuffer buffer2 = ByteBuffer. allocate ( 2048 ) ;
ByteBuffer[ ] array = new ByteBuffer [ ] { buffer1, buffer2} ;
channel. write ( array) ;
} finally { raf. close ( ) ; }
}
FileChannel之间的数据传输
transferForm():private static void dataConversion1 ( File source, File target) throws Exception{
RandomAccessFile from= null;
RandomAccessFile to= null;
try {
from = new RandomAccessFile ( source, "rw" ) ;
to = new RandomAccessFile ( target, "rw" ) ;
FileChannel fromChannel= from. getChannel ( ) ;
FileChannel toChannel= to. getChannel ( ) ;
long count= fromChannel. size ( ) ;
toChannel. transferFrom ( fromChannel, 0 , count) ;
} finally { from. close ( ) ; to. close ( ) ; }
}
transferTo():private static void dataConversion2 ( File source, File target) throws Exception{
RandomAccessFile from= null;
RandomAccessFile to= null;
try {
to = new RandomAccessFile ( target, "rw" ) ;
from = new RandomAccessFile ( source, "rw" ) ;
FileChannel toChannel= from. getChannel ( ) ;
FileChannel fromChannel= from. getChannel ( ) ;
long count= fromChannel. size ( ) ;
fromChannel. transferTo ( 0 , count, toChannel) ;
} finally { from. close ( ) ; to. close ( ) ; }
}
Buffer+FileChannel 的简单应用
private static void copyFile ( File source, File target) {
FileInputStream in= null;
FileOutputStream out= null;
try {
in= new FileInputStream ( source) ;
out= new FileOutputStream ( target) ;
FileChannel inChannel = in. getChannel ( ) ;
FileChannel outChannel = out. getChannel ( ) ;
ByteBuffer buffer= ByteBuffer. allocate ( 1024 ) ;
while ( true ) {
try {
buffer. clear ( ) ;
int read = inChannel. read ( buffer) ;
if ( read== - 1 ) break ;
buffer. flip ( ) ;
outChannel. write ( buffer) ;
} catch ( Exception e) { e. printStackTrace ( ) ; }
}
} catch ( FileNotFoundException e) { e. printStackTrace ( ) ;
} finally {
try {
in. close ( ) ;
out. close ( ) ;
} catch ( IOException e) { e. printStackTrace ( ) ; }
}
}
DatagramChannel
DatagramChannel使用的时UDP协议进行数据包传输,因为UDP是无连接的网络协议不需要像TCP一样进行连接,所以不同通过建立通道的方式进行数据传输,而是通过数据包的发送和接收进行传输。需要注意的是DatagramChannel接收数据时,如果buffer容不下收到的数据,多出的那部分数据就会被丢弃。 实例应用:
Selectors(选择器)
选择器允许单线程对多个通道进行管理,对于传统的IO模式而言,每一个IO都需要一个线程来进行处理,增大了服务器的负载,随着线程的增多线程之间的上下文切换开销也会变大,并且如果数据没有准备好线程还会进入阻塞状态,要等待数据准备好之后再对数据进行处理。而选择器的出现则解决了该问题,将通道注册到选择器中,一旦数据到达准备好之后再通知选择器对该数据进行处理,而当数据没有到达的时候线程不必等待可以去完成其他的任务。
传统IO处理方式 NIO处理方式 运行原理:服务端将所有的Channel注册到Selector,Selector则负责监视这些channel的IO状态,任何一个或多个Channel准备好可用的IO操作后,Selector调用select()将返回有多少个Channel处于就绪状态,并通过selectedKeys()方法获取到这些Channel对应的SelectionKey集合,每一个SelectionKey都代表了一个Channel,再通过SelectionKey对某个channel进行IO操作。 API解析
方法调用:
select():阻塞到至少有一个Channel就绪后,返回就绪的Channel数量 select(long timeout):阻塞立即返回,没有就绪Channel则返回0,每隔timeout毫秒后再次阻塞获取。 selectNow():不会进行阻塞,不管是否有就绪的Channel; selectorKeys():返回就绪Channel的SelectionKey集合 wakeUp():让Selector在调用select()后处于阻塞状态的线程立即返回不用等待Channel就绪后返回。 状态说明:上面提到Selector会判断channel处于就绪状态才会进行处理,在面介绍下Channel的状态。
SelectionKey.OP_CONNECT:Channel成功连接服务器后进入连接就绪状态 SelectionKey.OP_ACCEPT:服务器监听到了客户端的channel,服务器可以接收该连接 SelectionKey.OP_READ:Channel中具有可读的数据后进入读就绪状态 SelectionKey.OP_WRITE:Channel等待数据写入进入写就绪状态 Selector+ServerSocketChannel+Buffer的简单应用:
拓展
RandomAccessFile与内存映射文件
RandomAccessFile
RandomAccessFile是用来对文件内容进行随机访问的类,你可以通过设置你需要访问的位置对文件内容定位,当你在对文件进行读写的时候就是从你设置的位置开始读写。 RandomAccessFile设定的四种对文件的访问模式
r:以只读的方式打开文件,不可对文件进行写的操作 rw:可以对文件进行读写操作 rws:进行写操作时,同步刷新到磁盘,刷新内容和元数据(也就是该文件的注册信息之类) rwd:进行写操作时,同步刷新到磁盘,刷新内容 RandomAccessFile运用了很多关于输入流输出流的方法,但是并非是FileInputStream和FileOutputStream,而是自己从零开始,不与输入输出流之间产生联系,因为其内部需要实现在文件内进行随机读写,在行为上与输入输出流有底层有所不同,所以RandomAccessFile是一个独立的类。
内存映射文件(memory-mapped files)
上面提到的RandomAccessFile可以对文件进行随机访问,这样就可以对文件进行修改增加操作;而在jdk1.4后javaNIO通过内存映射文件取带了RandomAccessFile的绝大部分功能 内存映射文件通过将文件映射到内存中,通过修改内存数据映射到文件的修改;内存文件映射要求必须指明文件映射开始位置,和映射数据范围,所以你映射的可以是大文件中的一个小片段。但是文件最大不得超过2GB。 在内存映射之前,都必须通过RandomAccessFile来对文件进行输出,可能是在内存映射文件时因为指定位置的关系,所以通过RandomAccessFile对随机访问提供支持吧。 内存映射文件应用:
private static void fileMappedMemory ( File source) {
RandomAccessFile raf= null;
try {
raf= new RandomAccessFile ( source, "rw" ) ;
FileChannel fc= raf. getChannel ( ) ;
MappedByteBuffer mbb= fc. map ( FileChannel. MapMode. READ_WRITE, 0 , raf. length ( ) ) ;
while ( mbb. hasRemaining ( ) ) { System. out. println ( mbb. get ( ) ) ; }
mbb. put ( 0 , ( byte ) 98 ) ;
} catch ( FileNotFoundException e) {
e. printStackTrace ( ) ;
} catch ( IOException e) {
e. printStackTrace ( ) ;
} finally {
try {
raf. close ( ) ;
} catch ( IOException e) {
e. printStackTrace ( ) ;
}
}
}
Pipe(管道)
介绍:NIO的管道,用于两个线程之间的单向数据连接。pipe具有一个source通道和一个sink通道,数据会先写入到sink通道中,从source通道中进行读取。 pipe原理图: 3.实例应用:public class PipeTest {
public static void main ( String[ ] args) {
try {
Pipe pipe= Pipe. open ( ) ;
Thread td1= new Thread ( new WriteThread ( pipe) ) ;
Thread td2= new Thread ( new ReadThread ( pipe) ) ;
td2. start ( ) ;
td1. start ( ) ;
} catch ( IOException e) {
e. printStackTrace ( ) ;
}
}
}
class WriteThread implements Runnable {
private Pipe pipe;
public WriteThread ( Pipe pipe) {
this . pipe = pipe;
}
@Override
public void run ( ) {
Pipe. SinkChannel sinkChannel= null;
try {
sinkChannel= pipe. sink ( ) ;
ByteBuffer buffer= ByteBuffer. allocate ( 1024 ) ;
Thread. sleep ( 10000 ) ;
buffer. put ( Thread. currentThread ( ) . getName ( ) . concat ( "写入数据" ) . getBytes ( ) ) ;
buffer. flip ( ) ;
sinkChannel. write ( buffer) ;
} catch ( IOException e) {
e. printStackTrace ( ) ;
} catch ( InterruptedException e) {
e. printStackTrace ( ) ;
} finally {
try {
sinkChannel. close ( ) ;
} catch ( IOException e) {
e. printStackTrace ( ) ;
}
}
}
}
class ReadThread implements Runnable {
private Pipe pipe;
public ReadThread ( Pipe pipe) {
this . pipe = pipe;
}
@Override
public void run ( ) {
Pipe. SourceChannel sourceChannel= null;
try {
sourceChannel= pipe. source ( ) ;
ByteBuffer buffer= ByteBuffer. allocate ( 1024 ) ;
sourceChannel. read ( buffer) ;
System. out. println ( new String ( buffer. array ( ) ) ) ;
} catch ( IOException e) {
e. printStackTrace ( ) ;
} finally {
try {
sourceChannel. close ( ) ;
} catch ( IOException e) {
e. printStackTrace ( ) ;
}
}
}
}