网站建设高清图,惠州网站建设佳木斯,常见网页制作工具,百度网盘 做网站图床一.基本概念 我们通俗一点讲#xff1a; Level_triggered(水平触发)#xff1a;当被监控的文件描述符上有可读写事件发生时#xff0c;epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如… 一.基本概念 我们通俗一点讲 Level_triggered(水平触发)当被监控的文件描述符上有可读写事件发生时epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小)那么下次调用 epoll_wait()时它还会通知你在上没读写完的文件描述符上继续读写当然如果你一直不去读写它会一直通知你如果系统中有大量你不需要读写的就绪文件描述符而它们每次都会返回这样会大大降低处理程序检索自己关心的就绪文件描述符的效率 Edge_triggered(边缘触发)当被监控的文件描述符上有可读写事件发生时epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小)那么下次调用epoll_wait()时它不会通知你也就是它只会通知你一次直到该文件描述符上出现第二次可读写事件才会通知你这种模式比水平触发效率高系统不会充斥大量你不关心的就绪文件描述符 阻塞IO当你去读一个阻塞的文件描述符时如果在该文件描述符上没有数据可读那么它会一直阻塞(通俗一点就是一直卡在调用函数那里)直到有数据可读。当你去写一个阻塞的文件描述符时如果在该文件描述符上没有空间(通常是缓冲区)可写那么它会一直阻塞直到有空间可写。以上的读和写我们统一指在某个文件描述符进行的操作不单单指真正的读数据写数据还包括接收连接accept()发起连接connect()等操作... 非阻塞IO当你去读写一个非阻塞的文件描述符时不管可不可以读写它都会立即返回返回成功说明读写操作完成了返回失败会设置相应errno状态码根据这个errno可以进一步执行其他处理。它不会像阻塞IO那样卡在那里不动 二.几种IO模型的触发方式 select(),poll()模型都是水平触发模式信号驱动IO是边缘触发模式epoll()模型即支持水平触发也支持边缘触发默认是水平触发。 这里我们要探讨epoll()的水平触发和边缘触发以及阻塞IO和非阻塞IO对它们的影响下面称水平触发为LT边缘触发为ET。 对于监听的socket文件描述符我们用sockfd代替对于accept()返回的文件描述符(即要读写的文件描述符)用connfd代替。 我们来验证以下几个内容 1.水平触发的非阻塞sockfd 2.边缘触发的非阻塞sockfd 3.水平触发的阻塞connfd 4.水平触发的非阻塞connfd 5.边缘触发的阻塞connfd 6.边缘触发的非阻塞connfd 以上没有验证阻塞的sockfd因为epoll_wait()返回必定是已就绪的连接设不设置阻塞accept()都会立即返回。例外UNP里面有个例子在BSD上使用select()模型。设置阻塞的监听sockfd时当客户端发起连接请求由于服务器繁忙没有来得及accept()此时客户端自己又断开当服务器到达accept()时会出现阻塞。本机测试epoll()模型没有出现这种情况我们就暂且忽略这种情况 三.验证代码 文件名epoll_lt_et.c 1 /* 2 *url:http://www.cnblogs.com/yuuyuu/p/5103744.html3 *4 */5 6 #include stdio.h7 #include stdlib.h8 #include string.h9 #include errno.h10 #include unistd.h11 #include fcntl.h12 #include arpa/inet.h13 #include netinet/in.h14 #include sys/socket.h15 #include sys/epoll.h16 17 /* 最大缓存区大小 */18 #define MAX_BUFFER_SIZE 519 /* epoll最大监听数 */20 #define MAX_EPOLL_EVENTS 2021 /* LT模式 */22 #define EPOLL_LT 023 /* ET模式 */24 #define EPOLL_ET 125 /* 文件描述符设置阻塞 */26 #define FD_BLOCK 027 /* 文件描述符设置非阻塞 */28 #define FD_NONBLOCK 129 30 /* 设置文件为非阻塞 */31 int set_nonblock(int fd)32 {33 int old_flags fcntl(fd, F_GETFL);34 fcntl(fd, F_SETFL, old_flags | O_NONBLOCK);35 return old_flags;36 }37 38 /* 注册文件描述符到epoll并设置其事件为EPOLLIN(可读事件) */39 void addfd_to_epoll(int epoll_fd, int fd, int epoll_type, int block_type)40 {41 struct epoll_event ep_event;42 ep_event.data.fd fd;43 ep_event.events EPOLLIN;44 45 /* 如果是ET模式设置EPOLLET */46 if (epoll_type EPOLL_ET)47 ep_event.events | EPOLLET;48 49 /* 设置是否阻塞 */50 if (block_type FD_NONBLOCK)51 set_nonblock(fd);52 53 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, ep_event);54 }55 56 /* LT处理流程 */57 void epoll_lt(int sockfd)58 {59 char buffer[MAX_BUFFER_SIZE];60 int ret;61 62 memset(buffer, 0, MAX_BUFFER_SIZE);63 printf(开始recv()...\n);64 ret recv(sockfd, buffer, MAX_BUFFER_SIZE, 0);65 printf(ret %d\n, ret);66 if (ret 0)67 printf(收到消息:%s, 共%d个字节\n, buffer, ret);68 else69 {70 if (ret 0)71 printf(客户端主动关闭\n);72 close(sockfd);73 }74 75 printf(LT处理结束\n);76 }77 78 /* 带循环的ET处理流程 */79 void epoll_et_loop(int sockfd)80 {81 char buffer[MAX_BUFFER_SIZE];82 int ret;83 84 printf(带循环的ET读取数据开始...\n);85 while (1)86 {87 memset(buffer, 0, MAX_BUFFER_SIZE);88 ret recv(sockfd, buffer, MAX_BUFFER_SIZE, 0);89 if (ret -1)90 {91 if (errno EAGAIN || errno EWOULDBLOCK)92 {93 printf(循环读完所有数据\n);94 break;95 }96 close(sockfd);97 break;98 }99 else if (ret 0)
100 {
101 printf(客户端主动关闭请求\n);
102 close(sockfd);
103 break;
104 }
105 else
106 printf(收到消息:%s, 共%d个字节\n, buffer, ret);
107 }
108 printf(带循环的ET处理结束\n);
109 }
110
111
112 /* 不带循环的ET处理流程比epoll_et_loop少了一个while循环 */
113 void epoll_et_nonloop(int sockfd)
114 {
115 char buffer[MAX_BUFFER_SIZE];
116 int ret;
117
118 printf(不带循环的ET模式开始读取数据...\n);
119 memset(buffer, 0, MAX_BUFFER_SIZE);
120 ret recv(sockfd, buffer, MAX_BUFFER_SIZE, 0);
121 if (ret 0)
122 {
123 printf(收到消息:%s, 共%d个字节\n, buffer, ret);
124 }
125 else
126 {
127 if (ret 0)
128 printf(客户端主动关闭连接\n);
129 close(sockfd);
130 }
131
132 printf(不带循环的ET模式处理结束\n);
133 }
134
135 /* 处理epoll的返回结果 */
136 void epoll_process(int epollfd, struct epoll_event *events, int number, int sockfd, int epoll_type, int block_type)
137 {
138 struct sockaddr_in client_addr;
139 socklen_t client_addrlen;
140 int newfd, connfd;
141 int i;
142
143 for (i 0; i number; i)
144 {
145 newfd events[i].data.fd;
146 if (newfd sockfd)
147 {
148 printf(新一轮accept()\n);
149 printf(accept()开始...\n);
150
151 /* 休眠3秒模拟一个繁忙的服务器不能立即处理accept连接 */
152 printf(开始休眠3秒...\n);
153 sleep(3);
154 printf(休眠3秒结束\n);
155
156 client_addrlen sizeof(client_addr);
157 connfd accept(sockfd, (struct sockaddr *)client_addr, client_addrlen);
158 printf(connfd %d\n, connfd);
159
160 /* 注册已链接的socket到epoll并设置是LT还是ET是阻塞还是非阻塞 */
161 addfd_to_epoll(epollfd, connfd, epoll_type, block_type);
162 printf(accept()结束\n);
163 }
164 else if (events[i].events EPOLLIN)
165 {
166 /* 可读事件处理流程 */
167
168 if (epoll_type EPOLL_LT)
169 {
170 printf(水平触发开始...\n);
171 epoll_lt(newfd);
172 }
173 else if (epoll_type EPOLL_ET)
174 {
175 printf(边缘触发开始...\n);
176
177 /* 带循环的ET模式 */
178 epoll_et_loop(newfd);
179
180 /* 不带循环的ET模式 */
181 //epoll_et_nonloop(newfd);
182 }
183 }
184 else
185 printf(其他事件发生...\n);
186 }
187 }
188
189 /* 出错处理 */
190 void err_exit(char *msg)
191 {
192 perror(msg);
193 exit(1);
194 }
195
196 /* 创建socket */
197 int create_socket(const char *ip, const int port_number)
198 {
199 struct sockaddr_in server_addr;
200 int sockfd, reuse 1;
201
202 memset(server_addr, 0, sizeof(server_addr));
203 server_addr.sin_family AF_INET;
204 server_addr.sin_port htons(port_number);
205
206 if (inet_pton(PF_INET, ip, server_addr.sin_addr) -1)
207 err_exit(inet_pton() error);
208
209 if ((sockfd socket(PF_INET, SOCK_STREAM, 0)) -1)
210 err_exit(socket() error);
211
212 /* 设置复用socket地址 */
213 if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, reuse, sizeof(reuse)) -1)
214 err_exit(setsockopt() error);
215
216 if (bind(sockfd, (struct sockaddr *)server_addr, sizeof(server_addr)) -1)
217 err_exit(bind() error);
218
219 if (listen(sockfd, 5) -1)
220 err_exit(listen() error);
221
222 return sockfd;
223 }
224
225 /* main函数 */
226 int main(int argc, const char *argv[])
227 {
228 if (argc 3)
229 {
230 fprintf(stderr, usage:%s ip_address port_number\n, argv[0]);
231 exit(1);
232 }
233
234 int sockfd, epollfd, number;
235
236 sockfd create_socket(argv[1], atoi(argv[2]));
237 struct epoll_event events[MAX_EPOLL_EVENTS];
238
239 /* linux内核2.6.27版的新函数和epoll_create(int size)一样的功能并去掉了无用的size参数 */
240 if ((epollfd epoll_create1(0)) -1)
241 err_exit(epoll_create1() error);
242
243 /* 以下设置是针对监听的sockfd当epoll_wait返回时必定有事件发生
244 * 所以这里我们忽略罕见的情况外设置阻塞IO没意义我们设置为非阻塞IO */
245
246 /* sockfd非阻塞的LT模式 */
247 addfd_to_epoll(epollfd, sockfd, EPOLL_LT, FD_NONBLOCK);
248
249 /* sockfd非阻塞的ET模式 */
250 //addfd_to_epoll(epollfd, sockfd, EPOLL_ET, FD_NONBLOCK);
251
252
253 while (1)
254 {
255 number epoll_wait(epollfd, events, MAX_EPOLL_EVENTS, -1);
256 if (number -1)
257 err_exit(epoll_wait() error);
258 else
259 {
260 /* 以下的LTET以及是否阻塞都是是针对accept()函数返回的文件描述符即函数里面的connfd */
261
262 /* connfd:阻塞的LT模式 */
263 epoll_process(epollfd, events, number, sockfd, EPOLL_LT, FD_BLOCK);
264
265 /* connfd:非阻塞的LT模式 */
266 //epoll_process(epollfd, events, number, sockfd, EPOLL_LT, FD_NONBLOCK);
267
268 /* connfd:阻塞的ET模式 */
269 //epoll_process(epollfd, events, number, sockfd, EPOLL_ET, FD_BLOCK);
270
271 /* connfd:非阻塞的ET模式 */
272 //epoll_process(epollfd, events, number, sockfd, EPOLL_ET, FD_NONBLOCK);
273 }
274 }
275
276 close(sockfd);
277 return 0;
278 } 四.验证 1.验证水平触发的非阻塞sockfd关键代码在247行。编译运行 代码里面休眠了3秒模拟繁忙服务器不能很快处理accept()请求。这里我们开另一个终端快速用5个连接连到服务器 我们再看看服务器的反映可以看到5个终端连接都处理完成了返回的新connfd依次为5,6,7,8,9 上面测试完毕后我们批量kill掉那5个客户端方便后面的测试 1 $:for i in {1..5};do kill %$i;done 2.边缘触发的非阻塞sockfd我们注释掉247行的代码放开250行的代码。编译运行后用同样的方法快速创建5个客户端连接或者测试5个后再测试10个。再看服务器的反映5个客户端只处理了2个。说明高并发时会出现客户端连接不上的问题 3.水平触发的阻塞connfd我们先把sockfd改回到水平触发注释250行的代码放开247行。重点代码在263行。 编译运行后用一个客户端连接并发送1-9这几个数 再看服务器的反映可以看到水平触发触发了2次。因为我们代码里面设置的缓冲区是5字节处理代码一次接收不完水平触发一直触发直到数据全部读取完毕 4.水平触发的非阻塞connfd。注释263行的代码放开266行的代码。同上面那样测试我们可以看到服务器反馈的消息跟上面测试一样。这里我就不再截图。 5.边缘触发的阻塞connfd注释其他测试代码放开269行的代码。先测试不带循环的ET模式(即不循环读取数据跟水平触发读取一样)注释178行的代码放开181行的代码。 编译运行后开启一个客户端连接并发送1-9这几个数字再看看服务器的反映可以看到边缘触发只触发了一次只读取了5个字节 我们继续在刚才的客户端发送一个字符a告诉epoll_wait()有新的可读事件发生 再看看服务器服务器又触发了一次新的边缘触发并继续读取上次没读完的6789加一个回车符 这个时候如果继续在刚刚的客户端再发送一个a客户端这个时候就会读取上次没读完的a加上次的回车符2个字节还剩3个字节的缓冲区就可以读取本次的a加本次的回车符共4个字节 我们可以看到阻塞的边缘触发如果不一次性读取一个事件上的数据会干扰下一个事件 接下来我们就一次性读取数据即带循环的ET模式。注意我们这里测试的还是边缘触发的阻塞connfd只是换个读取数据的方式。 注释181行代码放开178的代码。编译运行依然用一个客户端连接发送1-9。看看服务器可以看到数据全部读取完毕 细心的朋友肯定发现了问题程序没有输出带循环的ET处理结束是因为程序一直卡在了88行的recv()函数上因为是阻塞IO如果没数据可读它会一直等在那里直到有数据可读。如果这个时候用另一个客户端去连接服务器不能受理这个新的客户端 6.边缘触发的非阻塞connfd不带循环的ET测试同上面一样数据不会读取完。这里我们就只需要测试带循环的ET处理即正规的边缘触发用法。注释其他测试代码放开272行代码。编译运行用一个客户端连接并发送1-9。再观测服务器的反映可以看到数据全部读取完毕处理函数也退出了因为非阻塞IO如果没有数据可读时会立即返回并设置error这里我们根据EAGAIN和EWOULDBLOCK来判断数据全部读取完毕了可以退出循环了 这个时候我们用另一个客户端去连接服务器依然可以正常接收请求 五.总结 1.对于监听的sockfd最好使用水平触发模式边缘触发模式会导致高并发情况下有的客户端会连接不上。如果非要使用边缘触发网上有的方案是用while来循环accept()。 2.对于读写的connfd水平触发模式下阻塞和非阻塞效果都一样不过为了防止特殊情况还是建议设置非阻塞。 3.对于读写的connfd边缘触发模式下必须使用非阻塞IO并要一次性全部读写完数据。