本文大纲如下:
- 1、什么是NIO
- 2、为什么使用NIO
- 3、NIO的基本使用
- 4、BIO、NIO、AIO区别以及总结
一、什么是NIO
NIO是JDK1.4新加入的,为了解决BIO(原始IO的不足)的非阻塞IO模型。原始的IO流是单向的,如InputStream只能读取不能写入,而NIO是面向通道(Channel)和缓冲区(Buffer),NIO可以输入也可以输出。
二、为什么使用NIO
探讨NIO之前,肯定先说明BIO的不足,只有在BIO满足不了需求的时候,才会出现新的技术代替。如下图所示,因为BIO是一个阻塞的IO模型,所以需要为每个IO配备一个线程去监听调度,如果在高并发的情况下,开辟新的线程以及切换线程会带来巨大的损耗。但是对于比较简单的文件读写,还是使用BIO。
NIO的出现解决了BIO的低效率不能高并发的问题,所以NIO不是阻塞的IO模型。他只需要调用一个线程去轮询Selector,是否有新的事件出现即可。再通过事件找到具体的Channel,这样就可以将N的线程降低为一个线程了。如下图所示,只需要单个线程监听Selector,当对应的Channel有消息到达时候,再调用工作线程去处理,这样可以减少了数据未到达之前的等待时间。
无论是输入还是输出,NIO中Channel和Buffer是配对使用的。在输入(Input)时,Channel的数据读取到Buffer中,使用者再从Buffer中读取数据。输出反之。调度者直接调度的只能是Buffer。
三、NIO的基本用法
1)、Channel、Buffer、Selector组件的介绍
① 、Channel通道
Channel通道是 I/O 传输发生时通过的入口。当Buffer数据写入到Channel时,他就开始传输了。反之当Channel接收到数据时,Buffer就可以读取了。Channel是真正开始数据传输的。
对应的Channel主要有:
- FileChannel 读写文件
- DatagramChannel 读写UDP数据
- SocketChannel 读写TCP数据
- ServerSocketChannel 监听TCP链接,并为服务端创建一个SocketChannel
②、Buffer缓冲区
Buffer即缓冲区。在NIO模式下,数据并非立刻到达的,在数据没到达之前都是阻塞的,所以在NIO中才引入缓冲区概念,数据到达时先写入到Buffer中,再交给调用者处理缓冲区。这样处理者线程就不需要阻塞了。
对应的Buffer有:
- ByteBuffer 以字节为单位
- CharBuffer 以Char为单位
- DoubleBuffer 以Double为单位
- FloatBuffer 以Float为单位
- IntBuffer 以Int为单位
- LongBuffer 以Long为单位
- ShortBuffer 以Short为单位
Buffer的属性:
- private int mark = -1;
- private int position = 0;
- private int limit;
- private int capacity;
Buffer中共有mask、position、limit、capacity属性,其中主要的是position、limit、capacity。
capacity: capacity是容器的大小,即缓冲区的大小。
limit : capacity意味着缓冲区的大小,但并不是所有的缓冲区,使用者都能用的上,使用者可以根据需要,划分出自己所需要的大小,但不能大于了capacity,这就是limit的意义。写的模式下,limit代表了,最多可以写多少。读模式下,意味着最多可以读到多少。当从写模式切换至读模式,即调用flip()函数后,limit=position,position=0;这样就可以开始读buffer了。
position:在写的模式下,position为0,position代表下一个写入的位置,所以position最多不能大于limit-1;在读模式下,position代表的是下一个读取的位置,大小同样不能大于limit-1。在初始状态下或者调用flip函数时,position会重新赋值等于0;
mark: mark从字面意思就是标记的意思,它就是当前position的一个快照。通过reset函数会让position重新赋值为mark。
Buffer中核心API:
Buffer既然是个内存,它的API就是对该内存位置的管理标记,即对属性管理。下面只介绍部分方法
- clear() 当该buffer中的数据处理完的时候,就可以调用该方法,他会使buffer变成初始状态。
- compact() 于clear一样,compact()也是清除的作用,但是他只清除读取完的数据,把未读取的数据copy到首部,移动position和limit
- hasRemaining() 代表是否有下一个位置可以处理。读时是不是读完了,写时为是不是写完了。通常是在这操作之前判断。
③、Selector选择器
当应用中有大量的Channel的时候,即IO连接时。还得有大量的线程去监听数据状态。为此NIO加入了Selector,由Channel向Selector注册,并支持一个Selector可以注册多个Channel。将监听 Channel换成监听Selector,从而减少线程使用。
由于Channel可以进行读或者写,所以向Selector注册时,需要表明是监听哪一种事件。最终轮询Selector即可知道哪种事件到达了。
Selector事件如下:
- OP_ACCEPT TCP服务端接收到客户端连接事件
- OP_CONNECT 该连接是否已经断开事件
- OP_READ 可以读取该数据事件
- OP_WRITE 可以写入数据事件
2)、NIO基本用法
① 、FileChannel
FileChannel是读取文件时的Channel
读取文件例子如下:
public static void main(String[] args) throws IOException {
RandomAccessFile file = new RandomAccessFile("D:\\proguard-rules.pro", "rw");
FileChannel channel = file.getChannel();
// 分配缓存空间
ByteBuffer buf = ByteBuffer.allocate(48);
// buf读取通道的数据 length为读到的数据长度
int length = channel.read(buf);
byte[] bytes = new byte[48];
while (length != -1) {
// buf由写的状态 切换至读状态
buf.flip();
while(buf.hasRemaining()){
buf.get(bytes,0,length);
System.out.print(new String(bytes));
}
// 清空buf数据
buf.clear();
length = channel.read(buf);
}
file.close();
}
上诉代码只是简单的读取文件,虽然简单,但包含了NIO的基本用法流程。生产Buffer对象有两种方法:
- ByteBuffer buf = ByteBuffer.allocate(48);
- ByteBuffer buf = ByteBuffer.wrap(bytes,0,length);
transferTo(long position, long count,
WritableByteChannel target)
现代的操作系统分为内核态和用户态,文件的copy是从内核态 ->用户态 ->内核态,它需要经过用户态才能copy。FileChannel中的拷贝transferTo(transferTo类似),直接从内核态到内核态,效率比较高。
② 、DatagramChannel
DatagramChannel是接收发送UDP的,而UDP是面向数据包,而非数据流的。
发送数据包:
DatagramChannel channel = DatagramChannel.open();
// 发送数据
String data = "DatagramChannel...";
ByteBuffer writeBuffer = ByteBuffer.allocate(48);
writeBuffer.clear();
writeBuffer.put(data.getBytes());
writeBuffer.flip();
int len = channel.send(writeBuffer, new InetSocketAddress("127.0.0.1", 5520));
System.out.println(len);
接收数据包:
DatagramChannel channel = DatagramChannel.open();
// 接收绑定端口
channel.socket().bind(new InetSocketAddress(5521));
ByteBuffer readBuffer = ByteBuffer.allocate(48);
channel.receive(readBuffer);
readBuffer.flip();
byte[] bytes = new byte[48];
while (readBuffer.hasRemaining()) {
readBuffer.get(bytes,0,readBuffer.limit());
System.out.print(new String(bytes));
}
③ 、ServerSocketChannel
利用ServerSocketChannel监听TCP链接
Selector selector = Selector.open();
ServerSocketChannel server = ServerSocketChannel.open();
// 设置为非阻塞,没有连接时也会返回null
server.configureBlocking(false);
// 绑定本地端口
server.socket().bind(new InetSocketAddress(5510));
// 注册客户端连接到达监听
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
try {
if (selector.select() == 0) {
continue;
}
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
// 客户端到达状态
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
// 非阻塞状态拿到客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
首先,生成ServerSocketChannel对象,并绑定本地端口。再用ServerSocketChannel向Selector注册SelectionKey.OP_ACCEPT。轮询Selector返回的事件,当有TCP连接时,则生成SocketChannel进行网络数据传输。
④ 、SocketChannel
SocketChannel专门就是接收发送TCP数据的,打开连接,将buffer中的数据写入到channel或者从channel读取数据到buffer中。举个简单的例子。
// 客户端打开SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 5510));
// 开启非阻塞模式 异步模式下调用connect(), read() 和write() 可以立即返回无阻塞
socketChannel.configureBlocking(false);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 连接还在
while (!socketChannel.finishConnect()) {
// 从channel中读取数据
socketChannel.read(byteBuffer);
// buffer切换到读模式
byteBuffer.flip();
// channel发送buffer中数据
socketChannel.write(byteBuffer);
}
在高并发的情况下,需要借助Selector,利用轮询Selector方式,找到SocketChannel,再根据需求做特定的处理。
Selector selector = Selector.open();
// 客户端打开SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 5510));
// 开启非阻塞模式 异步模式下调用connect(), read() 和write() 可以立即返回无阻塞
socketChannel.configureBlocking(false);
socketChannel.register(selector,SelectionKey.OP_READ);
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 连接还在
while (true) {
if (selector.select() == 0) {
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isValid()) {
// 取消对读事件的继续监听
selectionKey.interestOps(selectionKey.readyOps() & ~SelectionKey.OP_READ);
// 从channel中读取数据
socketChannel.read(byteBuffer);
}
iterator.remove();
}
}
}
channel向Selector注册是SelectionKey key = channel.register(selector, SelectionKey.OP_READ);结合上述的介绍可以看出SelectionKey与Channel是一对一的关系,而Selector和Channel是一对多关系。
将SelectionKey作为key,value是处理数据的Runnable,注册的时候,存入Runnable。事件到达时,根据key取出Runnable,这样就避免为每个连接创建线程执行了。
四、BIO、NIO、AIO区别
BIO,同步阻塞的IO模型。当用InputStream去读取数据的时候,必须等到数据操作完成才能进行下一步操作,否则会阻塞当前的线程。这种方式比较简单,但短流量高并发的情况下,会造成效率低下。基本用于本地文件操作等简单业务。
NIO,同步非阻塞IO模型。NIO还不是异步,它将一个线程操作一次请求,只是有效的减少了线程。即每一个连接注册到多路复用器Selector中,轮询Selector中是否有事件到达,有事件就代表一次请求到达,再用一个线程处理该请求。相比较BIO,它只是将多个线程去等待数据到达减低为一个线程去等待。多用于低流量多连接的场景,如聊天服务器。
AIO,异步非阻塞IO模型。当进行读写操作时,只须直接调用API的read或write方法。该方法都是异步的,方法执行完成会回调回来。从方法签名中可以看出,read时候会注入一个CompletionHandler回调方法,通知该次操作的状态。调用者发送一个read或者write操作请求,操作完成后会得到一个通知,真正的IO操作由操作系统内核进行处理。AIO多用于长连接业务。
public abstract <A> void read(ByteBuffer dst,
long timeout,
TimeUnit unit,
A attachment,
CompletionHandler<Integer,? super A> handler);