本文主要從 select 和 epoll 系統調用入手,來打開 Netty 的大門,從認識 Netty 的基礎原理 —— I/O 多路複用模型開始。php
Netty 底層的通訊機制是基於I/O多路複用模型構建的,簡單一句話歸納就是多路網絡鏈接能夠複用一個I/O線程,在 Java 層面也就是封裝了其 NIO API,可是 JDK 底層基於 Linux 的 epoll 機制實現(實際上是三個函數)。注意在老舊的 Linux 上,可能仍是 select,沒考證過,可是時下主流版本,確定早就是 epoll 機制了,不妨就認爲 JDK NIO 底層是基於 epoll 模型。react
想象這樣一個場景:老師站在講臺上提問,下面100個學生把答案寫在紙上,誰寫完誰舉手示意,讓老師來檢查,完成的好就能夠放學回家。若是學生張三舉手,李四也舉手,就表示他們已經完成了,老師就當即依次去檢查張三和李四的答案,檢查完畢,老師就能夠返回講臺休息或者溜達等等,接着王五,趙四兒又舉手,而後老師立刻去檢查他們的答案。。。以此往復。編程
如上這種生活現象就是 I/O 多路複用模型,Linux下的 select、poll,和epoll 就是實現的這種機制,這樣就避免了大量的無用操做,好比,老師不須要依次的等待一個學生寫完了,而後檢查一個學生,檢查完畢,再去等待下一個學生。。。(對應多客戶端單線程模型),也不須要請100個老師,每一個老師對應1個學生(一客戶端一線程的 BIO 模型),而是讓全部學生先本身悶頭寫答案,寫完才主動舉手示意,老師在去檢查答案,處理完畢,老師就能夠走了,繼續等待其它學生舉手,全程一個老師就能處理(epoll 函數),這就是所謂的非阻塞模式。另外,老師也不須要順序的詢問每一個學生的問題完成狀況(select 函數)只須要看誰舉手。。。這樣老師不煩躁,學生也能專心答題。數組
類比到通訊,整個I/O過程只在調用 select、poll、epoll 這些調用的時候纔會阻塞,收發客戶消息是不會阻塞的,整個進程或者線程就被充分利用起來,從而使得系統在單線程(進程)的狀況下,能夠同時處理多個客戶端請求,這就是I/O 多路複用模型。與傳統的多線程(單線程)模型相比,I/O多路複用的最大優點就是系統開銷小,系統不須要建立新的額外線程,也不須要維護這些線程的運行、切換、同步問題,下降了系統的開發和維護的工做量,節省了時間和系統資源。服務器
主要的應用場景,服務器須要同時處理多個處於監聽狀態或多個鏈接狀態的套接字,服務器須要同時處理多種網絡協議的套接字。網絡
支持I/O多路複用的系統調用主要有select、pselect、poll、epoll。而當前推薦使用的是epoll,優點以下:數據結構
- 支持一個進程打開的socket fd(file description)不受限制
- I/O效率不會隨着fd數目的增長而線性下將
- 使用mmap加速內核與用戶空間的消息傳遞。
- epoll擁有更加簡單的API。
而常見的一種 I/O 多路複用模型有所謂的 reactor 模式,Netty 就實現了多線程的 reactor 模型(reactor 模型有三種,單線程,多線程和主從),即當有感興趣的事件(event)發生,就通知對應的事件處理器(ChannelHandler)去處理這個事件,若是沒有就不處理。故用一個線程(NioEventLoop)作輪詢就能夠了。若是要得到更高性能,可使用少許的線程,一個負責接收請求(boss NioEventLoopGroup),其餘的負責處理請求(worker NioEventLoopGroup),對於多 CPU 時效率會更高(Netty 的線程池會默認啓動 2 倍的 CPU 核數個線程)。多線程
後續筆記會詳細分析。負載均衡
Socket是一種"打開—讀/寫—關閉"模式的實現,服務器和客戶端各自維護一個"文件",在創建鏈接打開後,能夠向本身文件寫入內容供對方讀取或者讀取對方內容,通信結束時關閉文件。異步
不一樣層次的抽象,對 Socket 的解釋是不同的,在計算機網絡中,解釋 Socket 是 ip 地址+端口號,都對,主要看是哪一層次的抽象。
在網絡編程層次,這些Socket函數是操做系統內核實現的,用戶代碼沒法觸及,只能使用,這些內核代碼把TCP/IP協議棧和網卡封裝,暴露出來對用戶友好的API,就成了所謂的 Socket 函數,用戶代碼能夠用這些 Socket 函數操縱本地的TCP/IP協議棧和網卡,和服務器通訊。
回到網絡層次,OSI 的上三層等價於 TCP/IP 協議族的應用層(典型的 Telnet、FTP 等應用), OSI 下兩層等價於 TCP/IP 協議族中隨系統提供的設備驅動程序和硬件。在一個網絡程序中, 對應OSI 模型,上三層處理應用自己的細節,卻對應用底層的通訊細節瞭解不多;下四層能夠處理全部的底層網絡的通訊細節。OSI 的上三層能夠對應所謂的用戶進程,下四層一般對應操做系統內核的一部分,所以,把第4層和第5層之間的接口抽象爲 Socket API 是天然而然的一個過程,即所謂的 Socket 所處的位置就是 TCP/IP 協議族應用層和傳輸層的交界處。
在 Linux 的網絡編程這個層次中,客戶機和服務器各有一個Socket文件,當兩臺主機通訊時,客戶機裏的客戶端應用進程 A 發送消息,經過 TCP協議數據包頭的 SYN 標誌位置1,進行主動打開,經 A 主機的 TCP/IP 協議棧發送到 LAN,而後經 WAN 中的路由器傳給服務端應用進程 B 的目的主機所在的 LAN,以後經目的主機的 LAN 將報文傳給目的主機,最後經目的主機的 TCP/IP 協議棧處理,服務器被動打開,將消息遞交給目的應用程序 B。
具體分析以下,在鏈接創建階段,客戶端調用 connect() 函數發起主動鏈接——觸發客戶端的 TCP 協議棧發送 SYN 報文,此時客戶端處於 SYN-SENT 態,以下。而在此以前,服務端的 Socket 須要已經處於監聽態(LISTEN),在 Linux 上就是調用 listen() 函數便可實現監聽 Socket。
服務端的 TCP 協議棧收到該 SYN 報文後,發送給處於 LISTEN 狀態的服務端 Socket,服務端應用進程經過調用 accept() 函數觸發其 TCP 協議棧發送 SYN+ACK 報文返回給客戶端,此時服務端從 LISTEN 態轉移到 SYN-RCVD 態。
客戶端收到服務端的 SYN+ACK 報文後,發送確認的 ACK 報文,此時客戶端從 SYN-SENT 態進入 ESTABLISHED 態,當服務端收到客戶端的 ACK 報文後,一樣會從 SYN-RCVD 態也進入 ESTABLISHED 態,此時服務端的 accept() 函數返回。
通過如上三個報文交互,TCP 鏈接創建,而後就能夠進行數據傳輸。
在數據傳輸階段,客戶端的 Socket 能夠調用 send() 函數發送數據,而後服務端的 Socket 接到客戶端 Socket 傳來的請求,調用 read() 函數讀取,調用 write() 函數寫入響應。
在鏈接斷開階段,以客戶端主動關閉爲例子。
客戶端的 TCP 協議棧主動發送一個 FIN 報文,主動關閉到服務端方向的鏈接,此時客戶端狀態從 ESTABLISHED 態轉移到 FIN-WAIT-1 態。經過調用 close() 函數便可實現。
服務端 TCP 協議棧收到 FIN 報文,就發回客戶端一個 ACK 報文確認關閉,此時,服務端狀態從 ESTABLISHED 態轉移到 CLOSE-WAIT 態(由於是被動關閉),和 SYN 同樣,一個 FIN 也佔用一個序號,同時服務端還向客戶端傳送一個文件結束符。當客戶端接受到服務端確認關閉的報文後,客戶端狀態從 FIN-WAIT-1 態轉移到 FIN-WAIT-2 態。
接着這個服務端程序就關閉它的鏈接,這會致使服務端的 TCP 協議棧也會發送一個 FIN 報文給客戶端,這裏也能清楚看到,ACK 不消耗序號。此時,服務端狀態從 CLOSE-WAIT 轉移到 LAST-ACK 態。
客戶端收到服務端的 FIN 報文,也必須發回一個ACK 確認報文。此時,客戶端狀態從 FIN-WAIT-2 態轉移到 TIME-WAIT 態。
至此,TCP 鏈接關閉。
對於運行在 Java 虛擬機上的 Java 語言來講,其自身的 Socket 函數,就是對操做系統的這些系統調用函數的封裝而已。看看這些系統調用函數,有助於理解非阻塞通訊原理,先認識一些輔助的 Socket 系統調用函數。
socket 函數:對應於普通文件的打開操做,要知道 Linux 中,一切都是文件,包括 Socket 自己也是一個文件,分別存在於客戶端和服務端機器上。前面也提到了 fd,即普通文件的打開操做會返回一個文件描述符——file description,即 fd,socket() 函數就是用來建立 Socket 描述符(socket descriptor,即 sd) 的,它惟一標識一個 Socket。這個 sd 跟 fd 同樣,後續的操做都會用到它,把它做爲參數,經過它來進行一些 Socket 的讀寫操做。
bind 函數:給一個 sd 綁定一個協議和地址+端口號。
listen 函數:socket() 函數建立的 Socket 默認是一個主動類型的,listen 函數將 Socket 變爲被動類型的,用於等待客戶的鏈接請求。
connect 函數:客戶端經過調用 connect 函數來創建與 TCP 服務器的鏈接。
accept 函數:TCP 服務端依次調用 socket()、bind()、listen() 以後,就會監聽指定的 Socket 地址了,TCP 客戶端依次調用 socket()、connect() 以後就向 TCP 服務端發送一個鏈接請求。服務端監聽到這個請求後,調用 accept() 函數接收請求,若是 accept() 函數成功返回,則標識服務端與客戶端已經正確創建鏈接,此時服務端能夠經過 accept 函數返回的 Socket 來完成與客戶端的通訊,以後的操做就和普通的 I/O 操做(read 函數和 write 函數)沒什麼區別。
Linux 提供了 select/poll 函數,這些系統調用的進程經過將一個或多個 fd(文件描述符,Linux 的一切都是文件) 傳遞給 select 或 poll 系統調用,阻塞在這兩個系統調用中的某一個之上,而不是阻塞在真正的 I/O 系統調用上,這樣 select/poll 能夠幫咱們偵測多個 fd 是否處於就緒狀態。
具體的說,聯繫老師和學生考試的例子,select/poll 順序掃描 fd 是否就緒,可是 select 支持的 fd 數量有限,所以它的使用受到了一些制約。Linux 還提供一個 epoll 系統調用,兩個東西本質是同樣的,只不過 epoll 高級一些,能力更強一些,是基於事件驅動方式代替順序掃描,所以性能更高——當有 fd 就緒時,當即回調函數rollback。該函數容許進程指示內核等待多個事件中的任何一個發生,並只在有一個或多個事件發生或經歷一段指定的時間後才喚醒它。
也就是說,咱們調用 select/epoll 告知內核對哪些描述符(讀、寫或異常條件〉感興趣以及等待多長時間。咱們感興趣的描述符不侷限於套接字,任何描述符均可以使用select來測試。
乍一看上面的解釋,可能會懵逼,固然,懂得就略過。下面就詳細分析下,畢竟人家都黑咱們 Javaer 不懂。。。
衆所周知,read、write、recv, 和 recvfrom 等函數都是阻塞的函數,所謂阻塞,簡單說,就是當函數不能成功執行完畢的時候,程序就會一直停在這裏,沒法繼續執行之後的代碼。
嚴格的說,Linux 對一個 fd 指定的文件或設備, 有兩種工做方式: 阻塞與非阻塞方式。阻塞方式是指當試圖對該 fd 進行讀寫時,若是當時沒有數據可讀,或者暫時不可寫,程序就進入等待狀態,直到可讀或者可寫爲止。非阻塞方式是指若是 fd 沒有數據可讀,或者不可寫,讀/寫的函數立刻返回,不會等待結果。使用 selcet/epoll 函數就能夠實現非阻塞編程。
先看 selcet 函數,它本質是一個輪循函數,即當循環詢問fd時,可設置超時時間,超時時間到了就跳過代碼繼續往下執行。
1 fd_set readfd; 2 struct timeval timeout; 3 4 FD_ZERO(&readfd); // 初始化 readfd 5 FD_SET(gps_fd, &readfd); // 把 gps_fd 加入 readfd 6 timeout.tv_sec = 3; // 設置 3 秒超時 7 timeout.tv_usec = 0; 8 9 j = select(gps_fd+1, &readfd, NULL, NULL, &timeout); // 用 select 對 gps_fd 進行輪循 10 if(j>0){ 11 if( FD_ISSET(gps_fd, &readfd) ){ // 若是 gps_fd 可讀 12 i = read(gps_fd, buf, SIZE); 13 buf[i] = '\0'; 14 } 15 }
C 系列的代碼 ztm 的很繁瑣,實現個簡單的聊天 demo,都要不少代碼和繁瑣的考慮。。。到了現在,Java 強大的生態系統愈發完善,其 Netty 已經能夠和 C++ 實現的異步非阻塞服務器抗衡,愈發想不通,爲何還有人要用 C++ 語言來實現相似項目(純屬我的吐槽。。。),這裏只是依靠以前的知識基礎,其實我也忘得差很少了。。。寫了一個小小的方法,直觀感覺下,咱們重點仍是在 Java 這塊。
主要看 select 函數的參數,幫助咱們理解它的工做原理。先看參數類型,fd_set 是一個集合(struct),其中存放的是 fd,有的書也叫文件句柄。timeval 也是一個 struct,表明時間值
int select(int maxfdp, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);
第一個參數 int maxfdp:指fd_set集合中全部 fd 的範圍,即全部文件描述符的最大值加1,不能錯。
第二個參數 fd_set *readfds:集合中包括 fd,select 會監視這些 fd 是否可讀,看名字 readfds 也能看出來,若是 readfds 中有一個文件可讀,select 就會返回一個大於 0 的值,表示有文件可讀,若是沒有可讀的,則根據 timeout 參數判斷是否超時,若超時,select 返回 0,若發生錯誤直接返回負值。也能夠傳入 NULL 值,表示不關心任何文件的讀變化。
第三個參數 fd_set *writefds:集合中包括 fd,select 監視這些 fd 是否可寫,若是有一個 fd 可寫,select 就返回一個大於 0 的值,不然根據 timeout 判斷是否超時,後續和 readfds 同樣。
第四個參數 fd_set *errorfds:同上,select 能夠監視 fd 的錯誤異常。
第五個參數 struct timeval *timeout:是 select 的超時時間,它可以使 select 處於三種狀態;
一、傳入 NULL,select 變爲阻塞函數,必定等到被監視的 fd 集合中,某個 fd 發生變化爲止;
二、設爲 0,select 變成非阻塞函數,無論 fd 是否有變化,都馬上返回,fd 無變化返回 0,有變化返回一個正值;
三、大於 0,select 的阻塞超時時間,時間內有事件到來就返回,不然在超時後就必定返回,返回值同上。
前面說了,selcet 函數本質是一個輪循函數,即 select 內部會循環詢問參數集合裏的 fd,原理其實也很簡單,每次輪詢發現有 fd 發生變化,就會返回,不然一直輪詢直到超時,若是沒有超時就直接返回,不阻塞。輪詢的目的就是發現 fd 可讀或者可寫,而後可讓單個進程去處理 I/O 事件,避免 fork 多個客戶進程。
熟悉 Java NIO 編程的都知道,JDK 裏也有 select() 方法,通常也叫它I/O多路複用器(網上有人翻譯爲選擇器,我的感受並不能突出其實現思想,我採納了《Netty 權威指南》做者的翻譯),它實際上底層並非基於 Linux 的 select 系統函數實現,不要被名字誤導,它是基於 epoll 系統函數而實現。下面就學習下這個系統函數,幫助咱們理解 Java NIO 編程思想。
epoll 是 Linux 下 I/O多路複用器——select/poll 的加強版,最先出如今 Linux 內核 2.5.44中,其實現與使用方式與 select/poll 有一些差別,epoll 是經過了一組函數來完成有關任務,而不是相似 select 函數那樣,只依靠一個函數。
簡單的看下 select 的執行流程;首先要設置 maxfdp,將 fd 加入 select 監控集,使用一個 array 保存放到 select 監控集中的 fd,一是用於在 select 返回後,array 做爲源數據和 fdset 進行 fd_isset 判斷。二是在 select 返回後會把之前加入的但並沒有事件發生的 fd 清空,則每次開始 select 都要從 array 取得 fd 逐一加入。select 的模型必須在 select 前循環 array(加fd,取 maxfd),返回後循環 array。下面的 demo 只一次調用,很簡單。
1 int main() { 2 char buf[10] = ""; 3 fd_set rdfds; // 監視可讀事件的 fd 集合 4 struct timeval tv; // 超時時間 5 int ret; 6 FD_ZERO(&rdfds); // 初始化 readfds 7 FD_SET(0, &rdfds); // fd==0 表示鍵盤輸入 8 tv.tv_sec = 3; 9 tv.tv_usec = 500; 10 ret = select(1, &rdfds, NULL, NULL, &tv); // 第一個參數是 maxfdp,值是監控的 fd 號 + 1,本例就是 0 + 1 11 if(ret < 0) 12 printf("selcet error \r\n"); 13 else if(ret == 0) 14 printf("timeout \r\n"); 15 else 16 printf("ret = %d \r\n", ret); 17 18 if(FD_ISSET(0, &rdfds)){ // 說明監控的 fd 可讀,stdin 輸入已經發生 19 printf(" reading 。。。"); 20 read(0, buf, 9); // 從鍵盤讀取輸入 21 } 22 write(1, buf, strlen(buf)); // 在終端回顯 23 printf(" %d \r\n", strlen(buf)); 24 return 0; 25 }
顯然,能夠發現 select 會作不少無用功。
一、即便只有一個 fd 就緒,select 也要遍歷整個 fd 集合,這顯然是無心義的操做。
二、若是事件須要循環處理,那麼每次 select 後,都要清空之前加入的但並沒有事件發生的 fd 數組(本例子就一個),在每次從新開始 select 時,都要再次從 array 取得 fd 逐一加入 fd_set 集合,每次這樣的操做都須要作一次從進程的用戶空間到內核空間的內存拷貝,使得 select 的效率較低。
三、select 可以處理的最大 fd 數目是有限制的,並且限制很低,通常爲 1024,若是客戶端過多,會大大下降服務器響應效率。
select 函數將當前進程輪流加入每一個 fd 對應設備的等待隊列去詢問該 fd 有無可讀/寫事件,無非是想,在哪個設備就緒時可以通知當前進程退出調用,Linux 的開發者想到,找個「代理」的回調函數代替當前進程,去加入 fd 對應設備的等待隊列,讓這個代理的回調函數去等待設備就緒,當有設備就緒就將本身喚醒,而後該回調函數就把這個設備的 fd 放到一個就緒隊列,同時通知可能在等待的輪詢進程來這個就緒隊列裏取已經就緒的 fd。當前輪詢的進程不須要遍歷整個被偵聽的 fd 集合。
簡單說:
epoll 由三個系統調用組成,分別是 epoll_create,epoll_ctl 和 epoll_wait。epoll_create 用於建立和初始化一些內部使用的數據結構,epoll_ctl 用於添加,刪除或修改指定的 fd 及其期待的事件,epoll_wait 就是用於等待任何先前指定的fd事件就緒。
服務端使用 epoll 步驟以下:
int main() { struct epoll_event ev, events[20]; struct sockaddr_in clientaddr, serveraddr; int epfd; int sd; int maxi; int nfds; int i; int sock_fd, conn_fd; char buf[10]; epfd = epoll_create(2560); // 生成 epoll 句柄,size 告訴內核監聽的 fd 數目最大值 // 當建立 epoll 句柄後,它就會佔用一個 fd 值,因此在使用完 epoll,必須調用 close() 釋放資源,不然可能致使 fd 被耗盡。 sd = socket(AF_INET, SOCK_STREAM, 0); // 建立 Socket ev.data.fd = sd; // 設置與要處理事件相關的 fd,這裏就是 Socket 的 sd ev.events = EPOLLIN; // 設置感興趣的 fd 事件類型, EPOLLIN 表示 fd 可讀(包括對端 Socket 正常關閉)事件 epoll_ctl(epfd, EPOLL_CTL_ADD, sd, &ev);// EPOLL_CTL_ADD:註冊新的 fd 到 epfd 中 memset(&serveraddr, 0, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons(SERV_PORT); bind(sd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)); // 綁定 Socket socklen_t clilen; listen(sd, 20); // 轉爲監聽的 Socket int n; while(1) { nfds = epoll_wait(epfd, events, 20, 500); //等待 fd 感興趣的事件發生,函數返回須要處理的事件數目 for(i = 0; i < nfds; i++) { if(events[i].data.fd == sd) { // 新鏈接到了 clilen = sizeof(struct sockaddr_in); conn_fd = accept(sd, (struct sockaddr*)&clientaddr, &clilen); printf("accept a new client : %s\n", inet_ntoa(clientaddr.sin_addr)); ev.data.fd = conn_fd; ev.events = EPOLLIN; // 設置 fd 的監聽事件爲可寫 epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev); // 註冊新的 Socket 到 epfd } else if(events[i].events & EPOLLIN) { // 可讀事件被觸發 if((sock_fd = events[i].data.fd) < 0) continue; if((n = recv(sock_fd, buf, 10, 0)) < 0) { if(errno == ECONNRESET) { close(sock_fd); events[i].data.fd = -1; } else { printf("readline error \n"); } } else if(n == 0) { close(sock_fd); printf("關閉 \n"); events[i].data.fd = -1; } printf("%d -- > %s\n",sock_fd, buf); ev.data.fd = sock_fd; ev.events = EPOLLOUT; epoll_ctl(epfd, EPOLL_CTL_MOD, sock_fd, &ev); // 修改監聽事件爲可讀 } else if(events[i].events & EPOLLOUT) { // 可寫事件被觸發 sock_fd = events[i].data.fd; printf("OUT\n"); scanf("%s",buf); send(sock_fd, buf, 10, 0); ev.data.fd = sock_fd; ev.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_MOD,sock_fd, &ev); } } } }
把 fd 加入到 epoll 的監聽隊列中, 當 fd 可讀/寫事件,這個條件發生時,在經驗看來,epoll_wait() 固然會當即返回(也叫被觸發),事實上確實是這樣的,並且返回值是須要處理的 fd 事件的數目。這裏要討論的是 epoll_wait() 函數返回的條件到底都有什麼。
若是 epoll_wait() 只在讀/寫事件發生時返回,就像前面舉的經驗例子,該觸發叫作邊緣觸發——ET(edge-triggered),也就是說若是事件處理函數只讀取了該 fd 的緩衝區的部份內容就返回了,接下來再次調用 epoll_wait(),雖然此時該就緒的 fd 對應的緩衝區中還有數據,但 epoll_wait() 函數也不會返回。
相反,不管當前的 fd 中是否有讀/寫事件反生了,只要 fd 對應的緩衝區中有數據可讀/寫,epoll_wait() 就當即返回,這叫作水平觸發——LT(level-triggered)。
一句話總結,在 ET 模式下,只有 fd 狀態發生改變,fd 纔會被再次選出。ET 模式的特殊性,使 ET 模式下的一次輪詢必須處理完本次輪詢出的 fd 緩衝區裏的的全部數據,不然該 fd 將不會在下次輪詢中被選出。
select/poll 使用的觸發方式是 LT,相對來講比較低效,而 ET 是 epoll 的高速工做方式。
epoll 每次只遍歷活躍的 fd (若是是 LT,也會遍歷先前活躍的 fd),在活躍 fd 較少的狀況下就會頗有優點,若是大部分 fd 都是活躍的,epoll 的效率可能還不如 select/poll。
若是寫過 Java NIO 代碼,那麼就能推測到 JDK NIO 的 epoll 模型是 LT,在 Netty 的實際開發中,也能體會到 NioServerSocketChannel 是 LT,固然若是使用 Netty 本身實現的 epoll Channel,就是 ET。
Netty 的 NioEventLoop 模型中,每次輪詢都會進行負載均衡,限制了每次從 fd 中讀取數據的最大值,形成一次讀事件處理並不會 100% 讀完 fd 緩衝區中的全部數據。在基於 LT 的 NioServerSocketChannel 中,Netty 不須要作特殊處理,在處理完一個 I/O 事件後直接從 SelectionKey 中移除該事件便可,若是有未讀完的數據,下次輪詢仍會得到該事件。而在EpollServerSocketChannel,若是一次事件處理不把數據讀完,須要手動地觸發一次事件,不然下次輪詢將不會讀取先前活躍的 fd 遺留的數據。