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

本文详细介绍了Java NIO中的通道、缓冲区和选择器的概念及用法,包括FileChannel的读写操作,各种缓冲区类型及其方法,以及缓冲区状态的转换。同时,通过示例展示了如何避免缓冲区读写文件时可能出现的死循环问题,并给出了从A文件到B文件的NIO读写实例。此外,文章还讲解了选择器的注册、监听和事件判断,以及如何使用选择器来管理多个通道。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先了解一下NIO中的重要概念 通道缓冲区选择器

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

  通道类型:  

o FileChannel:从文件中读写数据的通道通过使用一个InputStream、OutputStreamRandomAccessFile来获取一个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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值