深入NIO

NIO与IO的区别
IONIO
面向流面向缓冲区
阻塞IO非阻塞IO
选择器(Selectors)
通道与缓冲区

Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到IO设备(例如:文件,套接字)的连接。若需要使用NIO,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区,然后操作缓冲区,对数据进行处理。简而言之,Channel负责传输,Buffer负责存储

缓冲区

Buffer就像一个数组,可以保存多个相同类型的数据,根据数据类中不同(boolean除外),有以下Buffer常用子类

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
缓冲区基本属性
	//标记是一个索引,通过Buffer中的mark()方法指定Buffer中特定的position,之后调用reset()方法可以恢复到这个position
    private int mark = -1;
    //下一个要读取或写入的数据的索引,不能大于其限制
    private int position = 0;
    //代表第一个不应该读取或写入的数据的索引,即位于limit后的数据不可读写,limit不能大于容量
    private int limit;
    //容量-表示Buffer最大数据容量,创建后无法更改
    private int capacity;

标记,位置,限制,容量大小关系:0<=mark<=position<=limit<=capacity

public static void main(String[] args) {
		
		String bytes = "abcdef";
		ByteBuffer buf =ByteBuffer.allocate(1024);
		buf.put(bytes.getBytes());
		System.out.println("put:"+buf.position());
		System.out.println("put:"+buf.limit());
		System.out.println("put:"+buf.capacity());
		System.out.println("=========================");
		//切换成读数据模式
		buf.flip();
		
		byte[] dst = new byte[buf.limit()];
		buf.get(dst,0,2);
		System.out.println(new String(dst,0,2));
		System.out.println("get1:"+buf.position());
		//读数据模式下,limit即为限制只能读到曾经写到的position
		System.out.println("get1:"+buf.limit());
		System.out.println("=========================");
		buf.mark();
		System.out.println("mark:"+buf.mark());
		buf.get(dst,2,2);
		System.out.println("get2:"+buf.position());
		System.out.println("get2:"+buf.limit());						
	}

put:6
put:1024
put:1024
=========================
ab
get1:2
get1:6
=========================
mark:java.nio.HeapByteBuffer[pos=2 lim=6 cap=1024]
get2:4
get2:6

在这里插入图片描述

常用方法
//清空缓冲区并返回对缓冲区的引用
Buffer clear()
//将缓冲区的界限limit设置为当前位置position,并将当前位置充值为 0
Buffer flip()
//对缓冲区设置标记
Buffer mark() 
//将位置 position 转到以前设置的 mark 所在的位置
Buffer reset()
//将位置设为为 0, 取消设置的 mark
Buffer rewind() 
数据操作
//获取 Buffer 中的数据
//读取单个字节
get()
//批量读取多个字节到 dst 中
get(byte[] dst)
//读取指定索引位置的字节(不会移动 position)
get(int index)

//放入数据到 Buffer 中
//将给定单个字节写入缓冲区的当前位置
put(byte b)
//将 src 中的字节写入缓冲区的当前位置
put(byte[] src)
//将指定字节写入缓冲区的索引位置(不会移动 position)
put(int index, byte b)
直接与非直接缓冲区

字节缓冲区要么是直接的,要么是非直接的。

非直接缓冲区的运行原理

应用程序即JVM和内核之间数据的交互需要通过copy的方式,经过中间缓存区进行传输,大大降低传输效率
在这里插入图片描述

直接缓冲区的运行原理

直接缓冲区采用在内核和应用程序间开辟一块缓冲区,直接进行数据交互,大大提高传输效率。
直接字节缓冲区可以通过调用此类的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区

如果为直接字节缓冲区,则 Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说,在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)

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

直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回MappedByteBuffer 。Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常

字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定
直接缓存区只有ByteBuffer可以使用
在这里插入图片描述


通道

通道用于源节点和目标节点之间的连接,Channel本身不存储数据,因此需要配合缓冲区进行传输。
通常来说NIO中的所有IO都是从Channel(通道)开始的

  • 从通道进行数据读取:创建一个缓冲区:然后请求通道读取数据
  • 从通道进行数据写入:创建一个缓冲区,填充数据,并要求通道写入数据

在这里插入图片描述

Channel和流的区别:
  1. 通道既可以读也可以写,而流一般是单向的,只能读或者写(输入流和输出流)
  2. 通道可以异步读写
  3. 通道总是基于缓冲区Buffer来读写

在这里插入图片描述
通道有点类似DMA即直接内存读取,通道可以直接使数据到达内存绕过CPU的调度,节省CPU开销

Channel主要实现类
  1. FileChannel:用于读取、写入、映射和操作文件的通道
  2. DatagramChannel:通过UDP读写网络中的数据通道
  3. SocketChannel:通过TCP读写网络中的数据
  4. ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来
    的连接都会创建一个SocketChannel
获取通道
  • 获取通道的一种方式是对支持通道的对象调用getChannel() 方法,支持通道的类如下:
    • FileInputStream
    • FileOutputStream
    • RandomAccessFile
    • DatagramSocket
    • Socket
    • ServerSocket
  • 获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道。

第一种实现

//基于getChannel()方法
public static void main(String[] args) {
		try {
			//支持通道的对象
			FileInputStream fis = new FileInputStream("D:\\test\\1.jpg");
			FileOutputStream fos = new FileOutputStream("D:\\test\\2.jpg");
			
			//获取通道
			FileChannel inChannel = fis.getChannel();
			FileChannel outChannel = fos.getChannel();
			
			//分配指定大小的缓冲区
			ByteBuffer buf = ByteBuffer.allocate(2048);
			
			//将通道中数据存入缓冲区
			while(inChannel.read(buf)!= -1) {
				buf.flip();
				//将缓冲区中数据写入通道中
				outChannel.write(buf);
				buf.clear();
			}
			outChannel.close();
			inChannel.close();
			fos.close();
			fis.close();
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

第二种实现

//使用内存映射手段调用非直接缓冲区
public static void main(String[] args) {
		
		try {
			//调用FileChannel静态方法open()获取通道
			FileChannel inChannel = FileChannel.open(Paths.get("D:\\test\\1.jpg"),StandardOpenOption.READ);
			//输出通道设置可读,可写,若该路径下无文件则创建,有则抛出异常的参数
			FileChannel outChannel = FileChannel.open(Paths.get("D:\\test\\3.jpg"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW);
			
			//内存映射
			//MapperByteBuffer可以理解为在磁盘上的ByteBuffer映射到内存中的MappedByteBuffer
			MappedByteBuffer inMapperBuf = inChannel.map(MapMode.READ_ONLY,0,inChannel.size());
			MappedByteBuffer outMapperBuf = outChannel.map(MapMode.READ_WRITE,0,inChannel.size());
			
			//直接对缓冲区进行数据的读写操作
			//这里直接对内存映射MappedByteBuffer操作,不需要对Channel操作
			byte[] dst = new byte[inMapperBuf.limit()];
			inMapperBuf.get(dst);
			outMapperBuf.put(dst);
			
			inChannel.close();
			outChannel.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}

第三种实现-最简单的方式

public static void main(String[] args) {	
		try {
			FileChannel inChannel = FileChannel.open(Paths.get("D:\\test\\1.jpg"),StandardOpenOption.READ);
			FileChannel outChannel = FileChannel.open(Paths.get("D:\\test\\4.jpg"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE_NEW);
			
			//直接调用该方法即可
			inChannel.transferTo(0, inChannel.size(),outChannel);
			inChannel.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

分散(Scatter)和聚集(Gather)
分散读取是指从Channel中读取的数据"分散"到多个Buffer

在这里插入图片描述
注意:按照缓冲区的顺序,从Channel中读取的数据依次从Buffer填满

聚集写入是指将多个Buffer中的数据"聚集"到Channel

在这里插入图片描述
按照缓冲区的顺序,写入position和limit之间的数据到Channel

public static void main(String[] args) {	
		try {
		FileInputStream fis = new FileInputStream("D:\\test\\1.jpg");
		FileOutputStream fos = new FileOutputStream("D:\\test\\5.jpg");
		
		//获取通道
		FileChannel inChannel = fis.getChannel();
		FileChannel outChannel = fos.getChannel();
		
		//分配指定大小的缓冲区
		ByteBuffer buf1 = ByteBuffer.allocate(1);
		ByteBuffer buf2 = ByteBuffer.allocate(1024);
		
		//通过缓冲区数组的方式实现聚合和分散
		ByteBuffer[] bufs = {buf1,buf2};
		
		//将通道中数据存入缓冲区
		while(inChannel.read(bufs)!= -1) {
			for(ByteBuffer buf : bufs) {
				buf.flip();
			}
			//将缓冲区中数据写入通道中
			outChannel.write(bufs);
			for(ByteBuffer buf : bufs) {
				buf.clear();
			}
		}
		outChannel.close();
		inChannel.close();
		fos.close();
		fis.close();
		} catch (FileNotFoundException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
		} catch (IOException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
		}
	}

非阻塞

传统的IO流都是非阻塞式的,也就是说,当一个线程调用read()或write()方法使,该线程会被阻塞,知道有一些数据被读取或写入。NIO是非阻塞模式的,当线程从某通道进行读写数据时,若没有数据可用时,该线程可用执行其他任务,线程将非阻塞IO的空闲时间用于在其他通道上执行IO操作,单独的线程可以管理多个输入和输出通道。NIO可以让服务器端使用一个或有限几个线程同时处理服务器端的所有客户端

选择器(Selector)

选择器(Selector)是SelectorChannel对象的多路复用器,Selector可以同时监控多个SelectorChannel的IO状况,也就是说利用Selector可供一个单独的线程管理多个Channel,Selector是非阻塞IO的核心
在这里插入图片描述

选择器的使用

创建选择器

//创建选择器
Selector selector = Selector.open();

向选择器注册通道

//创建Socket套接字
Socket socket = new Socket(Inet4Address.getLocalHost(),12345);

//获取SocketChannel
SocketChannel channel = socket.getChannel();

//创建选择器
Selector selector = Selector.open();

//将SocketChannel切换到非阻塞模式
channel.configureBlocking(false);

//向选择器注册Channel
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
选择器的应用

当调用register()方法将通道注册到选择器上时,选择器对通道的监听事件,需要通过第二个参数ops指定:
可监听事件类型:

  1. 读:SelectionKey.OP_READ
  2. 写:SelectionKey.OP_WRITE
  3. 连接:SelectionKey.OP_WRITE
  4. 接收:SelectionKey.OP_CONNECT
    若注册时不止监听一个事件,则可以使用"位或"操作符连接
SelectionKey

SelectionKey:表示SelectableChannel和Selector之间的注册关系。每次向选择器注册通道就会选择一个事件。

//获取感兴趣事件集合
int interestOps() 

//获取通道已经准备就绪的操作的集合
int readyOps()

//获取注册通道
SelectableChannel channel()

//返回选择器
Selector selector()

//检测 Channal 中读事件是否就绪
boolean isReadable()

//检测 Channal 中写事件是否就绪
boolean isWritable() 

//检测 Channel 中连接是否就绪
boolean isConnectable() 

//检测 Channel 中接收是否就绪
boolean isAcceptable()
Selector常用方法
//所有的 SelectionKey 集合。代表注册在该Selector上的Channel
Set<SelectionKey> keys()

//被选择的 SelectionKey 集合。返回此Selector的已选择键集
selectedKeys()

//监控所有注册的Channel,当它们中间有需要处理的 IO 操作时,该方法返回,并将对应得的 SelectionKey 加入被选择的SelectionKey 集合中,该方法返回这些 Channel 的数量
int select()

//可以设置超时时长的 select() 操作
int select(long timeout)

//执行一个立即返回的 select() 操作,该方法不会阻塞线程
int selectNow()

//关闭该选择器
void close()
Demo
客户端
public void client() throws IOException{
		//1. 获取通道
		SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
		
		//2. 切换非阻塞模式
		sChannel.configureBlocking(false);
		
		//3. 分配指定大小的缓冲区
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		//4. 发送数据给服务端
		Scanner scan = new Scanner(System.in);
		
		while(scan.hasNext()){
			String str = scan.next();
			buf.put((new Date().toString() + "\n" + str).getBytes());
			//缓冲区切换到读数据模式
			buf.flip();
			sChannel.write(buf);
			buf.clear();
		}
		
		//5. 关闭通道
		sChannel.close();
	}
服务器端
public void server() throws IOException{
		//1. 获取通道
		ServerSocketChannel ssChannel = ServerSocketChannel.open();
		
		//2. 切换非阻塞模式
		ssChannel.configureBlocking(false);
		
		//3. 绑定连接
		ssChannel.bind(new InetSocketAddress(9898));
		
		//4. 获取选择器
		Selector selector = Selector.open();
		
		//5. 将通道注册到选择器上, 并且指定“监听接收事件”
		ssChannel.register(selector, SelectionKey.OP_ACCEPT);
		
		//6. 轮询式的获取选择器上已经“准备就绪”的事件
		while(selector.select() > 0){
			
			//7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
			Iterator<SelectionKey> it = selector.selectedKeys().iterator();
			
			while(it.hasNext()){
				//8. 获取准备“就绪”的是事件
				SelectionKey sk = it.next();
				
				//9. 判断具体是什么事件准备就绪
				if(sk.isAcceptable()){
					//10. 若“接收就绪”,获取客户端连接
					SocketChannel sChannel = ssChannel.accept();
					
					//11. 切换非阻塞模式
					sChannel.configureBlocking(false);
					
					//12. 将该通道注册到选择器上
					sChannel.register(selector, SelectionKey.OP_READ);
				}else if(sk.isReadable()){
					//13. 获取当前选择器上“读就绪”状态的通道
					SocketChannel sChannel = (SocketChannel) sk.channel();
					
					//14. 读取数据
					ByteBuffer buf = ByteBuffer.allocate(1024);
					
					int len = 0;
					while((len = sChannel.read(buf)) > 0 ){
						buf.flip();
						System.out.println(new String(buf.array(), 0, len));
						buf.clear();
					}
				}
				
				//15. 取消选择键 SelectionKey
				it.remove();
			}
		}
	}

管道

管道是2个线程之间的单向数据连接,Pipe有一个source通道和sink通道,数据会被写入sink通道,从source通道读取。

向管道写数据
//1. 获取管道
Pipe pipe = Pipe.open();

//2. 将缓冲区中的数据写入管道
ByteBuffer buf = ByteBuffer.allocate(1024);

Pipe.SinkChannel sinkChannel = pipe.sink();
buf.put("通过单向管道发送数据".getBytes());
buf.flip();
sinkChannel.write(buf);
从管道读取数据
//3. 读取缓冲区中的数据
Pipe.SourceChannel sourceChannel = pipe.source();
buf.flip();
int len = sourceChannel.read(buf);
System.out.println(new String(buf.array(), 0, len));
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值