Linux的epoll使用LT+非阻塞IO和ET+非阻塞IO的区别(转载)

Linux的epoll使用LT+非阻塞IO和ET+非阻塞IO的区别(转载)

综合 selectpoll 的一些优缺点,Linux 从内核2.6版本开始引入了更高效的 epoll 模型。

与 poll 的事件宏相比,epoll 新增了一个事件宏 EPOLLET,这就是所谓的边缘触发模式Edge Trigger,ET),而默认的模式我们称为 水平触发模式Level Trigger,LT)。这两种模式的区别在于:

  • 对于水平触发模式,一个事件只要有,就会一直触发;
  • 对于边缘触发模式,只有一个事件从无到有才会触发。

这两个词汇来自电学术语,你可以将 fd 上有数据认为是高电平,没有数据认为是低电平,将 fd 可写认为是高电平,fd 不可写认为是低电平。那么水平模式的触发条件是状态处于高电平,而边缘模式的触发条件是新来一次电信号将当前状态变为高电平,即:

水平模式的触发条件

1
2
1. 低电平 => 高电平
2. 处于高电平状态

边缘模式的触发条件

1
1. 低电平 => 高电平

说的有点抽象,以 socket 的读事件为例,对于水平模式,只要 socket 上有未读完的数据,就会一直产生 EPOLLIN 事件;而对于边缘模式,socket 上每新来一次数据就会触发一次,如果上一次触发后,未将 socket 上的数据读完,也不会再触发,除非再新来一次数据。对于 socket 写事件,如果 socket 的 TCP 窗口一直不饱和,会一直触发 EPOLLOUT 事件;而对于边缘模式,只会触发一次,除非 TCP 窗口由不饱和变成饱和再一次变成不饱和,才会再次触发 EPOLLOUT 事件。

socket 可读事件水平模式触发条件:

1
2
1. socket上无数据 => socket上有数据
2. socket处于有数据状态

socket 可读事件边缘模式触发条件:

1
2
1. socket上无数据 => socket上有数据
2. socket又新来一次数据

socket 可写事件水平模式触发条件:

1
2
1. socket可写   => socket可写
2. socket不可写 => socket可写

socket 可写事件边缘模式触发条件:

1
1. socket不可写 => socket可写

也就是说,如果对于一个非阻塞 socket,如果使用 epoll 边缘模式去检测数据是否可读,触发可读事件以后,一定要一次性把 socket 上的数据收取干净才行,也就是说一定要循环调用 recv 函数直到 recv 出错,错误码是 EWOULDBLOCKEAGAIN 一样)(此时表示 socket 上本次数据已经读完);如果使用水平模式,则不用,你可以根据业务一次性收取固定的字节数,或者收完为止。边缘模式下收取数据的代码写法示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

bool TcpSession::RecvEtMode()
{
//每次只收取256个字节
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 模式的区别。

先来测试一下 LT 模式ET 模式在处理读事件上的区别。

代码如下:

>folded epoll_server.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
/** 
* 验证epoll的LT与ET模式的区别, epoll_server.cpp
*/
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/epoll.h>
#include<poll.h>
#include<iostream>
#include<string.h>
#include<vector>
#include<errno.h>
#include<iostream>

int main()
{
//创建一个监听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));


//将监听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;
}

//启动监听
if (listen(listenfd, SOMAXCONN) == -1)
{
std::cout << "listen error." << std::endl;
close(listenfd);
return -1;
}


//创建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;

//将监听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)
{
//超时,继续
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;
//每次只收一个字节
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
{
//正常收到数据
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 模式,我们编译下程序并运行:

1
2
3
4
5
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ vim epoll_server.cpp 
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ g++ -g -o epoll_server epoll_server.cpp
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ ls
epoll_server epoll_server.cpp
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ ./epoll_server

然后再另外开启一个 shell 窗口,使用 nc 命令模拟一个客户端,连接服务器成功后,我们给服务器发送一个消息“abcef”

1
2
3
ubuntu@VM-0-9-ubuntu:~$ nc -v 127.0.0.1 3000
Connection to 127.0.0.1 3000 port [tcp/*] succeeded!
abdcef

此时服务器端输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ ./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, d
client fd: 5 recv data.
recv from client:5, c
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 命令实际发送了 a、b、c、d、e、f\n 七个字符,由于服务器端使用的是 LT 模式,每次接收一个字符,只要 socket 接收缓冲区中仍有数据可读,POLLIN 事件就会一直触发,所以服务器一共有 7 次输出,直到 socket 接收缓冲区没有数据为止。

我们将代码 79 行和 134 行注释取消掉,使用 ET 模式再试一下,修改代码并重新编译,然后重新运行一下。再次使用 nc 命令模拟一个客户端连接后发送”abcef”,服务器只会有一次输出,效果如下:

1
2
3
4
5
6
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ vim epoll_server.cpp 
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ g++ -g -o epoll_server epoll_server.cpp
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ ./epoll_server
new client accepted,clientfd: 5
client fd: 5 recv data.
recv from client:5, a

由于使用了 ET 模式,只会触发一次 POLLIN 事件,如果此时没有新数据到来,就再也不会触发。所以,如果我们继续给服务器发送一条新数据,如 123,服务器将再次触发一次 POLLIN 事件,然后打印出字母 b,效果如下:

1
2
3
4
ubuntu@VM-0-9-ubuntu:~$ nc -v 127.0.0.1 3000
Connection to 127.0.0.1 3000 port [tcp/*] succeeded!
abdcef
123
1
2
3
4
5
6
7
8
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ vim epoll_server.cpp 
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ g++ -g -o epoll_server epoll_server.cpp
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ ./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

所以如果使用 ET 模式 处理读事件,切记要将该次 socket 上的数据收完。

再来测试一下 LT 模式ET 模式在处理写事件上的区别。

修改上述代码如下:

>folded 点击>查看 epoll_server.cpp(EPOLLOUT LT)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
/** 
* 验证epoll的LT与ET模式的区别, epoll_server.cpp
*/
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/epoll.h>
#include<poll.h>
#include<iostream>
#include<string.h>
#include<vector>
#include<errno.h>
#include<iostream>

int main()
{
//创建一个监听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));


//将监听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;
}

//启动监听
if (listen(listenfd, SOMAXCONN) == -1)
{
std::cout << "listen error." << std::endl;
close(listenfd);
return -1;
}


//创建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;

//将监听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)
{
//超时,继续
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 ch;
//每次只收一个字节
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
{
//正常收到数据
std::cout << "recv from client:" << epoll_events[i].data.fd << ", " << ch << std::endl;
}
}
}
else if (epoll_events[i].events & EPOLLOUT)
{
//只处理客户端fd的可写事件
if (epoll_events[i].data.fd != listenfd)
{
//打印结果
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 行),再次编译程序执行:

1
2
3
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ vim epoll_server.cpp 
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ g++ -g -o epoll_server epoll_server.cpp
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ ./epoll_server

然后使用 nc 命令模拟一个客户端去连接 epoll_server

1
2
ubuntu@VM-0-9-ubuntu:~$ nc -v 127.0.0.1 3000
Connection to 127.0.0.1 3000 port [tcp/*] succeeded!

此时服务器端(epoll_server)会疯狂的输出可写事件触发消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
EPOLLOUT triggered,clientfd:5
E^C
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$

之所以是这样,是因为我们注册了可写事件且使用的是 LT 模式,LT 模式下,由于这里的服务器端对应的客户端 fd 一直是可写的,有写事件一直触发,所以看到屏幕不断输出。

我们再将服务器端与客户端建立连接时新建的 fd 设置为 ET 模式再实验一下:

>folded 点击>查看 epoll_server.cpp(EPOLLOUT ET)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
/** 
* 验证epoll的LT与ET模式的区别, epoll_server.cpp
*/
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/epoll.h>
#include<poll.h>
#include<iostream>
#include<string.h>
#include<vector>
#include<errno.h>
#include<iostream>

int main()
{
//创建一个监听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));


//将监听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;
}

//启动监听
if (listen(listenfd, SOMAXCONN) == -1)
{
std::cout << "listen error." << std::endl;
close(listenfd);
return -1;
}


//创建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;

//将监听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)
{
//超时,继续
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 };
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
{
//正常收到数据
std::cout << "recv from client:" << epoll_events[i].data.fd << ", " << ch << 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)
{
//打印结果
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 发送几条消息,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ vim epoll_server.cpp 
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ g++ -g -o epoll_server epoll_server.cpp
ubuntu@VM-0-9-ubuntu:~/recipes/epoll_server$ ./epoll_server
new client accepted,clientfd: 5
EPOLLOUT triggered,clientfd:5
client fd: 5 recv data.
recv from client:5, hello

epoll_ctl successfully, mode: EPOLL_CTL_MOD, clientfd:5
EPOLLOUT triggered,clientfd:5
client fd: 5 recv data.
recv from client:5, world

epoll_ctl successfully, mode: EPOLL_CTL_MOD, clientfd:5
EPOLLOUT triggered,clientfd:5
client fd: 5 recv data.
recv from client:5, !!!

epoll_ctl successfully, mode: EPOLL_CTL_MOD, clientfd:5
EPOLLOUT triggered,clientfd:5
1
2
3
4
5
ubuntu@VM-0-9-ubuntu:~$ nc -v 127.0.0.1 3000
Connection to 127.0.0.1 3000 port [tcp/*] succeeded!
hello
world
!!!

通过上述输出,我们可以发现,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),其优点是触发次数少。

原文链接:
epoll LT 模式和 ET 模式详解

评论

:D 一言句子获取中...

加载中,最新评论有1分钟缓存...