常見的網絡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 區別 和 底層實現-掘金