1. 回憶accept函數
之前在10-在accept之前中止連接(連接異常)這一篇中已經討論過在accept之前中止連接的情況了,不過從最終的結果來看,accept並沒有返回錯誤,而是之後調用read讀取已連接套接字時產生了錯誤。
另外,當一個已完成連接正等待被服務端accept時,select會把該連接的套接字作爲讀描述符並返回。這意味着之後的accept就不應該阻塞,但是會引發一個bug:當客戶端跟服務器建立連接之後發送了一個RST包,這時accept會阻塞,直到有下一個已完成的連接準備好被accept爲止。
2. accept引發的問題
爲了說明這種情況,修改之前TCP服務器的代碼:
//select會返回已連接的描述符
select();
if(FD_ISSET(listenfd , &rset)){
//sleep是爲了模擬accept阻塞的情況
sleep(5);
client_len = sizeof(client_addr);
connfd = accept(listenfd , (struct sockaddr *)&client_addr , &client_len);
}
如上所示,select返回已連接的描述符之後,接着就阻塞了,導致無法調用accept,通常情況下服務器是沒有問題的。考慮這麼一種情況:如果在建立tcp連接之後,客戶端又馬上發送了RST,就出現了問題。這意味着客戶端在服務器調用accept之前中止了這個連接,但是Berkeley版本的linux不會把這個中止的連接返回給服務端,其他linux版本可能返回EPROTO錯誤,而不會返回ECONNABORTED錯誤。
因爲客戶端發送了RST後,這個已完成的連接被服務器tcp從已完成連接隊列中刪除掉了,我們假設此時隊列中沒有任何其他已完成的連接,那麼之後服務器調用accept就會阻塞,直到已完成連接隊列不爲空爲止,就服務器在aceept處阻塞期間來說,它無法處理其他事情。
非阻塞accept實現
爲了防止accept阻塞,當select監聽的某個套接字有一個已完成連接正等待被accept時,把監聽的套接字設置爲非阻塞,然後調用accept忽略以下錯誤:
- EWOULDBLOCK (Berkeley實現,客戶端中止連接時)、 ECONNABORTED (POSIX實現,客戶中止連接時)
- EPROTO(SVR4實現,客戶端中止連接時) 和 EINTR(如果信號被捕獲)
實現accept非阻塞:
//設置套接字非阻塞
fcntl(listenfd , F_SETFL , O_NONBLOCK);
while(1){
//調用select函數
FD_SET(listenfd , &rset);
select(listenfd +1 , &rfds , NULL , NULL , ...);
if(FD_ISSET(listenfd , &rset)){
client_len = sizeof(client_addr);
connfd = accept(listenfd , (struct sockaddr *)&client_addr , &client_len);
if(connfd < 0){
//忽略EWOULDBLOCK錯誤,繼續循環
if(errno == EWOULDBLOCK)
continue;
perror("accept");
exit(-1);
}
}
}