(一)使用alarm 函數設置超時
#includeunsigned int alarm(unsigned int seconds);
它的主要功能是設置信號傳送鬧鐘。信號SIGALRM在經過seconds指定的秒數後傳送給目前的進程,如果在定時未完成的時間內再次調用了alarm函數,則後一次定時器設置將覆蓋前面的設置,當seconds設置為0時,定時器將被取消。它返回上次定時器剩余時間,如果是第一次設置則返回0。
void sigHandlerForSigAlrm(int signo) { return ; } signal(SIGALRM, sigHandlerForSigAlrm); alarm(5); int ret = read(sockfd, buf, sizeof(buf)); if (ret == -1 && errno == EINTR) { // 阻塞並且達到了5s,超時,設置返回錯誤碼 errno = ETIMEDOUT; } else if (ret >= 0) { // 正常返回(沒有超時), 則將鬧鐘關閉 alarm(0); }如果read一直處於阻塞狀態被SIGALRM信號中斷而返回,則表示超時,否則未超時已讀取到數據,取消鬧鐘。但這種方法不常用,因為有時可能在其他地方使用了alarm會造成混亂。
(二)套接字選項: SO_SNDTIMEO, SO_RCVTIMEO,調用setsockopt設置讀/寫超時時間
/示例: read超時 int seconds = 5; if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &seconds, sizeof(seconds)) == -1) err_exit("setsockopt error"); int ret = read(sockfd, buf, sizeof(buf)); if (ret == -1 && errno == EWOULDBLOCK) { // 超時,被時鐘信號打斷 errno = ETIMEDOUT; }SO_RCVTIMEO是接收超時,SO_SNDTIMEO是發送超時。這種方式也不經常使用,因為這種方案不可移植,並且有些套接字的實現不支持這種方式。
(三)使用select函數實現超時
#include返回:做好准備的文件描述符的個數,超時為0,錯誤為 -1.int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
struct timeval{ long tv_sec; /*秒 */ long tv_usec; /*微秒 */ }
select函數是在linux編程中很重要的一個函數,他有很多的功能,控制讀、寫、異常的集合,當然還有設置超時。
下面我們依次封裝read_timeout、write_timeout、accept_timeout、connect_timeout四個函數,來了解select在超時設置方面的使用。
1. read_timeout
/** *read_timeout - 讀超時檢測函數, 不包含讀操作 *@fd: 文件描述符 *@waitSec: 等待超時秒數, 0表示不檢測超時 *成功(未超時)返回0, 失敗返回-1, 超時返回-1 並且 errno = ETIMEDOUT **/ int read_timeout(int fd, long waitSec) { int returnValue = 0; if (waitSec > 0) { fd_set readSet; FD_ZERO(&readSet); FD_SET(fd,&readSet); //添加 struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_usec = 0; //將微秒設置為0(不進行設置),如果設置了,時間會更加精確 do { returnValue = select(fd+1,&readSet,NULL,NULL,&waitTime); } while(returnValue < 0 && errno == EINTR); //等待被(信號)打斷的情況, 重啟select if (returnValue == 0) //在waitTime時間段中一個事件也沒到達,超時 { returnValue = -1; //返回-1 errno = ETIMEDOUT; } else if (returnValue == 1) //在waitTime時間段中有事件產生 returnValue = 0; //返回0,表示成功 // 如果(returnValue == -1) 並且 (errno != EINTR), 則直接返回-1(returnValue) } return returnValue; }
FD_ZERO宏將一個 fd_set類型變量的所有位都設為 0,使用FD_SET將變量的某個位置位。清除某個位時可以使用 FD_CLR,我們可以使用FD_ISSET來測試某個位是否被置位。
當聲明了一個文件描述符集後,必須用FD_ZERO將所有位置零。之後將我們所感興趣的描述符所對應的位置位,操作如下:
fd_set rset; int fd; FD_ZERO(&rset); FD_SET(fd, &rset); FD_SET(stdin, &rset);select返回後,用FD_ISSET測試給定位是否置位:
if(FD_ISSET(fd, &rset) { ... }
2.write_timeout
實現方式和read_timeout基本相同。
/** *write_timeout - 寫超時檢測函數, 不包含寫操作 *@fd: 文件描述符 *@waitSec: 等待超時秒數, 0表示不檢測超時 *成功(未超時)返回0, 失敗返回-1, 超時返回-1 並且 errno = ETIMEDOUT **/ int write_timeout(int fd, long waitSec) { int returnValue = 0; if (waitSec > 0) { fd_set writeSet; FD_ZERO(&writeSet); //清零 FD_SET(fd,&writeSet); //添加 struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_usec = 0; do { returnValue = select(fd+1,NULL,&writeSet,NULL,&waitTime); } while(returnValue < 0 && errno == EINTR); //等待被(信號)打斷的情況 if (returnValue == 0) //在waitTime時間段中一個事件也沒到達 { returnValue = -1; //返回-1 errno = ETIMEDOUT; } else if (returnValue == 1) //在waitTime時間段中有事件產生 returnValue = 0; //返回0,表示成功 } return returnValue; }3.accept_timeout
/** *accept_timeout - 帶超時的accept *@fd: 文件描述符 *@addr: 輸出參數, 返回對方地址 *@waitSec: 等待超時秒數, 0表示不使用超時檢測, 使用正常模式的accept *成功(未超時)返回0, 失敗返回-1, 超時返回-1 並且 errno = ETIMEDOUT **/ int accept_timeout(int fd, struct sockaddr_in *addr, long waitSec) { int returnValue = 0; if (waitSec > 0) { fd_set acceptSet; FD_ZERO(&acceptSet); FD_SET(fd,&acceptSet); //添加 struct timeval waitTime; waitTime.tv_sec = waitSec; waitTime.tv_usec = 0; do { returnValue = select(fd+1,&acceptSet,NULL,NULL,&waitTime); } while(returnValue < 0 && errno == EINTR); if (returnValue == 0) //在waitTime時間段中沒有事件產生 { errno = ETIMEDOUT; return -1; } else if (returnValue == -1) // error return -1; } /**select正確返回: 表示有select所等待的事件發生:對等方完成了三次握手, 客戶端有新的鏈接建立,此時再調用accept就不會阻塞了 */ socklen_t socklen = sizeof(struct sockaddr_in); if (addr != NULL) returnValue = accept(fd,(struct sockaddr *)addr,&socklen); else returnValue = accept(fd,NULL,NULL); return returnValue; }
(1)我們為什麼需要這個函數?
TCP/IP在客戶端連接服務器時,如果發生異常,connect(如果是在默認阻塞的情況下)返回的時間是RTT(相當於客戶端阻塞了這麼長的時間,客戶需要等待這麼長的時間,顯然這樣的客戶端用戶體驗並不好(完成三次握手需要使用1.5RTT時間));會造成嚴重的軟件質量下降.
(注:
RTT(Round-Trip Time)介紹:
RTT往返時延:在計算機網絡中它是一個重要的性能指標,表示從發送端發送數據開始,到發送端收到來自接收端的確認(接收端收到數據後便立即發送確認),總共經歷的時延。
RTT由三個部分決定:即鏈路的傳播時間、末端系統的處理時間以及路由器的緩存中的排隊和處理時間。其中,前面兩個部分的值作為一個TCP連接相對固定,路由器的緩存中的排隊和處理時間會隨著整個網絡擁塞程度的變化而變化。所以RTT的變化在一定程度上反映了網絡擁塞程度的變化。簡單來說就是發送方從發送數據開始,到收到來自接受方的確認信息所經歷的時間。)
(2)客戶端調用int connect(int sockfd, const struct sockaddr *addr, socklen_t len);發起對服務器的socket的連接請求,如果客戶端socket描述符為阻塞模式則會一直阻塞到連接建立或者連接失敗(注意阻塞模式的超時時間可能為75秒到幾分鐘之間),而如果為非阻塞模式,則調用connect之後如果連接不能馬上建立則返回-1(errno設置為EINPROGRESS,注意連接也可能馬上建立成功比如連接本機的服務器進程),如果沒有馬上建立返回,此時TCP的三路握手動作在背後繼續,而程序可以做其他的東西,然後調用select檢測非阻塞connect是否完成(此時可以指定select的超時時間,這個超時時間可以設置為比connect的超時時間短),如果select超時則關閉socket,然後可以嘗試創建新的socket重新連接,如果select返回非阻塞socket描述符可寫則表明連接建立成功,如果select返回非阻塞socket描述符既可讀又可寫則表明連接出錯(注意:這兒必須跟另外一種連接正常的情況區分開來,就是連接建立好了之後,服務器端發送了數據給客戶端,此時select同樣會返回非阻塞socket描述符既可讀又可寫,這時可以通過以下方法區分:
1.調用getpeername獲取對端的socket地址.如果getpeername返回ENOTCONN,表示連接建立失敗,然後用SO_ERROR調用getsockopt得到套接口描述符上的待處理錯誤;
2.調用read,讀取長度為0字節的數據.如果read調用失敗,則表示連接建立失敗,而且read返回的errno指明了連接失敗的原因.如果連接建立成功,read應該返回0;
3.再調用一次connect.它應該失敗,如果錯誤errno是EISCONN,就表示套接口已經建立,而且第一次連接是成功的;否則,連接就是失敗的;
/* activate_nonblock - 設置IO為非阻塞模式 * fd: 文件描述符 */ void activate_nonblock( int fd) { int ret; int flags = fcntl(fd, F_GETFL); if (flags == - 1 ) ERR_EXIT( "fcntl error" ); flags |= O_NONBLOCK; ret = fcntl(fd, F_SETFL, flags); if (ret == - 1 ) ERR_EXIT( "fcntl error" ); } /* deactivate_nonblock - 設置IO為阻塞模式 * fd: 文件描述符 */ void deactivate_nonblock( int fd) { int ret; int flags = fcntl(fd, F_GETFL); if (flags == - 1 ) ERR_EXIT( "fcntl error" ); flags &= ~O_NONBLOCK; ret = fcntl(fd, F_SETFL, flags); if (ret == - 1 ) ERR_EXIT( "fcntl error" ); } /* connect_timeout - 帶超時的connect * fd: 套接字 * addr: 輸出參數,返回對方地址 * wait_seconds: 等待超時秒數,如果為0表示正常模式 * 成功(未超時)返回0,失敗返回-1,超時返回-1並且errno = ETIMEDOUT */ int connect_timeout( int fd, struct sockaddr_in *addr, unsigned int wait_seconds) { int ret; socklen_t addrlen = sizeof ( struct sockaddr_in); if (wait_seconds > 0 ) activate_nonblock(fd); ret = connect(fd, ( struct sockaddr *)addr, addrlen); if (ret < 0 && errno == EINPROGRESS) { fd_set connect_fdset; struct timeval timeout; FD_ZERO(&connect_fdset); FD_SET(fd, &connect_fdset); timeout.tv_sec = wait_seconds; timeout.tv_usec = 0 ; do { /* 一旦連接建立,套接字就可寫 */ ret = select(fd + 1 , NULL , &connect_fdset, NULL , &timeout); } while (ret < 0 && errno == EINTR); if (ret == 0 ) { errno = ETIMEDOUT; return - 1 ; } else if (ret < 0 ) return - 1 ; else if (ret == 1 ) { /* ret返回為1,可能有兩種情況,一種是連接建立成功,一種是套接字產生錯誤 * 此時錯誤信息不會保存至errno變量中(select沒出錯),因此,需要調用 * getsockopt來獲取 */ int err; socklen_t socklen = sizeof (err); int sockoptret = getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &socklen); if (sockoptret == - 1 ) return - 1 ; if (err == 0 ) ret = 0 ; else { errno = err; ret = - 1 ; } } } if (wait_seconds > 0 ) deactivate_nonblock(fd); return ret; }
int ret; ret = read_timeout(fd, 5 ); if (ret == 0 ) read(fd, buf, sizeof (buf)); else if (ret == - 1 && errno == ETIMEOUT) printf( "timeout...\n" ); else ERR_EXIT( "read_timeout" );
**測試:使用connect_timeout的client端完整代碼(server端如前)**/ int main() { int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd == -1) err_exit("socket error"); struct sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(8001); serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); int ret = connect_timeout(sockfd, &serverAddr, 5); if (ret == -1 && errno == ETIMEDOUT) { cerr << "timeout..." << endl; err_exit("connect_timeout error"); } else if (ret == -1) err_exit("connect_timeout error"); //獲取並打印對端信息 struct sockaddr_in peerAddr; socklen_t peerLen = sizeof(peerAddr); if (getpeername(sockfd, (struct sockaddr *)&peerAddr, &peerLen) == -1) err_exit("getpeername"); cout << "Server information: " << inet_ntoa(peerAddr.sin_addr) << ", " << ntohs(peerAddr.sin_port) << endl; close(sockfd); }