本文的重点不在各种I/O的区别,重点在nio中channel和buffer(提供给大家个工具,可以直观的动态的研究,点击跳转下载链接)的理解,帮助处于懵逼中的小伙伴脱离苦海。
Buffer(缓冲区)
- 常用ByteBuffer 和 CharBuffer,还有一系列值类型Buffer,可以应用于不同类型。
- 所有Buffer都是抽象类,无法直接实例化。创建缓冲区要调用XxxBuffer allocate(),参数是缓冲区容量。
- 缓冲区数据存放在内存中,读写效率高。缓冲区有记录指针,能改变读写的起始点,根据不同需求,灵活处理数据。
Buffer参数说明
- capacity(容量):缓冲区支持的最大容量。
- position(记录指针位置):是缓冲区读写数据的起始点,初始值为0。position随着数据的加入而改变,例如读取2个数据到Buffer中,则position = 2。
- limit(界限):是缓冲区读写数据的终止点,limit之后的区域无法访问。
- mark(标记):mark在0~position之间,设置该值就会把position移动到mark处。
Buffer的常用方法:
- flip():确定缓冲区数据的起始点和终止点,为输出数据做准备(即写入通道)。此时:limit = position,position = 0。
- clear():缓冲区初始化,准备再次接收新数据到缓冲区。position = 0,limit = capacity。
- hasRemaining():判断postion到limit之间是否还有元素。
- rewind():postion设为0,则mark值无效。
- limit(int newLt):设置界限值,并返回一个缓冲区,该缓冲区的界限和limit()设置的一样。
- get()和put():获取元素和存放元素。使用clear()之后,无法直接使用get()获取元素,需要使用get(int index)根据索引值来获取相应元素。
图片理解Buffer读写数据的流程变化
Channel(通道)
Channel通过Buffer(缓冲区)进行读写操作。read()表示读取通道数据到缓冲区,write()表示把缓冲区数据写入到通道。
Channel需要节点流作为创建基础,例如FileInputStream和FileOutputStream()的getChannel()。RandomAccessFile也能创建文件通道,支持读写模式。通过IO流创建的通道是单向的,使用RandomAccessFile创建的通道支持双向。
通道可以异步读写,异步读写表示通道执行读写操作时,也能做别的事情,解决线程阻塞。如果使用文件管道(FileChannel),建议用RandomAccessFile来创建管道,因为该类支持读写模式以及有大量处理文件的方法。
Channel实现类
FileChannel //读写文件通道
SocketChannel //通过TCP读写网络数据通道
ServerSocketChannel //监听多个TCP连接
DataChannel //通过UDP读写网络数据通道
Pipe.SinkChannel、Pipe.SourceChannel //线程通信管道传输数据
Channel常用方法
read() //从Buffer中读取数据。
write() //写入数据到Buffer中。
map() //把管道中部分数据或者全部数据映射成MappedByteBuffer,本质也是一个ByteBuffer。map()方法参数(读写模式,映射起始位置,数据长度)。
force() //强制将此通道的元数据也写入包含该文件的存储设备。
这里先跟大家说下,我们通常操作的channel类型的对象,其实都是不同类型的抽象对象,有些方法最终是调用的不同类型的channel的实现。我们拿file类型的filechannel来说明,关系如下:
java.nio.channels.Channel--------------是个接口interface(继承了Closeable接口)
java.nio.channels.FileChannel----------抽象类(继承自抽象类java.nio.channels.spi.AbstractInterruptibleChannel,而后者抽象类又实现了java.nio.channels.Channel接口),该类的很多方法都是抽象方法,比如:(基础差的同学好好理解下position)
1.position()---返回filechannel调用read(ByteBuffer dst)后,当前指针指向的的位置,也就是说如果分配的缓冲区buffer < filechannel.size()的时候,返回的是本次往buffer里面存的数据所在filechannel的原始数据的位置。直接上代码:
@Test
public void testChannelPosition() throws IOException{
//
FileChannel inChannel = new FileInputStream("E:/nio-test-files/test.txt").getChannel();
ByteBuffer buf = ByteBuffer.allocate(2);
System.out.println("设置缓冲区buffer大小为2");
int bytesRead = inChannel.read(buf);
StringBuffer strBuf = new StringBuffer();
while(bytesRead != -1){
System.out.println("===========================本轮将channel内容按照buffer指定大小存入缓冲区开始====================");
buf.flip();
//全部读出来
while (buf.hasRemaining()) {
System.out.println("-------------遍历缓冲区strat-------------");
System.out.println("解析缓冲区时position=" + inChannel.position());
char c = (char)buf.get();
System.out.println("此时读取的是:" + c);
strBuf.append(c + "");
System.out.println("-------------遍历缓冲区end-------------");
}
System.out.println("遍历完本轮缓冲区后的内容是:" + strBuf.toString()+" 此时position=" + inChannel.position());
//因为不知道上面的文件有多少字符,所以继续读,直到本次往缓冲区读的字节数为-1才能读完,先clear缓冲区
buf.clear();
bytesRead = inChannel.read(buf);
System.out.println("channel还可以继续read到缓冲区的大小为" + bytesRead);
System.out.println("继续往缓冲区读指定大小的数据后position=" + inChannel.position());
System.out.println("===========================本轮channel内容按照buffer指定大小存入缓冲区并解析结束=================");
}
System.out.println("最终知道文档内的内容是:" + strBuf.toString());
}
运行结果:(分析见底部)
2.size()------返回filechannel的大小;
3.truncate(long size);
4.force(boolean metaData),transferTo(long position, long count, WritableByteChannel target),
5.transferTo(long position, long count, WritableByteChannel target)----JDK解释:此方法不会改变filechannel对象的position;
6.transferFrom(ReadableByteChannel src,long position, long count)----JDK解释:此方法不会改变filechannel对象的position;
7.read(ByteBuffer dst, long position)----JDK解释:此方法不会改变filechannel对象的position;
8.write(ByteBuffer src, long position)----JDK解释:此方法不会改变filechannel对象的position;
sun.nio.ch.FileChannelImpl--------------java.nio.channels.FileChannel类的子类,实现了上面的抽象方法。
在NIO中,channel是数据的载体,需要操作的数据都要装在到channel中,但是!!!channel中的数据我们是无法直接操作,这时我们需要另一个东西,也就是buffer。除了通过channel到channel直接传递数据(transferTo和transferFrom)所有的数据肯定是也必须是通过buffer来获取内容。
下面贴两个类,自己研究下,看下注释就理解了
TestBuffer.java
package nio.day0;
import java.nio.ByteBuffer;
import org.junit.Test;
public class TestBuffer {
/**
* 缓冲区存取的两个方法:
* 1.get():获取缓冲区的数据
* 2.put():存入数据到缓冲区
*
* 缓冲区的4个核心属性:
* capacity :容量,缓冲区中最大存储数据的容量,一旦声明,不可改变
* limit : 界限,表示 缓冲区中可以操作数据的大小,limit后面的数我们不能读写:因为写(put())完数据之后调用flip()方法,limit的坐标就是刚刚写的结束位置
* position : 位置,表示缓冲区中正在操作的位置 ,就是当前所操作的位置
*
* mark :标记,表示记录当前position的位置。可以通过reset()方法将position恢复到mark的位置
*
* 0 <= mark <= position <= limit <= capacity
*
*/
@Test
public void testIO1(){
String str = "abcde";
//1.分配一个指定大小的缓冲区。 此时,position=0,limit和capacity都是最后一个位置
ByteBuffer buf = ByteBuffer.allocate(1024);
System.out.println("============allocate()分配空间之后=============");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//2.利用put存储数据
buf.put(str.getBytes());
//当put写完之后position的位置到达了写的内容的结束位置,limit和capacity位置没变
System.out.println("============put()写入数据之后=============");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//3.flip,切换读写模式
buf.flip();
//调用flip()方法,此缓冲区就进入读模式,position的位置又回到刚刚写的位置的开始,即0的位置,limit的位置到了刚刚写的结束的位置,调用flip之前position的位置
System.out.println("============flip()让buf缓冲区从写模式变为读模式之后=============");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//4.利用get获取数据,当读完缓冲区数据的时候position又到了数据结束的位置
byte[] dst = new byte[buf.limit()];//读取所有的数据
buf.get(dst);
System.out.println(new String(dst, 0, dst.length));
System.out.println("============get()完buf缓冲区所有数据之后=============");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//5.rewind()方法,在读模式中,将position的位置重置到0的位置。即可重读状态
buf.rewind();
System.out.println("============rewind()在读模式中重置position位置,使缓冲区所有数据可重读=============");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//6.clear()清空缓冲区
buf.clear();
//但是clear()方法并不是真正的情况缓冲区,此时缓冲区的数据还在,只是数据处于“被遗忘”状态
// ===》所谓“被遗忘”只是让position、limit、capacity的指针位置回到了allocate时的位置
System.out.println("============clear()后只是让position、limit、capacity的指针位置回到了allocate时的位置=============");
System.out.println(buf.position());
System.out.println(buf.limit());
System.out.println(buf.capacity());
//测试数据还在
System.out.println("============验证clear()之后数据还在=============");
System.out.println((char)buf.getChar());
}
@Test
public void testMark(){
String str = "abcde";
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put(str.getBytes());
buf.flip();
byte[] dst = new byte[buf.limit()];
//先读取两个字节
buf.get(dst, 0, 2);
System.out.println(new String(dst,0,2));
//打印此时position的位置
System.out.println(buf.position());
//mark()记录当前position的位置
buf.mark();
//继续读取两个字节
buf.get(dst, 2, 2);
System.out.println(new String(dst,2,2));
//打印此时position的位置
System.out.println(buf.position());
//reset()将position恢复到mark()的位置
buf.reset();
//打印此时position的位置
System.out.println(buf.position());
}
}
TestChannel.java
package nio.day1;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import org.junit.Test;
/**
* 3种获取通道的方式:
* 1.java针对支持通道的类提供了getChannel()方法
* 本地IO:
* FileInputStream/FileOutputStream
* RandomAccessFile
*
* 网络IO:
* Socket
* ServerSocket
* DatagramSocket
* 2.JDK1.7中NIO2针对各个通道提供了静态方法open()
* 3.JDK1.7中NIO2的Files工具类的newByteChannel()
*
*
* 通道之间的数据传输
* transferFrom
* transferTo
*
* @author Admin
*
*/
public class TestChannel {
//通道之间的数据传输(直接缓冲区)
@Test
public void test3() throws IOException{
FileChannel inChannel = FileChannel.open(Paths.get("E:/nio-test-files/nio.jpg"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("E:/nio-test-files/nio-test3.jpg"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);//create_new如果已经存在会报错
inChannel.transferTo(0, inChannel.size(), outChannel);
//outChannel.transferFrom(inChannel, 0, inChannel.size()); //和上面的一样
System.out.println(inChannel.position());
inChannel.close();
outChannel.close();
}
/**
* 利用直接缓冲区(os内存)完成文件的复制(内存映射文件)
* @throws IOException
*/
@Test
public void test2() throws IOException{
FileChannel inChannel = FileChannel.open(Paths.get("E:/nio-test-files/nio.jpg"), StandardOpenOption.READ);
FileChannel outChannel = FileChannel.open(Paths.get("E:/nio-test-files/nio-test2.jpg"), StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);//create_new如果已经存在会报错
//内存映射文件(这个文件和allocateDirect()一样)
MappedByteBuffer inMappedBuf = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMappedBuf = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());//MapMode.READ_WRITE对应了open中的StandardOpenOption.READ, StandardOpenOption.WRITE
//直接对缓冲区进行数据的读写操作
byte[] dst = new byte[inMappedBuf.limit()];
inMappedBuf.get(dst); //src.get(dst)表示This method transfers bytes from this buffer(src) into the given destination(dst) array
outMappedBuf.put(dst);//This method transfers the entire content of the given sourcebyte array into this buffer
inChannel.close();
outChannel.close();
}
/**
* 利用通道完成文件的复制(利用非直接缓冲区buffer)
* @throws IOException
*/
@Test
public void test1() throws IOException{
//1.通过打开与实际文件的连接创建一个 FileInputStream
FileInputStream fileInputStream = new FileInputStream("E:/nio-test-files/nio.jpg");
FileOutputStream fileOutputStream = new FileOutputStream("E:/nio-test-files/nio-test1.jpg");
//2.从流中获取channel
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = fileOutputStream.getChannel();
//3.分配指定大小的缓冲区
ByteBuffer buf = ByteBuffer.allocate(1024);
//4.将通道中的数据存入缓冲区
while(inChannel.read(buf) != -1){
//5.将缓冲区的数据写入通道中
buf.flip();
outChannel.write(buf);
buf.clear();
}
inChannel.close();
outChannel.close();
fileInputStream.close();
fileOutputStream.close();
}
@Test
public void test() throws IOException {
//第一种可以通过new FileInputStream(path)(或者FileOutStream(path)).getChnanel()方式获取FileChannel
//FileChannel inChannel = new FileInputStream("E:/nio-test-files/test.txt").getChannel();
//第二种通过new RandomAccessFile("E:/nio-test-files/test.txt", "rw").getChnannel()获取FileChannel
RandomAccessFile aFile = new RandomAccessFile("E:/nio-test-files/test.txt", "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(13);
int bytesRead = inChannel.read(buf);//从channel往buf里面存,相当于buf的put
while (bytesRead != -1) {
System.out.println("Read " + bytesRead);
buf.flip();//buf切换读写模式
while (buf.hasRemaining()) {
//System.out.println(inChannel.position());
System.out.print((char) buf.get());//从buf里面取,get
}
//System.out.println(inChannel.position());
buf.clear();//重新往buffer里面存数据必须先清空缓冲区,重置buffer的position,limit,size信息
bytesRead = inChannel.read(buf);
}
/*while(inChannel.read(buf) != -1){
System.out.println(inChannel.read(buf));
buf.flip();
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
buf.clear();
}*/
aFile.close();
}
@Test
public void testWrite() throws IOException{
String str = "ABDC";
FileOutputStream fileOutputStream = new FileOutputStream("E:/nio-test-files/test-out.txt") ;
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put(str.getBytes());//buf的put
buf.flip();//切换读写模式
FileChannel channel = fileOutputStream.getChannel();
channel.write(buf);//从buf中get数据,写入channel
fileOutputStream.close();
channel.close();
}
@Test
public void testChannelPosition() throws IOException{
//
FileChannel inChannel = new FileInputStream("E:/nio-test-files/test.txt").getChannel();
ByteBuffer buf = ByteBuffer.allocate(2);
System.out.println("设置缓冲区buffer大小为2");
int bytesRead = inChannel.read(buf);
StringBuffer strBuf = new StringBuffer();
while(bytesRead != -1){
System.out.println("===========================本轮将channel内容按照buffer指定大小存入缓冲区开始====================");
buf.flip();
//全部读出来
while (buf.hasRemaining()) {
System.out.println("-------------遍历缓冲区strat-------------");
System.out.println("解析缓冲区时position=" + inChannel.position());
char c = (char)buf.get();
System.out.println("此时读取的是:" + c);
strBuf.append(c + "");
System.out.println("-------------遍历缓冲区end-------------");
}
System.out.println("遍历完本轮缓冲区后的内容是:" + strBuf.toString()+" 此时position=" + inChannel.position());
//因为不知道上面的文件有多少字符,所以继续读,直到本次往缓冲区读的字节数为-1才能读完,先clear缓冲区
buf.clear();
bytesRead = inChannel.read(buf);
System.out.println("channel还可以继续read到缓冲区的大小为" + bytesRead);
System.out.println("继续往缓冲区读指定大小的数据后position=" + inChannel.position());
System.out.println("===========================本轮channel内容按照buffer指定大小存入缓冲区并解析结束=================");
}
System.out.println("最终知道文档内的内容是:" + strBuf.toString());
}
}
补上刚刚上面对于position的分析过程: