1. 程式人生 > >Linux下的socket程式設計實踐(五)設定套接字I/O超時的方案

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


(一)使用alarm 函式設定超時


#include <unistd.h> 
unsigned 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 <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_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;  
}

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


--------------------- 
作者:NK_test 
來源:CSDN 
原文:https://blog.csdn.net/nk_test/article/details/49050379 
版權宣告:本文為博主原創文章,轉載請