您的位置:首页 > 其它

NIO对文件的简易读写操作以及说明

2018-01-01 14:47 225 查看
首先了解一下NIO中的重要概念 通道、缓冲区、选择器


通道- 类似于流,但是可以异步读写数据(流只能同步读写),通道是双向的,(流是单向的),通道的数据总是要先读到一个buffer
或者 从一个buffer写入,即通道与buffer进行数据交互。

  通道类型:  

o FileChannel:从文件中读写数据的通道。通过使用一个InputStream、Out
4000
putStream或RandomAccessFile来获取一个FileChannel实例,FileChannel比较特殊,它可以与通道进行数据交互,
不能切换到非阻塞模式,套接字通道可以切换到非阻塞模式。 

o DatagramChannel:能通过UDP读写网络中的数据的通道,直接通过DatagramChannel.open()获取一个DatagramChannel实例。  

o SocketChannel:能通过TCP读写网络中的数据的通道,直接通过SocketChannel.open()获取一个SocketChannel实例。  

o ServerSocketChannel:可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel,直接通过ServerSocketChannel.open()获取一个ServerSocketChannel实例。  

  

缓冲区-
本质上是一块可以存储数据的内存,被封装成了buffer对象!

  

   缓冲区类型:

o  ByteBuffer  

o  MappedByteBuffer  

o  CharBuffer  

o  DoubleBuffer  

o  FloatBuffer  

o  IntBuffer  

o  LongBuffer  

o  ShortBuffer  

   缓冲区常用方法:

o  allocate() -
分配一块缓冲区,可以指定缓冲区大小  

o  put() -  向缓冲区写数据

o  get() -
向缓冲区读数据

o  array() -
返回缓冲区内容的字节数组  

o  filp() -
翻转缓冲区,读写模式进行翻转 

o  clear() -
从写模式切换到读模式,不会清空数据,如果想循环将缓冲区中的数据提取出来进行输出,不进行clear()和filp()的话,将会出现死循环,如果是在一个循环内filp()放置在clear()的后面,也会出现死循环,使用时根据实际情况进行设置。 

o  compact() -
从读数据切换到写模式,数据不会被清空,会将所有未读的数据copy到缓冲区头部,后续写数据不会覆盖,而是在这些数据之后写数据

o  mark() -
对position做出标记,配合reset使用

Ÿo  reset() -
将position置为标记值    

   缓冲区的一些属性:

o  capacity -
缓冲区大小,无论是读模式还是写模式,为激活时设定大小,此属性值不会变;

          o  position -
写数据时,position表示当前写的位置,每写一个数据,会向下移动一个数据单元,初始为0;

   最大为capacity - 1;切换到读模式时,position会被置为0,表示当前读的位置从哪开始
         o limit -
写模式下,limit
相当于capacity
表示最多可以向缓冲区写多少数据,切换到读模式时,
   limit
等于原先的position,表示最多可以从缓冲区读多少数据。
下面是一个File的读写示例:case.txt中内容为this is test

public static void readFileByNIO(){
File f = new File("C:\\Users\\zht\\Desktop\\case.txt");
try {
FileInputStream in = new FileInputStream(f);
//从文件字节流中获取一个文件通道
FileChannel channel = in.getChannel();
//构建一个缓冲区,并指定容量为100个字节
ByteBuffer buffer = ByteBuffer.allocate(100);
System.out.println("写入缓冲区前限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
int bytes = -1;
//从通道中写入缓冲区,为写模式,buffer的position值变为从通道中读取的字节数
while((bytes = channel.read(buffer)) != -1){
System.out.println("字节数:"+bytes);
System.out.println("写入缓冲区后限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
//将字节转换编码进行输出
System.out.println("读取文件内容:"+new String(buffer.array(),0,bytes,"utf-8"));
}
in.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}输出结果:
写入缓冲区前限制数:100  容量是:100 位置为:0

字节数:15

写入缓冲区后限制数:100 容量是:100 位置为:15

读取文件内容:this is test

从以上这些结果中,看似正常,实际在应用中,我们的文件中的内容不可能只有100个字节以内,所以一旦超出100个字节,上面示例将会出现死循环,下面就针对这个问题进行修改:

public static void readFileByNIO(){
File f = new File("C:\\Users\\zht\\Desktop\\case.txt");
try {
FileInputStream in = new FileInputStream(f);
//从文件字节流中获取一个文件通道
FileChannel channel = in.getChannel();
//构建一个缓冲区,并指定容量为100个字节
ByteBuffer buffer = ByteBuffer.allocate(10);
StringBuilder builder = new StringBuilder();
System.out.println("写入缓冲区前限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
int bytes = -1;
//从通道中写入缓冲区,为写模式,buffer的position值变为从通道中读取的字节数
while((bytes = channel.read(buffer)) != -1){
System.out.println("字节数:"+bytes);
System.out.println("写入缓冲区后限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
//使用clear获或者是flip
buffer.clear();//这里是将缓冲区从写模式转换为读模式,因为下面要将数据从缓冲区中将数据拿出来打印
// buffer.flip();//这里是对缓冲区进行翻转,如果这里两个方法一起用也将导致死循环
System.out.println("翻转缓冲区后限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
//将字节转换编码进行输出
builder.append(new String(buffer.array(),0,bytes,"utf-8"));
}
System.out.println("读取文件内容:"+builder.toString());
in.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}输出结果:
写入缓冲区前限制数:10  容量是:10 位置为:0

字节数:10

写入缓冲区后限制数:10 容量是:10 位置为:10

翻转缓冲区后限制数:10 容量是:10 位置为:0

字节数:5

写入缓冲区后限制数:10 容量是:10 位置为:5

翻转缓冲区后限制数:10 容量是:10 位置为:0

读取文件内容:this is test

以上修改后的代码,我将缓冲区容量改小了,文件中内容没变,从结果可以看出从通道写入缓冲区两次,写入缓冲区后pos位置是不一样的,是根据字节数的变化而变化,filp后或者是clear后,pos位置是都回到了初始化的位置。你可以认为是在filp或者clear后,你不指定起止位置的情况下,从缓冲区提取数据都是从0位置开始取到limit位置,包含空闲的缓冲区空间,而我这里是因为设置了起止值new
String(buffer.array(),0,bytes,"utf-8")。下面我们再来验证一下clear或者filp后,原缓冲区的数据是否还存在?

public static void readFileByNIO(){
File f = new File("C:\\Users\\zht\\Desktop\\case.txt");
try {
FileInputStream in = new FileInputStream(f);
//从文件字节流中获取一个文件通道
FileChannel channel = in.getChannel();
//构建一个缓冲区,并指定容量为100个字节
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println("写入缓冲区前限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
int bytes = -1;
//从通道中写入缓冲区,为写模式,buffer的position值变为从通道中读取的字节数
while((bytes = channel.read(buffer)) != -1){
System.out.println("字节数:"+bytes);
System.out.println("写入缓冲区后限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
//使用clear获或者是flip
buffer.clear();//这里是将缓冲区从写模式转换为读模式,因为下面要将数据从缓冲区中将数据拿出来打印
// buffer.flip();//这里是对缓冲区进行翻转
System.out.println("翻转缓冲区后限制数:" + buffer.limit() + " 容量是:" + buffer.capacity()
+ " 位置为:" + buffer.position());
//将字节转换编码进行输出
System.out.println("读取文件内容:"+new String(buffer.array(),"utf-8"));
}
in.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
输出结果:
写入缓冲区前限制数:10  容量是:10 位置为:0

字节数:10

写入缓冲区后限制数:10 容量是:10 位置为:10

翻转缓冲区后限制数:10 容量是:10 位置为:0

读取文件内容:this is

字节数:5

写入缓冲区后限制数:10 容量是:10 位置为:5

翻转缓冲区后限制数:10 容量是:10 位置为:0

读取文件内容: testis is  //这里很明显的内容不对

通过以上结果可以看出,当你在缓冲区提取数据时,如果没有指定提取的起止位置,那么后面提取到的内容并不是我们想要的。也就是说我们在调用clear方法或者flip方法后,再次从通道中去写入数据到缓冲区,缓冲区中原有的数据是不会进行清除的,会直接根据本次所需空间进行覆盖,上面输出结果第二次读取5个字节写入到缓冲区,因为缓冲区最大容量10个字节.所以就会覆盖前5个字节,还保留着第一次缓冲区中的后5个字节内容,这样结合起来的数据就完全变了样。
一个从A文件到B文件的NIO读写例子:case.txt中内容不变

public static void writFileByNIO(){
File f = new File("C:\\Users\\zht\\Desktop\\case.txt");
File f2= new File("C:\\Users\\zht\\Desktop\\case2.txt");
try {
FileInputStream in = new FileInputStream(f);
//从文件字节流中获取一个文件通道
FileChannel channel = in.getChannel();
//构建一个缓冲区,并指定容量为100个字节
ByteBuffer buffer = ByteBuffer.allocate(10);

FileOutputStream out = new FileOutputStream(f2);
FileChannel channel2 =out.getChannel();
System.out.println("缓冲区输入前限制是:" + buffer.limit() + "容量是:" + buffer.capacity()
+ "位置是:" + buffer.position());
int bytes = -1;
while((bytes = channel.read(buffer)) != -1){
System.out.println("缓冲区输入后限制是:" + buffer.limit() + "容量是:" + buffer.capacity()
+ "位置是:" + buffer.position());
ByteBuffer buffer2 = Charset.forName("utf-8").encode(new String(buffer.array(),0,bytes,"utf-8"));
while(buffer2.hasRemaining()){
channel2.write(buffer2);
System.out.println("缓冲区输出后限制是:" + buffer.limit() + "容量是:" + buffer.capacity()
+ "位置是:" + buffer.position());
}
buffer.flip();
System.out.println("缓冲区翻转后限制是:" + buffer.limit() + "容量是:" + buffer.capacity()
+ "位置是:" + buffer.position());
}
out.close();
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

选择器:相当于一个观察者,用来监听通道感兴趣的事件,一个选择器可以绑定多个通道。

   通道向选择器注册时,需要指定感兴趣的事件,选择器支持以下事件:

o  SelectionKey.OP_CONNECT

o  SelectionKey.OP_ACCEPT

o  SelectionKey.OP_READ

o  SelectionKey.OP_WRITE  

   如果你对不止一种事件感兴趣,那么可以用“位或”操作符将常量连接起来,如下:

     int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

   通道向选择器注册时,会返回一个 SelectionKey对象,具有如下属性

o  interest集合

o  ready集合  

o  Channel  

o  Selector

o  附加的对象(可选)  

  用“位与”操作interest 集合和给定的SelectionKey常量,可以确定某个确定的事件是否在interest 集合中。

   int interestSet = selectionKey.interestOps();

   boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
   boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;

   boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;

   boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

  ready 集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,你会首先访问这个ready set。Selection将在下一小节进行解释。

    可以这样访问ready集合:

  int readySet = selectionKey.readyOps();

   可以使用以下四个方法获取已就绪事件,返回值为boolean:

  selectionKey.isAcceptable();

  selectionKey.isConnectable();

  selectionKey.isReadable();

  selectionKey.isWritable();

   可以将一个对象或者更多信息附着到SelectionKey上,即记录在附加对象上,方法如下:

  selectionKey.attach(theObject);

  Object attachedObj
= selectionKey.attachment();

   可以通过选择器的select方法获取是否有就绪的通道,返回值表示上次执行select之后,就绪通道的个数。

 int select();

 int select(long timeout);

 int selectNow();

   

   可以通过selectedKeySet获取已就绪的通道。返回值是SelectionKey 的集合,处理完相应的通道之后,需要removed
因为Selector不会自己removed

 

  select阻塞后,可以用wakeup唤醒;执行wakeup时,如果没有阻塞的select那么执行完wakeup后下一个执行select就会立即返回。
  调用close() 方法关闭selector
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐