一、写在开头

我们在上一篇博文中提到了Java IO中常见得三大模型(BIO,NIO,AIO),其中NIO是我们在日常开发中使用比较多的一种IO模型,我们今天就一起来详细的学习一下。

在传统的IO中,多以这种同步阻塞的IO模型为主,程序发起IO请求后,处理线程处于阻塞状态,直到请求的IO数据从内核空间拷贝到用户空间。如下图可以直观的体现整个流程(图源:沉默王二)。

如果发起IO的应用程序并发量不高的情况下,这种模型是没问题的。但很明显,当前的互联网中,很多应用都有高并发IO请求的情况,这时就迫切的需要一款高效的IO模型啦。

NIO中的这个N既可以命名为NEW代表一种新型的IO模型,又可以理解为Non-Blocking,非阻塞之意。Java NIO 是 Java 1.4 版本引入的,基于通道(Channel)和缓冲区(Buffer)进行操作,采用非阻塞式 IO 操作,允许线程在等待 IO 时执行其他任务。常见的 NIO 类有 ByteBuffer、FileChannel、SocketChannel、ServerSocketChannel 等。(图源:深入拆解Tomcat & Jetty)

虽然在应用发起IO请求时,之多多次发起,无须阻塞。但在内核将数据拷贝到用户空间时,还是会阻塞的,为了保证数据的准确性和系统的安全稳定。

二、NIO的三大组件

在计算机与外部通信过程中,并非所有场景下NIO的性能都会好,对于连接少,并发地的应用系统中传统的BIO性能反而更好,因为在NIO中应用程序需要不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。

为了更好的熟悉和掌握NIO,我们这里从NIO的三大组件入手,这也是很多大厂面试官在面试时会问到的点,虽然频率不高,但一定得会!

三个核心组件:

  • Selector(选择器): 一种基于事件驱动的I/O多路复用模型,允许一个线程处理多个Channel,多个Channel注册到一个Selector上,然后由Selector进行轮询监听每一个Channel的变化。
  • Channel(通道): 是一个双向的,可读可写的数据传输管道,通过它来实现数据的输入与输出工作,它只负责运输数据,不负责处理数据,处理数据在Buffer中。一般将管道分为文件通道套接字通道
  • Buffer(缓冲区): NIO中数据的操作都是在缓冲区中完成的。读操作是将Channel中运输过来的数据填充到Buffer中;写操作是将Buffer中的数据写入到Channel中。

为了更好的理解NIO基于三大核心组件的运行流程,画了一个思维导图,如下:

三、组件详解

下面,我们针对上一章总结的三大组件,进行一个个的详细介绍。

3.1 Buffer(缓冲区)

在传统的BIO中,数据的读写操作是基于流的,写入采用输入字节流或字符流,而写出采用都的是输出字节流或者字符流,本质上都是基于字节的数据操作。而NIO库中,采用的是缓冲区,无论是写入还是写出数据,都不会进入到缓冲区里,由缓冲区进行下一步的操作。

上图是Buffer子类的继承关系结构图,我们可以看到,在Buffer中命名是基于基本数据类型的,而我们在日常使用中,ByteBuffer缓冲类最多,它是基于字节存储的,这一点和流一样。

而进入到这些缓冲类的内部够,我们可以发现,其实它们就相当于一个数组容器。在Buffer的源码中,有这样的几个参数:

public abstract class Buffer {
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
}

这四个成员变量的具体含义如下:

  1. 容量(capacity):Buffer可以存储的最大数据量,Buffer创建时设置且不可改变;
  2. 界限(limit):Buffer 中可以读/写数据的边界。写模式下,limit 代表最多能写入的数据,一般等于 capacity(可以通过limit(int newLimit)方法设置);读模式下,limit 等于 Buffer 中实际写入的数据大小;
  3. 位置(position):下一个可以被读写的数据的位置(索引)。从写操作模式到读操作模式切换的时候(flip),position 都会归零,这样就可以从头开始读写了;
  4. 标记(mark):Buffer允许将位置直接定位到该标记处,这是一个可选属性。

并且,上述变量满足如下的关系:0 <= mark <= position <= limit <= capacity

这里我们需要注意一点,Buffer拥有读和写两种模式。Buffer被创建后,默认是写模式 ,调用flip()可以切换到读模式,再调用clear()或者compact()方法切换为写模式。

1️⃣ Buffer的实例化

Buffer无法通过调用构造方法来创建对象,而是需要通过静态方法进行实例化。我们以ByteBuffer为例:

// 分配堆内存,将缓冲区建立在JVM的内存中
public static ByteBuffer allocate(int capacity);
// 分配直接内存,将缓冲区建立在物理内存中,可以提交效率。但这里的数据不会被垃圾回收,容易导致内存溢出。
public static ByteBuffer allocateDirect(int capacity);

2️⃣ Buffer的核心方法

Buffer中我们常用的方法有:

  1. get : 读取缓冲区的数据;
  2. put :向缓冲区写入数据;
  3. flip :将缓冲区从写模式切换到读模式,它会将 limit 的值设置为当前 position 的值,将 position 的值设置为 0;
  4. clear: 清空缓冲区,将缓冲区从读模式切换到写模式,并将 position 的值设置为 0,将 limit 的值设置为 capacity 的值。

3️⃣ Buffer的测试用例

基于以上的理论知识学习后,我们写一个小的测试demo,来感受一下Buffer的使用。

【测试案例】

public class TestBuffer {
public static void main(String[] args) { // 分配一个容量为8的CharBuffer,默认为写模式
CharBuffer buffer = CharBuffer.allocate(8);
System.out.println("起始状态:");
printState(buffer); // 向buffer写入3个字符
buffer.put('a').put('b').put('c');
System.out.println("写入3个字符后的状态:");
printState(buffer); // 调用flip()方法,切换为读模式,
// 准备读取buffer中的数据,将 position 置 0,limit 置 3
buffer.flip();
System.out.println("调用flip()方法后的状态:");
printState(buffer); // 读取字符
//hasRemaining()方法用于判断当前位置和限制之间是否有任何元素。
//当且仅当此缓冲区中至少剩余一个元素时,此方法才会返回true。
while (buffer.hasRemaining()) {
System.out.println("读取字符:" + buffer.get());
}
// 调用clear()方法,清空缓冲区,将 position 的值置为 0,将 limit 的值置为 capacity 的值
//调用clear()方法后,由读模式切换为写模式。
buffer.clear();
System.out.println("调用clear()方法后的状态:");
printState(buffer); } // 打印buffer的capacity、limit、position、mark的位置
private static void printState(CharBuffer buffer) {
//容量
System.out.print("capacity: " + buffer.capacity());
//界限
System.out.print(", limit: " + buffer.limit());
//下一个读写位置
System.out.print(", position: " + buffer.position());
//标记
System.out.print(", mark 开始读取的字符: " + buffer.mark());
System.out.println("\n");
}
}

【输出:】

起始状态:
capacity: 8, limit: 8, position: 0, mark 开始读取的字符: 写入3个字符后的状态:
capacity: 8, limit: 8, position: 3, mark 开始读取的字符: 调用flip()方法后的状态:
capacity: 8, limit: 3, position: 0, mark 开始读取的字符: abc 读取字符:a
读取字符:b
读取字符:c 调用clear()方法后的状态:
capacity: 8, limit: 8, position: 0, mark 开始读取的字符: abc

3.2 Channel(通道)

在上面的总结中,我们已经提过了,Channel作为一种双向的数据通道,给外部属于与程序之间搭建了一个传输的桥梁。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。甚至还可以同时读写!

Channel 的子类如下图所示。

这里虽然有很多通道类,但我们在日常生活中常用的,无非是 FileChannel:文件访问通道;SocketChannel、ServerSocketChannel:TCP 通信通道;DatagramChannel:UDP 通信通道;

FileChannel:用于文件 I/O 的通道,支持文件的读、写和追加操作。FileChannel 允许在文件的任意位置进行数据传输,支持文件锁定以及内存映射文件等高级功能。FileChannel 无法设置为非阻塞模式,因此它只适用于阻塞式文件操作。

SocketChannel:用于 TCP 套接字 I/O 的通道。SocketChannel 支持非阻塞模式,可以与 Selector(下文会讲)一起使用,实现高效的网络通信。SocketChannel 允许连接到远程主机,进行数据传输。

与之匹配的有ServerSocketChannel:用于监听 TCP 套接字连接的通道。与 SocketChannel 类似,ServerSocketChannel 也支持非阻塞模式,并可以与 Selector 一起使用。ServerSocketChannel 负责监听新的连接请求,接收到连接请求后,可以创建一个新的 SocketChannel 以处理数据传输。

DatagramChannel:用于 UDP 套接字 I/O 的通道。DatagramChannel 支持非阻塞模式,可以发送和接收数据报包,适用于无连接的、不可靠的网络通信。

1️⃣ Channel的核心方法

  1. read :读取数据并写入到 Buffer 中;
  2. write :将 Buffer 中的数据写入到 Channel 中。

2️⃣ Channel的测试案例

RandomAccessFile reader = new RandomAccessFile("E:\\testChannel.txt", "r");
FileChannel channel = reader.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
System.out.println("读取字符:" + new String(buffer.array()));

3.3 Selector(选择器)

选择器的概念在上面已经介绍过了,我们现在主要介绍它的运作原理:

通过 Selector 注册通道的事件,Selector 会不断地轮询注册在其上的 Channel。当事件发生时,比如:某个 Channel 上面有新的 TCP 连接接入、读和写事件,这个 Channel 就处于就绪状态,会被 Selector 轮询出来。Selector 会将相关的 Channel 加入到就绪集合中。通过 SelectionKey 可以获取就绪 Channel 的集合,然后对这些就绪的 Channel 进行相应的 I/O 操作。

主要监视事件类型:

  • SelectionKey.OP_ACCEPT:表示通道接受连接的事件,这通常用于 ServerSocketChannel;
  • SelectionKey.OP_CONNECT:表示通道完成连接的事件,这通常用于 SocketChannel;
  • SelectionKey.OP_READ:表示通道准备好进行读取的事件,即有数据可读;
  • SelectionKey.OP_WRITE:表示通道准备好进行写入的事件,即可以写入数据。

SelectionKey集合:

  • 所有的 SelectionKey 集合:代表了注册在该 Selector 上的 Channel,这个集合可以通过 keys() 方法返回;
  • 被选择的 SelectionKey 集合:代表了所有可通过 select() 方法获取的、需要进行 IO 处理的 Channel,这个集合可以通过 selectedKeys() 返回;
  • 被取消的 SelectionKey 集合:代表了所有被取消注册关系的 Channel,在下一次执行 select() 方法时,这些 Channel 对应的 SelectionKey 会被彻底删除,程序通常无须直接访问该集合,也没有暴露访问的方法。

Selector中的select()方法:

  • int select():监控所有注册的 Channel,当它们中间有需要处理的 IO 操作时,该方法返回,并将对应的 SelectionKey 加入被选择的 SelectionKey 集合中,该方法返回这些 Channel 的数量;
  • int select(long timeout):可以设置超时时长的 select() 操作;
  • int selectNow():执行一个立即返回的 select() 操作,相对于无参数的 select() 方法而言,该方法不会阻塞线程;
  • Selector wakeup():使一个还未返回的 select() 方法立刻返回。

【测试案例】

public static void main(String[] args) {
try {
//1、通过open()方法构建一个服务套接字通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
//封装8080端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080)); //2、通过open方法构建一个选择器对象
Selector selector = Selector.open();
// 将 ServerSocketChannel 注册到 Selector 并监听 OP_ACCEPT 事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) {
//监听已注册的通道中是否有连接事件,并将对应的 SelectionKey 加入被选择的 SelectionKey 集合中
int readyChannels = selector.select();
if (readyChannels == 0) {
continue;
}
//通过selectedKeys返回所有需要进行 IO 处理的 Channel
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 处理连接事件
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
// 将客户端通道注册到 Selector 并监听 OP_READ 事件
client.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 处理读事件
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = client.read(buffer);
if (bytesRead > 0) {
buffer.flip();
System.out.println("收到数据:" +new String(buffer.array(), 0, bytesRead));
// 将客户端通道注册到 Selector 并监听 OP_WRITE 事件
client.register(selector, SelectionKey.OP_WRITE);
} else if (bytesRead < 0) {
// 客户端断开连接
client.close();
}
} else if (key.isWritable()) {
// 处理写事件,立刻返回结果
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.wrap("Hello, Client!".getBytes());
client.write(buffer); // 将客户端通道注册到 Selector 并监听 OP_READ 事件
client.register(selector, SelectionKey.OP_READ);
} keyIterator.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}

上面的代码创建了一个基于 Java NIO 的简单 TCP 服务器。它使用 ServerSocketChannel 和 Selector 实现了非阻塞 I/O 和 I/O 多路复用。服务器循环监听事件,当有新的连接请求时,接受连接并将新的 SocketChannel 注册到 Selector,关注 OP_READ 事件。当有数据可读时,从 SocketChannel 中读取数据并写入 ByteBuffer,然后将数据从 ByteBuffer 写回到 SocketChannel。

四、总结

到这里基本上就把NIO的几个重要的组件介绍完啦,肯定不能面面俱到,大家想更多了解的,还是要多翻看不同的书籍。同时,后面我们将基于这部分内容,写一个小型的聊天室。

NIO的三大核心组件详解,充分说明为什么NIO在网络IO中拥有高性能!的更多相关文章

  1. Java网络编程和NIO详解9:基于NIO的网络编程框架Netty

    Java网络编程和NIO详解9:基于NIO的网络编程框架Netty 转自https://sylvanassun.github.io/2017/11/30/2017-11-30-netty_introd ...

  2. Java网络编程和NIO详解4:浅析NIO包中的Buffer、Channel 和 Selector

    Java网络编程与NIO详解4:浅析NIO包中的Buffer.Channel 和 Selector 转自https://www.javadoop.com/post/nio-and-aio 本系列文章首 ...

  3. Java网络编程和NIO详解2:JAVA NIO一步步构建IO多路复用的请求模型

    Java网络编程与NIO详解2:JAVA NIO一步步构建IO多路复用的请求模型 知识点 nio 下 I/O 阻塞与非阻塞实现 SocketChannel 介绍 I/O 多路复用的原理 事件选择器与 ...

  4. Mysql 三大特性详解

    Mysql 三大特性详解 Mysql Innodb后台线程 工作方式 首先Mysql进程模型是单进程多线程的.所以我们通过ps查找mysqld进程是只有一个. 体系架构 InnoDB存储引擎的架构如下 ...

  5. ansible安装与核心组件详解

    第1章 安装anisble 1.1 安装epel源 rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-6.noarc ...

  6. 详解BOM用途分类及在汽车企业中的应用

    摘要:在整车企业中,信息系统的BOM是联系CAD.CAPP.PDM和ERP的纽带,按照用途划分产品要经过产品设计,工程设计.工艺制造设计.生产制造4个阶段,相应的在这4个过程中分别产生了名称十分相似但 ...

  7. Java网络编程与NIO详解4:浅析NIO包中的Buffer、Channel 和 Selector

    微信公众号[黄小斜]作者是蚂蚁金服 JAVA 工程师,目前在蚂蚁财富负责后端开发工作,专注于 JAVA 后端技术栈,同时也懂点投资理财,坚持学习和写作,用大厂程序员的视角解读技术与互联网,我的世界里不 ...

  8. IO模型之NIO代码及其实践详解

    一.简介 NIO我们一般认为是New I/O(也是官方的叫法),因为它是相对于老的I/O类库新增的( JDK 1.4中的java.nio.*包中引入新的Java I/O库).但现在都称之为Non-bl ...

  9. 详解API Gateway流控实现,揭开ROMA平台高性能秒级流控的技术细节

    摘要:ROMA平台的核心系统ROMA Connect源自华为流程IT的集成平台,在华为内部有超过15年的企业业务集成经验. 本文分享自华为云社区<ROMA集成关键技术(1)-API流控技术详解& ...

  10. Keepalived详解(五):Keepalived集群中MASTER和BACKUP角色选举策略【转】

    一.Keepalived集群中MASTER和BACKUP角色选举策略 在keepalived集群中,其实并没有严格意义上的主.备节点,虽然可以在keepalived配置文件中设置state选项为MAS ...

随机推荐

  1. Http 代理工具 实战 支持网页与QQ代理

    前言: 有些公司不让员工上Q或封掉某些网站,这时候,干着急没办法,只能鄱墙.如果上网搜代理IP,很少能用,用HTTP-Tunnel Client代理软件,免费的也是经常性的掉线.正好手头上有N台服务器 ...

  2. Uni-app极速入门(二) - 登录demo

    需求 背景 1.进入小程序,默认页面判断用户是否已经登录,已经登录则进入首页,没有登录则进入登录页面 2.首页为tabbar,包括首页和设置页,设置页可以退出登录,回到登录页面 页面流转 graph ...

  3. IPsecVPN 服务器一键安装脚本

    IPsec VPN 服务器一键安装脚本 使用 Linux 脚本一键快速搭建自己的 IPsec VPN 服务器.支持 IPsec/L2TP, Cisco IPsec 和 IKEv2 协议.你只需提供自己 ...

  4. opensuse tw快速部署

    使用GUI快速配置opensusetw 先看官方配置指南 换源 清华源之oss+non-oss links 清华源之packman links sudo zypper ar -cfg 'https:/ ...

  5. Centos7无法ping通内网、外网

    主要检查网络的配置是否正确,我测试时使用的是VMware虚拟机,需要保证centos中的网络配置和VMware中的一致. (1)VMware的配置 网络适配器选择NAT模式 查看NAT设置,这里需要记 ...

  6. 记录nodejs做编辑和新增时候对数据库的操作

    server.js文件 const dao = require("../dao/user.dao"); saveDat是个对象自己处理一下 if (updataFlag) {//编 ...

  7. php分页查询 子查询

     分页查询                 将查询结果只显示一部分                 通过两个参数:参数1 起始数据的索引下标                             参 ...

  8. HP惠普战66电源黄灯闪烁无法充电

    HP惠普战66电源黄灯闪烁无法充电 TYPE-C PD 无法充电. 解决办法:关机状态下,拔除外部设备,长按电源键30秒以释放主板静电,再插电源线可以开机.

  9. sshd服务部署

    sshd服务部署 软件安装修改配置文件启动使用​ 1.搭建所有服务的套路 关闭防火墙和selinux(实验环境都先关闭掉) 配置yum源(公网源或者本地源) 软件安装和检查 了解并修改配置文件 启动服 ...

  10. 导出excel文件接口代码示例

    导出excel文件接口代码示例 1.该导出接口,token不能通过请求头来传输,需要在get请求的参数中带出来2.验证token的方法除了在拦截器中统一拦截,针对get接口传参数的方式也需要单独在接口 ...