Linux下的socket编程实践(五)设置套接字I/O超时的方案


\

(一)使用alarm 函数设置超时

 

#include  
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    
    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);  
}  

相关内容