套接字IO超時設置和使用select實現超時管理


在涉及套接字IO超時的設置上有一下3種方法:

1、調用alarm,它在指定的時期滿時產生SIGALRM信號。這個方法涉及信號的處理,而信號處理在不同的實現上存在差異,而且可能干擾進程中現有的alarm調用。

  程序大概框架如下所示,如果read在5s內被SIGALRM信號中斷而返回,則表示超時,否則未超時已讀取到數據則取消鬧鍾。為了在超時時中斷read函數,可以用信號處理函數來捕捉SIGALRM信號。

void handler(int sig)
{
    return 0;      //只是用來中斷read函數,不需要進行處理
}

signal(SIGALRM,handler);

alarm(5);         //開啟鬧鍾
int ret =read(fd,buf,sizeof(buf));
if(ret==-1 && errno == EINTR)   //read被超時中斷
{
    errno = ETIMEDOUT;
}
else if (ret>0)       //讀到數據
{
    alarm(0);         //關閉鬧鍾
}

2、使用SO_RCVTIMEO和SO_SNDTIMEO兩個套接字選項。此方法的問題在於並非所有的實現都支持這兩個套接字選項。此種方法僅適用於套接字描述符。

struct timeval timeout={3,0};    //設置超時時間為3秒

setsockopt(fd,SOL_SOCKET,SO_RCVTIMEO,(char*)&timeout,sizeof(struct timeval));  //設置套接字選項

int ret=read(fd,buf,sizeof(buf));
if(ret==-1 && errno == EWOULDBLOCK)  //IO超時中斷
    errno = ETIMEDOUT;
 .................

3、在select中阻塞等待的IO(select有內置的時間限制),以此代替直接阻塞在read和write調用上。

 (1)利用select封裝read超時函數

  如果 read_timeout(fd, 0); 則表示不檢測超時,函數直接返回為0,此時再調用read 將會阻塞。

  當wait_seconds 參數大於0,則進入if 括號執行,將超時時間設置為select函數的超時時間結構體,select會阻塞直到檢測到事件發生或者超時。如果select返回-1且errno 為EINTR,說明是被信號中斷,需要重啟select;如果select返回0表示超時;如果select返回1表示檢測到可讀事件;否則select返回-1 表示出錯。

/**
 *read_timeout 讀超時檢測函數,不含讀操作
 * fd:文件描述符
 *wait_seconds:等待超時秒數,如果為0則表示不檢測超時
 *成功(未超時)返回0,失敗返回-1,超時返回-1並且errno=ETIMEDOUT
 */
 int read_timeout(int fd,unsigned int wait_seconds)
 {
     int ret=0;           //默認為0,當wait_seconds==0時,不檢測直接返回0
     if(wait_seconds>0)  //需要檢測超時
     {
         fd_set read_fdset;      //描述符集合
         struct timeval timeout;  //超時時間
         
         FD_ZERO(&read_fdset);
         FD_SET(fd,&read_fdset);
         
         timeout.tv_sec = wait_seconds;
         timeout.tv_usec = 0;
         do
         {
             ret=select(fd+1,&read_fdset,NULL,NULL,&timeout);
             /*select會阻塞直到檢測到事件或則超時,如果超時,select會返回0,
             如果檢測到事件會返回1,如果異常會返回-1,如果是由於信號中斷引起的異常errno==EINTR*/
         }while(ret<0 && errno == EINTR);   //如果是有信號引起的異常則繼續阻塞select,直到檢測到事件或則超時
         
         if(ret==0) //select超時退出
         {
             ret=-1;
             errno= ETIMEDOUT;
         }
         else if(ret==1) //select檢測到可讀事件
            ret= 0;
     }
     return ret;
 }

測試程序框架

 /***********測試程序***************/
 int ret;
 ret=read_timeout(fd,5);  
if(ret==0)  //檢測到套接字可讀
{
    read(fd,......); //進行讀套接字的操作
}    
else if(ret==-1 && errno == ETIMEDOUT)  //超時即指定事件內為檢測到套接字可讀
{
    timeout.......... ; //進行超時處理
}
else
{
    ERR_EXIT("read_timeout");  //進行錯誤處理
}
 

(2)利用select封裝write超時函數

write超時函數的封裝與read超時函數的封裝基本一樣,不同的只是把文件描述符加入到寫集合write_fdset中,其關心的只是寫事件。

/**
 *write_timeout 寫超時檢測函數,不含寫操作
 * fd:文件描述符
 *wait_seconds:等待超時秒數,如果為0則表示不檢測超時
 *成功(未超時)返回0,失敗返回-1,超時返回-1並且errno=ETIMEDOUT
 */
 int write_timeout(int fd,unsigned int wait_seconds)
 {
     int ret=0;           //默認為0,當wait_seconds==0時,不檢測直接返回0
     if(wait_seconds>0)  //需要檢測超時
     {
         fd_set write_fdset;      //描述符集合
         struct timeval timeout;  //超時時間
         
         FD_ZERO(&write_fdset);
         FD_SET(fd,&write_fdset);
         
         timeout.tv_sec = wait_seconds;
         timeout.tv_usec = 0;
         do
         {
             ret=select(fd+1,NULL,&write_fdset,NULL,&timeout); /*select會阻塞直到檢測到事件或則超時,如果超時,select會返回0, 如果檢測到事件會返回1,如果異常會返回-1,如果是由於信號中斷引起的異常errno==EINTR*/ }while(ret<0 && errno == EINTR); //如果是有信號引起的異常則繼續阻塞select,直到檢測到事件或則超時 if(ret==0) //select超時退出  { ret=-1; errno= ETIMEDOUT; } else if(ret==1) //select檢測到可寫事件 ret= 0; } return ret; }

測試代碼

 /***********測試程序***************/
 int ret;
 ret=write_timeout(fd,5);  
if(ret==0)  //檢測到描述符可寫
{
    write(fd,......); //進行寫描述符的操作
}    
else if(ret==-1 && errno == ETIMEDOUT)  //超時即指定事件內為檢測到套接字可寫
{
    timeout.......... ; //進行超時處理
}
else
{
    ERR_EXIT("read_timeout");  //進行錯誤處理
}

 (3)accept超時函數的封裝

  accept超時函數是帶超時的accept 函數,如果能從if (wait_seconds > 0) 括號執行后向下執行,說明select 返回為1,檢測到已連接隊列不為空,此時再調用accept 不再阻塞,當然如果wait_seconds == 0 則像正常模式一樣,accept 阻塞等待,注意,accept 返回的是已連接套接字。

/**
 *accept_timeout 帶超時的accept函數
 *fd:文件描述符
 *addr 輸出參數,accept返回的對等方的地址結構
 *wait_seconds:等待超時秒數,如果為0則表示正常模式
 *成功(未超時)返回已連接的套接字,失敗返回-1,超時返回-1並且errno=ETIMEDOUT
 */
 int accept_timeout(int fd,struct sockaddr_in *addr,unsigned int wait_seconds)
 {
     int ret=0;           //默認為0,當wait_seconds==0時,不檢測直接返回0
     socklen_t addrlen =sizeof(struct sockaddr_in);
     if(wait_seconds>0)  //需要檢測超時
     {
         fd_set accept_fdset;      //描述符集合
         struct timeval timeout;  //超時時間
         
         FD_ZERO(&accept_fdset);
         FD_SET(fd,&accept_fdset);
         
         timeout.tv_sec = wait_seconds;
         timeout.tv_usec = 0;
         do
         {
             ret=select(fd+1,&accept_fdset,NULL,NULL,&timeout);
             /*select會阻塞直到檢測到事件或則超時,如果超時,select會返回0,
             如果檢測到事件會返回1,如果異常會返回-1,如果是由於信號中斷引起的異常errno==EINTR*/
         }while(ret<0 && errno == EINTR);   //如果是有信號引起的異常則繼續阻塞select,直到檢測到事件或則超時
         
         if(ret==-1) //失敗
         {
             return -1;
         }
         if(ret==0) //select超時退出
         {
             errno= ETIMEDOUT;
             return -1;
         }
     }
     //select在指定時間內檢測到套接字可連接即三次握手完成或者不需要select檢測
     if(addr!=NULL)
     {
         ret=accept(fd,(struct sockaddr *)addr,&addr);
     }
     else
     {
         ret=accept{fd,NULL,NULL};
     }
     if (ret==-1) //連接失敗
        ERR_EXIT("accept");
        
     return ret;   //返回連接的套接字
  }

(4)connect超時函數的封裝

   在調用connect前需要使用fcntl 函數將套接字標志設置為非阻塞,如果網絡環境很好,則connect立即返回0,不進入if 大括號執行;如果網絡環境擁塞,則connect返回-1且errno == EINPROGRESS,表示正在處理。此后調用select與前面3個函數類似,但這里關注的是可寫事件,因為一旦連接建立,套接字就可寫。還需要注意的是當select 返回1,可能有兩種情況,一種是連接成功,一種是套接字產生錯誤,由這里可知,這兩種情況都會產生可寫事件,所以需要使用getsockopt來獲取一下。退出之前還需重新將套接字設置為阻塞。

/**  
 * activiate_nonblock  設置IO為非阻塞模式
 * fd 文件描述符
 */
void activiate_nonblock(int fd)
{
    int ret;
    int flags = fcntl(fd,F_GETFL);  //獲取fd的當前標記
    if(flags == -1)
        ERR_EXIT("fcntl");
    
    flags |= O_NONBLOCK;            //與新標記邏輯或
    ret = fcntl(fd,F_SETFL,flags);  //設置標記
    if(ret == -1)
        ERR_EXIT("fntl");
}
/**  
 * deactiviate_nonblock  設置IO為阻塞模式
 * fd 文件描述符
 */
 void deactiviate_nonblock(int fd)
{
    int ret;
    int flags = fcntl(fd,F_GETFL);  //獲取fd的當前標記
    if(flags == -1)
        ERR_EXIT("fcntl");
    
    flags &=~O_NONBLOCK;            //與新標記邏輯與
    ret = fcntl(fd,F_SETFL,flags);  //設置標記
    if(ret == -1)
        ERR_EXIT("fntl");
}

/**
 *connect_timeout 帶超時的accept函數
 *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=0;           //默認為0,當wait_seconds==0時,不檢測直接返回0
     socklen_t addrlen =sizeof(struct sockaddr_in);
     if(wait_seconds>0)  //需要檢測超時
        activiate_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);
             /*select會阻塞直到檢測到事件或則超時,如果超時,select會返回0,
             如果檢測到事件會返回1,如果異常會返回-1,如果是由於信號中斷引起的異常errno==EINTR*/
         }while(ret<0 && errno == EINTR);   //如果是有信號引起的異常則繼續阻塞select,直到檢測到事件或則超時
         
         if(ret==-1) //失敗
         {
             return -1;
         }
         else if(ret==0) //select超時退出
         {
             errno= ETIMEDOUT;
             return -1;
         }
         else if(ret==1) 
         {
            /*ret為1有兩種情況,一種是連接建立成功,一種是套接字產生錯誤 
              此時錯誤信息不回保存在errno變量中,因此,需要調用getsockopt函數來獲取。*/
             int err;
             socklen_t socklen = sizeof(err);
             int sockoptret = getsockopt(fd,SOL_SOCKET,SO_ERROR,&err,&socklen);//獲取套接字的錯誤放在err中
             if(sockoptret == -1)  //調用getsockopt失敗
             {
                  return -1;
             }
             if(err==0) //表示沒有錯誤即套接字建立連接成功
                ret=0;
             else    //套接字產生錯誤
             {
                 errno=err;
                 ret=-1;
             }                 
         }
     }
    
    if(wait_seconds>0)
    {
        deactiviate_nonblock(fd);     //重新將套接字設為阻塞模式
    }
     return ret;   
  }

 connect超時測試程序

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/un.h>
#include<sys/wait.h>      //*進程用的頭文件*/
#include<netinet/in.h>
#include<arpa/inet.h>
#include <unistd.h>       //fcntl的頭文件
#include <fcntl.h>
#include <sys/time.h>

#define MAXBYTEMUN   1024

/**  
 * activiate_nonblock  設置IO為非阻塞模式
 * fd 文件描述符
 */
void activiate_nonblock(int fd)
{
    int ret;
    int flags = fcntl(fd,F_GETFL);  //獲取fd的當前標記
    if(flags == -1)
        perror("fcntl");
    
    flags |= O_NONBLOCK;            //與新標記邏輯或
    ret = fcntl(fd,F_SETFL,flags);  //設置標記
    if(ret == -1)
        perror("fntl");
}
/**  
 * deactiviate_nonblock  設置IO為阻塞模式
 * fd 文件描述符
 */
 void deactiviate_nonblock(int fd)
{
    int ret;
    int flags = fcntl(fd,F_GETFL);  //獲取fd的當前標記
    if(flags == -1)
        perror("fcntl");
    
    flags &=~O_NONBLOCK;            //與新標記邏輯與
    ret = fcntl(fd,F_SETFL,flags);  //設置標記
    if(ret == -1)
        perror("fntl");
}

/**
 *connect_timeout 帶超時的accept函數
 *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=0;           //默認為0,當wait_seconds==0時,不檢測直接返回0
     socklen_t addrlen =sizeof(struct sockaddr_in);
     if(wait_seconds>0)  //需要檢測超時
        activiate_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);
             /*select會阻塞直到檢測到事件或則超時,如果超時,select會返回0,
             如果檢測到事件會返回1,如果異常會返回-1,如果是由於信號中斷引起的異常errno==EINTR*/
         }while(ret<0 && errno == EINTR);   //如果是有信號引起的異常則繼續阻塞select,直到檢測到事件或則超時
         
         if(ret==-1) //失敗
         {
             return -1;
         }
         else if(ret==0) //select超時退出
         {
             errno= ETIMEDOUT;
             return -1;
         }
         else if(ret==1) 
         {
            /*ret為1有兩種情況,一種是連接建立成功,一種是套接字產生錯誤 
              此時錯誤信息不回保存在errno變量中,因此,需要調用getsockopt函數來獲取。*/
             int err;
             socklen_t socklen = sizeof(err);
             int sockoptret = getsockopt(fd,SOL_SOCKET,SO_ERROR,&err,&socklen);//獲取套接字的錯誤放在err中
             if(sockoptret == -1)  //調用getsockopt失敗
             {
                  return -1;
             }
             if(err==0) //表示沒有錯誤即套接字建立連接成功
                ret=0;
             else    //套接字產生錯誤
             {
                 errno=err;
                 ret=-1;
             }                 
         }
     }
    
    if(wait_seconds>0)
    {
        deactiviate_nonblock(fd);     //重新將套接字設為阻塞模式
    }
     return ret;    
  }
 
 /***********測試程序***************/
 int main(int argc,char *argv[])
{
    int sock_fd,numbytes,maxfd,fd_stdin,nready;
//    char buf[MAXBYTEMUN];
    struct hostent;
    struct sockaddr_in client_addr;//客戶機的地址信息
    ssize_t ret;
    char recvbuf[1024]={'0'},sendbuf[1024]={'0'};
    fd_set  rset;
        int stdineof;
    
    if(argc!=2)
    {
        fprintf(stderr,"usage: client IPAddress\n");   //執行客戶端程序時,輸入客戶端程序名稱和其IP地址
        exit(1);    
    }
    
    /*創建套接字*/
    sock_fd=socket(AF_INET,SOCK_STREAM,0);//采用IPv4協議
    if(sock_fd==-1)
    {
        perror("creat socket failed");
        exit(1);
    }
    
    /*服務器地址參數*/
    client_addr.sin_family=AF_INET;  
    client_addr.sin_port=htons(3490);
    client_addr.sin_addr.s_addr=inet_addr(argv[1]);
    bzero(&client_addr.sin_zero,sizeof(struct sockaddr_in));//bzero位清零函數,將sin_zero清零,sin_zero為填充字段,必須全部為零
    
    
    /*連接到服務器*/
    ret=connect_timeout(sock_fd,(struct sockaddr*)&client_addr,5)
    if(ret==-1 && errno ==ETIMEDOUT)
    {
        perror("connect timedout\n");
        exit(1);
    }
    else if(ret==-1)
        perror("connect error\n")
    
    if((numbytes=recv(sock_fd,recvbuf,MAXBYTEMUN,0))==-1)
        {       
            perror("receive failed");
            exit(1);
        }
        
    recvbuf[numbytes]='\0';//在字符串末尾加上\0,否則字符串無法輸出
    printf("Received: %s\n",recvbuf);
    return 0;
}

因為是在本機上測試,所以不會出現超時的情況,但出錯的情況還是可以看到的,比如不要啟動服務器端程序,而直接啟動客戶端程序,會報錯如下

在connect_timeout函數里面打印輸出信息,可以跟蹤程序的執行

同樣只開啟客戶端程序會出現如下結果

如果開啟服務端之后在開啟客戶端,出現如下結果

根據這些打印輸出的結果可以看到connect_timeout函數的執行路勁

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM