一、NIo的简介
Java NIO (New IO,Non-Blocking IO)是从Java 1.4版本开始引入的一套新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。Non-Blocking应该是最好的理解
随着 JDK 7 的发布,Java对NIO进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称他们为 NIO.2。因为 NIO 提供的一些功能,NIO已经成为文件处理中越来越重要的部分。
二、NIO和传统IO的区别
IO: 面向流 通道是单向的 阻塞IO
NIO:面向缓冲区(Buffer Oriented) 通道可以是单向的,也可以是双向的
非阻塞IO(Non Blocking IO)
选择器(Selectors)
三、NIO的核心
Java NIO是由以下几个核心部分组成的
Channels
Buffers
Selectors
Channel和Buffer
通道和缓冲区(Buffer),通道表示IO源到IO设备(例如:文件,套接字)的连接,若需要时使用NIO需要获取IO设备中的通道以及用于容纳的数据缓冲区,对数据进行处理
缓冲区底层就是数组
简而言之:Channel 负责传输,Buffer负责存储
基本上,所有的IO和NIO都是从一个Channel开始。Channel有点像流,数据可以从Channel读到Buffer中,也可以从Buffer写到Channel中
Channel和Buffer有好几种类型。下面是Java NIO中一些主要Channel的实现
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
这些通道涵盖了UDP和TCP网络IO,以及文件IO
以下是Java NIO里关键的Buffer实现
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
这些Buffer覆盖了你能通过IO发送的基本数据类型:byte,short,int,long,float,double和char
JavaNIO还有个MappedByteBuffer,用于表示内存映射文件
Selector
selector允许单线程处理多个Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用
Selector就会很方便。
要使用Selector,Selector需要注册Channel,然后调用他的Select()方法,这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方向返回,线程就可以处理这些事件,事件的例子有如新链接进来,数据接受等
四、Buffer
Java NIO中的Buffer用于和NIO通道进行交互,如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便地访问该块内存
Buffer的基本用法
使用Buffer读写数据一般遵循以下四个步骤:
1、写入数据到Buffer
2、调用flip()方法
3、从Buffer中读取数据
4、调用clear()方法或者compact()方法
当向buffer写入数据时候,buffer会记录下写了多少数据,一旦要读取数据,需要通过flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到buffer的所有数据
一旦读完了所有数据,就需要清空缓存区,让它可以再次被写入。有两种方式能清空缓存区:调用clear()和compact()方法。clear()方法会清空整个缓存区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区维度数据的后面
RandomAccessFile raf = new RandomAccessFile("D:/dioTest.txt", "rw");
FileChannel fChannel = raf.getChannel();
ByteBuffer bBuffer = ByteBuffer.allocate(1024);
int readBuff= fChannel.read(bBuffer);//read into Buffer
while(readBuff !=-1){
bBuffer.flip();//make buffer ready to read;
while(bBuffer.hasRemaining()){
System.out.println((char)bBuffer.get());//read 1 byte at a time
}
bBuffer.clear();//make buffer ready to write
readBuff = fChannel.read(bBuffer);
}
fChannel.close();
Buffer的capacity,position和limit
缓冲区本质上是一块可以先写入数据,然后可以从中读取数据的内存,这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便访问该块内存
为了理解Buffer的工作原理需要熟悉它的三个属性
capacity
position
limit
position和limit的含义取决于Buffer处在读模式还是写模式,不管Buffer处于什么模式,capacity含义总是一样的
这里有一个关于capacity,position和limit在读写模式中的说明,详细的解释在查图后面
标记、位置、限制、容量遵守以下不变式:
0 <= mark <= position <= limit <= capacity
1、容量 (capacity) :作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”,你只能往里写capacity个byte、long、char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据,往里写数据。表示 Buffer 最大数据容量,一旦声明后,不能更改。通过Buffer中的capacity()获取。缓冲区capacity不能为负。
2、限制 (limit):通过Buffer中的limit()获取。缓冲区的limit不能为负,并且不能大于其capacity。在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。在写数据下,limit等于Buffer的capacity。当切换到读模式时候,limit表示你最多能读到多少数据。因此当切换到Buffer到读模式时候,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成写数据的数量,在这个值在写模式下就是position)。第一个不应该读取或写入的数据的索引,即位于 limit后的数据不可读写。
3、位置 (position):通过Buffer中的position()获取。缓冲区的position不能为负,并且不能大于其limit。当你写数据到Buffer中时候,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后,position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity-1。当读取数据时候,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0,当从Buffer的position处读取数据时候,position向前移动到下一个可读的位置。当前要读取或写入数据的索引。
4、标记 (mark):标记是一个索引,通过 Buffer 中的 mark() 方法将mark标记为当前position位置。
之后可以通过调用 reset() 方法将 position恢复到标记的mark处。
Buffer的分配,要想获得一个Buffer对象首先要进行分配,每一个Buffer类都有一个allocate方法,下面是一个分配48字节capacity的ByteBuffer的例子
ByteBuffer bBuffer = ByteBuffer.allocate(48);
这是一个分配一个可存储1024个字符的CharBuffer
CharBuffer cBuffer = CharBuffer.allocate(1024);
/**
* 一.Nio中传输中的两个重要的概念:
* Buffer:缓冲区 负责数据的存储(读写)
* channel :通道 代表了数据源与IO节点 (文件,网络socket) 之间的链接,负责传输Buffer
*
* 二。 java.nio.Buffer
* |--------ByteBuffer
* |--------CharBuffer
*
* |--------ShortBuffer
* |--------IntBuffer
* |--------LongBuffer
* |--------FloatBuffer
* |--------DoubleBuffer
底层对应类型的数组
xxxBuffer的常用属性
capacity :容量
limit : 限制 默认的时候与capation 的值一样
position : 位置
mark : 标记
*/
@Test
public void test(){
java.nio.ByteBuffer buffer= java.nio.ByteBuffer.allocate(10);//底层为10的byte数组
System.out.println(buffer.capacity());//10
System.out.println(buffer.limit());//10
System.out.println(buffer.position());//0
System.out.println(buffer.mark());//java.nio.HeapByteBuffer[pos=0 lim=10 cap=10]
buffer.put("ddd".getBytes());
System.out.println(buffer.capacity());//10
System.out.println(buffer.limit());//10
System.out.println(buffer.position());//3
System.out.println(buffer.mark());//每一次put都会将值移动一次
//java.nio.HeapByteBuffer[pos=3 lim=10 cap=10]
buffer.flip();//将存入数据模式变成取出数据模式 已经存入的数据posstion又变成0,从头继续读
System.out.println(buffer.capacity());//10
System.out.println(buffer.limit());//3
System.out.println(buffer.position());//0
System.out.println(buffer.mark());//java.nio.HeapByteBuffer[pos=0 lim=3 cap=10]
//buffer.get();//每get一次position+1
System.out.println((char)buffer.get());//d
System.out.println((char)buffer.get());//d
System.out.println(buffer.capacity());//10
System.out.println(buffer.limit());//3
System.out.println(buffer.position());//2
System.out.println(buffer.mark());//java.nio.HeapByteBuffer[pos=2 lim=3 cap=10]
buffer.rewind();//重置position
buffer.clear();//清空回到用户初始状态
System.out.println((char)buffer.get());//d
System.out.println(buffer.position());//1
System.out.println(buffer.limit());//10
System.out.println(buffer.mark());//java.nio.HeapByteBuffer[pos=1 lim=10 cap=10]
}
flip()方法
flip()方法将Buffer从写模式切换到读模式,调用flip()方法会将position设回0,并将limit设置成之前position的值
换句话说,position现在用于标记读的位置,limit表示之前写进了多少个byte、char等,——现在能读取多少个byte、char等
从Buffer中读取数据
从Buffer中读取数据有两种方式:
1)从Buffer读取数据到Channel中
2)使用get()方法从Buffer中读取数据
从Buffer中读取数据到Channel中的例子:
int writeBuff = fChannel.write(bBuffer);//wirte into Channel from Buffer
使用get()方法从Buffer中读取数据的例子
byte aBytes = bBuffer.get();
get方法有很多版本,允许你以不同的方式从Buffre中读取数据,例如,从指定position读取,或者从Buffer中读取数据到字节数组。更多Buffer实现的细节参考JavaDoc。
rewind()方法
Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据,limit
保持不变,仍然表示能从Buffer中读取多少个元素(btye、char等)
clear()与compact()方法
一旦读完Bufer中的数据,需要让Buffer准备好再次被写入。可以通过clear()或
comapct()方法来完成。
如果调用的是clear()方法,position将被设回0,limit被设置成capacity的值,换句话说,Buffer被清空了,Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。
如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先写数据,那么使用compact()方法
compact()方法将所有未读的数据拷贝到Buffer起始处,然后将position设到最后一个未读元素后面,limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据
mark()与reset()方法
通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。例如:
bBuffer.mark();//mark position
bBuffer.reset();//go back position
equals()与compareTO()方法
Buffer可以使用equals()和compareTo()两个方法
equals()
当满足下列条件时候,表示两个Buffer相等:
1、有相同的类型(byte、char、int等)
2、Buffer中剩余的btye、char等的个数相等
3、Buffer中所有剩余的byte、char等值都相等
equals只是比较Buffer的一部分,不是每一个在它里面的元素都比较,实际上,它只比较Buffer中的剩余元素
compareTo()
compareTo()方法比较两个Buffer的剩余元素(byte、char等)如满足下列条件,则认为一个BUffer小于“另外一个”Buffer
1、第一个不相等的元素小于另一个Buffer中对应的元素
2、所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。
五、Channel
channel 表示IO源与目标节点打开的链接
channel本身不能存数据,只能与buffer交互 ,JavaNIO的通道类似流,但有些不同
即可以从通道中读取数据,又可以写数据到通道,但流的读写通常是单向的
通道可以异步地读写,通道中的数据总要先读到一个Buffer,或者总是要从一个Buffer中写入
正如上面所说,从通道读取数据到缓存区,从缓存区写入数据到通道,如下所示
Channel的实现
主要实现类:
- java.nio.channels.Channel:
- |—-FileChannel:适用于本地数据传输 ,从文件中读写数据。
- |—-SocketChannel:能通过TCP读写网络中的数据。
- |—-ServerSocketChannel:用于TCP中的网络传输的服务器端 ,可以监听新进来的TCP连接,像*Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
- |—-DatagramChannel:能通过UDP读写网络中的数据。
- |—-Pipe.SinkChannel:
- |—-Pipe.SourceChannel:
实例化Channel:
- 方式一: 调用getChannel()
- FileInputStream—>FileChannel
- FileOutpuStream—>FileChannel
- RandomAccessFile—>FileChannel
- ————-
- Socket—>SocketChannel
- ServerSocket—>ServerSocketChannel
- DatagramSocket—>DatagramChannel
- 方式二: jdk7.0以上才可以使用
- 调用XxxChannel的静态方法:open(),得到XxxChannel实例。
- 方式三:jdk7.0以上才可以使用
- Files.newByteBuffer(),返回一个字节通道
- 四、Channel特点:既可以是单向的,也可以是双向的。
- |————-ServerSocketChannel TCP 监听TCP读写网络中
- |————-DatagramChannel 适用于UDP 读写网络中的数据通道
基本的Channel示例
RandomAccessFile racAccessFile = new RandomAccessFile("D:\random.txt", "rw");
FileChannel channelIn = racAccessFile.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channelIn.read(buffer);
while(bytesRead != -1){
System.out.println("read:"+bytesRead);
buffer.flip();
while(buffer.hasRemaining()){
System.out.println((char)buffer.get());
}
buffer.compact();
bytesRead = channelIn.read(buffer);
}
racAccessFile.close();
注意
buf.flip()的调用,首先读取数据到Buffer,然后反转Buffer,接着再从Buffer中读取数据。
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()或compact()方法。clear()方法会清空整个缓冲区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
非直接缓冲区代码实例
/**
* channleb只能与buffer交互
* //实现文件的复制 FileChannel +ByteBuffer(非直接缓冲区)
*/
@Test
public void test1()throws Exception{
// 1、提供相应的输入输出流
FileInputStream fiStream = new FileInputStream("");
FileOutputStream foStream = new FileOutputStream("");
// 2、创建相应的Channel 通过我们的流去创建对应的通道
FileChannel inChannel = fiStream.getChannel();
FileChannel outChannel = foStream.getChannel();
// 3、提供缓冲区
ByteBuffer byteBuffer = java.nio.ByteBuffer.allocate(100);
while((inChannel.read(byteBuffer)) != -1){
//切换为读数据模式
byteBuffer.flip();
// 如果这里不切换的话缓存区会满
outChannel.write(byteBuffer);
byteBuffer.clear();//清空,读完当前数据然后再继续
}
fiStream.close();
foStream.close();
inChannel.close();
outChannel.close();
}
Scatter/Gather
JavaNIO开始支持scatter/gather,scatter/gather用于描述从Channel中读取或者写入到Channel的操作
分散(scatter)从Channel中读取是指在读操作时候将读取的数据写入多个buffer中,因此Channel将从Channel中读取的数据“分散”到多个Buffer中
聚集(gather)写入Channel是指在写操作中将多个buffer的数据写入到同一个Channel,因此Channel将多个Buffer中的数据“聚集gather”后发送到Channel
scatter/gather经常用于需要将传输的数据分开处理的场合,例如,传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体
ScatteringReads
Scattering Reads是指数据从一个channel读取到多个buffer中
ByteBuffer bBuffer = ByteBuffer.allocate(1024);
ByteBuffer buByteBuffer = ByteBuffer.allocate(64);
ByteBuffer[] buffers = {bBuffer,buByteBuffer};
fChannel.read(buffers);//channel read into buffer
注意buffer首先被插入到数组,然后再将数组作为channel.read()的输入参数。read()方法按照buffer在数组中的顺序将从channel中读取的数据写到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。
Scattering Reads在移动下一个buffer前,必须填满当前的buffer,这也意味着它不适用于动态消息(消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充(eg、128byte),Scattering Reads才能正常工作。
Gathering Writes
ByteBuffer bBuffer = ByteBuffer.allocate(1024);
ByteBuffer buByteBuffer = ByteBuffer.allocate(64);
ByteBuffer[] buffers = {bBuffer,buByteBuffer};
fChannel.write(buffers);//from buffer write into channel
buffers数组是write()方法的入参,write()方法会按照buffer在数组中的顺序,将数据写入到channel,注意只是position和limit之间的数据才会被写入,因此,如果一个buffer的容量为128byte,但是仅仅包含58byte
的数据,那么这58byte的数据将被写入到channel中。因此与Scattering Reads相反,Gathering Writes能较好的
处理动态消息。
JavaNIO通道之间的数据传输
在java的NIO中,如果两个通道中有一个是FileChannel,那么你可以直接将数据从一个channel传输到另一个channel。
transferFrom()
FileChannel的transferFrom()方法可以将数据从源通道传输到FileChannel中(这个方法在JDK文档中的解释为将字节从给定的可读取字节通道传输到此通道的文件中)。
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position =0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel,position, count);
方法的输入参数position表示从position处开始向目标文件写入数据,count表示最多传输的字节数,如果源通道的剩余空间小雨count个字节,则所传输的字节数要小于请求的字节数。此处要注意,在SocketChannel的实现中,SocketChannel只会传输此刻准备好数据(可能不足count字节)。因此SocketChannel可能不会讲请求的所有数据(count个字节)全部传输到FileChannel中。
transerTo()
transerTo()方法将数据从FileChannel传输到其他的channel中
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt", "rw");
FileChannel toChannel = toFile.getChannel();
long position =0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
上面说的关于SocketChannel的问题在transferTo()方法中同样存在,SocketChannel会一直传输数据直到目标buffer被填满