先说明一个问题:在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。网络编程其实就是多了一个连接的过程,常用在Netty一些框架。本文主要讲述标准输入输出NIO
一.BIO,NIO,AIO的区别
先说一些IO的模式
- 我们发现,IO不是我们想象中那么简单。要想进行IO操作,需要发一个请求给CPU,CPU得到通知后,此时CPU就需要调用操作系统内核程序(磁盘控制器)。这就是用户态->内核态。
- 磁盘控制器接到通知,使用DMA技术将数据放到PageCache中,再由CPU把数据从内核程序传回用户程序(buffer)。这就是内核态->用户态。
- 关于上图的操作系统知识,如果不是清楚,请先移步操作系统,分3章讲清楚操作系统核心内容。
根据上面的模型,其实我们可以把一次IO粗略的分为2个步骤:「内核数据准备好」和「数据从内核态拷贝到用户态」
- 同步IO:
- 阻塞I/O:两个步骤都要等待。(BIO)
- 适用于连接数目比较小,且固定的架构
- 阻塞I/O:两个步骤都要等待。(BIO)
-
- 非阻塞I/O:第一个步骤无序等待,但是会一直询问内核是否准备好。(BIO的多线程版)
-
- 多路复用I/O:在非阻塞的前提下优化,不会一直询问。但是多一个select期。(NIO)
- 适用于连接数目多且连接比较短的操作(轻操作)。比如rpc框架,聊天服务器等。
- 多路复用I/O:在非阻塞的前提下优化,不会一直询问。但是多一个select期。(NIO)
- 异步I/O: 两个步骤都无需等待,询问完交给内核自动完成。(AIO)
- 适用于连接数目多且连接比较长的操作(重操作),比如相框服务器,充分调用OS参与并发操作。
多路复用的系统方法说明:一直在升级
- select:用一个线程,接收到IO的请求,就将这些进行IO的文件描述符复制一份到内核,然后内存进行遍历,看哪个文件已经准备好数据,将准备好的文件描述符的个数返回给用户态,用户态再进行遍历,找到是哪个文件描述符的IO。有3个缺点如下
- select 调用需要传入 fd 数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。(可优化为不复制)
- select 在内核层仍然是通过遍历的方式检查文件描述符的就绪状态,是个同步过程,只不过无系统调用切换上下文的开销。(内核层可优化为异步事件通知)
- select 仅仅返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的文件描述符,无需用户做无效的遍历)
- poll:它和 select 的主要区别就是,去掉了 select 只能监听 1024 个文件描述符的限制。
- epoll:epoll 是最终的大 boss,它解决了 select 和 poll 的一些问题。
- 内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分即可。
- 内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒。
- 内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。
这里附上一个大佬写的关于IO多路复用的动画:IO多路复用
二.NIO的组成
Channel:
- 数据传输的双向通道,既可以用来读数据又可以用来写数据。主要实现类如下:
- FileChannel:文件IO
- DatagramChannel:UDP的IO
- SocketChannel 和 ServerSocketChannel:TCP的IO
Buffer:
- 用于和 NIO 通道进行交互。
- Buffer 是一个顶层父类,它是一个抽象类,常用的 Buffer 的子类有:ByteBuffer、 IntBuffer、 CharBuffer、 LongBuffer、 DoubleBuffer、 FloatBuffer、ShortBuffer等
- 下图为网络IO的一个简单图
Selector:
- 选择器 ,也可以翻译为 多路复用器 。它是 Java NIO 核心组件中的一个,用于检查一个或多个 NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个 channels,也就是可以管理多个网络链接,就可以避免线程上下文切换。一个Selector对应一个线程,
三.Buffer中对应的Position, Mark, Capacity,Limit都啥?
- capacity:缓冲区容量的大小,就是里面包含的数据大小。
- limit:对buffer缓冲区使用的一个限制,从这个index开始就不能读取数据了。
- position:代表着数组中可以开始读写的index, 不能大于limit。
- mark:是类似路标的东西,在某个position的时候,设置一下mark,此时就可以设置一个标记
-
后续调用reset()方法可以把position复位到当时设置的那个mark上。去把position或limit调整为小于mark的值时,就丢弃这个mark,如果使用的是Direct模式创建的Buffer的话,就会减少中间缓冲直接使用DirectorBuffer来进行数据的存储。
-
四.Java中的IO流(BIO):
分类
- 按照操作数据单位不同:字节流(8bit),字符流(16bit)
- 按照数据流的流向不同:输入流,输出流
- 按照流的角色分为:
- 节点流:直接从数据源或目的地读写数据
- 处理流:不直接连接到数据源或目的地,而是连接在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提 供更为强大的读写功能。
实现类:
寄语:我努力奔跑是为了追上那个曾经被寄予厚望的自己