NIO 的工作方式
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 的工作方式的更多相关文章
- Java NIO的工作方式
1.BIO带来的挑战 BIO即阻塞IO,不管是磁盘IO,还是网络IO,数据在写入OutputStream或者从InputStream读取时都有可能发生阻塞,一旦有阻塞,当前线程将会被挂起,即线程进入非 ...
- 读书笔记-NIO的工作方式
读书笔记-NIO的工作方式 1.BIO是阻塞IO,一旦阻塞线程将失去对CPU的使用权,当前的网络IO有一些解决办法:1)一个客户端对应一个处理线程:2)采用线程池.但也会出问题. 2.NIO的关键类C ...
- NIO的工作方式
BIO带来的挑战 BIO 就是我们常说的阻塞I/O , 不论磁盘I/O 还是网络/O ,数据在写入OutputStream 或者从 InutStream 读取数据时都有可能会阻塞,一旦有了阻塞,线程就 ...
- Nio经典工作方式
public void selector() throws IOException { ByteBuffer buffer = ByteBuffer.allocate(1024); Selector ...
- Buffer的工作方式
1.Buffer的工作方式 前面<java NIO的工作方式>介绍了Selector检测到通信信道I/O有数据传输时,通过select()方法取得SocketChannel,将数据读取或写 ...
- dicom通讯的工作方式及dicom标准简介
本文主要讲述dicom标准及dicom通讯的工作方式.dicom全称医学数字图像与通讯 其实嘛就两个方面 那就是“存储”跟“通讯”. 文件数据组织方式 网络数据组织方式.文件数据组织方式就是解析静态 ...
- 通过iMindMap改善你的工作方式的教程
对于iMindMap 10,已经介绍了很多新增与改进的功能,你以为已经结束了?其实不然,本文,小编还会继续和你分享它的一个新功能与一个更新功能.这两个功能将在不经意间改善你的工作方式. 多媒体支持 在 ...
- 输入/输出系统的四种不同工作方式对CPU利用率比较
程序控制工作方式:输入/输出完全由CPU控制,整个I/O过程中CPU必须等待其完成,因此对CPU的能力限制很大,利用率较低 程序中断工作方式:CPU不再定期查询I/O系统状态,而是当需要I/O处理时再 ...
- 从一个简单例子来理解js引用类型指针的工作方式
<script> var a = {n:1}; var b = a; a.x = a = {n:2}; console.log(a.x);// --> undefined conso ...
随机推荐
- .net core3.1开始页面实时编译
安装NuGet包 Install-Package Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation startup.cs 中的ConfigureSe ...
- 推荐系统实践 0x0a 冷启动问题
什么是冷启动问题 如何在没有大量用户数据的情况下设计个性化推荐系统并且让用户对推荐结果满意从而愿意使用推荐系统,就是冷启动问题.冷启动问题主要分为三类: 用户冷启动 物品冷启动 系统冷启动 下面我们将 ...
- Feign 超时设置
问题描述 微服务之间使用 Feign 调用,偶发超时问题,配置如下: feign: client: config: default: connectTimeout: 10000 readTimeout ...
- day6(celery配置与基本使用)
1.celery配置与基本使用 1.1 安装celery pip install celery @ https://github.com/celery/celery/tarball/master 1. ...
- QQFishing QQ钓鱼站点搭建
答:为什么要写这个代码? 当然不是做黑产去盗别人扣扣,也没有啥查看别人隐私信息的癖好,搭建该站点的适用对象为->使用社会工程学定向钓鱼攻击的安全渗透人员 另外管理员界面后端写的很丑+很烂,除了我 ...
- 性能测试平台nGrinder
ngrinder简介 ngrinder是NAVER(韩国大型互联网公司)开源的性能测试工具平台,直接部署成web服务,平台化,支持多用户使用,可扩展性好,可自定义plugin插件 开源地址:http: ...
- 密码学系列之:明文攻击和Bletchley Park
目录 简介 crib和明文攻击 布莱奇利公园(Bletchley Park) 简介 明文攻击就是指已经知道了部分明文和它对应的加密后的字段,从而可以推测出使用的加密手段或者密码本.明文攻击这个故事还要 ...
- 零基础的Java小白如何准备初级开发的面试
对于各位Java程序员来说,只要能有实践的机会,哪怕工资再低,公司情况再一般,只要自己上心努力,就可能在短时间内快速提升,甚至在工作2年后进大厂都有希望,因为项目里真实的开发实践环境是平时学习不能模拟 ...
- 安卓基于谷歌串口api进行串口开发
准备材料 AndroidStudio 谷歌android-serialport-api 前情提要 网上提供很多基于c语言对安卓串口开发,有jni.cmake等等,不过都太高深,谷歌提供的api已经可以 ...
- CentOS 7.6安装MariaDB10.4.8超详细教程
MariaDB数据库管理系统是MySQL的一个分支,主要由开源社区在维护,采用GPL授权许可 MariaDB的目的是完全兼容MySQL,包括API和命令行,使之能轻松成为MySQL的代替品. Cent ...