1、同步阻塞式IO - NIO
BIO就是Blocking IO的简称,Java中BIO是由ServerSocket负责绑定IO地址启动监听端口等待客户端连接。客户端的Socket类发起连接。 BIO的阻塞主要体现在:
- 服务端程序启动后,会一直等待客户端的连接,在此期间线程是阻塞的,不能干其他的事。
- 在连接建立后,在读取到socket数据之前,线程仍然是阻塞状态。
2、同步非阻塞式IO - NIO
NIO是JDK1.4版本引入的,NIO弥补了BIO的种种不足,BIO是面向字节流,而NIO则是面向缓冲区。 BIO在调用read、write等方法时,线程会阻塞,直到有数据到达。NIO的非阻塞模式下,线程从某个通道读取数据,如果没有可用的数据时候就不会阻塞,可以去做其它事情。一个独立的线程管理多个输入和输出通道,这样可以将阻塞等待时间拿来在其它通道上执行IO操作。 NIO有三大核心组件:
2.1 Selector 选择器
Selector选择器又称为事件订阅器、轮询代理器,Java的NIO的Selector选择器允许一个单独的线程来监视多个通道,然后使用一个单独的线程拿来操作这个选择器。 客户端向Selector注册它所关注的某个通道Channel以及关注的哪些事件(例如连接事件、读取事件等),当然Selector中会维护一个Channel列表。
2.1.1 SelectionKey
这个key代表channe通道在Selector上注册的唯一标识。
2.1.2 Selector注册事件类型
- `OP_READ 读事件`:当读缓冲区有数据可读时触发的事件。
- `OP_WRITE 写事件`:当写缓冲区有空间空间时触发的事件。
- `OP_CONNECT 连接事件`:#connect( ) 请求连接成功后触发的事件,给客户端使用。
- `OP_ACCEPT 连接请求事件`:#accetp( ) 当接收到一个客户端连接请求时触发的事件,给服务端使用。
2.2 Channels 通道
channel通道,是对应用程序和操作系统交互数据通道的抽象。应用程序可以通过通道读取数据也可以通过通道向操作系统写入数据。
2.3 Buffer 缓冲区
JDK的NIO是面向缓冲区的,这个Buffer就是缓冲区,用于和通道进行交互。 数据从Channel读入缓冲区,也可以从缓冲区写入通道。Buffer的本质其实就是字节数组,JDK提供了一系列的方法来操作访问这个Buffer对象。
2.3.1 Capacity
代表Buffer缓冲区的容量。
2.3.2 Position
代表当前写入数据的相对偏移量。
2.3.3 Limit
写模式下,limit代表最多能往Buffer里面写多少数据,此时limit == capacity。 读模式下,limit代表最多能读多少数据。
2.3.4 Buffer缓冲区的分配
- `HeapByteBuffer`:在JVM的堆空间中分配,在发送阶段会先从堆空间拷贝到直接内存,然后再通过操作系统的内核态切换和拷贝到网卡缓冲区。
- `DirectByteBuffer`:直接分配在直接内存,性能上相对于堆内存更快,但是在大容量或者频繁的申请直接内存可能导致性能下降。
3、Reactor模型
Reactor模型是对事件处理流程的一种模式抽象,是对IO多路复用模式的一种封装,Reactor又叫反应器,在这里特指的是对各种事件的反应处理。Reactor模型有2个重要的组件:
- `Reactor`:专门用于监听和响应各种IO事件,例如连接事件、读事件、写事件等,当检测到有一个新的事件发生时,就会交给相应的Handler去处理。
- `Handler`:专门用来处理特定的事件的执行者。
3.1 单Reactor单线程模型
- 服务端的Reactor线程对象,不断循环监听各种IO事件,还会注册一个accepter的特殊Handler到Reactor中,这个accepter专门负责处理连接的事件。
- 客户端发起请求,服务端的Reactor监听到accept事件,然后将这个accept事件分派给accepter组件,accepter组件通过#accept( )方法和客户端建立对应的channel。然后将这个连接所关注的read事件注册到Reactor中,这样Reactor就会监听read事件的发生。
- 当服务端Reactor监听到read事件,将这个read事件发送给对应的读请求Handler进行数据的读取。
3.1.1 单Reactor单线程模型的优点
最基础的Reactor模型,实现简单,不用考虑并发问题。JDK的NIO的Selector选择器就是最简单的单Reactor单线程模型。
3.1.2 单Reactor单线程模型的缺点
Reactor和Handler的所有操作都是在同一个线程中完成的,无法充分利用多核优势,性能不佳。 Redis6.0之前就是典型的单Reactor单线程模型,虽然6.0以后引入了多线程,但是它的多线程只是用来处理耗时的网络IO操作上,实际执行命令的Handler仍然是单线程。
3.2 单Reactor多线程模型
大体上单Reactor多线程模型和基本的单Reactor单线程模式差不多,只不过单Reactor多线程模型多引入了一个线程池,这个线程池负责将一些非IO操作的事件进行处理,例如计算、编解码任务等。 虽然这种模式下引入了线程池,效率得到了一定的提升,但是毕竟是采用单Reactor架构,所有的事件都是交给单个Reactor负责,在面对瞬间的高并发连接场景,单Reactor多线程模型仍然性能不佳。
3.3 多Reactor多线程模型(主从Reactor模型)
为了优化单Reactor模型的性能瓶颈,将原来单独的Reactor的功能进行分解为连接处理器和通信处理器,由多个不同的Reactor共同完成网络通信任务。
- 主Reactor拥有自己的Selector,通过select监控连接事件,事件发生后交给accepter组件处理,然后accepter组件将连接分配给某一个从Reactor。
- 从Reactor也有自己的Selector,从Reactor监听并执行读、写等事件。
- 线程池的任务没有变化,负责处理非IO的事件任务,例如编解码序列化、计算等。
- 主从Reactor模型中,主Reactor和从Reactor都可以存在多个,每个Reactor都有自己的Selector,都是独立的线程工作,这样充分利用了多核CPU的优势。
但是多Reactor多线程架构仍然不能根治IO操作对其他Client的效能影响,毕竟有可能某个从Reactor可能有多个client的连接。所以诞生了异步IO模型的Proactor模型来实现真正的异步IO。 Netty、Memcached、Nginx都是采用的多Reactor多线程的模型。不过Netty支持多种Reactor模型的配置。
4、Linux网络IO模型
4.1 同步和异步、阻塞和非阻塞的区别
- `同步`:调用方需要主动等待结果的返回。
- `异步`:调用方调用了函数不需要等待结果的返回,后续通过回调等手段来处理结果。
- `阻塞`:在调用方拿到结果之前,线程会被挂起,不能做任何事。
- `非阻塞`:在调用方拿到结果之前,线程可以执行一些其他的事情,不会被挂起。
4.2 同步、异步、阻塞、非阻塞的相互组合模式
- `同步阻塞模式`:最常见的组合模式,调用方在拿到结果之前,线程处于挂起状态,直到有数据触达为止。
- `同步非阻塞模式`:调用方发起了函数调用,通过轮询的方式来检查结果,这个过程线程并未阻塞,但是轮询的目的是等待结果的返回。
- `异步阻塞模式`:最不常用的方式,这种方式没有发挥出异步的真实效果。
- `异步非阻塞模式`:调用方发起函数调用后,给接受方一个回调函数,然后自己去执行其他的方法。后续接受方有了数据后主动调用回调函数来实现结果的返回。
4.3 Linux的5种IO模型
Linux的5种IO模型:阻塞IO、非阻塞IO、IO多路复用、信号驱动IO、异步IO,前面四种都是同步的模式,只有最后一种是异步的。 注:Java的BIO和Linux的BIO是一致的,但是Java里面的NIO对应的是Linux的IO多路复用。
下面几张图片取材自百度,因为比较通用我就没有自己画了。
4.3.1 阻塞式IO(BIO)
4.3.2 非阻塞式IO(NIO(并非Java里面的NIO))
4.3.3 IO多路复用
IO多路复用的方式会有2次系统调用,从这一点看并不比BIO有优势。IO多路复用相比于BIO来说最大的优势就是可以用select少量的线程处理多个连接请求。 所以,在连接量少的情况下,BIO的效率反而更好,IO多路复用则是在大连接量场景有优势。
4.3.4 信号驱动式IO
通过进程的SIGIO信号作为事件驱动来进行IO的处理,这种模式运用比较少。
4.3.5 异步IO模型
5、Linux的IO多路复用
IO多路复用机制就是通过一个进程可以监视多个文件描述符,一旦有socket描述符就绪,就能够通知应用程序做对应的读写处理。
5.1 文件描述符 File Descriptor(FD)
在Linux系统中,文件、外接设备、套接字socket都视为文件,统一文件接口方便操作系统访问。同理,Socket套接字也是一种文件,当应用程序请求内核打开一个文件时,内核就会为此返回一个文件描述符。
5.2 IO多路复用的实现方案
Linux的IO多路复用有3种实现手段:select、poll、epoll:
5.2.1 select
select函数可以监视3类文件描述符:writeFds、readFds、execptFds。调用select函数后进程会阻塞,直到有就绪的文件描述符。当select又返回时,就会唤醒进程来处理io事件,而且是会遍历整个描述符数组来处理就绪的io事件。 select的优点是跨平台支持,缺点是单个进程的文件描述符是有OS控制的,Linux最大是1024个,但是可以修改。另一个缺点就是需要遍历整个文件描述符数组。
5.2.2 poll
和select十分相似,只不过poll对文件描述符的管理时基于链表,而且poll没有限制描述符数量,当连接量较大时性能会急剧下降。和select一样,调用poll函数后,一旦有就绪事件需要轮询整个链表。
5.2.3 epoll
epoll是在Linux2.6版本推出的一种IO多路实现手段。相对于poll和select,epoll做了很大优化和改进。
epoll的优化思路有2点:
- 功能分离:创建epoll、socket监视注册、等待数据分为3个API实现。
- 就绪列表:select和poll低效的根本原因是要遍历所有的socket,epoll则维护了一个就绪列表,引用了收到了数据的socket,这样就避免了全链表扫描。
当socket收到数据后,中断程序会操作eventpoll,将对应的socket挂载到就绪列表里面,后续恢复进程来处理,此时只需要处理就绪列表里面的socket。 当进程调用epoll_creat函数时,Linux内核会创建一个Eventpoll结构体,然后在内核缓冲区中创建一个红黑树来存储后续连接注册的socket,然后也会在Eventpoll中创建一个就绪链表。后续只需要监听这个链表有没有就绪的socket即可。 epoll相对于select和poll有了很大的优势,保持数万数十万连接也不是问题。但是在极端情况下,例如就序列表的元素十分多时,也会有瓶颈。
5.3 触发方式
5.3.1 水平触发 LT
当被监控的文件描述符上有可读写事件发生时,epoll_wait( )会通知应用程序去读写,如果这次读写没有处理完数据,那么下次调用epoll_wait( )是,OS还会继续通知应用程序继续读写,如果你一直不处理完数据,OS会一直通知你。
5.3.2 边缘触发 ET
和水平触发不同的是,应用程序没有处理完这一次通知事件的数据时,调用epoll_wait( )时OS并不会通知你有数据读写事件,直到这个文件描述符上出现第二次可读写事件发生时才通知应用程序。这种方式相比LT,ET更加清净,一般来说ET性能要高于LT,但是实际应用中看不出有啥大区别。
作者:Minor
链接:https://juejin.cn/post/7120529881229852685
来源:稀土掘金
网友评论