java nio

  • allocate(int capacity) : 分配指定大小的缓冲区(非直接缓冲区)
  • allocateDirect(int capacity) : 分配指定大小的缓冲区(直接缓冲区)
  • put() :向缓冲区中存储数据
  • get(byte[] dst) :从缓冲区获取数据,这里的dst的容量必须和缓冲区的大小一致
  • get(byte[] dst,int offest,int length) :读取指定长度的内容到dst中,这里的dst容量没有要求
  • flip() : 缓冲区从写模式切换到读模式
  • clear() :清空缓冲区,数据依然存在,只是处于一个“被遗忘”状态,改变的只是 limitposition
  • array() :返回实现此缓冲区的 byte 数组
  • mark() : 标记当前位置(position)
  • reset() :恢复到mark的位置

核心属性

capacity
limit
position
position<=limit<=capacity
@Test
public void test1(){
	String str="abcd";
	ByteBuffer buffer=ByteBuffer.allocate(1024);//分配1024个字节大小的缓冲区
	buffer.put(str.getBytes());  //写入数据
	System.out.println(buffer.capacity());  //容量 1024
	System.out.println(buffer.limit()); //界限,1024
	System.out.println(buffer.position());  //正在操作数据的位置 0
	
	buffer.flip();   //切换到读模式,读取数据的时候一定要切换,否则将会没有界限
	
	System.out.println(buffer.capacity());  //容量 1024
	System.out.println(buffer.limit()); //界限,4,允许读取的位置只能到4,因为就存储了这么多的数据
	System.out.println(buffer.position());  //正在操作数据的位置 0
	System.err.println(buffer.get(4));   //超出界限了,下标记从0开始,0<=index<limit
}
/**
 * 读取缓冲区中的数据到指定的字节数组中
 * 1、字节数组的大小一定要和buffer.limit()一样大小,否则会报错
 */
@Test
public void test2(){
	String str="abcdefg";
	ByteBuffer buffer=ByteBuffer.allocate(1024);  //申请空间大小
	buffer.put(str.getBytes());  //存入数据
	buffer.flip();   //切换到读模式
	//申请一个字节数组和实际数据一样大,这里必须和缓冲区的实际数据大小一样,否则将会报错
	byte[] dst=new byte[buffer.limit()]; 
	buffer.get(dst);   //读取缓冲区的数据到dst字节数组中
	System.out.println(new String(dst));
}


/**
 * 读取一个字节
 */
@Test
public void test3(){
	String str="abcdefg";
	ByteBuffer buffer=ByteBuffer.allocate(10);  //申请空间大小
	buffer.put(str.getBytes());  //存入数据
	buffer.flip();   //切换到读模式
	System.out.println((char)buffer.get());
}
/**
	 * 测试remark和rest
	 */
	@Test
	public void test1(){
		ByteBuffer buffer=ByteBuffer.allocate(1024);
		String str="abcdcdscdscds";
		buffer.put(str.getBytes());   //向缓冲区中写入数据
		buffer.flip();  //切换到读的模式
		byte[] dst=new byte[1024];   //创建byte数组
		System.out.println("---------------------读取两个字节的数据----------------------------");
		buffer.get(dst,0,2);   //读取两个字节长度的数据到dst中,此时的position的位置位2
		System.out.println(new String(dst));
		System.out.println("----------------------标记此时的位置------------------------------");
		buffer.mark();  //标记位置,此时的position的位置位2
		System.out.println("---------------------继续读取两个字节的数据----------------------------");
		buffer.get(dst,buffer.position(),2);  //继续从当前位置读取两个字节到dst中
		System.out.println(new String(dst));
		System.out.println(buffer.position());  //此时的position的位置为4
		
		System.out.println("---------------------重置缓冲区到remark的位置----------------------------");
		buffer.reset();  //重置缓冲区到rmark的位置
		System.out.println(buffer.position());   //此时的position为2
	}

直接缓冲区

  • 直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法 来创建。此方法返回的 缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区 。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的机 本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
  • java nio

非直接缓冲区

  • JVM中内存中创建,在每次调用基础操作系统的一个本机IO之前或者之后,虚拟机都会将缓冲区的内容复制到中间缓冲区(或者从中间缓冲区复制内容),缓冲区的内容驻留在JVM内,因此销毁容易,但是占用JVM内存开销,处理过程中有复制操作。
  • java nio

    • 写入步骤如下:
      1. 创建一个临时的直接ByteBuffer对象。
      2. 将非直接缓冲区的内容复制到临时缓冲中。
      3. 使用临时缓冲区执行低层次I/O操作。
      4. 临时缓冲区对象离开作用域,并最终成为被回收的无用数据。

通道(Channel)

  • 通道是双向的,流是单向的
  • 通道相当于输出和输入流
  • 主要的实现类如下:
    FileChannel
    SocketChannel
    ServerSocketChannel
    DatagramChannel
    

获取通道

  1. 本地IO,提供了 getChannel() 方法获取通道
    FileInputStream
    FileOutputStram
    RandomAccessFile
    
  2. 在JDK1.7中的NIO,针对各个通道提供了静态方法 open()
  3. 在JDK1.7中的NIO的Files工具类的 newByteChannel()

实例

  • 利用通道实现文件的复制(非直接缓冲区)
/**
 * 使用getChannel获取通道,实现文件的复制
 * @throws IOException 
 */
@Test
public void test1()throws IOException{
	FileInputStream inputStream=new FileInputStream(new File("C:/images/lifecrystal.png"));
	FileOutputStream outputStream=new FileOutputStream(new File("C:/images/2.png"));
	
	FileChannel inchannel = inputStream.getChannel();  //获取通道,用于读取
	FileChannel outchannel=outputStream.getChannel();  //获取通道,用于写入
	
	ByteBuffer buffer=ByteBuffer.allocate(1024);  //申请缓冲区
	//将通道中的数据写入缓冲区
	while (inchannel.read(buffer)!=-1) {
		buffer.flip(); //切换到读模式
		//将缓冲区中的数据写入通道
		outchannel.write(buffer);  
		buffer.clear();  //清空缓冲区,继续读取数据
	}
	
	//关闭通道
	inchannel.close();
	outchannel.close();
	inputStream.close();
	outchannel.close();
}
  • 使用直接缓冲区完成文件的复制,使用open()的方法获取通道
/**
	 * 使用直接缓冲区完成文件的复制
	 * 使用open()的方法获取通道
	 * @throws IOException
	 */
	@Test
	public void test2()throws IOException{
		//获取一个读取数据的通道,使用的读模式
		FileChannel inchannel=FileChannel.open(Paths.get("C:/images/2.png"), StandardOpenOption.READ);
		
		/**
		 * StandardOpenOption.CREATE : 如果文件不存在,那么就创建,如果存在将会覆盖,不报错
		 * StandardOpenOption.CREATE_NEW : 如果不存在就创建,如果存在,将会报错
		 */
		FileChannel outchannel=FileChannel.open(Paths.get("C:/images/3.png"), StandardOpenOption.CREATE,StandardOpenOption.WRITE,StandardOpenOption.READ);
		
		//创建一个内存映射文件,操作直接缓冲区,和allocatDirect()一样,MapMode.READ_ONLY表示只读的模式,用于读取
		MappedByteBuffer inMappedBuff = inchannel.map(MapMode.READ_ONLY, 0, inchannel.size());
		
		//创建一个内容映射文件,MapMode.READ_WRITE表示读写模式,可以读写
		MappedByteBuffer outMappedBuffer = outchannel.map(MapMode.READ_WRITE, 0, inchannel.size());
		
		byte[] dst=new byte[inMappedBuff.limit()];
		//将数据读入到dst中
		inMappedBuff.get(dst);
		
		//将数据从dst中读取到outMappedBuffer
		outMappedBuffer.put(dst);
	}

通道之间指定进行数据传输

transferTo(long position,long count,WritableByteChannel target)
transferFrom(ReadableByteChannel from,long position,long count)
	/**
	 * 通道之间直接进行传输
	 * 	1、transferTo(long position,long count,WritableByteChannel target):将数据从通道写入可写的通道target中
	 * 	2、transferFrom(ReadableByteChannel from,long position,long count):将数据从通道from中读取到通道中
	 * @throws IOException
	 */
	@Test
	public void test3()throws IOException{
		//获取一个读取数据的通道,使用的读模式
		FileChannel inchannel = FileChannel.open(Paths.get("C:/images/2.png"),
				StandardOpenOption.READ);

		/**
		 * StandardOpenOption.CREATE : 如果文件不存在,那么就创建,如果存在将会覆盖,不报错
		 * StandardOpenOption.CREATE_NEW : 如果不存在就创建,如果存在,将会报错
		 */
		FileChannel outchannel=FileChannel.open(Paths.get("C:/images/4.png"), StandardOpenOption.CREATE,StandardOpenOption.WRITE,StandardOpenOption.READ);
		
		//将通道inchannel中的数据直接写入outchannel中
		inchannel.transferTo(0, inchannel.size(), outchannel);
		
		//和上面一样的效果
// outchannel.transferFrom(inchannel, 0, inchannel.size());
		
		inchannel.close();
		outchannel.close();
	}

分散读取

  • 将通道中的数据分散到各个缓冲区中
/**
 * 分散读取:将通道中的数据写入各个缓冲区中,是按照顺序写入的,第一个缓冲区写满才会写入第二个缓冲区
 * @throws IOException 
 */
@Test
public void test4()throws IOException{
	//创建读写模式的RandomAccessFile
	RandomAccessFile accessFile=new RandomAccessFile(new File("C:/images/2.png"), "rw");
	FileChannel inchannel=accessFile.getChannel(); //读取
	
	ByteBuffer buffer1=ByteBuffer.allocate(10); //第一个缓冲区,10个字节大小
	ByteBuffer buffer2=ByteBuffer.allocate(1024);//第二个缓冲区
	
	ByteBuffer[] dst={buffer1,buffer2};
	
	//分散读取
	inchannel.read(dst);
	
	for (ByteBuffer byteBuffer : dst) {
		byteBuffer.flip();  //切换到读的模式
	}
	
	//输出第一个缓冲区的数据
	System.out.println(new String(buffer1.array()));
	
	//输出第二个缓冲区中的数据
	System.out.println(new String(buffer2.array()));
}

聚集写入

  • 将各个缓冲区的数据读入到通道中
@Test
public void test4()throws IOException{
	//创建读写模式的RandomAccessFile
	RandomAccessFile accessFile=new RandomAccessFile(new File("C:/images/2.png"), "rw");
	FileChannel inchannel=accessFile.getChannel(); //读取
	
	ByteBuffer buffer1=ByteBuffer.allocate(10); //第一个缓冲区,10个字节大小
	ByteBuffer buffer2=ByteBuffer.allocate(1024);//第二个缓冲区
	
	ByteBuffer[] dst={buffer1,buffer2};
	
	//分散读取
	inchannel.read(dst);
	
	for (ByteBuffer byteBuffer : dst) {
		byteBuffer.flip();  //切换到读的模式
	}
	
	//输出第一个缓冲区的数据
	System.out.println(new String(buffer1.array()));
	
	//输出第二个缓冲区中的数据
	System.out.println(new String(buffer2.array()));
	
	System.out.println("---------------------聚集写入-------------------");
	
	RandomAccessFile accessFile2=new RandomAccessFile(new File("C:/images/6.png"), "rw");
	FileChannel outChannel=accessFile2.getChannel();   //写入数据的通道
	//聚集写入,将数据从各个缓冲区中写入到通道中
	outChannel.write(dst); 
	
	inchannel.close();
	outChannel.close();
	
}

NIO阻塞式

  • 阻塞或者不阻塞是针对 SocketChannelServerSocketChannel
  • NIO中的套接字可以轻松在阻塞和非阻塞之间切换,这里我们使用NIO实现阻塞式的TCP数据传输
/**
	 * 客户端使用SocketChannel
	 * 客户端使用SocketChannel中的write()方法向服务端发送数据,使用read()读取服务端返回的反馈
	 * 在数据发送完成之后如果不调用shutdownOutput告知服务端数据已传送完成,那么将会一直阻塞下去
	 * @throws Exception
	 */
	@Test
	public void testClient()throws Exception{
		//获取通道
		SocketChannel clientChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
		
		FileChannel inchannel = FileChannel.open(Paths.get("C:/images/2.png"), StandardOpenOption.READ);
		
		ByteBuffer buffer=ByteBuffer.allocate(1024);
		
		//循环读取本地图片,并且发送到服务端
		//1、先使用FileChannel将数据读取到缓冲区中
		//2、再使用SocketChannel的write方法将缓冲区的数据发送到服务端
		while(inchannel.read(buffer)!=-1){
			buffer.flip(); //切换读模式
			clientChannel.write(buffer);  //发送数据
			buffer.clear(); //清空缓冲区
		}
		
		//告诉服务端数据已经传送完成,否则将会一直阻塞
		clientChannel.shutdownOutput();  
		
		//接收服务端的反馈
		//使用read()方法接收服务端的反馈,将其读入到缓冲区中
		while(clientChannel.read(buffer)>0){
			buffer.flip();  //切换读模式
			System.out.println("服务端:"+new String(buffer.array()));
			buffer.clear();
		}
		
		//关闭通道
		inchannel.close();
		clientChannel.close();
	}
	
	/**
	 * 服务端使用ServerSocketChannel
	 * 服务端使用SocketChannel的read()方法读取客户端发送的数据,使用write()方法向客户端返回数据
	 * @throws Exception
	 */
	@Test
	public void testServer()throws Exception{
		//获取服务端的通道
		ServerSocketChannel serverChannel = ServerSocketChannel.open();
		
		//绑定连接
		serverChannel.bind(new InetSocketAddress(9898));
		
		//获取客户端的连接通道
		SocketChannel clientChannel = serverChannel.accept();
		
		//申请缓冲区
		ByteBuffer byteBuffer=ByteBuffer.allocate(1024);
		
		
		FileChannel outChannel = FileChannel.open(Paths.get("C:/images/12.png"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
		
		//循环接收客户端发送过来的数据,并且将其保存在本地
		while(clientChannel.read(byteBuffer)>0){
			byteBuffer.flip(); //切换读模式
			outChannel.write(byteBuffer);  //写入到本地
			byteBuffer.clear(); //清空缓冲区
		}
		
		//服务端发送反馈信息给客户端,使用的还是SocketChannel的write方法
		byteBuffer.put("服务端接收数据成功".getBytes());
		byteBuffer.flip(); //切换模式
		clientChannel.write(byteBuffer);
		clientChannel.shutdownOutput();  //告知客户端传输完成
		
		//关闭通道
		outChannel.close();
		clientChannel.close();
		serverChannel.close();
	}

Selector(选择器)

  • 总的来说,选择器是对通道进行监听,这样就会避免阻塞的发生,实现了多路复用

SelectionKey

  • 某个Channel成功连接到另一个服务器称为“ 连接就绪 ”。一个Server Socket Channel准备好接收新进入的连接称为“ 接收就绪 ”。一个有数据可读的通道可以说是“ 读就绪 ”。等待写数据的通道可以说是“ 写就绪 ”。

  • 选择器是用来轮询监听通道的状态,其中有四种状态如下:

    SelectionKey.OP_CONNECT
    SelectionKey.OP_ACCEPT
    SelectionKey.OP_READ
    SelectionKey.OP_WRITE
    

NIO非阻塞式

/**
 * 客户端需要使用configureBlocking(false)设置成非阻塞模式的
 * @throws Exception
 */
@Test
public void testClient()throws Exception{
	//获取通道
	SocketChannel client = SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
	//切换成非阻塞模式
	client.configureBlocking(false);
	
	ByteBuffer buf=ByteBuffer.allocate(1024);  //申请缓冲区
	
	Scanner scanner=new Scanner(System.in);
	while(scanner.hasNext()){
		String line=scanner.next();  //读取控制台输入的内容
		buf.put((new Date().toString()+"/n"+line).getBytes());  //向缓冲区写入数据
		buf.flip(); //切换到读模式
		client.write(buf);  //向服务端发送数据
		buf.clear();  //清空缓存区
	}
	
	scanner.close();
	client.close();
}


/**
 * 服务端
 * 1、将通道注册到选择器中,并且指定监听的事件
 * 2、程序每次都会轮询的从选择器中选择事件,可以选择不同状态的通道进行操作
 * @throws Exception
 */
@Test
public void testServer()throws Exception{
	//获取通道
	ServerSocketChannel server = ServerSocketChannel.open();
	
	//绑定端口
	server.bind(new InetSocketAddress(9898));
	
	//配置非阻塞
	server.configureBlocking(false);
	
	//获取选择器
	Selector selector = Selector.open();
	
	//将通道注册到选择器上,并且知道指定监听的事件为"接收就绪"
	server.register(selector, SelectionKey.OP_ACCEPT);
	
	//轮询式获取选择器上已经准备就绪的事件
	while(selector.select()>0){
		//获取当前选择器中所有的选择键(已经准备就绪的)
		Set<SelectionKey> keys = selector.selectedKeys();
		
		//获取迭代器
		Iterator<SelectionKey> iterator = keys.iterator();
		
		//迭代器遍历所有的选择键
		while(iterator.hasNext()){
			//获取当前选择键
			SelectionKey key = iterator.next();
			iterator.remove(); //删除选择键
			
			if (key.isAcceptable()) {  //如果接收就绪了
				SocketChannel client = server.accept();  //获取SocketChannel
				client.configureBlocking(false);  //设置非阻塞模式
				client.register(selector, SelectionKey.OP_READ);  //将此通道注册到选择器中,指定监听的事件是读就绪
			}else if(key.isReadable()){  //如果读就绪
				SocketChannel client = (SocketChannel) key.channel();  //读就绪了,那么可以获取通道直接读取数据
				ByteBuffer buf=ByteBuffer.allocate(1024);  //声明一个缓冲区
				//循环接收客户端的到缓冲区中
				int len=0;
				while((len=client.read(buf))>0){
					buf.flip();
					System.out.println(new String(buf.array(),0,len));
					buf.clear();
				}
			}else if (key.isWritable()) {  //如果写就绪
				
			}else if (key.isConnectable()) {  //如果连接就绪
				
			}
			
		}
	}
	server.close();
}

参考文章

  • https://www.cnblogs.com/tengpan-cn/p/5809273.html
  • 并发编程
  • http://www.cnblogs.com/snailclimb/p/9086334.html

原文 

http://chenjiabing666.github.io/2018/12/01/java-nio/

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » java nio

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址