在一个非阻塞的socket上调用read/write函数,返回EAGAIN或者EWOULDBLOCK(注:EAGAIN就是EWOULDBLOCK)。
从字面上看,意思是:
总结:
这个错误表示资源暂时不够,可能read时, 读缓冲区没有数据, 或者write时,写缓冲区满了。
遇到这种情况,如果是阻塞socket、 read/write就要阻塞掉。而如果是非阻塞socket、 read/write立即返回-1, 同 时errno设置为EAGAIN。
所以对于阻塞socket、 read/write返回-1代表网络出错了。但对于非阻塞socket、read/write返回-1不一定网络真的出错了。可能是Resource temporarily unavailable。这时你应该再试,直到Resource available。
综上, 对于non-blocking的socket,正确的读写操作为:
对于select和epoll的LT模式,这种读写方式是没有问题的。 但对于epoll的ET模式,这种方式还有漏洞。
epoll的两种模式 LT 和 ET
二者的差异在于 level-trigger 模式下只要某个 socket 处于 readable/writable 状态,无论什么时候进行 epoll_wait 都会返回该 socket;而 edge-trigger 模式下只有某个 socket 从 unreadable 变为 readable 或从unwritable 变为 writable 时,epoll_wait 才会返回该 socket。如下两个示意图:
从socket读数据:
往socket写数据:
所以在epoll的ET模式下,正确的读写方式为:
正确的读:
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; }
正确的accept,accept 要考虑 2 个问题:参考<<UNIX网络编程——epoll的 et,lt关注点>>讲解的更加详细
(1) LT模式下或ET模式下,阻塞的监听socket, accept 存在的问题
accept每次都是从已经完成三次握手的tcp队列中取出一个连接,考虑这种情况: TCP 连接被客户端夭折,即在服务器调用 accept 之前,客户端主动发送 RST 终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会一直阻塞在 accept 调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept 调用上,就绪队列中的其他描述符都得不到处理。
解决办法是:把监听套接口设置为非阻塞,当客户在服务器调用 accept 之前中止某个连接时,accept 调用可以立即返回 -1, 这时源自 Berkeley 的实现会在内核中处理该事件,并不会将该事件通知给 epool,而其他实现把 errno 设置为 ECONNABORTED 或者 EPROTO 错误,我们应该忽略这两个错误。
(2) ET 模式下 accept 存在的问题
考虑这种情况:多个连接同时到达,服务器的 TCP 就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll 只会通知一次,accept 只处理一个连接,导致 TCP 就绪队列中剩下的连接都得不到处理。
解决办法是:将监听套接字设置为非阻塞模式,用 while 循环抱住 accept 调用,处理完 TCP 就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢? accept 返回 -1 并且 errno 设置为 EAGAIN 就表示所有连接都处理完。
综合以上两种情况,服务器应该使用非阻塞地 accept, accept 在 ET 模式下 的正确使用方式为:
while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) { handle_client(conn_sock); } if (conn_sock == -1) { if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR) perror("accept"); }
一道腾讯后台开发的面试题:
使用Linux epoll模型,水平触发模式;当socket可写时,会不停的触发 socket 可写的事件,如何处理?
需要向 socket 写数据的时候才把 socket 加入 epoll ,等待可写事件。接受到可写事件后,调用 write 或者 send 发送数据。当所有数据都写完后,把 socket 移出 epoll。
这种方式的缺点是,即使发送很少的数据,也要把 socket 加入 epoll,写完后在移出 epoll,有一定操作代价。
开始不把 socket 加入 epoll,需要向 socket 写数据的时候,直接调用 write 或者 send 发送数据。如果返回 EAGAIN,把 socket 加入 epoll,在 epoll 的驱动下写数据,全部数据发送完毕后,再移出 epoll。
这种方式的优点是:数据不多的时候可以避免 epoll 的事件处理,提高效率。
最后贴一个使用epoll,ET模式的简单HTTP服务器代码:
#include <sys/socket.h> #include <sys/wait.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <sys/epoll.h> #include <sys/sendfile.h> #include <sys/stat.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> #include <fcntl.h> #include <errno.h> #define MAX_EVENTS 10 #define PORT 8080 //设置socket连接为非阻塞模式 void setnonblocking(int sockfd) { int opts; opts = fcntl(sockfd, F_GETFL); if(opts < 0) { perror("fcntl(F_GETFL)\n"); exit(1); } opts = (opts | O_NONBLOCK); if(fcntl(sockfd, F_SETFL, opts) < 0) { perror("fcntl(F_SETFL)\n"); exit(1); } } int main(){ struct epoll_event ev, events[MAX_EVENTS]; int addrlen, listenfd, conn_sock, nfds, epfd, fd, i, nread, n; struct sockaddr_in local, remote; char buf[BUFSIZ]; //创建listen socket if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("sockfd\n"); exit(1); } setnonblocking(listenfd); bzero(&local, sizeof(local)); local.sin_family = AF_INET; local.sin_addr.s_addr = htonl(INADDR_ANY);; local.sin_port = htons(PORT); if( bind(listenfd, (struct sockaddr *) &local, sizeof(local)) < 0) { perror("bind\n"); exit(1); } listen(listenfd, 20); epfd = epoll_create(MAX_EVENTS); if (epfd == -1) { perror("epoll_create"); exit(EXIT_FAILURE); } ev.events = EPOLLIN; ev.data.fd = listenfd; if (epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev) == -1) { perror("epoll_ctl: listen_sock"); exit(EXIT_FAILURE); } for (;;) { nfds = epoll_wait(epfd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_pwait"); exit(EXIT_FAILURE); } for (i = 0; i < nfds; ++i) { fd = events[i].data.fd; if (fd == listenfd) { while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote,(size_t *)&addrlen)) > 0) { setnonblocking(conn_sock); //设置连接socket为非阻塞 ev.events = EPOLLIN | EPOLLET; //边沿触发要求套接字为非阻塞模式;水平触发可以是阻塞或非阻塞模式 ev.data.fd = conn_sock; if (epoll_ctl(epfd, EPOLL_CTL_ADD, conn_sock,&ev) == -1) { perror("epoll_ctl: add"); exit(EXIT_FAILURE); } } if (conn_sock == -1) { if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR) perror("accept"); } continue; } if (events[i].events & EPOLLIN) { n = 0; while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) { n += nread; } if (nread == -1 && errno != EAGAIN) { perror("read error"); } ev.data.fd = fd; ev.events = events[i].events | EPOLLOUT; if (epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) == -1) { perror("epoll_ctl: mod"); } } if (events[i].events & EPOLLOUT) { sprintf(buf, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\nHello World", 11); 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; } close(fd); } } } close(epfd); close(listenfd); return 0; }