初步理解NIO

简述

本篇文章是观看尚硅谷NIO视频,整理而出的笔记内容。

Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。

NIO与IO的区别

NIOIO
面向缓冲区(Buffer Oriented)面向流(Stream Oriented)
非阻塞IO(Non Blocking IO)阻塞IO(Blocking IO)
选择器(Selectors)

缓冲区

  • 缓冲区(Buffer):一个用于特定基本数据类型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类。
  • Java NIO 中的 Buffer 主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
  • Buffer 就像一个数组,可以保存多个相同类型的数据。根据数据类型不同(boolean 除外) ,有以下 Buffer 常用子类:ByteBuffer、 CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。他们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个 Buffer 对象:
    static XxxBuffer allocate(int capacity) : 创建一个容量为 capacity 的 XxxBuffer 对象

基本属性

  • 容量 (capacity) :表示 Buffer 最大数据容量,缓冲区容量不能为负,并且创建后不能更改。
  • 限制 (limit):缓冲区可操作数据的范围或大小,即位于 limit 后的数据不可读写。缓冲区的限制不能为负,并且不能大于其容量。
  • 位置 (position):即将操作数据的位置索引。缓冲区的位置不能为负,并且不能大于其限制
  • 标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这个 position.
  • 标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity
    @Test
	public void testBuffer() {
		// 分配指定大小的缓冲区
		ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
		System.out.println("------allocate()-------");
		//System.out.println(byteBuffer.mark());
		System.out.println(byteBuffer.position()); // 0
		System.out.println(byteBuffer.limit());    // 1024
		System.out.println(byteBuffer.capacity()); // 1024
		
		// put() 存入数据
		String content = "abcde";
		byteBuffer.put(content.getBytes());
		System.out.println("------put()--------");
		System.out.println(byteBuffer.position()); // 5
		System.out.println(byteBuffer.limit());    // 1024
		System.out.println(byteBuffer.capacity()); // 1024
		
		// 切换到读模式  flip 将 limit = position,同时 position = 0
		byteBuffer.flip();
		System.out.println("------flip()--------");
		System.out.println(byteBuffer.position()); // 0
		System.out.println(byteBuffer.limit());    // 5
		System.out.println(byteBuffer.capacity()); // 1024
		
		// get() 读取数据
		byte[] dest = new byte[byteBuffer.limit()];
		byteBuffer.get(dest);
		System.out.println("------get()--------");
		System.out.println("读取到数据:" +new String(dest));
		System.out.println(byteBuffer.position()); // 5
		System.out.println(byteBuffer.limit());    // 5
		System.out.println(byteBuffer.capacity()); // 1024
		
		// rewind position = 0 ,可重复读
		System.out.println("------rewind()--------");
		byteBuffer.rewind();
		System.out.println(byteBuffer.position()); // 0
		System.out.println(byteBuffer.limit());    // 5
		System.out.println(byteBuffer.capacity()); // 1024
		
		// clear 清空缓冲区(缓冲区的数据还在,只是处于一种"被遗忘的状态",不知道缓冲区寸了多少数据) 回复初始状态
		System.out.println("------clear()--------");
		byteBuffer.clear();
		System.out.println(byteBuffer.position()); // 0
		System.out.println(byteBuffer.limit());    // 1024
		System.out.println(byteBuffer.capacity()); // 1024
		
		System.out.println((char)byteBuffer.get());// a
	}

常用方法总结

方法描述
Buffer clear()清空缓冲区并返回对缓冲区的引用(put的数据并不会被清除)
Buffer flip()将缓冲区的界限设置为当前位置,并将当前位置充值为 0
int capacity()返回 Buffer 的 capacity 大小
boolean hasRemaining()判断缓冲区中是否还有元素
int limit()返回 Buffer 的界限(limit) 的位置
Buffer limit(int n)将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象
Buffer mark()对缓冲区设置标记
int position()返回缓冲区的当前位置 position
Buffer position(int n)将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象
int remaining()返回 position 和 limit 之间的元素个数
Buffer reset()将位置 position 转到以前设置的 mark 所在的位置
Buffer rewind()将位置设为为 0, 取消设置的 mark

直接缓冲区与非直接缓冲区

非直接缓冲区: 通过 allocate() 方法分配缓冲区,将缓冲区建立在JVM的内存中。
直接缓冲区: 通过allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。

非直接缓冲区
在这里插入图片描述
直接缓冲区
在这里插入图片描述

ByteBuffer direct = ByteBuffer.allocateDirect(1024);
// isDirect() 可判断 缓冲区是否是直接缓冲区
System.out.println(direct.isDirect());// true

通道(Channel)

通道: 表示IO源目标打开的连接,类似传统IO的流,只不过,channel不能直接访问数据。通道负责传输,缓冲区负责存储数据。
java.nio.channels.Channel 有几个重要的实现类

java.nio.channels.FileChannel

java.nio.channels.SocketChannel : 适用于TCP

java.nio.channels.ServerSocketChannel: 适用于TCP

java.nio.channels.DatagramChannel: 适用于UDP传输

获取通道

JAVA对支持通道的类提供了 getChannel() 方法,用于获取通道。

本地IO :
FileInputStream
FileOutputStream
RandomAccessFile

网络IO:
Socket
ServerSocket
DatagramSocket

途径二: jdk 1.7中的NIO.2提供了静态方法 open()

途径三: jdk 1.7 NIO.2 Files工具类 newByteChannel() 方法

案例: 本地IO实现文件复制

	@Test
	public void testChannel() throws IOException{
		
		FileInputStream  fis= new FileInputStream("002.jpg");
		FileOutputStream fos = new FileOutputStream("2.jpg");
		
		// 获取通道
		FileChannel inChannel = fis.getChannel();
		FileChannel outChannel = fos.getChannel();
		
		// 指定缓冲区
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		
		// 将通道中的数据 写入 缓冲区
		while (inChannel.read(buffer) != -1) {
			buffer.flip();//切换到 读模式
			//将缓冲区的数据写入通道
			outChannel.write(buffer);
			buffer.clear();			
		}
		
		// 关闭通道
		outChannel.close();
		inChannel.close();
		fos.close();
		fis.close();	
	}

采用直接缓冲区,内存映射的方式实现复制

    @Test
	public void test2() throws IOException{
		//采用直接缓冲区 
		//创建通道
		FileChannel inChannel = FileChannel.open(Paths.get("002.jpg"), StandardOpenOption.READ);
		// StandardOpenOption.CREATE_NEW 目标文件不存在就创建,存在会报错 
		// StandardOpenOption.CREATE 文件覆盖
		FileChannel outChannel = FileChannel.open(Paths.get("3.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW);
	
		// 内存映射文件
	    MappedByteBuffer intMappedBuffer = inChannel.map(MapMode.READ_ONLY, 0, inChannel.size());
	    MappedByteBuffer outMappedBuffer = outChannel.map(MapMode.READ_WRITE, 0, inChannel.size());
	
	    // 直接对缓冲区 进行读写操作
	    byte[] dest = new byte[intMappedBuffer.limit()];
	    intMappedBuffer.get(dest);
	    outMappedBuffer.put(dest);
	    
	    outChannel.close();
	    inChannel.close();
	}

通道间的数据传输(直接缓冲区)

transferTo
transferFrom

    //通道间的数据传输(直接缓冲区)
	@Test
	public void test3() throws IOException{

		FileChannel inChannel = FileChannel.open(Paths.get("002.jpg"), StandardOpenOption.READ);
		FileChannel outChannel = FileChannel.open(Paths.get("3.jpg"), StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
	
        inChannel.transferTo(0, inChannel.size(), outChannel);
	    
	    outChannel.close();
	    inChannel.close();
	}

分散(Scatter) 与 聚集(Gather)

分散读取: 将通道中的数据分散到多个缓冲区中(按照缓冲区的顺序,一次将缓冲区填满)。
聚集写入: 将多个缓冲区中的数据聚集到通道中(按照缓冲区的顺序,将position到limit之间的数据写入到通道)。

案例:

    @Test
	public void test4() throws IOException{
		RandomAccessFile randomAccessFile = new RandomAccessFile("快捷指令.txt","rw");
		// 获取通道
		FileChannel channel = randomAccessFile.getChannel();
		ByteBuffer bf1 = ByteBuffer.allocate(100);
		ByteBuffer bf2 = ByteBuffer.allocate(1024);
		
		// 分散读取
		ByteBuffer[] bfs = new ByteBuffer[]{bf1,bf2};
		channel.read(bfs);
		for (ByteBuffer byteBuffer : bfs) {
			byteBuffer.flip();
		}
		
		System.out.println(new String(bfs[0].array(),0,bfs[0].limit()));
		System.out.println("=======");
		System.out.println(new String(bfs[1].array(),0,bfs[1].limit()));
		channel.close();
		
		// 聚集写入
		RandomAccessFile randomAccessFile2 = new RandomAccessFile("聚集写入.txt","rw");
		FileChannel channel2 = randomAccessFile2.getChannel();
		channel2.write(bfs);
		channel2.close();
	}

非阻塞网络通信

阻塞与非阻塞

传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。

Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。

选择器(Selector)

选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector 可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心。
在这里插入图片描述
创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。

// 获取选择器
Selector selector = Selector.open();

向选择器注册通道:SelectableChannel.register(Selector sel, int ops)

当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。

可以监听的事件类型(可使用 SelectionKey 的四个常量表示):

  • 读 : SelectionKey.OP_READ (1)
  • 写 : SelectionKey.OP_WRITE (4)
  • 连接 : SelectionKey.OP_CONNECT (8)
  • 接收 : SelectionKey.OP_ACCEPT (16)

若注册时不止监听一个事件,则可以使用“位或”操作符连接。

int selectionKeys = SelectionKey.OP_READ|SelectionKey.OP_ACCEPT;

SelectionKey:表示 SelectableChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。选择键包含两个表示为整数值的操作集。操作集的每一位都表示该键的通道所支持的一类可选择操作。

方法描述
int interestOps()获取感兴趣事件集合
int readyOps()获取通道已经准备就绪的操作的集合
SelectableChannel channel()获取注册通道
Selector selector()返回选择器
boolean isReadable()检测 Channal 中读事件是否就绪
boolean isWritable()检测 Channal 中写事件是否就绪
boolean isConnectable()检测 Channel 中连接是否就绪
boolean isAcceptable()检测 Channel 中接收是否就绪

Selector 的常用方法

方法描述
Set keys()所有的 SelectionKey 集合。代表注册在该Selector上的Channel
selectedKeys()被选择的 SelectionKey 集合。返回此Selector的已选择键集
int select()监控所有注册的Channel,当它们中间有需要处理的 IO 操作时,该方法返回,并将对应得的 SelectionKey 加入被选择的SelectionKey 集合中,该方法返回这些 Channel 的数量
int select(long timeout)可以设置超时时长的 select() 操作
int selectNow()执行一个立即返回的 select() 操作,该方法不会阻塞线程
Selector wakeup()使一个还未返回的 select() 方法立即返回
void close()关闭该选择器

案例演示,网络通信

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.time.LocalDateTime;
import java.util.Iterator;
import org.junit.Test;

public class NonBlockNIO {

	@Test
	public void client() throws IOException {
		// 获取通道
		SocketChannel scockChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
	    
		//切换成非阻塞模式
		scockChannel.configureBlocking(false);
		
		// 分配缓冲区
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		
		// 发送数据给服务端
		buffer.put(LocalDateTime.now().toString().getBytes());
		buffer.flip();		
		scockChannel.write(buffer);
		buffer.clear();
		
		// 关闭通道
		scockChannel.close();
	}
	
	@Test
	public void server() throws IOException {
		// 获取通道
		ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
		// 切换到非阻塞模式
		serverSocketChannel.configureBlocking(false);
		// 绑定连接
		serverSocketChannel.bind(new InetSocketAddress("127.0.0.1",9898));
		// 获取选择器
		Selector selector = Selector.open();
		// 将通道注册到选择器上,指定监听事件
		serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
		
		//一个选择器上可以注册多个通道
		// 所以 轮询获取选择器上已经"准备就绪"的事件
		while(selector.select() > 0) {
			// 获取选择器 已就绪的监听事件
			Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
			while(iterator.hasNext()) {
				SelectionKey selectionKey = iterator.next();
				if(selectionKey.isAcceptable()) {// 接收事件
					// 获取客户端连接
					SocketChannel socketChannel = serverSocketChannel.accept();
					// 切换 非阻塞
					socketChannel.configureBlocking(false);
					// 通道 注册到选择器
					socketChannel.register(selector, selectionKey.OP_READ);
				}else if(selectionKey.isReadable()) {// 读事件
					//获取 "读"就绪的通道
					SocketChannel channel = (SocketChannel) selectionKey.channel();
					ByteBuffer buffer = ByteBuffer.allocate(1024);
					int len = 0;
					while((len = channel.read(buffer)) > 0) {
						buffer.flip();
						System.out.println(new String(buffer.array(),0,len));
						buffer.clear();
					}
				}else if(selectionKey.isWritable()) {// 写事件
					
				}else if(selectionKey.isConnectable()) {// 连接事件
					
				}else if(selectionKey.isValid()) {//是否有效
					
				}
			}
			// 取消选择键
			iterator.remove();
		}
	}
}

DatagramChannel

Java NIO中的DatagramChannel是一个能收发UDP包的通道。

操作步骤:

  1. 打开 DatagramChannel
  2. 接收/发送数据
public class TestDatagramChannel {
	
	@Test
	public void client() throws IOException {
		DatagramChannel datagramChannel = DatagramChannel.open();
		datagramChannel.configureBlocking(false);
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		buffer.put("hello world".getBytes());
		buffer.flip();
		datagramChannel.send(buffer, new InetSocketAddress("127.0.0.1", 9898));
		buffer.clear();
	}
	
	@Test
	public void server() throws IOException {
		DatagramChannel datagramChannel = DatagramChannel.open();
		datagramChannel.configureBlocking(false);
		datagramChannel.bind(new InetSocketAddress(9898));
		
		Selector selector = Selector.open();
		datagramChannel.register(selector, SelectionKey.OP_READ);
		while(selector.select() > 0) {
			
			Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
			
			while(iterator.hasNext()) {
				SelectionKey selectionKey = iterator.next();
				if(selectionKey.isAcceptable()) {// 接收事件
					
				}else if(selectionKey.isReadable()) {// 读事件
					ByteBuffer buffer = ByteBuffer.allocate(1024);
					datagramChannel.receive(buffer);
					buffer.flip();
					System.out.println(new String(buffer.array(),0,buffer.limit()));
					buffer.clear();
				}else if(selectionKey.isWritable()) {// 写事件
					
				}else if(selectionKey.isConnectable()) {// 连接事件
					
				}else if(selectionKey.isValid()) {//是否有效
					
				}
			}
			// 取消选择键
			iterator.remove();
		}
	}

}

管道 (Pipe)

Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
在这里插入图片描述

     @Test
	public void testPipe() throws IOException {
		//获取管道
		Pipe pipe = Pipe.open();
		
		//分配缓冲区
		ByteBuffer buffer = ByteBuffer.allocate(1024);		
		SinkChannel sink = pipe.sink();		
		buffer.put("单向管道pipe 发送数据".getBytes());
		buffer.flip();
		
		// 写入管道
		sink.write(buffer);
		
		//读取缓冲区
		SourceChannel source = pipe.source();
		buffer.flip();
		int read = source.read(buffer);
		System.out.println(new String(buffer.array(),0,read));
		
		source.close();
		sink.close();
		
	}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值