这个问题是我以前很早做爬虫的时候遇到过的一个问题,都忘记当时是什么导致的怎么解决的了,无意中刚好看到这篇文章,就贴过来了,以后再用到epoll的时候,就可以看一下,不要再犯这个错误了。
最近用c++实现了贝叶斯分类算法,做了个自动识别垃圾信息的小工具。工具中有个功能,通过绑定指定端口,和客户端通信。服务端使用的是epoll 网络模型。在测试的时候发现,单用户的情况下客户端和服务器通信正常。但是在多用户并发的情况下,客户端和服务端通信不正常。此时,客户端能正常的链接, 发送数据,但是一直卡在接收数据部分。如下图:

出现这种问题,是因为不正确的使用了epoll中的ET(edge-trigger)模式。代码如下:
01 | /************************************************** |
05 | ***************************************************/ |
06 | void acceptConn( int srvfd) |
08 | struct sockaddr_in sin ; |
09 | socklen_t len = sizeof ( struct sockaddr_in); |
12 | int confd = accept(srvfd, ( struct sockaddr*)& sin , &len); |
16 | printf ( "%s: bad accept\n" ); |
20 | printf ( "Accept Connection: %d" , confd); |
23 | setNonblocking(confd); |
26 | struct epoll_event event; |
27 | event.data.fd = confd; |
28 | event.events = EPOLLIN|EPOLLET; |
29 | epoll_ctl(epollfd, EPOLL_CTL_ADD, confd, &event); |
注意倒数第二行:event.events = EPOLLIN|EPOLLET; 采用的是ET模式。下面我们来具体说下,问题出在那里。
在epoll中有两种模式:level-trigger模式,简称LT模式,和edge-trigger模式,简称ET模式。其中,LT是默认的工作模式。
这两种模式的工作方式有些不同。在level-trigger模式下只要某个socket处于readable/writable状态,无论什么时 候进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socket从unreadable变为readable 或从unwritable变为writable时,epoll_wait才会返回该socket。
在ET模式socket非阻塞的情况下(上面代码中就是这种情况),多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由于是边缘触 发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列中剩下的连接都得不到处理。因此,就出现了上面所提及的问题。
解决办法是用while循环抱住accept调用,处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept返回-1并且errno设置为EAGAIN就表示所有连接都处理完。
修改后的代码如下:
01 | /************************************************** |
05 | ***************************************************/ |
06 | void acceptConn( int srvfd) |
08 | struct sockaddr_in sin ; |
09 | socklen_t len = sizeof ( struct sockaddr_in); |
12 | while ((confd = accept(srvfd, ( struct sockaddr*)& sin , &len)) > 0) { |
14 | printf ( "Accept Connection: %d" , confd); |
16 | setNonblocking(confd); |
19 | struct epoll_event event; |
20 | event.data.fd = confd; |
21 | event.events = EPOLLIN|EPOLLET; |
22 | epoll_ctl(epollfd, EPOLL_CTL_ADD, confd, &event); |
25 | if ( errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR){ |
26 | printf ( "%s: bad accept\n" ); |
同理,接收数据和发送数据时如果是ET模式,且非阻塞,也得用循环。
读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN
写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN
正确的读:
2 | while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) { |
5 | if (nread == -1 && errno != EAGAIN) { |
正确的写:
01 | int nwrite, data_size = strlen (buf); |
04 | nwrite = write(fd, buf + data_size - n, n); |
06 | if (nwrite == -1 && errno != EAGAIN) { |
07 | perror ( "write error" ); |