Linux下的socket編程實踐(五)設置套接字I/O超時的方案


(一)使用alarm 函數設置超時html


#include <unistd.h> 
unsigned int alarm(unsigned int seconds); 
它的主要功能是設置信號傳送鬧鐘。信號SIGALRM在通過seconds指定的秒數後傳送給目前的進程,若是在定時未完成的時間內再次調用了alarm函數,則後一次定時器設置將覆蓋前面的設置,當seconds設置爲0時,定時器將被取消。它返回上次定時器剩餘時間,若是是第一次設置則返回0。linux

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 <sys/select.h>   
    int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
返回:作好準備的文件描述符的個數,超時爲0,錯誤爲 -1.
struct timeval{      
        long tv_sec;   /*秒 */
        long tv_usec;  /*微秒 */   
    }
select函數是在linux編程中很重要的一個函數,他有不少的功能,控制讀、寫、異常的集合,固然還有設置超時。服務器

下面咱們依次封裝read_timeout、write_timeout、accept_timeout、connect_timeout四個函數,來了解select在超時設置方面的使用。網絡

1. read_timeoutsocket


/** 
 *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;  
}

4.connect_timeout

(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; 
}

對read_timeout的測試

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的測試

**測試:使用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);  
}  

參考: http://www.cnblogs.com/zhangmo/archive/2013/04/02/2995824.html

相關文章
相關標籤/搜索