慢吞云雾缓吐愁 发表于 2024-9-1 04:51:32

一文搞懂网络IO和java中的IO模子

目录
1.绪论
2.IO分类
3.用户空间和内核空间
4.同步阻塞IO
5.同步非阻塞IO
6.IO多路复用
6.1 根本原理
6.2 linux对IO多路复用的实现方式
6.3.1 select
1.实现原理
2.缺点
6.3.2 poll
1.实现原理
6.3.3 epoll
1.epoll数据结构
2.epoll的函数
3.epoll的长处
4. epoll的两种触发模式
6.3 reactor模子
7.信号驱动IO
7.1 原理
7.2 缺点
8.异步IO
8.1 原理
8.2 缺点
9. 同步 VS 异步 / 阻塞 VS 非阻塞
9.1 同步 VS 异步
9.2 阻塞 VS 非阻塞
10.总结
11.引用

1.绪论

只要涉及网络传输,就肯定需要IO,而高性能的网络IO也是保证框架性能的基石。好比redis性能如此高的原因主要有两个,一是高性能的网络组件,二是redis的险些全部操纵都是基于内存。以及大名顶顶的网络框架Netty和Tomcat,有如此高的性能,都离不开优秀的网络IO模子计划。
2.IO分类

在《Unix网络编程》这本书中将网络IO分为了5类,主要是同步阻塞IO,同步非阻塞IO,异步IO多路复用,信号驱动,异步非阻塞IO这5类。而在java中又有BIO、NIO、AIO这三种,那他们的关系是什么呢。
BIO:其实就是同步非阻塞IO。
NIO:NIO英文名称其实是New IO,它对应的其实是IO多路复用,而IO多路复用其实是对同步非阻塞IO的优化,以是将NIO称之为同步非阻塞IO也不无道理。
AIO:其实就是异步非阻塞IO。
3.用户空间和内核空间

在前面讲Mmap的时候,我们说过计算机为了保证内核安全,不允许用户直接操控驱动步伐对硬件举行修改。而是操纵体系向用户袒露接口,用户假如要操纵内存大概CPU需要调用操纵体系提供的接口完成操纵。
https://i-blog.csdnimg.cn/direct/638c5b93576d4bb39c09a0aea18ff7d1.png
而计算机为了保证操纵体系的运行不被用户步伐访问到,以是将寻址空间(简单来说就是内存)分为两部分,分别是内核空间和用户空间。用户假如要读写数据,需要先将数据读入到内核空间的缓存中,然后拷贝到用户空间的缓存中。同理,写数据时,也需要先将数据写入到用户空间缓存,再拷贝到内核空间缓存,最后写入到磁盘大概网卡中。
https://i-blog.csdnimg.cn/direct/edfa2b41802a4fcb82c160df21c33748.png

4.同步阻塞IO

同步阻塞IO其实就是java中的BIO,它的步调如图所示。
https://i-blog.csdnimg.cn/direct/f645344e1ede459bac73fbe002bd52ba.png
可以看出同步阻塞在用户历程调用内核的recvfrom方法后,会一直等待,直到结果返回。在内核缓冲区无数据并且拷贝期间,用户线程会一直等待。
同步阻塞IO的第一个阶段也是阻塞状态,第二个阶段也是阻塞状态。
5.同步非阻塞IO

https://i-blog.csdnimg.cn/direct/81f22d02b77546eebe83a6750dd0159a.png
可以看出同步非阻塞IO是会一直循环调用recvfrom方法,扣问内核历程数据是否到达,他和同步阻塞IO的主要区别是在数据未停当的时候,线程并不会阻塞,而是一直巡扣问操纵体系。
同步非阻塞IO只管在数据未停当的时候未阻塞,但是它在这段时间内并没有干其他事变,而是一直在与操纵体系交互,这样其实读取数据的泯灭时间和同步阻塞IO是一样的,而且会频繁与CPU交互,性能可能更低。那为什么还会出现这种IO模子呢?现在的同步非阻塞IO是一个线程读取数据的时候都会与操纵体系举行交互,判断当前是否数据停当。那我们可以不可以让一个专门的线程来替多个线程去扣问操纵体系是否停当呢?答案是可以的,就是后面将要先容的IO多路复用。
同步非阻塞IO的第一个阶段也是非阻塞状态,第二个阶段是阻塞状态。
6.IO多路复用

6.1 根本原理

https://i-blog.csdnimg.cn/direct/f91ffddd01084865b2ff450f676c63ca.png
可以看出IO多路复用的步调如下:
1.历程调用操纵体系提供的select函数,监听多个socket连接,假如全部socket都没有数据,历程便会阻塞等待;
2.假如某个socket数据停当后,便会给历程返回readable,并唤醒历程;
3.历程调用操纵体系的recvfrom方法,将数据从内核缓冲区拷贝到用户缓冲区;
4.给用户历程返回结果。
IO多路复用主要是使用select方法,可以监听多个socket,这也是其性能高的原因。
6.2 linux对IO多路复用的实现方式

在linux中,万物皆文件,而每个文件都有句柄-文件形貌符fd来表示。而linux中的IO多路复用就是使用一个历程监听多个fd。一样平常有三种实现方式,select,poll,epoll。
6.3.1 select

1.实现原理

//类型别名,__fd_mask其实就是long int的别名
typedef long int __fd_mask;
typedef struct {
   //fds_bits是一个长度为 1024/32 = 32 位的long数组
   //c语言的long类型占32个字节,所以这个数组共有1024位
    //如果位为0表未就绪,为1表示就绪
    __fd_mask fds_bits;
...
} fd_set;

//linux提供的select函数
int select(int nfds, //需要监听的最大的文件描述符值+1
   fd_set *readfds, //需要监听的读事件的文件描述符位数组
   fd_set *writefds, //需要监听写事件的文件描述符数组
   fd_set *exceptfds, //需要监听异常时间的fd数组
   struct timeval *timeout //如果超过改时间,还是没有数据,便返回,null:永不超时,0:不等待,大于0:固定等待时间
); 步调如下:
1.历程假如想听多个fd,select提供了3个长度为32的long范例的数组(总共用1024位),将这个数组的需要监听的fd设置为1,并且拷贝到内核空间中,这里以读fd(readfds)数组为例子;
2.内核空间假如没有数据到达,便休眠,假如有fd到达,会将对应的readfds中对应位置设置为1,并且拷贝到用户空间;
3.用户空间变量readfds数组,得到哪些fd停当;
4.针对这些fd,调用revcfrom函数,将对应fd在内核缓冲区中的数据拷贝到用户缓冲区。
https://i-blog.csdnimg.cn/direct/36ea38ebc2ce4e588dc9c24def51b516.png
2.缺点

其实从上面的实现我们也看出主要缺点如下:
1.select采用1024位的数组来存储哪些fd需要被监听,以是select最多只支持同时监听1024个fd;
2.select会频繁的将需要监听的fd数组从内核空间到用户空间相互拷贝;
3.select函数不会返回具体哪些fd已经停当,而是整个fds数组,以是用户空间想要获取到哪些fd停当,需要遍历整个数组。
6.3.2 poll

poll是对select的改进
1.实现原理

struct pollfd {
    int fd; //需要监听的fd
    short int events; //想要监听的事件类型
    short int revents; //实际发生的事件类型
}
//poll函数
int poll(struct pollfd *fds,//需要监听的事件,采用链表存储
    nfds_t nfds, //需要监听的pollfd个数
    int timeout; 超时时间
); poll相对于select其实就解决了上面的select采用1024位的数组来存储fd导致select每次最多只能监听1024个fd问题。poll采用链表存储,理论上监听的fd个数没有上限,但是假如返回的还是整个链表,以是用户历程想要获取到哪些fd被监控,还是需要遍历整个链表,若监听的fd数目很多,反而性能很差。
6.3.3 epoll

1.epoll数据结构

epoll在内存空间,采用红黑树来存储需要监听fd,用链表来存储停当的fd。
2.epoll的函数

epoll向用户空间提供了几个函数来操纵器内部的数据结构:
epoll_create:创建一个epoll实例,内部是event poll,返回对应的句柄epfd int epoll_create(int size);

int epoll_create(int size);
epoll_ctl:用户历程调用epoll_ctl可以将待监听的fd加到红黑树上去,并且为期其绑定一个回调函数,当该fd停当的时候,会将这个fd加入到停当链表头部;
// 2.将一个FD添加到epoll的红黑树中,并设置ep_poll_callback
// callback触发时,就把对应的FD加入到rdlist这个就绪列表中
int epoll_ctl(
    int epfd,  // epoll实例的句柄
    int op,    // 要执行的操作,包括:ADD、MOD、DEL
    int fd,    // 要监听的FD
    struct epoll_event *event // 要监听的事件类型:读、写、异常等
);
epoll_wait:调用该函数会在用户态创建一个event数组,用于吸收停当的fd,如在监听的红黑树中有fd到达,会触发回调函数,并将其加入到停当链表中。并将数据加入到events数组中,拷贝给用户历程,此时events数组中就是已经停当的fd。
// 3.检查rdlist列表是否为空,不为空则返回就绪的FD的数量
int epoll_wait(
    int epfd,                   // epoll实例的句柄
    struct epoll_event *events, // 空event数组,用于接收就绪的FD
    int maxevents,              // events数组的最大长度
    int timeout   // 超时时间,-1用不超时;0不阻塞;大于0为阻塞时间
);
3.epoll的长处

其实上面epoll已经解决了select的三个问题:
1.存储长度问题:采用红黑树来存储需要监听的fd,理论上没有上限;
2.频繁拷贝问题:用户需要监听某个fd的时候只需要调用epoll_ctl方法将其加入到红黑树上即可;
3.结果遍历问题:epoll采用单独的停当链表来存储停当的fd,以是只会将停当的fd拷贝大用户空间传过来的events数组中。
4. epoll的两种触发模式

当我们调用epoll_wait的时候,可以得到事故通知,epoll有两种事故通知方式:


[*] LevelTriggered:简称LT,也叫做水平触发。只要某个FD中有数据可读,每次调用epoll_wait都会得到通知。
[*] EdgeTriggered:简称ET,也叫做边沿触发。只有在某个FD有状态变革时,调用epoll_wait才会被通知。
主要区别是:假如内核缓冲区中数据较多,一次性不能完全拷贝到用户缓冲区中,假如是LT模式,下一次这个fd还会再停当链表上面,而ET模式,下一次读取便不会再停当链表上。我们一样平常采用LT模式。
6.3 reactor模子

reactor模子主要是将主要分成两部分,分别是selector和handler,selector其实就是调用前面的epoll_wait函数,等待客户端的创建连接哀求大概读写哀求,假如哀求很多,selector也可以交给线程处理。handler主要根据对应的哀求范例交给差别的线程池处理。这其实对应的就是netty中的boossGroup和workerGroup。步调如下:
1.客户端发现创建连接哀求给selector;
2.selector监听到客户端哀求后,发现是创建连接,范例为accept,交给acceptor处理;
3.accptor会创建为该客户端创建一个chnnel并且注册到selector上去,监听事故为读写;
4.客户端发送读哀求给selector,selector判断事故范例为read,将其给处理读IO操纵的handler处理。
https://i-blog.csdnimg.cn/direct/4a3da694c43b4ad98b2a1447b8e68f10.png
7.信号驱动IO

7.1 原理

信号驱动IO步调如下:
1.用户历程调用sigaction函数,内核函数会监听对应fd,此时用户历程不用阻塞可以做其他操纵。
2.当用数据停当时,递交回调信号给用户历程;
3.用户历程收到回调信号过后,调用revcfrom函数将内核空间的数据拷贝到用户空间。
https://i-blog.csdnimg.cn/direct/f3503f36d5f84220871ad0c5e2e175d9.png

7.2 缺点

1.当IO操纵过多时,SIGIO处理函数不能实时处理可能导致信号队列溢出;
2.内核空间与用户空间的频繁信号交互性能也较低。
8.异步IO

8.1 原理

1.用户历程调用aio_read函数,并且给信号绑定一个回调函数;
2.用户历程等待数据,假如数据到达,将数据从内核空间拷贝到用户空间,并且给用户历程返回信号;
3.触发用户历程中信号绑定回调函数,处理数据。
https://i-blog.csdnimg.cn/direct/3cc7c2a4c3bb40b9a599e20460dd66b4.png
8.2 缺点

异步IO在等待数据和将数据从内核空间拷贝到用户空间这整个过程,用户历程都是不阻塞的。以是其性能特殊高,但是和信号驱动一样,假如IO特殊多,可能导致信号队列溢出等问题。
9. 同步 VS 异步 / 阻塞 VS 非阻塞

9.1 同步 VS 异步

其实从上面可以看出,同步和异步的最主要的区别是第二阶段从内核缓冲区拷贝到用户缓冲区这个过程是否需要阻塞等待。同步IO会阻塞等待,而异步IO不会。
9.2 阻塞 VS 非阻塞

阻塞和非阻塞的主要确保在第一阶段,等待数据停当这个过程是用户历程是否会阻塞等待。
10.总结

本文主要先容了网络中的几种IO模子,并且分析了他们的优缺点。在现在各种框架中,最常用的还是IO多路复用这一模子。同步阻塞IO和同步非阻塞IO在整个过程中,可以以为用户历程是阻塞的,这两种模子的吞吐量是相对较低的。信号驱动IO在等待数据的过程中是非阻塞的,异步IO在整个过程中都是非阻塞的。按理想情况,这两种IO模子应该是吞吐量很大的,但是假如在并发很高的场景下,可能导致内核历程为特殊多的信号监听fd,导致吞吐量降低。以是IO多路复用是同步阻塞IO和异步IO的折中,它联合reactor模子,也可以大概拥有很好的吞吐量,成为现在的主流选择。
11.引用

图解Linux select机制_从内核到应用
Redis入门到实战教程,深度透析redis底层原理+redis分布式锁+企业解决方案+黑马点评实战项目
一文搞懂Reactor模子与实现



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 一文搞懂网络IO和java中的IO模子