• 套接字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
    ");   //执行客户端程序时,输入客户端程序名称和其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
    ");
            exit(1);
        }
        else if(ret==-1)
            perror("connect error
    ")
        
        if((numbytes=recv(sock_fd,recvbuf,MAXBYTEMUN,0))==-1)
            {       
                perror("receive failed");
                exit(1);
            }
            
        recvbuf[numbytes]='';//在字符串末尾加上,否则字符串无法输出
        printf("Received: %s
    ",recvbuf);
        return 0;
    }

    因为是在本机上测试,所以不会出现超时的情况,但出错的情况还是可以看到的,比如不要启动服务器端程序,而直接启动客户端程序,会报错如下

    在connect_timeout函数里面打印输出信息,可以跟踪程序的执行

    同样只开启客户端程序会出现如下结果

    如果开启服务端之后在开启客户端,出现如下结果

    根据这些打印输出的结果可以看到connect_timeout函数的执行路劲

  • 相关阅读:
    浏览器网络相关概念
    量化投资:以python为工具
    Quantitative Strategies for Achieving Alpha (三)
    Quantitative Startegies for Achieving Alpha(二)
    Quantitative Strategies for Achieving Alpha(一)
    打开量化投资的黑箱(二)
    打开量化投资的黑箱(一)
    量化投资学习(一)
    handy源码阅读(六):tcp类
    handy源码阅读(六):udp类
  • 原文地址:https://www.cnblogs.com/wujing-hubei/p/5584441.html
Copyright © 2020-2023  润新知