本文主要讲解NIO
BIO(Blocking I/O) -- 阻塞IO
1. 不管是从本地还是从网络上使用InputStream来读取或则使用OutputStream来写入数据时,都有可能被阻塞掉,从而失去对CPU的使用权,当遇到大规模的访问量和对性能有较高要求的时候,这种方式及显示力不从心了。
2. 当然也可以使用多个线程来处理多个请求,甚至是可以使用线程池来提高服务端的性能。但这样的话,又会产生很多问题,比如我们通过给线程设置优先级来实现服务更高优先级时就难以完成。
3. 除此之外,这些线程难免会涉及到访问竞争资源的问题,去同时处理多个线程资源竞争问题比处理单个线程复杂得多。=>所以NIO应运而生。
NIO(non-blocking I/O) -- 非阻塞IO
NIO概述
NIO主要包含有三个主要构件Selector,Channel,Buffer。
1. Channel就相当于传输数据的一个通道,Buffer就相当于通道中具体用来传输数据的运载体,Selector就相当于用于调度管理通道的一个调度管理器。
2. 在以往BIO中,程序员只能看到两个东西Stream和Socket,而不能操纵它们内部数据传输方式。
3. 而NIO中,Buffer就好比Stream的具体化,可以让程序员来进行控制数据发送/接受长度,比如使用BIO时,使用write方法对sendQ进行写数据,当一次写入的数据长度超过sendQ长度时,还需要对传入的数据进行分割,分割这个数据就是最耗时的时候了,数据需要从用户空间到内核空间的切换,这个切换过程程序员是无法控制的。但使用NIO的Buffer来控制Buffer的容量,是否扩容,如何扩容来有效避免数据空间切换的成本消耗。
* 其中sendQ不懂得可以去看看计算机网络 TCP的接受/发送队列
4. 由于Socket封装了信息,我们不知道在传输过程中有多少数据传输了,有多少没传输,但通过Channel我们可以知道这个通道还能够继续传输多少数据。再者我们还可以通过selector监控所有已经注册到调度器上的通道。
使用NIO的例子 -- 各个部分的意思也注释在代码中
这位大佬写的例子就十分清晰例子
NIO中重点 -- Buffer工作方式
1. 四个重要索引:
2. 在实际运用中比如:
//创建一个24个的byte数组缓冲区,初始状态 position指向数组起始位置0,limit与capacity都是数组长度位置
ByteBuffer.allocate(24);
//当需要将10个字节写入channel信道时,就使用如下,position就会移动到10的数组位置,limit和capacity不变。
byteBuffer.put(strings.getBytes());
//然后使用如下方法,这个时候操作系统就能够从缓冲区里面将这10个字节发送出去了。position重回到0,limit变为之前position位置即10,capacity不变。
byteBuffer.flip();
//然后客户端write Buffer就发送了数据
client.write(byteBuffer);
// ps:在下次写数据之前调用一下clear(),Buffer索引回归起始状态
// 使用mark(),可以将当前position的前一个位置记录下来,当调用reset的时候position会恢复mark记录下来的值
比传统文件访问方式更好的NIO文件访问方式 -- FileChannel.transferTo/transferFrom与FileChannel.map
FileChannel.transferTo/transferFrom:
1. 为什么说比传统的好,比如传统的文件访问需要将文件从磁盘中读取数据到内核空间中,然后从内核空间读取到用户空间,在用户空间进行一些自定义操作,再把操作后的数据读入内核空间,存入磁盘。
2. 而transferTo/transferFrom只需要把数据从磁盘读取到内核空间,然后就直接在内核空间进行操作,然后再写入磁盘就行了。孰优孰劣,显而易见。
##### FileChannel.map
1. 那么map是怎么操作的呢?它显示将文件按照一定大小映射到一块内存区域中,当程序访问时,直接操作这块内存区域数据,但这种方式只适合于大文件的只读性操作。比如 MD5校验。
AIO(asynchronous I/O) -- 异步非阻塞IO
当需要对线程实现异步调用的时候就可以使用AIO
最后总结一下它们的应用场景:
1. BIO方式适用于连接数目比较小且固定的架构,并发局限于应用中。
2. NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中。
3. AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作。