linux服務器開發(fā)相關視頻解析:
linux下的epoll實戰(zhàn)揭秘——支撐億級IO的底層基石
epoll的網(wǎng)絡模型,從redis,memcached到nginx,一起搞定
問題:
Linux 的 epoll 使用 LT + 非阻塞 IO 和 ET + 非阻塞 IO 有效率上的區(qū)別嗎? 問題補充:請看清楚。。都是非阻塞IO,這是否意味著他們的系統(tǒng)調(diào)用次數(shù)基本是一致的?那么ET+非阻塞存在的意義是什么呢?
綜合 select 和 poll 的一些優(yōu)缺點,Linux 從內(nèi)核 2.6 版本開始引入了更高效的 epoll 模型,本文我們來詳細介紹 epoll 模型。
要想使用 epoll 模型,必須先需要創(chuàng)建一個 epollfd,這需要使用 epoll_create 函數(shù)去創(chuàng)建:
#include int epoll_create(int size);
參數(shù) size 從 Linux 2.6.8 以后就不再使用,但是必須設置一個大于 0 的值。epoll_create 函數(shù)調(diào)用成功返回一個非負值的 epollfd,調(diào)用失敗返回 -1。
有了 epollfd 之后,我們需要將我們需要檢測事件的其他 fd 綁定到這個 epollfd 上,或者修改一個已經(jīng)綁定上去的 fd 的事件類型,或者在不需要時將 fd 從 epollfd 上解綁,這都可以使用 epoll_ctl 函數(shù):
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
參數(shù)說明:
- 參數(shù) epfd 即上文提到的 epollfd;
- 參數(shù) op,操作類型,取值有 EPOLL_CTL_ADD、EPOLL_CTL_MOD 和 EPOLL_CTL_DEL,分別表示向 epollfd 上添加、修改和移除一個其他 fd,當取值是 EPOLL_CTL_DEL,第四個參數(shù) event 忽略不計,可以設置為 NULL;
- 參數(shù) fd,即需要被操作的 fd;
- 參數(shù) event,這是一個 epoll_event 結(jié)構體的地址,epoll_event 結(jié)構體定義如下:
struct epoll_event { uint32_t events; /* 需要檢測的 fd 事件,取值與 poll 函數(shù)一樣 */ epoll_data_t data; /* 用戶自定義數(shù)據(jù) */ };
epoll_event 結(jié)構體的 data 字段的類型是 epoll_data_t,我們可以利用這個字段設置一個自己的自定義數(shù)據(jù),它本質(zhì)上是一個 Union 對象,在 64 位操作系統(tǒng)中其大小是 8 字節(jié),其定義如下:
typedef union epoll_data { void* ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t;
- 函數(shù)返回值:epoll_ctl 調(diào)用成功返回 0,調(diào)用失敗返回 -1,你可以通過 errno 錯誤碼獲取具體的錯誤原因。
創(chuàng)建了 epollfd,設置好某個 fd 上需要檢測事件并將該 fd 綁定到 epollfd 上去后,我們就可以調(diào)用 epoll_wait 檢測事件了,epoll_wait 函數(shù)簽名如下:
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
參數(shù)的形式和 poll 函數(shù)很類似,參數(shù) events 是一個 epoll_event 結(jié)構數(shù)組的首地址,這是一個輸出參數(shù),函數(shù)調(diào)用成功后,events 中存放的是與就緒事件相關 epoll_event 結(jié)構體數(shù)組;參數(shù) maxevents 是數(shù)組元素的個數(shù);timeout 是超時時間,單位是毫秒,如果設置為 0,epoll_wait 會立即返回。
當 epoll_wait 調(diào)用成功會返回有事件的 fd 數(shù)目;如果返回 0 表示超時;調(diào)用失敗返回 -1。
epoll_wait 使用示例如下:
while (true) { epoll_event epoll_events[1024]; int n = epoll_wait(epollfd, epoll_events, 1024, 1000); if (n < 0) { //被信號中斷 if (errno == EINTR) continue; //出錯,退出 break; } else if (n == 0) { //超時,繼續(xù) continue; } for (size_t i = 0; i < n; ++i) { // 處理可讀事件 if (epoll_events[i].events & EPOLLIN) { } // 處理可寫事件 else if (epoll_events[i].events & EPOLLOUT) { } //處理出錯事件 else if (epoll_events[i].events & EPOLLERR) { } } }
epoll_wait 與 poll 的區(qū)別
通過前面介紹 poll 與 epoll_wait 函數(shù)的介紹,我們可以發(fā)現(xiàn):
epoll_wait 函數(shù)調(diào)用完之后,我們可以直接在 event 參數(shù)中拿到所有有事件就緒的 fd,直接處理即可(event 參數(shù)僅僅是個出參);而 poll 函數(shù)的事件集合調(diào)用前后數(shù)量都未改變,只不過調(diào)用前我們通過 pollfd 結(jié)構體的 events 字段設置待檢測事件,調(diào)用后我們需要通過 pollfd 結(jié)構體的 revents 字段去檢測就緒的事件( 參數(shù) fds 既是入?yún)⒁彩浅鰠ⅲ?/p>
舉個生活中的例子,某人不斷給你一些蘋果,這些蘋果有生有熟,調(diào)用 epoll_wait 相當于:
1. 你把蘋果挨個投入到 epoll 機器中(調(diào)用 epoll_ctl); 2. 調(diào)用 epoll_wait 加工,你直接通過另外一個袋子就能拿到所有熟蘋果。
調(diào)用 poll 相當于:
1. 把收到的蘋果裝入一個袋子里面然后調(diào)用 poll 加工; 2. 調(diào)用結(jié)束后,拿到原來的袋子,袋子中還是原來那么多蘋果,只不過熟蘋果被貼上了標簽紙,你還是需要挨個去查看標簽紙?zhí)暨x熟蘋果。
當然,這并不意味著,poll 函數(shù)的效率不如 epoll_wait,一般在 fd 數(shù)量比較多,但某段時間內(nèi),就緒事件 fd 數(shù)量較少的情況下,epoll_wait 才會體現(xiàn)出它的優(yōu)勢,也就是說 socket 連接數(shù)量較大時而活躍連接較少時 epoll 模型更高效。
【文章福利】需要C/C++ Linux服務器架構師學習資料加群812855908(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協(xié)程,DPDK,ffmpeg等)
LT 模式和 ET 模式
與 poll 的事件宏相比,epoll 新增了一個事件宏 EPOLLET,這就是所謂的邊緣觸發(fā)模式(Edge Trigger,ET),而默認的模式我們稱為 水平觸發(fā)模式(Level Trigger,LT)。這兩種模式的區(qū)別在于:
- 對于水平觸發(fā)模式,一個事件只要有,就會一直觸發(fā);
- 對于邊緣觸發(fā)模式,只有一個事件從無到有才會觸發(fā)。
這兩個詞匯來自電學術語,你可以將 fd 上有數(shù)據(jù)認為是高電平,沒有數(shù)據(jù)認為是低電平,將 fd 可寫認為是高電平,fd 不可寫認為是低電平。那么水平模式的觸發(fā)條件是狀態(tài)處于高電平,而邊緣模式的觸發(fā)條件是新來一次電信號將當前狀態(tài)變?yōu)楦唠娖?,即?/p>
水平模式的觸發(fā)條件
1. 低電平 => 高電平 2. 處于高電平狀態(tài)
邊緣模式的觸發(fā)條件
1. 低電平 => 高電平
說的有點抽象,以 socket 的讀事件為例,對于水平模式,只要 socket 上有未讀完的數(shù)據(jù),就會一直產(chǎn)生 POLLIN 事件;而對于邊緣模式,socket 上每新來一次數(shù)據(jù)就會觸發(fā)一次,如果上一次觸發(fā)后,未將 socket 上的數(shù)據(jù)讀完,也不會再觸發(fā),除非再新來一次數(shù)據(jù)。對于 socket 寫事件,如果 socket 的 TCP 窗口一直不飽和,會一直觸發(fā) POLLOUT 事件;而對于邊緣模式,只會觸發(fā)一次,除非 TCP 窗口由不飽和變成飽和再一次變成不飽和,才會再次觸發(fā) POLLOUT 事件。
socket 可讀事件水平模式觸發(fā)條件:
1. socket上無數(shù)據(jù) => socket上有數(shù)據(jù) 2. socket處于有數(shù)據(jù)狀態(tài)
socket 可讀事件邊緣模式觸發(fā)條件:
1. socket上無數(shù)據(jù) => socket上有數(shù)據(jù) 2. socket又新來一次數(shù)據(jù)
socket 可寫事件水平模式觸發(fā)條件:
1. socket可寫 => socket可寫 2. socket不可寫 => socket可寫
socket 可寫事件邊緣模式觸發(fā)條件:
1. socket不可寫 => socket可寫
也就是說,如果對于一個非阻塞 socket,如果使用 epoll 邊緣模式去檢測數(shù)據(jù)是否可讀,觸發(fā)可讀事件以后,一定要一次性把 socket 上的數(shù)據(jù)收取干凈才行,也就是說一定要循環(huán)調(diào)用 recv 函數(shù)直到 recv 出錯,錯誤碼是EWOULDBLOCK(EAGAIN 一樣)(此時表示 socket 上本次數(shù)據(jù)已經(jīng)讀完);如果使用水平模式,則不用,你可以根據(jù)業(yè)務一次性收取固定的字節(jié)數(shù),或者收完為止。邊緣模式下收取數(shù)據(jù)的代碼寫法示例如下:
bool TcpSession::RecvEtMode() { //每次只收取256個字節(jié) char buff[256]; while (true) { int nRecv = ::recv(clientfd_, buff, 256, 0); if (nRecv == -1) { if (errno == EWOULDBLOCK) return true; else if (errno == EINTR) continue; return false; } //對端關閉了socket else if (nRecv == 0) return false; inputBuffer_.add(buff, (size_t)nRecv); } return true; }
下面我們來看幾個具體的例子來比較一下 LT 模式與 ET 模式的區(qū)別。
先來測試一下 LT 模式 與 ET 模式在處理讀事件上的區(qū)別。
代碼如下:
/** * 驗證epoll的LT與ET模式的區(qū)別, epoll_server.cpp * zhangyl 2019.04.01 */ #include #include #include #include #include #include #include #include #include #include #include #include int main() { //創(chuàng)建一個監(jiān)聽socket int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd == -1) { std::cout << 'create listen socket error' << std::endl; return -1; } //設置重用ip地址和端口號 int on = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*)&on, sizeof(on)); setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*)&on, sizeof(on)); //將監(jiān)聽socker設置為非阻塞的 int oldSocketFlag = fcntl(listenfd, F_GETFL, 0); int newSocketFlag = oldSocketFlag | O_NONBLOCK; if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1) { close(listenfd); std::cout << 'set listenfd to nonblock error' << std::endl; return -1; } //初始化服務器地址 struct sockaddr_in bindaddr; bindaddr.sin_family = AF_INET; bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); bindaddr.sin_port = htons(3000); if (bind(listenfd, (struct sockaddr*)&bindaddr, sizeof(bindaddr)) == -1) { std::cout << 'bind listen socker error.' << std::endl; close(listenfd); return -1; } //啟動監(jiān)聽 if (listen(listenfd, SOMAXCONN) == -1) { std::cout << 'listen error.' << std::endl; close(listenfd); return -1; } //創(chuàng)建epollfd int epollfd = epoll_create(1); if (epollfd == -1) { std::cout << 'create epollfd error.' << std::endl; close(listenfd); return -1; } epoll_event listen_fd_event; listen_fd_event.data.fd = listenfd; listen_fd_event.events = EPOLLIN; //取消注釋掉這一行,則使用ET模式 //listen_fd_event.events |= EPOLLET; //將監(jiān)聽sokcet綁定到epollfd上去 if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event) == -1) { std::cout << 'epoll_ctl error' << std::endl; close(listenfd); return -1; } int n; while (true) { epoll_event epoll_events[1024]; n = epoll_wait(epollfd, epoll_events, 1024, 1000); if (n < 0) { //被信號中斷 if (errno == EINTR) continue; //出錯,退出 break; } else if (n == 0) { //超時,繼續(xù) continue; } for (size_t i = 0; i < n; ++i) { //事件可讀 if (epoll_events[i].events & EPOLLIN) { if (epoll_events[i].data.fd == listenfd) { //偵聽socket,接受新連接 struct sockaddr_in clientaddr; socklen_t clientaddrlen = sizeof(clientaddr); int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &clientaddrlen); if (clientfd != -1) { int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); int newSocketFlag = oldSocketFlag | O_NONBLOCK; if (fcntl(clientfd, F_SETFD, newSocketFlag) == -1) { close(clientfd); std::cout << 'set clientfd to nonblocking error.' << std::endl; } else { epoll_event client_fd_event; client_fd_event.data.fd = clientfd; client_fd_event.events = EPOLLIN; //取消注釋這一行,則使用ET模式 //client_fd_event.events |= EPOLLET; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &client_fd_event) != -1) { std::cout << 'new client accepted,clientfd: ' << clientfd << std::endl; } else { std::cout << 'add client fd to epollfd error' << std::endl; close(clientfd); } } } } else { std::cout << 'client fd: ' << epoll_events[i].data.fd << ' recv data.' << std::endl; //普通clientfd char ch; //每次只收一個字節(jié) int m = recv(epoll_events[i].data.fd, &ch, 1, 0); if (m == 0) { //對端關閉了連接,從epollfd上移除clientfd if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1) { std::cout << 'client disconnected,clientfd:' << epoll_events[i].data.fd << std::endl; } close(epoll_events[i].data.fd); } else if (m < 0) { //出錯 if (errno != EWOULDBLOCK && errno != EINTR) { if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1) { std::cout << 'client disconnected,clientfd:' << epoll_events[i].data.fd << std::endl; } close(epoll_events[i].data.fd); } } else { //正常收到數(shù)據(jù) std::cout << 'recv from client:' << epoll_events[i].data.fd << ', ' << ch << std::endl; } } } else if (epoll_events[i].events & EPOLLERR) { // TODO 暫不處理 } } } close(listenfd); return 0; }
我們先來看水平模式的行為,將代碼 79 行和 134 行注釋掉則使用 LT 模式,我們編譯下程序并運行:
[root@localhost testepoll]# g++ -g -o epoll_server epoll_server.cpp [root@localhost testepoll]# ./epoll_server
然后再另外開啟一個 shell 窗口,使用 nc 命令模擬一個客戶端,連接服務器成功后,我們給服務器發(fā)送一個消息'abcef':
[root@localhost ~]# nc -v 127.0.0.1 3000 Ncat: Version 7.50 ( https:///ncat ) Ncat: Connected to 127.0.0.1:3000. abcdef
此時服務器端輸出:
[root@localhost testepoll]# ./epoll_server new client accepted,clientfd: 5 client fd: 5 recv data. recv from client:5, a client fd: 5 recv data. recv from client:5, b client fd: 5 recv data. recv from client:5, c client fd: 5 recv data. recv from client:5, d client fd: 5 recv data. recv from client:5, e client fd: 5 recv data. recv from client:5, f client fd: 5 recv data. recv from client:5,
nc 命令實際發(fā)送了 a、b、c、d、e、f 和 \n 七個字符,由于服務器端使用的是 LT 模式,每次接收一個字符,只要 socket 接收緩沖區(qū)中仍有數(shù)據(jù)可讀,POLLIN 事件就會一直觸發(fā),所以服務器一共有 7 次輸出,直到 socket 接收緩沖區(qū)沒有數(shù)據(jù)為止。
我們將代碼 79 行和 134 行注釋取消掉,使用 ET 模式再試一下,修改代碼并重新編譯,然后重新運行一下。再次使用 nc 命令模擬一個客戶端連接后發(fā)送'abcef',服務器只會有一次輸出,效果如下:
由于使用了 ET 模式,只會觸發(fā)一次 POLLIN 事件,如果此時沒有新數(shù)據(jù)到來,就再也不會觸發(fā)。所以,如果我們繼續(xù)給服務器發(fā)送一條新數(shù)據(jù),如 123,服務器將再次觸發(fā)一次 POLLIN 事件,然后打印出字母 b,效果如下:
所以如果使用 ET 模式 處理讀事件,切記要將該次 socket 上的數(shù)據(jù)收完。
再來測試一下 LT 模式 與 ET 模式在處理寫事件上的區(qū)別。
修改上述代碼如下:
/** * 驗證epoll的LT與ET模式的區(qū)別, epoll_server.cpp * zhangyl 2019.04.01 */ #include #include #include #include #include #include #include #include #include #include #include #include int main() { //創(chuàng)建一個監(jiān)聽socket int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd == -1) { std::cout << 'create listen socket error' << std::endl; return -1; } //設置重用ip地址和端口號 int on = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*)& on, sizeof(on)); setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*)& on, sizeof(on)); //將監(jiān)聽socker設置為非阻塞的 int oldSocketFlag = fcntl(listenfd, F_GETFL, 0); int newSocketFlag = oldSocketFlag | O_NONBLOCK; if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1) { close(listenfd); std::cout << 'set listenfd to nonblock error' << std::endl; return -1; } //初始化服務器地址 struct sockaddr_in bindaddr; bindaddr.sin_family = AF_INET; bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); bindaddr.sin_port = htons(3000); if (bind(listenfd, (struct sockaddr*) & bindaddr, sizeof(bindaddr)) == -1) { std::cout << 'bind listen socker error.' << std::endl; close(listenfd); return -1; } //啟動監(jiān)聽 if (listen(listenfd, SOMAXCONN) == -1) { std::cout << 'listen error.' << std::endl; close(listenfd); return -1; } //創(chuàng)建epollfd int epollfd = epoll_create(1); if (epollfd == -1) { std::cout << 'create epollfd error.' << std::endl; close(listenfd); return -1; } epoll_event listen_fd_event; listen_fd_event.data.fd = listenfd; listen_fd_event.events = EPOLLIN; //取消注釋掉這一行,則使用ET模式 //listen_fd_event.events |= EPOLLET; //將監(jiān)聽sokcet綁定到epollfd上去 if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event) == -1) { std::cout << 'epoll_ctl error' << std::endl; close(listenfd); return -1; } int n; while (true) { epoll_event epoll_events[1024]; n = epoll_wait(epollfd, epoll_events, 1024, 1000); if (n < 0) { //被信號中斷 if (errno == EINTR) continue; //出錯,退出 break; } else if (n == 0) { //超時,繼續(xù) continue; } for (size_t i = 0; i < n; ++i) { //事件可讀 if (epoll_events[i].events & EPOLLIN) { if (epoll_events[i].data.fd == listenfd) { //偵聽socket,接受新連接 struct sockaddr_in clientaddr; socklen_t clientaddrlen = sizeof(clientaddr); int clientfd = accept(listenfd, (struct sockaddr*) & clientaddr, &clientaddrlen); if (clientfd != -1) { int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); int newSocketFlag = oldSocketFlag | O_NONBLOCK; if (fcntl(clientfd, F_SETFD, newSocketFlag) == -1) { close(clientfd); std::cout << 'set clientfd to nonblocking error.' << std::endl; } else { epoll_event client_fd_event; client_fd_event.data.fd = clientfd; //同時偵聽新來連接socket的讀和寫時間 client_fd_event.events = EPOLLIN | EPOLLOUT; //取消注釋這一行,則使用ET模式 //client_fd_event.events |= EPOLLET; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &client_fd_event) != -1) { std::cout << 'new client accepted,clientfd: ' << clientfd << std::endl; } else { std::cout << 'add client fd to epollfd error' << std::endl; close(clientfd); } } } } else { std::cout << 'client fd: ' << epoll_events[i].data.fd << ' recv data.' << std::endl; //普通clientfd char recvbuf[1024] = { 0 }; //每次只收一個字節(jié) int m = recv(epoll_events[i].data.fd, recvbuf, 1024, 0); if (m == 0) { //對端關閉了連接,從epollfd上移除clientfd if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1) { std::cout << 'client disconnected,clientfd:' << epoll_events[i].data.fd << std::endl; } close(epoll_events[i].data.fd); } else if (m < 0) { //出錯 if (errno != EWOULDBLOCK && errno != EINTR) { if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1) { std::cout << 'client disconnected,clientfd:' << epoll_events[i].data.fd << std::endl; } close(epoll_events[i].data.fd); } } else { //正常收到數(shù)據(jù) std::cout << 'recv from client:' << epoll_events[i].data.fd << ', ' << recvbuf << std::endl; } } } else if (epoll_events[i].events & EPOLLOUT) { //只處理客戶端fd的可寫事件 if (epoll_events[i].data.fd != listenfd) { //打印結(jié)果 std::cout << 'EPOLLOUT triggered,clientfd:' << epoll_events[i].data.fd << std::endl; } } else if (epoll_events[i].events & EPOLLERR) { // TODO 暫不處理 } } } close(listenfd); return 0; }
上述代碼中,我們對新來的連接 fd 同時注冊讀和寫事件(代碼 133 行),再次編譯程序執(zhí)行:
[root@iZ238vnojlyZ testepollet]# vi epoll.cpp [root@iZ238vnojlyZ testepollet]# g++ -g -o epoll epoll_server.cpp [root@iZ238vnojlyZ testepollet]# ./epoll_server
然后使用 nc 命令模擬一個客戶端去連接 epoll_server:
[root@iZ238vnojlyZ ~]# nc -v 127.0.0.1 3000 Ncat: Version 6.40 ( http:///ncat ) Ncat: Connected to 127.0.0.1:3000.
此時服務器端(epoll_server)會瘋狂的輸出可寫事件觸發(fā)消息:
之所以是這樣,是因為我們注冊了可寫事件且使用的是 LT 模式,LT 模式下,由于這里的服務器端對應的客戶端 fd 一直是可寫的,有寫事件一直觸發(fā),所以看到屏幕不斷輸出。
我們再將服務器端與客戶端建立連接時新建的 fd 設置為 ET 模式再實驗一下:
/** * 驗證epoll的LT與ET模式的區(qū)別, epoll_server.cpp * zhangyl 2019.04.01 */ #include #include #include #include #include #include #include #include #include #include #include #include int main() { //創(chuàng)建一個監(jiān)聽socket int listenfd = socket(AF_INET, SOCK_STREAM, 0); if (listenfd == -1) { std::cout << 'create listen socket error' << std::endl; return -1; } //設置重用ip地址和端口號 int on = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*)& on, sizeof(on)); setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*)& on, sizeof(on)); //將監(jiān)聽socker設置為非阻塞的 int oldSocketFlag = fcntl(listenfd, F_GETFL, 0); int newSocketFlag = oldSocketFlag | O_NONBLOCK; if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1) { close(listenfd); std::cout << 'set listenfd to nonblock error' << std::endl; return -1; } //初始化服務器地址 struct sockaddr_in bindaddr; bindaddr.sin_family = AF_INET; bindaddr.sin_addr.s_addr = htonl(INADDR_ANY); bindaddr.sin_port = htons(3000); if (bind(listenfd, (struct sockaddr*) & bindaddr, sizeof(bindaddr)) == -1) { std::cout << 'bind listen socker error.' << std::endl; close(listenfd); return -1; } //啟動監(jiān)聽 if (listen(listenfd, SOMAXCONN) == -1) { std::cout << 'listen error.' << std::endl; close(listenfd); return -1; } //創(chuàng)建epollfd int epollfd = epoll_create(1); if (epollfd == -1) { std::cout << 'create epollfd error.' << std::endl; close(listenfd); return -1; } epoll_event listen_fd_event; listen_fd_event.data.fd = listenfd; listen_fd_event.events = EPOLLIN; //取消注釋掉這一行,則使用ET模式 //listen_fd_event.events |= EPOLLET; //將監(jiān)聽sokcet綁定到epollfd上去 if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event) == -1) { std::cout << 'epoll_ctl error' << std::endl; close(listenfd); return -1; } int n; while (true) { epoll_event epoll_events[1024]; n = epoll_wait(epollfd, epoll_events, 1024, 1000); if (n < 0) { //被信號中斷 if (errno == EINTR) continue; //出錯,退出 break; } else if (n == 0) { //超時,繼續(xù) continue; } for (size_t i = 0; i < n; ++i) { //事件可讀 if (epoll_events[i].events & EPOLLIN) { if (epoll_events[i].data.fd == listenfd) { //偵聽socket,接受新連接 struct sockaddr_in clientaddr; socklen_t clientaddrlen = sizeof(clientaddr); int clientfd = accept(listenfd, (struct sockaddr*) & clientaddr, &clientaddrlen); if (clientfd != -1) { int oldSocketFlag = fcntl(clientfd, F_GETFL, 0); int newSocketFlag = oldSocketFlag | O_NONBLOCK; if (fcntl(clientfd, F_SETFD, newSocketFlag) == -1) { close(clientfd); std::cout << 'set clientfd to nonblocking error.' << std::endl; } else { epoll_event client_fd_event; client_fd_event.data.fd = clientfd; //同時偵聽新來連接socket的讀和寫時間 client_fd_event.events = EPOLLIN | EPOLLOUT; //取消注釋這一行,則使用ET模式 client_fd_event.events |= EPOLLET; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &client_fd_event) != -1) { std::cout << 'new client accepted,clientfd: ' << clientfd << std::endl; } else { std::cout << 'add client fd to epollfd error' << std::endl; close(clientfd); } } } } else { std::cout << 'client fd: ' << epoll_events[i].data.fd << ' recv data.' << std::endl; //普通clientfd char recvbuf[1024] = { 0 }; //每次只收一個字節(jié) int m = recv(epoll_events[i].data.fd, recvbuf, 1024, 0); if (m == 0) { //對端關閉了連接,從epollfd上移除clientfd if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1) { std::cout << 'client disconnected,clientfd:' << epoll_events[i].data.fd << std::endl; } close(epoll_events[i].data.fd); } else if (m < 0) { //出錯 if (errno != EWOULDBLOCK && errno != EINTR) { if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1) { std::cout << 'client disconnected,clientfd:' << epoll_events[i].data.fd << std::endl; } close(epoll_events[i].data.fd); } } else { //正常收到數(shù)據(jù) std::cout << 'recv from client:' << epoll_events[i].data.fd << ', ' << recvbuf << std::endl; epoll_event client_fd_event; client_fd_event.data.fd = epoll_events[i].data.fd; //再次給clientfd注冊檢測可寫事件 client_fd_event.events = EPOLLIN | EPOLLOUT | EPOLLET; if (epoll_ctl(epollfd, EPOLL_CTL_MOD, epoll_events[i].data.fd, &client_fd_event) != -1) { std::cout << 'epoll_ctl successfully, mode: EPOLL_CTL_MOD, clientfd:' << epoll_events[i].data.fd << std::endl; } } } } else if (epoll_events[i].events & EPOLLOUT) { //只處理客戶端fd的可寫事件 if (epoll_events[i].data.fd != listenfd) { //打印結(jié)果 std::cout << 'EPOLLOUT triggered,clientfd:' << epoll_events[i].data.fd << std::endl; } } else if (epoll_events[i].events & EPOLLERR) { // TODO 暫不處理 } } } close(listenfd); return 0; }
上述邏輯中,服務器端在每次收到客戶端消息時會重新給客戶端 fd 注冊檢測可寫事件(EPOLLOUT),重新編譯代碼,啟動 epoll_server,再次使用 nc 命令模擬客戶端給 epoll_server 發(fā)送幾條消息,結(jié)果如下:
通過上述輸出,我們可以發(fā)現(xiàn),epoll_server 使用 ET 模式下即使給客戶端 fd 注冊了檢測可寫事件不會一直觸發(fā),只會觸發(fā)一次,觸發(fā)完后只有再次注冊檢測可寫事件才會繼續(xù)觸發(fā),這里是靠客戶端來新消息驅(qū)動再次注冊檢測可寫事件。也就是說,如果我們使用 ET 模式去處理可寫事件時不必像 LT 模式那樣為了避免不必要的可寫觸發(fā)在觸發(fā)后需要立即移除檢測可寫事件。
這就意味著,使用 LT 模式,如果你的實現(xiàn)依賴于可寫事件觸發(fā)去發(fā)送數(shù)據(jù),那么你一定要在數(shù)據(jù)發(fā)送完之后移除檢測可寫事件,避免沒有數(shù)據(jù)發(fā)送時無意義的觸發(fā);使用 ET 模式時,如果你的實現(xiàn)也依賴于可寫事件觸發(fā)去發(fā)送數(shù)據(jù),可寫事件觸發(fā)后,你調(diào)用 send 函數(shù)(Linux 平臺也可以使用 write)去發(fā)送數(shù)據(jù),如果數(shù)據(jù)本次不能全部發(fā)送完(對于非阻塞的 socket,此時 send 函數(shù)返回 -1,錯誤碼為 EAGAIN 或 EWOULDBLOCK),你一定要繼續(xù)注冊檢測可寫事件,否則你剩余的數(shù)據(jù)就再也沒有機會發(fā)送了,因為 ET 模式的可寫事件再也不會觸發(fā)。
在目前主流的網(wǎng)絡庫中,發(fā)數(shù)據(jù)的邏輯都不是上面所說的依賴于寫事件觸發(fā),在寫事件觸發(fā)時去發(fā)數(shù)據(jù)。這種做法不好,那好的做法是什么呢?
最后容我再啰嗦幾句,總結(jié)起來:
- LT 模式下,讀事件觸發(fā)后,可以按需收取想要的字節(jié)數(shù),不用把本次接收到的數(shù)據(jù)收取干凈(即不用循環(huán)到 recv 或者 read 函數(shù)返回 -1,錯誤碼為 EWOULDBLOCK 或 EAGAIN);ET 模式下,讀事件必須把數(shù)據(jù)收取干凈,因為你不一定有下一次機會再收取數(shù)據(jù)了,即使有機會,也可能存在上次沒讀完的數(shù)據(jù)沒有及時處理,造成客戶端響應延遲。
- LT 模式下,不需要寫事件一定要及時移除,避免不必要的觸發(fā),浪費 CPU 資源;ET 模式下,寫事件觸發(fā)后,如果還需要下一次的寫事件觸發(fā)來驅(qū)動任務(例如發(fā)上次剩余的數(shù)據(jù)),你需要繼續(xù)注冊一次檢測可寫事件。
你一定要透徹地理解 epoll 的 LT 模式和 ET 模式在數(shù)據(jù)讀寫時的區(qū)別。因為,現(xiàn)代互聯(lián)網(wǎng)大環(huán)境下作為后臺服務載體的主流操作系統(tǒng)是 Linux,而 epoll 系統(tǒng)調(diào)用是 Linux 下實現(xiàn)高性能服務網(wǎng)絡模塊的必備組件!只有理解了它們,你才能編寫出高性能的網(wǎng)絡通信庫乃至整個服務。