0. 前言
這篇文章主要記錄在使用epoll實現NIO接入時所遇到的問題。
1. epoll簡介
epoll是Linux下提供的NIO,其主要有兩種模式,ET(Edge trige)和LT(Level trige)。在linux下使用man epoll手冊即可知道這兩種模式主要的區別:
ET:邊緣觸發,故名思議,所添加的描述符,只在當其改變狀態的時候才會觸發一次,就如同數電里面電平的邊緣觸發。
在man里面列舉了一個例子,當一個fd添加到epoll中時,當有2KB數據到達時,epoll_wait會返回事件個數以及其fd,此時,當程序讀取了1KB數據后繼續調用epoll_wait,1)對於ET來說會繼續等待,2)而LT則是繼續觸發返回。
2. epoll錯誤程序分析
下述為錯誤的示例:

1 #include <sys/epoll.h> 2 #include <unistd.h> 3 #include <fcntl.h> 4 #include <sys/types.h> 5 #include <sys/socket.h> 6 #include <errno.h> 7 #include <string.h> 8 9 #define MAX_BACKLOG 256 10 #define MAX_EVENTS 1024 11 12 static int SetNonBlock(int nFd) 13 { 14 int nOldOpt = fcntl(nFd, F_GETFD, 0); 15 int nNewOpt = nOldOpt | O_NONBLOCK; 16 17 return fcntl(nFd, F_SETFD, nNewOpt); 18 } 19 20 static void AddEvent(int nEpfd, int nFd) 21 { 22 struct epoll_event event; 23 event.data.fd = nFd; 24 event.events = EPOLLIN | EPOLLOUT | EPOLLRDHUP | EPOLLET; 25 26 return epoll_ctl(nEpfd, EPOLL_CTL_ADD, &event); 27 } 28 29 int main(int argc, char const *argv[]) 30 { 31 if (argc != 2) 32 { 33 printf("usage: CMD Port\n"); 34 exit(-1); 35 } 36 37 int nPort = atoi(argv[1]); 38 if (nPort < 0) 39 { 40 printf("Port Invalid\n"); 41 exit(-1); 42 } 43 44 int nSvrFd = socket(AF_INET, SOCK_STREAM, 0); 45 //設置非阻塞 46 SetNonBlock(nSvrFd); 47 48 //綁定地址 49 struct sockaddr_in addr; 50 bzero(&addr, sizeof(addr)); 51 52 addr.sin_addr = 0; 53 addr.sin_port = htons(argv[1]); 54 addr.sin_family = htos(AF_INET); 55 56 if (0 != bind(nSvrFd, (struct sockaddr*)&addr, sizeof(addr))) 57 { 58 perror("Bind Failure:"); 59 exit(-1); 60 } 61 62 //監聽端口 63 if (0 != listen(nSvrFd, MAX_BACKLOG)) 64 { 65 perror("Listen Failure:"); 66 exit(-1); 67 } 68 69 int nEpfd = epoll_create(1024); 70 struct epoll_event events[MAX_EVENTS]; 71 72 AddEvent(nEpfd, nSvrFd); 73 74 while (1) 75 { 76 //等待事件到來 77 int nReadyNums = epoll_wait(nEpfd, events, MAX_EVENTS, -1); 78 79 for (int i = 0; i < nReadyNums; ++i) 80 { 81 if (events[i].data.fd == nSvrFd) 82 { 83 //這里對於ET模式來說是有問題的 84 int nClientFd = accept(nSvrFd, NULL, NULL); 85 if (-1 != nClientFd) 86 { 87 //設置為非阻塞 88 SetNonBlock(nClientFd); 89 //添加事件監聽 90 AddEvent(nEpfd, nClientFd); 91 } 92 93 } else 94 { 95 //處理FD事件 96 } 97 } 98 } 99 100 return 0; 101 }
分析:這里的程序使用ET + 非阻塞,對於accept沒有使用循環接收,則會導致當兩個連接同時接入的時候,只觸發一次,則accept一次,另外一個則停留在SYN隊列中。當終端超時后發送FIN狀態,這邊只是將該連接標識為只讀狀態。並連接處於CLOSE_WAIT狀態。當下一次接入的時候,觸發epoll,這次accept的卻是上一次的連接,這次的連接依然停留在SYN隊列中。如果后續都是單次觸發的話,則會導致后續交易都失敗。
這里對KEEPALIVE選項做個補充,KEEPALIVE是用於檢測到連接斷開后有操作系統自動釋放資源,但是並不會釋放SYN隊列里面的連接,也就是說,CLOSE_WAIT狀態會被清除,但是問題還是存在,會把現象遮蔽了。
正確應該是在accept加上一個循環:
while ((nClientFd = accept(nSvrFd, NULL, NULL)) > 0) { //設置為非阻塞 SetNonBlock(nClientFd); //添加事件監聽 AddEvent(nEpfd, nClientFd); }
3. 總結
對於ET模式+非阻塞,無論是recv還是accept,都需要加上循環處理