概述
Java IO模型同步阻塞IO(BIO)、同步非阻塞IO(NIO)、异步非阻塞IO(AIO/NIO2),Java中的BIO、NIO和AIO理解为是Java语言对操作系统的各种IO模型的封装
IO模型
BIO(Blocking I/O)
概述
BIO是一种同步并阻塞模式,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善,但是本质上的缺点并没有得到改进(Java IO流(一)IO基础实践代码均为基于BIO)
适用场景
适用于连接数目比较少(小于单机1000)且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解
NIO(New I/O)
概述
NIO是一种同步非阻塞模式,是从JDK1.4引入的新的IO API,可以替代标准的BIO,NIO支持面向缓冲区的、基于Channel的IO操作(Channel负责传输,Buffer负责存储),以更加高效的方式进行文件的读写操作
执行流程
# selector+selectionKey(SocketChannel+selector)可监听连接、读、写
1.服务端:启动,新建ServerSocketChannel,注册到selector,生成selectionKey(ServerSocketChannel+selector),负责监听连接事件。
2.客户端:启动,新建SocketChannel和selector,然后与服务端端口建立连接。
3.服务端:selector监听到连接,取出第1步的selectionKey,取到ServerSocketChannel,用ServerSocketChannel新建一个SocketChannel,注册到selector负责监听读操作。
4.客户端:建立连接成功后,把SocketChannel注册到客户端的selector,生成selectionKey,负责监听连接事件。
5.客户端:监听到第4步连接成功。取出第4步新建的selectionKey,取出SocketChannel,向该Channel写入"HelloServer",并把该Channel注册到selector,负责监听读事件。
6.服务端:第3步中监听读事件的selector,监听到第5步客户端的事件。从selector中取出channel(第3步中的那个channel),从通道中读取到数据"HelloServer"。然后向通道写数据"HelloClient"。
7.客户端:第5步中最后负责监听的selector,监听到第6步中服务端的数据,收到"HelloClient"。客户端完成,客户端的selector继续轮询事件。
8.服务端:监听到第6步中自己的写事件,取到channel,取消监听写事件,只监听读事件。
核心组件
Channel(通道)
概述
Channel可以理解为通道,通过它可以从不同源(文件/网络等)读取和写入数据(通道是基于Buffer进行读写交互的,因为 Buffer特性所以通道可以异步地读写),因为Channel是全双工的,所以它可以比流更好地映射底层操作系统的APl
分散Scatter && 聚集Gather
- 分散读取: 将Channel中的数据按照顺序分散到多个Buffer中(即缓冲区数组)
- 聚集写入: 将多个Buffer(即缓冲区数组)中数据聚集到Channel
实现类
- FileChannel:主要是用于文件的读写
- DatagramChannel:主要用于UDP读写网络中的数据
- SocketChannel:通过TCP读写网络中的数据
- ServerSocketChannel:主要用于服务端,可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel
Channel与Stream流的区别
Buffer(缓冲区)
概述
Buffer是一个数组对象,我们可以把它理解为临时存储固定数量的写入或者读出的数据的容器.在Java NIO,任何时候访问NIO中的数据都需要通过缓冲区(Buffer)进行操作。读取数据时直接从缓冲区中读取,写入数据时写入至缓冲区
分类
- 非直接缓冲区: 通过allocate方法将缓冲区分配在JVM内存中
- 直接缓冲区:通过allocateDirect方法将缓冲区分配在物理内存中,可提高效率
ByteBuffer buf = ByteBuffer.allocate(1024); //创建非直接缓冲区大小为1024
ByteBuffer buffer_direct = ByteBuffer.allocateDirect(1024);//创建直接缓冲区大小为1024
System.out.println("判断是否为直接缓冲区 =" + buffer_direct.isDirect()); //true
子类
核心方法
- allocate:分配一个缓冲区
- put:存入数据到缓冲区
- get:获取缓冲区的数据
核心属性
- capacity:表示缓冲区中最大存储数据的容量,一旦声明不能改变
- position:表示缓冲区中当前操作数据的位置
- 写模式下,position表示当前写入的位置,position最大为capacity-1
- 读模式下,为读入数据的当前位置
- limit:表示缓冲区中可以操作数据的大小(limit后数据无法读写)
- 写模式下,写入多少的数据,limit等于多少
- 读模式下,表示有多少数据可读
- 0 <= position <= limit <= capacity(始终不变)
package com.bierce.io; import java.nio.ByteBuffer; public class TestBuffer{ public static void main(String[] args) { ByteBuffer buf = ByteBuffer.allocate(1024); //创建缓冲区大小为1024 System.out.println("--------------未写入数据前获取各属性值-------------"); System.out.println("position = " + buf.position()); // position = 0 System.out.println("limit = " + buf.limit()); //limit = 1024 System.out.println("capacity = " + buf.capacity()); //capacity = 1024 System.out.println("--------------写入数据后获取各属性值--------------"); String data = "data"; buf.put(data.getBytes()); System.out.println("position = " + buf.position()); // position = 4 System.out.println("limit = " + buf.limit()); //limit = 1024 System.out.println("capacity = " + buf.capacity()); //capacity = 1024 System.out.println("--------------切换到读模式下获取各属性值--------------"); buf.flip(); buf.put(data.getBytes()); System.out.println("position = " + buf.position()); // position = 4 System.out.println("limit = " + buf.limit()); //limit = 4 System.out.println("capacity = " + buf.capacity()); //capacity = 1024 System.out.println("--------------切换到写模式下获取各属性值---------------"); buf.flip(); System.out.println("position = " + buf.position()); // position = 0 System.out.println("limit = " + buf.limit()); //limit = 4 System.out.println("capacity = " + buf.capacity()); //capacity = 1024 } }
Selector(选择器)
- 多路复用器Selector是Java NIO编程的基础,熟练地掌握Selector对于掌握NIO编程至关重要。
- 多路复用器提供选择已就绪任务的能力,即Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
- 一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以介入成千上万的客户端
Pipe(管道)
概述
Java NIO管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取
示例
Pipe pipe = Pipe.open(); //获取管道
//相当于一个线程写入数据到管道
Pipe.SinkChannel sinkChannel = pipe.sink(); //获取sink管道,用来传送数据
ByteBuffer byteBuffer_write = ByteBuffer.allocate(1024);
byteBuffer_write.put("bierce Never Give up!".getBytes());
byteBuffer_write.flip(); //写入完成后转换为读模式
sinkChannel.write(byteBuffer_write); //通过sink管道发送数据
//相当于另一个线程从管道读取数据
Pipe.SourceChannel sourceChannel = pipe.source(); //获取source管道,用来读取数据
ByteBuffer byteBuffer_read = ByteBuffer.allocate(1024);
int length = sourceChannel.read(byteBuffer_read);
System.out.println(new String(byteBuffer_read.array(), 0, length)); //bierce Never Give up!
//关闭管道资源
sourceChannel.close();
sinkChannel.close();
适用场景
适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂
AIO(Asynchronous I/O)
概述
AIO也称为NIO2,jdk7后出现的一种异步非阻塞模式,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数(AIO 应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了)
适用场景
JDK7开始支持,适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂