NIO 的工作方式

BIO 带来的挑战

BIO : BIO 通信模型,通常由一个独立的 Acceptor 线程负责监听客户端的连接,接受到请求之后,为每个客户端创建一个新的线程进行链路处理,处理完成之后,线程销毁。是典型的 请求-应答通信模型。

BIO 即阻塞 IO,不管是磁盘IO 还是 网络 IO,数据在写入 OutputStream 或者从 InputStream 读取时都有可能会阻塞,一旦有阻塞,线程将会失去 CPU 的使用权,这在当前的大规模访问量和有性能要求的情况下是不能被接受的。

虽然当前的网络IO 有一些解决方法,如一个客户端对应一个处理线程,出现阻塞时只是一个线程阻塞而不会影响到其他线程的工作;还有问了减少系统线程开销,使用线程池的办法减少线程创建和回收成本,但是在一些场景下仍然无法解决。

如:当前一些需要大量 HTTP 长连接的情况,像淘宝现在使用的 web 旺旺,服务端需要同时保持几百万的 HTTP 连接,但并不是每时每刻这些连接都需要传输数据,在这种情况下不可能同时创建这么多线程来保持连接.

即使线程数量不是问题,也仍然会有一些问题无法避免:比如我们想给某些客户端更高的服务优先级,很难通过设计线程的优先级来完成。

另外一种情况是,每个客户端的请求在服务端可能需要访问一些竞争资源,这些客户端在不同的线程中,因此需要同步,要实现这种同步操作远比单线程复杂得多。以上这些情况都说明,我们需要一种新的 IO 操作方式。

NIO 的工作机制

首先看下 NIO 相关的类图:



图中有两个关键的类:Selector 和 Channel,他们是 NIO 中两个核心概念。

我们使用城市交通工具来描述 NIO 的工作方式。这里的 Channel 比 Socket 更加具体,可以比喻为某种具体的交通工具,如汽车,而把 Selector 理解为车辆运行调度系统,它负责监控每辆车的运行状态,是未出站还是在路上等。也就是 Selector 可以轮询每个 Channel 的状态。这里还有一个 Buffer 类,它比 Stream 更加具体的概念,Stream 代表座位,但没有描述座位是什么,以及是否还有座位。而 NIO 引入了 Channel、Selector 、Buffer 就是想把这些信息具体化,让程序员有机会去控制他们。

例如:当我们调用 write() 方法向 sendQ 中写入数据时,当一次写入的数据超过了 SendQ 的长度时,需要按照 SendQ 的长度进行分割,在这个过程中需要将用户地址和内核地址空间进行切换,而这个切换是你不能控制的,但在 Buffer 中我们可以自定义 Buffer 容量、是否扩容以及如何扩容等。

下面 show me the code,看下实际是怎么工作的:

public void selector() throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);//设置为非阻塞方式
ssc.socket().bind(new InetSocketAddress(8080));
ssc.register(selector, SelectionKey.OP_ACCEPT);// 注册监听的事件
while(true){
Set selectedKeys = selector.selectedKeys();// 获取所有 key 集合
Iterator it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = (SelectionKey) it.next();
if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel accept = channel.accept();
accept.configureBlocking(false);
accept.register(selector, SelectionKey.OP_READ);
it.remove();
} else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
SocketChannel channel = (SocketChannel) key.channel();
while (true) {
buffer.clear();
int n = channel.read(buffer);
if (n <= 0) {
break;
}
buffer.flip();
}
it.remove();
}
}
} }

流程简析如下:调用 Selector 的静态方法创建一个选择器,创建一个服务端的 Channel,绑定到一个 Socket 对象,并把这个通信信道注册到选择器上,然后把通信信道设置为“非阻塞”模式。

然后就可以调用 Selector 的 selectedKeys 方法检查已经注册在这个选择器上的所有通信信道是否有事件发生。

如果有,将会返回所有的 SelectionKey ,通过这个对象的 channel() 方法可以获取对应的通信信道对象,从而读取通信的数据,而这里读取的数据是 buffer,这个 Buffer 就是我们可以控制的缓冲器。

在上面的这段程序中,将 Server 端的 监听连接请求的事件和处理请求的事件放在一个线程中,但是在事件应用中,我们通常会把他们放在两个线程中:一个线程专门负责监听客户端的连接请求,而且是以阻塞方式执行;另外一个线程专门负责处理请求,这个专门负责处理请求的线程才会真正采用 NIO 的方式,像 web 服务器 Tomcat 和 Jetty 都是使用这个处理方式。

Selector 可以同时监听一组通信信道(Channel)上的 IO 状态,前提是这个 Selector 已经注册到这些信道中。Selector 的 select() 方法检查已经注册的通信信道上 IO 是否已经准备好,如果没有至少一个信道 IO 状态有变化,那么 select 方法会阻塞等待或在超时时间后返回0。如果有多个信道有数据,那么将会把这些数据分配到对应的数据 Buffer 中。所以关键的地方是,有一个线程来处理所有连接的数据交互,每个连接的数据交互都不是阻塞方式,所以可同时处理大量的连接请求。

Buffer 的工作方式

前面介绍了 Selector 检测到通信信道 IO 有数据传输时,通过 select 方法取得 SocketChannel,将数据读取或写入 Buffer 缓冲区,下面介绍 Buffer 如何接收和写出数据。

简单理解:把 Buffer 当成是一个基本数据类型的元素列表,它通过几个变量来保存列表中数据的当前位置状态,有4个值--capacity、limit、position、mark

  • capacity :缓冲区数组的长度
  • position:下一个要操作的数据元素位置
  • limit:缓冲区数组中不可操作的下一个元素的位置,limit<=capacity
  • mark:用于记录上一次 position 的位置或者默认为0



1、起始位置:position = 0,capacity=limit=length 数组长度

2、写入3个数据之后,如图3;

3、图3 到 图4 是调用了 flip 方法之后,position 回到起始位置,limit取原position值,此时可以从缓冲区中正确读取这3个数据;

4、在下一次写入数据之前,调用 clear 方法,缓冲区的索引状态回到初始位置;

mark的作用呢?当我们调用 mark 方法时,它会记录当前 position 的前一个位置,当我们调用 reset 时,position 会恢复 mark 记录下来的值。

注意:通过 Channel 获取 IO 数据首先要经过操作系统的 Socket 缓冲区,再将数据复制到 Buffer 中,这个操作系统缓冲区就是底层 TCP 所关联的 RecvQ 或者 SendQ 队列,从操作系统缓冲区到用户缓冲区复制数据比较耗费性能,Buffer 提供了一种直接操作操作系统缓冲区的方式,即 ByteBuffer.allocateDirector(size),这个方法返回的 DirectByteBuffer 就是底层存储空间关联的缓冲区,它通过 Native 代码操作非 JVM 堆的内存空间。每次创建或释放的时候都调用一次 System.gc() 。注意,使用 DirectByteBuffer 可能会引起内存泄漏的问题。在数据量大,生命周期较长的情况下比较合适。

NIO 的数据访问方式

NIO 提供了两种访问文件的优化方法,FileChannel.transferTo/FileChannel.transferFrom;另一个是 FileChannel.map

  • FileChannel.transferXXX

    与传统方式相比,减少了数据从内核到用户空间的复制,数据直接在内核空间中移动,在linux 中使用 sendFile 系统调用。

  • FileChannel.map

    FileChannel.map 将文件按照一定大小块映射为内存区域,当程序访问这个内存区域时,将直接操作这个文件数据,这种方式省去了数据从内核空间向用户空间复制的损耗。这种方式适合对大文件的只读性操作,如文件的MD5校验。炼丹师这种方式是和操作系统的底层 IO 实现相关,如以下代码:

public static void map(String[] args) {
int BUFFER_SIZE = 1024;
String fileName = "test.db";
long fileLength = new File(fileName).length();
int bufferCount = 1 + (int) (fileLength / BUFFER_SIZE);
MappedByteBuffer[] buffers = new MappedByteBuffer[bufferCount];
long remaining = fileLength;
for (int i=0;i<bufferCount;i++) {
RandomAccessFile file;
try {
file = new RandomAccessFile(fileName, "r");
buffers[i] = file.getChannel().map(FileChannel.MapMode.READ_ONLY, i * BUFFER_SIZE, Math.min(remaining, BUFFER_SIZE));
} catch (Exception e) {
e.printStackTrace();
}
remaining -= BUFFER_SIZE;
}
}

NIO 的工作方式的更多相关文章

  1. Java NIO的工作方式

    1.BIO带来的挑战 BIO即阻塞IO,不管是磁盘IO,还是网络IO,数据在写入OutputStream或者从InputStream读取时都有可能发生阻塞,一旦有阻塞,当前线程将会被挂起,即线程进入非 ...

  2. 读书笔记-NIO的工作方式

    读书笔记-NIO的工作方式 1.BIO是阻塞IO,一旦阻塞线程将失去对CPU的使用权,当前的网络IO有一些解决办法:1)一个客户端对应一个处理线程:2)采用线程池.但也会出问题. 2.NIO的关键类C ...

  3. NIO的工作方式

    BIO带来的挑战 BIO 就是我们常说的阻塞I/O , 不论磁盘I/O 还是网络/O ,数据在写入OutputStream 或者从 InutStream 读取数据时都有可能会阻塞,一旦有了阻塞,线程就 ...

  4. Nio经典工作方式

    public void selector() throws IOException { ByteBuffer buffer = ByteBuffer.allocate(1024); Selector ...

  5. Buffer的工作方式

    1.Buffer的工作方式 前面<java NIO的工作方式>介绍了Selector检测到通信信道I/O有数据传输时,通过select()方法取得SocketChannel,将数据读取或写 ...

  6. dicom通讯的工作方式及dicom标准简介

    本文主要讲述dicom标准及dicom通讯的工作方式.dicom全称医学数字图像与通讯 其实嘛就两个方面 那就是“存储”跟“通讯”. 文件数据组织方式  网络数据组织方式.文件数据组织方式就是解析静态 ...

  7. 通过iMindMap改善你的工作方式的教程

    对于iMindMap 10,已经介绍了很多新增与改进的功能,你以为已经结束了?其实不然,本文,小编还会继续和你分享它的一个新功能与一个更新功能.这两个功能将在不经意间改善你的工作方式. 多媒体支持 在 ...

  8. 输入/输出系统的四种不同工作方式对CPU利用率比较

    程序控制工作方式:输入/输出完全由CPU控制,整个I/O过程中CPU必须等待其完成,因此对CPU的能力限制很大,利用率较低 程序中断工作方式:CPU不再定期查询I/O系统状态,而是当需要I/O处理时再 ...

  9. 从一个简单例子来理解js引用类型指针的工作方式

    <script> var a = {n:1}; var b = a; a.x = a = {n:2}; console.log(a.x);// --> undefined conso ...

随机推荐

  1. MAT内存分析工具安装指南(MAT)

    https://blog.csdn.net/mahl1990/article/details/79298616

  2. 基于ARM64的Qemu/KVM学习环境搭建

    作者:pengdonglin137@163.com 在没有aarch64架构的开发板的情况下,可以使用Qemu来模拟一个支持KVM的AArch64位的host,然后再在其上运行一个开启KVM加速的Qe ...

  3. IdentityServer4系列 | 资源密码凭证模式

    一.前言 从上一篇关于客户端凭证模式中,我们通过创建一个认证授权访问服务,定义一个API和要访问它的客户端,客户端通过IdentityServer上请求访问令牌,并使用它来控制访问API.其中,我们也 ...

  4. 第8.4节 Python类中不是构造方法却胜似构造方法的__new方法__深入剖析:语法释义

    一.    引言 在本博前面的内容都对构造方法__init__进行了介绍,也在前面章节引入了__new__方法,但老猿认为__new__方法比构造方法__init__更应该属于构造方法.这是因为在Py ...

  5. 【NOI2019】弹跳(KDT优化建图)

    Description 平面上有 \(n\) 个点,分布在 \(w \times h\) 的网格上.有 \(m\) 个弹跳装置,由一个六元组描述.第 \(i\) 个装置有参数:\((p_i, t_i, ...

  6. 四、git学习之——分支管理、解决冲突

    分支就是科幻电影里面的平行宇宙,当你正在电脑前努力学习Git的时候,另一个你正在另一个平行宇宙里努力学习SVN. 如果两个平行宇宙互不干扰,那对现在的你也没啥影响.不过,在某个时间点,两个平行宇宙合并 ...

  7. [从源码学设计]蚂蚁金服SOFARegistry之推拉模型

    [从源码学设计]蚂蚁金服SOFARegistry之推拉模型 目录 [从源码学设计]蚂蚁金服SOFARegistry之推拉模型 0x00 摘要 0x01 相关概念 1.1 推模型和拉模型 1.1.1 推 ...

  8. Vscode:常用的插件

    Chinese (Simplified) Language Pack for Visual Studio Code==>汉化 Live Server==>运行代码 ESLint==> ...

  9. 自定义Django认证系统的技术方案

    Django已经提供了开箱即用的认证系统,但是可能并不满足我们的个性化需求.自定义认证系统需要知道哪些地方可以扩展,哪些地方可以替换.本文就来介绍自定义Django认证系统的相关技术细节. 自定义认证 ...

  10. I/O-基本概念

    目录 演变过程 I/O系统基本组成 I/O接口 I/O方式简介 小结 演变过程 I/O系统基本组成 分成软件和硬件 I/O接口 接口可以看作是两个部件之间的交接部分 I/O方式简介 小结