最近用c++实现了贝叶斯分类算法,做了个自动识别垃圾信息的小工具。工具中有个功能,通过绑定指定端口,和客户端通信。服务端使用的是epoll网络模型。在测试的时候发现,单用户的情况下客户端和服务器通信正常。但是在多用户并发的情况下,客户端和服务端通信不正常。此时,客户端能正常的链接,发送数据,但是一直卡在接收数据部分。如下图:
出现这种问题,是因为不正确的使用了epoll中的ET(edge-trigger)模式。代码如下:
/************************************************** 函数名:acceptConn 功能:接受客户端的链接 参数:srvfd:监听SOCKET ***************************************************/ void acceptConn(int srvfd) { struct sockaddr_in sin; socklen_t len = sizeof(struct sockaddr_in); bzero(&sin, len); int confd = accept(srvfd, (struct sockaddr*)&sin, &len); if (confd < 0) { printf("%s: bad accept "); return; }else { printf("Accept Connection: %d", confd); } setNonblocking(confd); //将新建立的连接添加到EPOLL的监听中 struct epoll_event event; event.data.fd = confd; event.events = EPOLLIN|EPOLLET; 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就表示所有连接都处理完。
修改后的代码如下:
/************************************************** 函数名:acceptConn 功能:接受客户端的链接 参数:srvfd:监听SOCKET ***************************************************/ void acceptConn(int srvfd) { struct sockaddr_in sin; socklen_t len = sizeof(struct sockaddr_in); bzero(&sin, len); int confd = 0; while((confd = accept(srvfd, (struct sockaddr*)&sin, &len)) > 0) { printf("Accept Connection: %d", confd); setNonblocking(confd); //将新建立的连接添加到EPOLL的监听中 struct epoll_event event; event.data.fd = confd; event.events = EPOLLIN|EPOLLET; epoll_ctl(epollfd, EPOLL_CTL_ADD, confd, &event); } if (confd == -1) { if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR){ printf("%s: bad accept "); return; } } }
同理,接收数据和发送数据时如果是ET模式,且非阻塞,也得用循环。
读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN
写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN
正确的读:
n = 0; while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) { n += nread; } if (nread == -1 && errno != EAGAIN) { perror("read error"); }
正确的写:
int nwrite, data_size = strlen(buf); n = data_size; while (n > 0) { nwrite = write(fd, buf + data_size - n, n); if (nwrite < n) { if (nwrite == -1 && errno != EAGAIN) { perror("write error"); } break; } n -= nwrite; }
技术交流
原文链接:linux下epoll模型并发问题,转载请注明来源!
哦,我说搜索技术博客没人维护了。原来跑到这里来了,恩,代码是有高亮了,但你每看到代码里有很多经过转义的字符吗?
谢谢提醒。已经修复。呵呵
您好,请教一个问题,对于epoll ET模式下,如果客户端write完之后就close socket,server怎么检测这个close事件?看起来它会和前面的write连在一起,又因为没有数据,所以我目前检测不到这个事件?谢谢
一般从服务端的read的返回值可以看出来。具体的你可以看下 http://wenku.baidu.com/link?url=0yYZODyaBGdv3p5fW3hzd6ZhR9jWe8XSDHyk2f9-mf67ItmuUpYP7iEJ-7nAZ5eWw_O03luF-4qJ4ZWvBg0T359qasAgeAMICUUfn4qOqs3