在一個非阻塞的socket上調用read/write函數,返回EAGAIN或者EWOULDBLOCK(注:EAGAIN就是EWOULDBLOCK)。面試
從字面上看,意思是:編程
總結:服務器
這個錯誤表示資源暫時不夠,可能read時, 讀緩衝區沒有數據, 或者write時,寫緩衝區滿了。 網絡
遇到這種狀況,若是是阻塞socket、 read/write就要阻塞掉。而若是是非阻塞socket、 read/write當即返回-1, 同 時errno設置爲EAGAIN。socket
因此對於阻塞socket、 read/write返回-1表明網絡出錯了。但對於非阻塞socket、read/write返回-1不必定網絡真的出錯了。多是Resource temporarily unavailable。這時你應該再試,直到Resource available。tcp
綜上, 對於non-blocking的socket,正確的讀寫操做爲:函數
對於select和epoll的LT模式,這種讀寫方式是沒有問題的。 但對於epoll的ET模式,這種方式還有漏洞。spa
epoll的兩種模式 LT 和 ET.net
兩者的差別在於 level-trigger 模式下只要某個 socket 處於 readable/writable 狀態,不管何時進行 epoll_wait 都會返回該 socket;而 edge-trigger 模式下只有某個 socket 從 unreadable 變爲 readable 或從unwritable 變爲 writable 時,epoll_wait 纔會返回該 socket。以下兩個示意圖:pwa
從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; }