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. macOS tips

    1.设置常用linux命令的快捷键 打开terminal command+space,搜索"terminal"关键字 进入"~/"目录 cd ~/ touch ...

  2. CentOS下设置ipmi

    1.载入支持 ipmi 功能的系统模块 modprobe ipmi_msghandler modprobe ipmi_devintf modprobe ipmi_poweroff modprobe i ...

  3. oracle sql%notfound

    SQL%NOTFOUND 是一个布尔值.与最近的sql语句(update,insert,delete,select)发生交互,当最近的一条sql语句没有涉及任何行的时候,则返回true.否则返回fal ...

  4. python模块wifi使用小记

    安装命令 pip install wifi 连接命令 sudo wifi connect --add-hoc ssid,使用该命令会修改/etc/network/interfaces配置文件,导致启动 ...

  5. linux离线安装docker + docker-compose

    1 准备阶段(docker) 1.1 卸载旧版本 如果电脑上已经存在docker,需要先卸载可能存在的旧版本: 1. 删除某软件,及其安装时自动安装的所有包 sudo apt-get autoremo ...

  6. 推荐4个Flutter重磅开源项目

    早上好,骚年,我是小 G,我的公众号「菜鸟翻身」会推荐 GitHub 上有用的项目,一分钟 get 一个优秀的开源项目,挖掘开源的价值,欢迎关注我. 近年来,随着移动智能设备的快速普及,移动多端统一开 ...

  7. PyQt+moviepy音视频剪辑实战2:实现一个剪裁视频文件精华内容留存工具

    专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 PyQt+moviepy音视频剪辑实战 专栏:PyQt入门学习 老猿Python博文目录 老猿学5G博文目录 一.引言 ...

  8. CSS基础-列表

    列表字体和间距 当创建样式列表时,需要调整样式,使其保持与周围元素相同的垂直间距和相互间的水平间距.   示例代码 /* 基准样式 */ html { font-family: Helvetica, ...

  9. 题解-FJOI2018 领导集团问题

    题面 FJOI2018 领导集团问题 给一棵树 \(T(|T|=n)\),每个点有个权值 \(w_i\),从中选出一个子点集 \(P=\{x\in {\rm node}|x\in T\}\),使得 \ ...

  10. hashmap为什么是二倍扩容?

    这个很简单,首先我们考虑一个问题,为什么hashmap的容量为2的幂次方,查看源码即可发现在计算存储位置时,计算式为: (n-1)&hash(key) 容量n为2的幂次方,n-1的二进制会全为 ...