一、簡介
與 poll 的事件宏相比,epoll 新增了一個事件宏 EPOLLET,這就是所謂的邊緣觸發模式(Edge Trigger,ET),而預設的模式我們稱為 水準觸發模式(Level Trigger,LT)。這兩種模式的差別在于:
- 對于水準觸發模式,一個事件隻要有,就會一直觸發;
- 對于邊緣觸發模式,隻有一個事件從無到有才會觸發。
這兩個詞彙來自電學術語,你可以将 fd 上有資料認為是高電平,沒有資料認為是低電平,将 fd 可寫認為是高電平,fd 不可寫認為是低電平。那麼水準模式的觸發條件是狀态處于高電平,而邊緣模式的觸發條件是新來一次電信号将目前狀态變為高電平,即:
水準模式的觸發條件
- 1. 低電平 => 高電平
- 2. 處于高電平狀态
邊緣模式的觸發條件
說的有點抽象,以 socket 的讀事件為例,對于水準模式,隻要 socket 上有未讀完的資料,就會一直産生 EPOLLIN 事件;而對于邊緣模式,socket 上每新來一次資料就會觸發一次,如果上一次觸發後,未将 socket 上的資料讀完,也不會再觸發,除非再新來一次資料。對于 socket 寫事件,如果 socket 的 TCP 視窗一直不飽和,會一直觸發 EPOLLOUT 事件;而對于邊緣模式,隻會觸發一次,除非 TCP 視窗由不飽和變成飽和再一次變成不飽和,才會再次觸發 EPOLLOUT 事件。
二、觸發條件
2.1 socket 可讀事件水準模式觸發條件
1、socket上無資料 => socket上有資料
2、socket處于有資料狀态
2.2 socket 可讀事件邊沿模式觸發條件
2、socket又新來一次資料
2.3 socket 可寫事件水準模式觸發條件
1、socket可寫=>socket可寫
2、socket不可寫=>socket可寫
2.3 socket 可寫事件邊沿模式觸發條件
1、socket不可寫=>socket可寫
三、使用
如果對于一個非阻塞 socket,如果使用 epoll 邊緣模式去檢測資料是否可讀,觸發可讀事件以後,一定要一次性把 socket 上的資料收取幹淨才行,也就是說一定要循環調用 recv 函數直到 recv 出錯,錯誤碼是EWOULDBLOCK(EAGAIN 一樣)(此時表示 socket 上本次資料已經讀完);如果使用水準模式,則不用,你可以根據業務一次性收取固定的位元組數,或者收完為止。邊緣模式下收取資料的代碼寫法示例如下:
1 bool TcpSession::RecvEtMode()
2 {
3 //每次隻收取256個位元組
4 char buff[256];
5 while (true)
6 {
7 int nRecv = ::recv(clientfd_, buff, 256, 0);
8 if (nRecv == -1)
9 {
10 if (errno == EWOULDBLOCK)
11 return true;
12 else if (errno == EINTR)
13 continue;
14
15 return false;
16 }
17 //對端關閉了socket
18 else if (nRecv == 0)
19 return false;
20
21 inputBuffer_.add(buff, (size_t)nRecv);
22 }
23
24 return true;
25 }
下面我們來看幾個具體的例子來比較一下 LT 模式與 ET 模式的差別。
先來測試一下 LT 模式 與 ET 模式在處理讀事件上的差別。
代碼如下:
1 /**
2 * 驗證epoll的LT與ET模式的差別, epoll_server.cpp
3 * zhangyl 2019.04.01
4 */
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<unistd.h>
9 #include<fcntl.h>
10 #include<sys/epoll.h>
11 #include<poll.h>
12 #include<iostream>
13 #include<string.h>
14 #include<vector>
15 #include<errno.h>
16 #include<iostream>
17
18 int main()
19 {
20 //建立一個監聽socket
21 int listenfd = socket(AF_INET, SOCK_STREAM, 0);
22 if (listenfd == -1)
23 {
24 std::cout << "create listen socket error" << std::endl;
25 return -1;
26 }
27
28 //設定重用ip位址和端口号
29 int on = 1;
30 setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*)&on, sizeof(on));
31 setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*)&on, sizeof(on));
32
33
34 //将監聽socker設定為非阻塞的
35 int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
36 int newSocketFlag = oldSocketFlag | O_NONBLOCK;
37 if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1)
38 {
39 close(listenfd);
40 std::cout << "set listenfd to nonblock error" << std::endl;
41 return -1;
42 }
43
44 //初始化伺服器位址
45 struct sockaddr_in bindaddr;
46 bindaddr.sin_family = AF_INET;
47 bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
48 bindaddr.sin_port = htons(3000);
49
50 if (bind(listenfd, (struct sockaddr*)&bindaddr, sizeof(bindaddr)) == -1)
51 {
52 std::cout << "bind listen socker error." << std::endl;
53 close(listenfd);
54 return -1;
55 }
56
57 //啟動監聽
58 if (listen(listenfd, SOMAXCONN) == -1)
59 {
60 std::cout << "listen error." << std::endl;
61 close(listenfd);
62 return -1;
63 }
64
65
66 //建立epollfd
67 int epollfd = epoll_create(1);
68 if (epollfd == -1)
69 {
70 std::cout << "create epollfd error." << std::endl;
71 close(listenfd);
72 return -1;
73 }
74
75 epoll_event listen_fd_event;
76 listen_fd_event.data.fd = listenfd;
77 listen_fd_event.events = EPOLLIN;
78 //取消注釋掉這一行,則使用ET模式
79 //listen_fd_event.events |= EPOLLET;
80
81 //将監聽sokcet綁定到epollfd上去
82 if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event) == -1)
83 {
84 std::cout << "epoll_ctl error" << std::endl;
85 close(listenfd);
86 return -1;
87 }
88
89 int n;
90 while (true)
91 {
92 epoll_event epoll_events[1024];
93 n = epoll_wait(epollfd, epoll_events, 1024, 1000);
94 if (n < 0)
95 {
96 //被信号中斷
97 if (errno == EINTR)
98 continue;
99
100 //出錯,退出
101 break;
102 }
103 else if (n == 0)
104 {
105 //逾時,繼續
106 continue;
107 }
108 for (size_t i = 0; i < n; ++i)
109 {
110 //事件可讀
111 if (epoll_events[i].events & EPOLLIN)
112 {
113 if (epoll_events[i].data.fd == listenfd)
114 {
115 //偵聽socket,接受新連接配接
116 struct sockaddr_in clientaddr;
117 socklen_t clientaddrlen = sizeof(clientaddr);
118 int clientfd = accept(listenfd, (struct sockaddr*)&clientaddr, &clientaddrlen);
119 if (clientfd != -1)
120 {
121 int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
122 int newSocketFlag = oldSocketFlag | O_NONBLOCK;
123 if (fcntl(clientfd, F_SETFD, newSocketFlag) == -1)
124 {
125 close(clientfd);
126 std::cout << "set clientfd to nonblocking error." << std::endl;
127 }
128 else
129 {
130 epoll_event client_fd_event;
131 client_fd_event.data.fd = clientfd;
132 client_fd_event.events = EPOLLIN;
133 //取消注釋這一行,則使用ET模式
134 //client_fd_event.events |= EPOLLET;
135 if (epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &client_fd_event) != -1)
136 {
137 std::cout << "new client accepted,clientfd: " << clientfd << std::endl;
138 }
139 else
140 {
141 std::cout << "add client fd to epollfd error" << std::endl;
142 close(clientfd);
143 }
144 }
145 }
146 }
147 else
148 {
149 std::cout << "client fd: " << epoll_events[i].data.fd << " recv data." << std::endl;
150 //普通clientfd
151 char ch;
152 //每次隻收一個位元組
153 int m = recv(epoll_events[i].data.fd, &ch, 1, 0);
154 if (m == 0)
155 {
156 //對端關閉了連接配接,從epollfd上移除clientfd
157 if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
158 {
159 std::cout << "client disconnected,clientfd:" << epoll_events[i].data.fd << std::endl;
160 }
161 close(epoll_events[i].data.fd);
162 }
163 else if (m < 0)
164 {
165 //出錯
166 if (errno != EWOULDBLOCK && errno != EINTR)
167 {
168 if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
169 {
170 std::cout << "client disconnected,clientfd:" << epoll_events[i].data.fd << std::endl;
171 }
172 close(epoll_events[i].data.fd);
173 }
174 }
175 else
176 {
177 //正常收到資料
178 std::cout << "recv from client:" << epoll_events[i].data.fd << ", " << ch << std::endl;
179 }
180 }
181 }
182 else if (epoll_events[i].events & EPOLLERR)
183 {
184 // TODO 暫不處理
185 }
186 }
187 }
188
189 close(listenfd);
190 return 0;
191 }
View Code
我們先來看水準模式的行為,将代碼 79 行和 134 行注釋掉則使用 LT 模式,我們編譯下程式并運作:
1 [root@localhost testepoll]# g++ -g -o epoll_server epoll_server.cpp
2 [root@localhost testepoll]# ./epoll_server
然後再另外開啟一個 shell 視窗,使用 nc 指令模拟一個用戶端,連接配接伺服器成功後,我們給伺服器發送一個消息"abcef":
1 [root@localhost ~]# nc -v 127.0.0.1 3000
2 Ncat: Version 7.50 ( https://nmap.org/ncat )
3 Ncat: Connected to 127.0.0.1:3000.
4 abcdef
此時伺服器端輸出:
1 [root@localhost testepoll]# ./epoll_server
2 new client accepted,clientfd: 5
3 client fd: 5 recv data.
4 recv from client:5, a
5 client fd: 5 recv data.
6 recv from client:5, b
7 client fd: 5 recv data.
8 recv from client:5, c
9 client fd: 5 recv data.
10 recv from client:5, d
11 client fd: 5 recv data.
12 recv from client:5, e
13 client fd: 5 recv data.
14 recv from client:5, f
15 client fd: 5 recv data.
16 recv from client:5,
nc 指令實際發送了 a、b、c、d、e、f 和 \n 七個字元,由于伺服器端使用的是 LT 模式,每次接收一個字元,隻要 socket 接收緩沖區中仍有資料可讀,POLLIN 事件就會一直觸發,是以伺服器一共有 7 次輸出,直到 socket 接收緩沖區沒有資料為止。
我們将代碼 79 行和 134 行注釋取消掉,使用 ET 模式再試一下,修改代碼并重新編譯,然後重新運作一下。再次使用 nc 指令模拟一個用戶端連接配接後發送"abcef",伺服器隻會有一次輸出,效果如下:
由于使用了 ET 模式,隻會觸發一次 POLLIN 事件,如果此時沒有新資料到來,就再也不會觸發。是以,如果我們繼續給伺服器發送一條新資料,如 123,伺服器将再次觸發一次 POLLIN 事件,然後列印出字母 b,效果如下:
是以如果使用 ET 模式 處理讀事件,切記要将該次 socket 上的資料收完。
再來測試一下 LT 模式 與 ET 模式在處理寫事件上的差別。
修改上述代碼如下:
1 /**
2 * 驗證epoll的LT與ET模式的差別, epoll_server.cpp
3 * zhangyl 2019.04.01
4 */
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<unistd.h>
9 #include<fcntl.h>
10 #include<sys/epoll.h>
11 #include<poll.h>
12 #include<iostream>
13 #include<string.h>
14 #include<vector>
15 #include<errno.h>
16 #include<iostream>
17
18 int main()
19 {
20 //建立一個監聽socket
21 int listenfd = socket(AF_INET, SOCK_STREAM, 0);
22 if (listenfd == -1)
23 {
24 std::cout << "create listen socket error" << std::endl;
25 return -1;
26 }
27
28 //設定重用ip位址和端口号
29 int on = 1;
30 setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*)& on, sizeof(on));
31 setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*)& on, sizeof(on));
32
33
34 //将監聽socker設定為非阻塞的
35 int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
36 int newSocketFlag = oldSocketFlag | O_NONBLOCK;
37 if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1)
38 {
39 close(listenfd);
40 std::cout << "set listenfd to nonblock error" << std::endl;
41 return -1;
42 }
43
44 //初始化伺服器位址
45 struct sockaddr_in bindaddr;
46 bindaddr.sin_family = AF_INET;
47 bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
48 bindaddr.sin_port = htons(3000);
49
50 if (bind(listenfd, (struct sockaddr*) & bindaddr, sizeof(bindaddr)) == -1)
51 {
52 std::cout << "bind listen socker error." << std::endl;
53 close(listenfd);
54 return -1;
55 }
56
57 //啟動監聽
58 if (listen(listenfd, SOMAXCONN) == -1)
59 {
60 std::cout << "listen error." << std::endl;
61 close(listenfd);
62 return -1;
63 }
64
65
66 //建立epollfd
67 int epollfd = epoll_create(1);
68 if (epollfd == -1)
69 {
70 std::cout << "create epollfd error." << std::endl;
71 close(listenfd);
72 return -1;
73 }
74
75 epoll_event listen_fd_event;
76 listen_fd_event.data.fd = listenfd;
77 listen_fd_event.events = EPOLLIN;
78 //取消注釋掉這一行,則使用ET模式
79 //listen_fd_event.events |= EPOLLET;
80
81 //将監聽sokcet綁定到epollfd上去
82 if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event) == -1)
83 {
84 std::cout << "epoll_ctl error" << std::endl;
85 close(listenfd);
86 return -1;
87 }
88
89 int n;
90 while (true)
91 {
92 epoll_event epoll_events[1024];
93 n = epoll_wait(epollfd, epoll_events, 1024, 1000);
94 if (n < 0)
95 {
96 //被信号中斷
97 if (errno == EINTR)
98 continue;
99
100 //出錯,退出
101 break;
102 }
103 else if (n == 0)
104 {
105 //逾時,繼續
106 continue;
107 }
108 for (size_t i = 0; i < n; ++i)
109 {
110 //事件可讀
111 if (epoll_events[i].events & EPOLLIN)
112 {
113 if (epoll_events[i].data.fd == listenfd)
114 {
115 //偵聽socket,接受新連接配接
116 struct sockaddr_in clientaddr;
117 socklen_t clientaddrlen = sizeof(clientaddr);
118 int clientfd = accept(listenfd, (struct sockaddr*) & clientaddr, &clientaddrlen);
119 if (clientfd != -1)
120 {
121 int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
122 int newSocketFlag = oldSocketFlag | O_NONBLOCK;
123 if (fcntl(clientfd, F_SETFD, newSocketFlag) == -1)
124 {
125 close(clientfd);
126 std::cout << "set clientfd to nonblocking error." << std::endl;
127 }
128 else
129 {
130 epoll_event client_fd_event;
131 client_fd_event.data.fd = clientfd;
132 //同時偵聽新來連接配接socket的讀和寫時間
133 client_fd_event.events = EPOLLIN | EPOLLOUT;
134 //取消注釋這一行,則使用ET模式
135 //client_fd_event.events |= EPOLLET;
136 if (epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &client_fd_event) != -1)
137 {
138 std::cout << "new client accepted,clientfd: " << clientfd << std::endl;
139 }
140 else
141 {
142 std::cout << "add client fd to epollfd error" << std::endl;
143 close(clientfd);
144 }
145 }
146 }
147 }
148 else
149 {
150 std::cout << "client fd: " << epoll_events[i].data.fd << " recv data." << std::endl;
151 //普通clientfd
152 char recvbuf[1024] = { 0 };
153 //每次隻收一個位元組
154 int m = recv(epoll_events[i].data.fd, recvbuf, 1024, 0);
155 if (m == 0)
156 {
157 //對端關閉了連接配接,從epollfd上移除clientfd
158 if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
159 {
160 std::cout << "client disconnected,clientfd:" << epoll_events[i].data.fd << std::endl;
161 }
162 close(epoll_events[i].data.fd);
163 }
164 else if (m < 0)
165 {
166 //出錯
167 if (errno != EWOULDBLOCK && errno != EINTR)
168 {
169 if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
170 {
171 std::cout << "client disconnected,clientfd:" << epoll_events[i].data.fd << std::endl;
172 }
173 close(epoll_events[i].data.fd);
174 }
175 }
176 else
177 {
178 //正常收到資料
179 std::cout << "recv from client:" << epoll_events[i].data.fd << ", " << recvbuf << std::endl;
180 }
181 }
182 }
183 else if (epoll_events[i].events & EPOLLOUT)
184 {
185 //隻處理用戶端fd的可寫事件
186 if (epoll_events[i].data.fd != listenfd)
187 {
188 //列印結果
189 std::cout << "EPOLLOUT triggered,clientfd:" << epoll_events[i].data.fd << std::endl;
190 }
191 }
192 else if (epoll_events[i].events & EPOLLERR)
193 {
194 // TODO 暫不處理
195 }
196 }
197 }
198
199 close(listenfd);
200 return 0;
201 }
上述代碼中,我們對新來的連接配接 fd 同時注冊讀和寫事件(代碼 133 行),再次編譯程式執行:
1 [root@iZ238vnojlyZ testepollet]# vi epoll.cpp
2 [root@iZ238vnojlyZ testepollet]# g++ -g -o epoll epoll_server.cpp
3 [root@iZ238vnojlyZ testepollet]# ./epoll_server
然後使用 nc 指令模拟一個用戶端去連接配接 epoll_server:
1 [root@iZ238vnojlyZ ~]# nc -v 127.0.0.1 3000
2 Ncat: Version 6.40 ( http://nmap.org/ncat )
3 Ncat: Connected to 127.0.0.1:3000.
此時伺服器端(epoll_server)會瘋狂的輸出可寫事件觸發消息:
之是以是這樣,是因為我們注冊了可寫事件且使用的是 LT 模式,LT 模式下,由于這裡的伺服器端對應的用戶端 fd 一直是可寫的,有寫事件一直觸發,是以看到螢幕不斷輸出。
我們再将伺服器端與用戶端建立連接配接時建立的 fd 設定為 ET 模式再實驗一下:
1 /**
2 * 驗證epoll的LT與ET模式的差別, epoll_server.cpp
3 * zhangyl 2019.04.01
4 */
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<arpa/inet.h>
8 #include<unistd.h>
9 #include<fcntl.h>
10 #include<sys/epoll.h>
11 #include<poll.h>
12 #include<iostream>
13 #include<string.h>
14 #include<vector>
15 #include<errno.h>
16 #include<iostream>
17
18 int main()
19 {
20 //建立一個監聽socket
21 int listenfd = socket(AF_INET, SOCK_STREAM, 0);
22 if (listenfd == -1)
23 {
24 std::cout << "create listen socket error" << std::endl;
25 return -1;
26 }
27
28 //設定重用ip位址和端口号
29 int on = 1;
30 setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char*)& on, sizeof(on));
31 setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char*)& on, sizeof(on));
32
33
34 //将監聽socker設定為非阻塞的
35 int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
36 int newSocketFlag = oldSocketFlag | O_NONBLOCK;
37 if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1)
38 {
39 close(listenfd);
40 std::cout << "set listenfd to nonblock error" << std::endl;
41 return -1;
42 }
43
44 //初始化伺服器位址
45 struct sockaddr_in bindaddr;
46 bindaddr.sin_family = AF_INET;
47 bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
48 bindaddr.sin_port = htons(3000);
49
50 if (bind(listenfd, (struct sockaddr*) & bindaddr, sizeof(bindaddr)) == -1)
51 {
52 std::cout << "bind listen socker error." << std::endl;
53 close(listenfd);
54 return -1;
55 }
56
57 //啟動監聽
58 if (listen(listenfd, SOMAXCONN) == -1)
59 {
60 std::cout << "listen error." << std::endl;
61 close(listenfd);
62 return -1;
63 }
64
65
66 //建立epollfd
67 int epollfd = epoll_create(1);
68 if (epollfd == -1)
69 {
70 std::cout << "create epollfd error." << std::endl;
71 close(listenfd);
72 return -1;
73 }
74
75 epoll_event listen_fd_event;
76 listen_fd_event.data.fd = listenfd;
77 listen_fd_event.events = EPOLLIN;
78 //取消注釋掉這一行,則使用ET模式
79 //listen_fd_event.events |= EPOLLET;
80
81 //将監聽sokcet綁定到epollfd上去
82 if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event) == -1)
83 {
84 std::cout << "epoll_ctl error" << std::endl;
85 close(listenfd);
86 return -1;
87 }
88
89 int n;
90 while (true)
91 {
92 epoll_event epoll_events[1024];
93 n = epoll_wait(epollfd, epoll_events, 1024, 1000);
94 if (n < 0)
95 {
96 //被信号中斷
97 if (errno == EINTR)
98 continue;
99
100 //出錯,退出
101 break;
102 }
103 else if (n == 0)
104 {
105 //逾時,繼續
106 continue;
107 }
108 for (size_t i = 0; i < n; ++i)
109 {
110 //事件可讀
111 if (epoll_events[i].events & EPOLLIN)
112 {
113 if (epoll_events[i].data.fd == listenfd)
114 {
115 //偵聽socket,接受新連接配接
116 struct sockaddr_in clientaddr;
117 socklen_t clientaddrlen = sizeof(clientaddr);
118 int clientfd = accept(listenfd, (struct sockaddr*) & clientaddr, &clientaddrlen);
119 if (clientfd != -1)
120 {
121 int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
122 int newSocketFlag = oldSocketFlag | O_NONBLOCK;
123 if (fcntl(clientfd, F_SETFD, newSocketFlag) == -1)
124 {
125 close(clientfd);
126 std::cout << "set clientfd to nonblocking error." << std::endl;
127 }
128 else
129 {
130 epoll_event client_fd_event;
131 client_fd_event.data.fd = clientfd;
132 //同時偵聽新來連接配接socket的讀和寫時間
133 client_fd_event.events = EPOLLIN | EPOLLOUT;
134 //取消注釋這一行,則使用ET模式
135 client_fd_event.events |= EPOLLET;
136 if (epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &client_fd_event) != -1)
137 {
138 std::cout << "new client accepted,clientfd: " << clientfd << std::endl;
139 }
140 else
141 {
142 std::cout << "add client fd to epollfd error" << std::endl;
143 close(clientfd);
144 }
145 }
146 }
147 }
148 else
149 {
150 std::cout << "client fd: " << epoll_events[i].data.fd << " recv data." << std::endl;
151 //普通clientfd
152 char recvbuf[1024] = { 0 };
153 //每次隻收一個位元組
154 int m = recv(epoll_events[i].data.fd, recvbuf, 1024, 0);
155 if (m == 0)
156 {
157 //對端關閉了連接配接,從epollfd上移除clientfd
158 if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
159 {
160 std::cout << "client disconnected,clientfd:" << epoll_events[i].data.fd << std::endl;
161 }
162 close(epoll_events[i].data.fd);
163 }
164 else if (m < 0)
165 {
166 //出錯
167 if (errno != EWOULDBLOCK && errno != EINTR)
168 {
169 if (epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
170 {
171 std::cout << "client disconnected,clientfd:" << epoll_events[i].data.fd << std::endl;
172 }
173 close(epoll_events[i].data.fd);
174 }
175 }
176 else
177 {
178 //正常收到資料
179 std::cout << "recv from client:" << epoll_events[i].data.fd << ", " << recvbuf << std::endl;
180
181 epoll_event client_fd_event;
182 client_fd_event.data.fd = epoll_events[i].data.fd;
183 //再次給clientfd注冊檢測可寫事件
184 client_fd_event.events = EPOLLIN | EPOLLOUT | EPOLLET;
185 if (epoll_ctl(epollfd, EPOLL_CTL_MOD, epoll_events[i].data.fd, &client_fd_event) != -1)
186 {
187 std::cout << "epoll_ctl successfully, mode: EPOLL_CTL_MOD, clientfd:" << epoll_events[i].data.fd << std::endl;
188 }
189
190 }
191 }
192 }
193 else if (epoll_events[i].events & EPOLLOUT)
194 {
195 //隻處理用戶端fd的可寫事件
196 if (epoll_events[i].data.fd != listenfd)
197 {
198 //列印結果
199 std::cout << "EPOLLOUT triggered,clientfd:" << epoll_events[i].data.fd << std::endl;
200 }
201 }
202 else if (epoll_events[i].events & EPOLLERR)
203 {
204 // TODO 暫不處理
205 }
206 }
207 }
208
209 close(listenfd);
210 return 0;
211 }
上述邏輯中,伺服器端在每次收到用戶端消息時會重新給用戶端 fd 注冊檢測可寫事件(EPOLLOUT),重新編譯代碼,啟動 epoll_server,再次使用 nc 指令模拟用戶端給 epoll_server 發送幾條消息,結果如下:
通過上述輸出,我們可以發現,epoll_server 使用 ET 模式下即使給用戶端 fd 注冊了檢測可寫事件不會一直觸發,隻會觸發一次,觸發完後隻有再次注冊檢測可寫事件才會繼續觸發,這裡是靠用戶端來新消息驅動再次注冊檢測可寫事件。也就是說,如果我們使用 ET 模式去處理可寫事件時不必像 LT 模式那樣為了避免不必要的可寫觸發在觸發後需要立即移除檢測可寫事件。
這就意味着,使用 LT 模式,如果你的實作依賴于可寫事件觸發去發送資料,那麼你一定要在資料發送完之後移除檢測可寫事件,避免沒有資料發送時無意義的觸發;使用 ET 模式時,如果你的實作也依賴于可寫事件觸發去發送資料,可寫事件觸發後,你調用 send 函數(Linux 平台也可以使用 write)去發送資料,如果資料本次不能全部發送完(對于非阻塞的 socket,此時 send 函數傳回 -1,錯誤碼為 EAGAIN 或 EWOULDBLOCK),你一定要繼續注冊檢測可寫事件,否則你剩餘的資料就再也沒有機會發送了,因為 ET 模式的可寫事件再也不會觸發。
總結起來:
- LT 模式下,讀事件觸發後,可以按需收取想要的位元組數,不用把本次接收到的資料收取幹淨(即不用循環到 recv 或者 read 函數傳回 -1,錯誤碼為 EWOULDBLOCK 或 EAGAIN);ET 模式下,讀事件必須把資料收取幹淨,因為你不一定有下一次機會再收取資料了,即使有機會,也可能存在上次沒讀完的資料沒有及時處理,造成用戶端響應延遲。
- LT 模式下,不需要寫事件一定要及時移除,避免不必要的觸發,浪費 CPU 資源;ET 模式下,寫事件觸發後,如果還需要下一次的寫事件觸發來驅動任務(例如發上次剩餘的資料),你需要繼續注冊一次檢測可寫事件。
- LT 模式和 ET 模式各有優缺點,無所謂孰優孰劣。使用 LT 模式,我們可以自由決定每次收取多少位元組(對于普通 socket)或何時接收連接配接(對于偵聽 socket),但是可能會導緻多次觸發;使用 ET 模式,我們必須每次都要将資料收完(對于普通 socket)或必須了解調用 accept 接收連接配接(對于偵聽socket),其優點是觸發次數少。
四、轉載于
https://cloud.tencent.com/developer/article/1636224
作者:Mr-xxx,轉載請注明原文連結