EPOLLLT,EPOLLET是epoll两种不同的模式,前面已经讲过他们的区别:触发的时机不一致。读取数据的方式因此也不一样,下面我们分别讨论。
在EPOLLLT(水平触发)模式下,也就是默认的模式,epoll_wait返回可读事件,表明socket一定收到了数据,我们可以调用read函数来读取数据。如果指定读取的数据大于缓冲区数据,无论socket是阻塞还是非阻塞的,read不会阻塞,read返回读取的真实数据。在read之后再次调用read,如果socket是阻塞的,read将阻塞,再次收到数据read才返回。此时如果指定读取的数据大于缓冲区,epoll_wait则不再触发,否则epoll_wait将再次触发,因为还有未读完的数据在缓冲区。
在EPOLLET(电平触发)模式下,只有新的数据来到时才会触发,因此在这种情况下,有数据时必须循环读取数据直到read返回-1,并且错误码为EAGAIN,才算读取了全部的缓冲区数据。
我突然想到一个问题,就是使用epoll时一定要将socket设置为非阻塞吗?正好知乎上有关于和这个的讨论:使用epoll时需要将socket设为非阻塞吗
发现看来看去仍然得不到正解。俗话说的好,纸上得来终觉浅,要知此事须躬行。自己实现一遍,答案自然有了。以下是我验证后画的思维导图,很能够说明各种模式下sokcet的动作:
上面的再次read指epoll触发后调用一次read后再调用一次,在具体的情况中可以看作while 循环读取数据。
通过上面的图,我们可以得出结论:
我觉得只有边沿触发才必须设置为非阻塞。
边沿触发的问题:
1. sockfd 的边缘触发,高并发时,如果没有一次处理全部请求,则会出现客户端连接不上的问题。不需要讨论 sockfd 是否阻塞,因为 epoll_wait() 返回的必定是已经就绪的连接,所以不管是阻塞还是非阻塞,accept() 都会立即返回。
2. 阻塞 connfd 的边缘触发,如果不一次性读取一个事件上的数据,会干扰下一个事件,所以必须在读取数据的外部套一层循环,这样才能完整的处理数据。但是外层套循环之后会导致另外一个问题:处理完数据之后,程序会一直卡在 recv() 函数上,因为是阻塞 IO,如果没数据可读,它会一直等在那里,直到有数据可读。但是这个时候,如果用另一个客户端去连接服务器,服务器就不能受理这个新的客户端了。
3. 非阻塞 connfd 的边缘触发,和阻塞版本一样,必须在读取数据的外部套一层循环,这样才能完整的处理数据。因为非阻塞 IO 如果没有数据可读时,会立即返回,并设置 errno。这里我们根据 EAGAIN 和 EWOULDBLOCK 来判断数据是否全部读取完毕了,如果读取完毕,就会正常退出循环了。
总结一下:
1. 对于监听的 sockfd,最好使用水平触发模式,边缘触发模式会导致高并发情况下,有的客户端会连接不上。如果非要使用边缘触发,可以用 while 来循环 accept()。
2. 对于读写的 connfd,水平触发模式下,阻塞和非阻塞效果都一样,因为在阻塞模式下,如果数据读取不完全则返回继续触发,反之读取完则返回继续等待。全建议设置非阻塞。
3. 对于读写的 connfd,边缘触发模式下,必须使用非阻塞 IO,并要求一次性地完整读写全部数据。
附上代码:
#include <sys/socket.h> #include <sys/epoll.h> #include <arpa/inet.h> #include <fcntl.h> #include <errno.h> #include <string.h> #include <unistd.h> #include <stdio.h> #define MAX_LINE 10 #define MAX_EVENTS 500 #define MAX_LISTENFD 5 int createAndListen() { int on = 1; int listenfd = socket(AF_INET, SOCK_STREAM, 0); struct sockaddr_in servaddr; fcntl(listenfd, F_SETFL, O_NONBLOCK); setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(5859); if (-1 == bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) { printf("bind errno, errno : %d ", errno); } if (-1 == listen(listenfd, MAX_LISTENFD)) { printf("listen error, errno : %d ", errno); } printf("listen in port 5859 !!! "); return listenfd; } int main(int argc, char const *argv[]) { struct epoll_event ev, events[MAX_EVENTS]; int epollfd = epoll_create(1); //这个参数已经被忽略,但是仍然要大于 if (epollfd < 0) { printf("epoll_create errno, errno : %d ", errno); } int listenfd = createAndListen(); ev.data.fd = listenfd; ev.events = EPOLLIN; epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev); for ( ;; ) { int fds = epoll_wait(epollfd, events, MAX_EVENTS, -1); //时间参数为0表示立即返回,为-1表示无限等待 if (fds == -1) { printf("epoll_wait error, errno : %d ", errno); break; } else { printf("trig %d !!! ", fds); } for (int i = 0; i < fds; i++) { if (events[i].data.fd == listenfd) { struct sockaddr_in cliaddr; socklen_t clilen = sizeof(struct sockaddr_in); int connfd = accept(listenfd, (sockaddr*)&cliaddr, (socklen_t*)&clilen); if (connfd > 0) { printf("new connection from %s : %d, accept socket fd: %d ", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), connfd); } else { printf("accept error, connfd : %d, errno : %d ", connfd, errno); } fcntl(connfd, F_SETFL, O_NONBLOCK); ev.data.fd = connfd; ev.events = EPOLLIN | EPOLLET; if (-1 == epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev)) { printf("epoll_ctl error, errno : %d ", errno); } } else if (events[i].events & EPOLLIN) { int sockfd; if ((sockfd =events[i].data.fd) < 0) { printf("EPOLLIN socket fd < 0 error "); continue; } char szLine[MAX_LINE + 1] ; int readLen = 0; bzero(szLine, MAX_LINE + 1); if ((readLen = read(sockfd, szLine, MAX_LINE)) < 0) { printf("readLen is %d, errno is %d ", readLen, errno); if (errno == ECONNRESET) { printf("ECONNRESET closed socket fd : %d ", events[i].data.fd); close(sockfd); } } else if (readLen == 0) { printf("read 0 closed socket fd : %d ", events[i].data.fd); //epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd , NULL); //close(sockfd); } else { printf("read %d content is %s ", readLen, szLine); } bzero(szLine, MAX_LINE + 1); if ((readLen = read(sockfd, szLine, MAX_LINE)) < 0) { printf("readLen2 is %d, errno is %d , ECONNRESET is %d ", readLen, errno, ECONNRESET); if (errno == ECONNRESET) { printf("ECONNRESET2 closed socket fd : %d ", events[i].data.fd); close(sockfd); } } else if (readLen == 0) { printf("read2 0 closed socket fd : %d ", events[i].data.fd); } else { printf("read2 %d content is %s ", readLen, szLine); } } } } return 0; }
再补充一个关于EPOLLONESHOT的选项的问题,该选项是指epoll触发一次之后再也不会触发,即使水平模式下没有完全读取缓冲区的数据,再也不会有触发,更别提电平模式下了。
在水平模式下添加了写事件,只要写缓冲还有空间,那么会一直触发。一般来说写缓冲区不会满,所以导致连接的socket一直触发写事件,这点会不会有损效率?因为我看redis源码中,连接的socket一直触发了写事件,虽然写的回调函数会判断没有要写的数据,但是这仍然会空转cpu。这是我学习redis源码想到的一个问题,不知大家怎么看。
在recv读取数据时,指定要读取的大小大于缓冲区数据大小时,即使是阻塞socket也会返回。如果想要读取指定大小数据才让返回,recv函数必须加一个标志MSG_WAITALL。
还有就是epoll_wait函数的阻塞与在其队列中socket是否阻塞没有关系,即添加在epoll中的socket全为阻塞,epoll_wait也不会阻塞,除非有事件触发或timeout。