常见的网络IO模型
网络 IO 模型分为四种:同步阻塞 IO(Blocking IO, BIO)、同步非阻塞IO(NIO, NewIO)、IO 多路复用、异步非阻塞 IO(Async IO, AIO),其中AIO为异步IO,其他都是同步IO
同步阻塞IO
同步阻塞IO:在线程处理过程中,如果涉及到IO操作,那么当前线程会被阻塞,直到IO处理完成,线程才接着处理后续流程。如下图,服务器针对客户端的每个socket都会分配一个新的线程处理,每个线程的业务处理分2步,当步骤1处理完成后遇到IO操作(比如:加载文件),这时候,当前线程会被阻塞,直到IO操作完成,线程才接着处理步骤2。
同步阻塞IO 演示图
实际使用场景
在Java中使用线程池的方式去连接数据库,使用的就是同步阻塞IO模型。
模型的缺点
因为每个客户端存都需要一个新的线程,势必导致线程被频繁阻塞和切换带来开销。
同步非阻塞 IO-NIO(New IO)
同步非阻塞IO:在线程处理过程中,如果涉及到IO操作,那么当前的线程不会被阻塞,而是会去处理其他业务代码,然后等过段时间再来查询 IO 交互是否完成。如下图:Buffer 是一个缓冲区,用来缓存读取和写入的数据;Channel 是一个通道,负责后台对接 IO 数据;而 Selector 实现的主要功能,是主动查询哪些通道是处于就绪状态。Selector复用一个线程,来查询已就绪的通道,这样大大减少 IO 交互引起的频繁切换线程的开销。
实际使用场景
Java NIO 正是基于这个 IO 交互模型,来支撑业务代码实现针对 IO 进行同步非阻塞的设计,从而降低了原来传统的同步阻塞 IO 交互过程中,线程被频繁阻塞和切换带的开销。
NIO使用的经典案例是Netty框架,Elasticsearch底层实际上就是采用的这种机制。
IO多路复用
- IO多路复用是一种同步IO模型,实现一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;没有文件句柄就绪时会阻塞应用程序,交出cpu。多路是指网络连接,复用指的是同一个线程
所以,每个客户端和服务器的socket 连接就可以看做”一路“,多个客户端和该服务器的socket连接就是”多路“,从而,IO多路就是多个socket连接上的输入输出流,复用就是多个socket连接上的输入输出流由一个线程处理。 因此 IO多路复用可以定义如下:
Linux中的 IO多路复用是指:一个线程处理多个IO流
IO多路复用3种实现方式
select/pool/epool
基本socket模型
先看下socket模型,以便与下面几种实现方式对比:
listenSocket = socket() // 系统调用socket(),创建一个主动socketbind(listenSocket) // 给主动socket绑定地址和端口listen(listenSocket) // 将默认的主动socket转换为服务器的被动socket(也叫监听socket)while(true) {connSocket = accept(listenSocket) // 接受客户端连接,获取已链接socketrecv(connSocket) // 从客户端读取数据,只能同时处理一个客户端send(connSocket) // 往客户端发送数据,只能同时处理一个客户端
}
实现网络通信流程如下图
基础的socket模型,能够实现服务器端和客户端的通信,但程序每调用一次accept函数,只能处理一个客户端请求,当有大量客户端连接时,这种模型处理性能较差,因此linux提供了高性能的IO多路复用机制来解决这种困境。
select机制
select是最古老的I/O多路复用机制,可以同时监听多个文件描述符的读写事件。它使用的fd_set数据结构来存储待监听的文件描述符集合,并通过select()函数将fd_set集合传递给内核,等待内核返回文件描述符的状态变化。
fd_set数据结构 (bitmap)
typedef struct {unsigned long fds_bits[__FDSET_LONGS];
} fd_set;
/**
* 参数说明
* 监听的文件描述符数量__nfds、
* 被监听描述符的三个集合*__readfds,*__writefds和*__exceptfds
* 监听时阻塞等待的超时时长*__timeout
* 返回值:返回一个socket对应的文件描述符
*/
int select(int __nfds, fd_set * __readfds, fd_set * __writefds, fd_set * __exceptfds, struct timeval * __timeout)
select实现网络通信流程如下图:
缺点
1、select使用的fd_set数据结构对单个进程能监听的文件描述符是有限制的,默认是1024
2、select()函数返回后,需要遍历文件描述符集合,才能找到就绪的描述符,遍历过程会产生一定开销,降低性能。
poll机制
poll与select类似,也可以同时监听多个文件描述符的读写事件。它使用的pollfd数据结构来存储待监听的文件描述符集合,并通过pool()函数将pollfd集合传递给内核,等待内核返回文件描述符的状态变化。相对于select,poll没有fd_set集合大小的限制,但并没有解决轮询获取就绪fd的问题,效率也不高。
pollfd结构体的定义
struct pollfd {int fd; //进行监听的文件描述符short int events; //要监听的事件类型short int revents; //实际发生的事件类型
};
poll实现网络通信流程如下图:
epoll机制
epoll是linux下最新的I/O多路复用机制,它使用红黑树数据结构来存储待监听的文件描述符集合,并通过epoll_create、epoll_ctl、epoll_wait等函数实现文件描述符的添加、删除、监听操作。相对于select和poll,epoll具有更高的效率和更好的扩展性。
epoll_event 结构体以及 epoll_data 结构体的定义
// 数据结构
// 每一个epoll对象都有一个独立的eventpoll结构体
// 用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
// epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
struct eventpoll {/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/struct rb_root rbr;/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/struct list_head rdlist;
};
epoll接口
1、int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。epoll 实例内部维护了两个结构,分别是记录要监听的fd和已经就绪的fd,而对于已经就绪的文件描述符来说,它们会被返回给用户程序进行处理。
2、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,epoll_ctl向 epoll对象中添加、修改或者删除感兴趣的事件,成功返回0,否则返回–1。此时需要根据errno错误码判断错误类型。它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。epoll_wait方法返回的事件必然是通过 epoll_ctl添加到 epoll中的。
3、int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents是events集合的大小,且不大于epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。函数返回需要处理的事件数目,返回0表示已超时,返回–1表示错误,需要检查 errno错误码判断错误类型。
epoll 进行网络通信的流程如下图:
ET模式与LT模式的区别
- epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。
- LT模式下,只要fd还有数据可读,每次epoll_wait都会返回它的事件,提醒用户去操作
- ET模式下,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。所以在ET模式下,read它的fd一定要把它的buffer读完,或者遇到EAGAIN错误
- 因此,在 LT模式下开发基于 epoll的应用要简单一些,不太容易出错,而在 ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。
3种机制底层实现的区别
select和poll都是通过轮询的方式,即内核每次要遍历监听的文件描述符集合,判断每个文件描述符是否有I/O事件发生;
而epoll底层实现是基于事件通知的方式,即当文件描述符状态发生变化时,内核会向应用程序发起事件通知,这种方式避免了无效的遍历,从而提高了效率。
在epoll中,使用epoll_wait函数进行事件监听时,内核将发生的事件文件描述符加入到一个就绪队列中,等待应用程序处理。如果就绪队列中没有任何文件描述符,则epoll_wait函数会阻塞,直到有文件描述符加入就绪队列,这种方式实现了I/O事件的高效处理和调度。
select | poll | epoll | |
---|---|---|---|
数据结构 | bitmap | 数组 | 红黑树 |
最大连接数 | 1024 | 无上限 | 无上限 |
fd拷贝 | 每次调用select拷贝 | 每次调用poll拷贝 | fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝 |
工作效率 | 轮询:O(n) | 轮询:O(n) | 回调:O(1) |
参考资料:
https://juejin.cn/post/6844904200141438984
IO多路复用机制详解 - 知乎
select poll epoll 区别 和 底层实现-掘金