zoukankan      html  css  js  c++  java
  • UNIX网络编程——使用select 实现套接字I/O超时

         下面程序包含read_timeout、write_timeout、accept_timeout、connect_timeout 四个函数封装:

    /* read_timeout - 读超时检测函数,不含读操作
     * fd:文件描述符
     * wait_seconds:等待超时秒数, 如果为0表示不检测超时;
     * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
     */
    
    int read_timeout(int fd, unsigned int wait_seconds)
    {
        int ret = 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检测到可读事件发送,则此时调用read不会阻塞
            }
            while (ret < 0 && errno == EINTR);
    
            if (ret == 0)
            {
                ret = -1;
                errno = ETIMEDOUT;
            }
            else if (ret == 1)
                return 0;
    
        }
    
        return ret;
    }
    
    /* write_timeout - 写超时检测函数,不含写操作
     * fd:文件描述符
     * wait_seconds:等待超时秒数, 如果为0表示不检测超时;
     * 成功(未超时)返回0,失败返回-1,超时返回-1并且errno = ETIMEDOUT
     */
    
    int write_timeout(int fd, unsigned int wait_seconds)
    {
        int ret = 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);
            }
            while (ret < 0 && errno == EINTR);
    
            if (ret == 0)
            {
                ret = -1;
                errno = ETIMEDOUT;
            }
            else if (ret == 1)
                return 0;
    
        }
    
        return ret;
    }
    
    /* accept_timeout - 带超时的accept
     * fd: 套接字
     * addr: 输出参数,返回对方地址
     * wait_seconds: 等待超时秒数,如果为0表示正常模式
     * 成功(未超时)返回已连接套接字,失败返回-1,超时返回-1并且errno = ETIMEDOUT
     */
    
    int accept_timeout(int fd, struct sockaddr_in *addr, unsigned int wait_seconds)
    {
        int ret;
        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);
            }
            while (ret < 0 && errno == EINTR);
    
            if (ret == -1)
                return -1;
            else if (ret == 0)
            {
                errno = ETIMEDOUT;
                return -1;
            }
        }
    
        if (addr != NULL)
            ret = accept(fd, (struct sockaddr *)addr, &addrlen);
        else
            ret = accept(fd, NULL, NULL);
        if (ret == -1)
            ERR_EXIT("accpet error");
    
        return ret;
    }
    
    /* 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;
    }
    下面来解析一下这些函数的封装:


    1、read_timeout :如注释所写,这只是读超时检测函数,并不包含读操作,如果从此函数成功返回,则此时调用read将不再阻塞,测试代码可以这样写:

    int ret;
    ret = read_timeout(fd, 5);
    if (ret == 0)
        read(fd, buf, sizeof(buf));
    else if (ret == -1 && errno == ETIMEOUT)
        printf("timeout...
    ");
    else
        ERR_EXIT("read_timeout");

         如果 read_timeout(fd, 0); 则表示不检测超时,函数直接返回为0,此时再调用read 将会阻塞。

         当wait_seconds 参数大于0,则进入if 括号执行,将超时时间设置为select函数的超时时间结构体,select会阻塞直到检测到事件发生或者超时。如果select返回-1且errno 为EINTR,说明是被信号中断,需要重启select;如果select返回0表示超时;如果select返回1表示检测到可读事件;否则select返回-1 表示出错。


    2、write_timeout :此函数跟read_timeout 函数类似,只是select 关心的是可写事件,不再赘述。


    3、accept_timeout :此函数是带超时的accept 函数,如果能从if (wait_seconds > 0) 括号执行后向下执行,说明select 返回为1,检测到已连接队列不为空,此时再调用accept 不再阻塞,当然如果wait_seconds == 0 则像正常模式一样,accept 阻塞等待,注意,accept 返回的是已连接套接字。


    4、connect_timeout :在调用connect前需要使用fcntl 函数将套接字标志设置为非阻塞如果网络环境很好,则connect立即返回0,不进入if 大括号执行;如果网络环境拥塞,则connect返回-1且errno == EINPROGRESS,表示正在处理。此后调用select与前面3个函数类似,但这里关注的是可写事件,因为一旦连接建立,套接字就可写。还需要注意的是当select 返回1,可能有两种情况,一种是连接成功,一种是套接字产生错误,由这里可知,这两种情况都会产生可写事件,所以需要使用getsockopt来获取一下。退出之前还需重新将套接字设置为阻塞。


    我们可以写个小程序测试一下connect_timeout 函数,客户端程序如下:

    #include<stdio.h>  
    #include<sys/types.h>  
    #include<sys/socket.h>  
    #include<unistd.h>  
    #include<stdlib.h>  
    #include<errno.h>  
    #include<arpa/inet.h>  
    #include<netinet/in.h>  
    #include<string.h>  
    #include<signal.h>
    #include <fcntl.h>  
    #define ERR_EXIT(m) 
        do { 
            perror(m); 
            exit(EXIT_FAILURE); 
        } while (0)
    
    /* 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;  
    }  
    
    
    
    int main(void)
    {
        int sock;
        if ((sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
            ERR_EXIT("socket");
    
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(5188);
        servaddr.sin_addr.s_addr = inet_addr("192.168.2.103");
    
    
        int ret = connect_timeout(sock, &servaddr, 5);
        if (ret == -1 && errno == ETIMEDOUT)
        {
            printf("timeout...
    ");
            return 1;
        }
        else if (ret == -1)
            ERR_EXIT("connect_timeout");
    
        struct sockaddr_in localaddr;
        socklen_t addrlen = sizeof(localaddr);
        if (getsockname(sock, (struct sockaddr *)&localaddr, &addrlen) < 0)
            ERR_EXIT("getsockname");
    
        printf("ip=%s port=%d
    ", inet_ntoa(localaddr.sin_addr), ntohs(localaddr.sin_port));
    
    
        return 0;
    }

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

    huangcheng@ubuntu:~$ ./cli
    connect_timeout: Connection refused
         很明显是connect_timeout 函数返回了-1,我们也可以推算出connect_timeout 函数中,select返回1,但却是套接字发生错误的情况,errno = ECONNREFUSED,所以打印出Connection refused。



  • 相关阅读:
    [kuangbin带你飞]专题七 线段树
    [kuangbin带你飞]专题六 最小生成树
    [kuangbin带你飞]专题五 并查集
    [kuangbin带你飞]专题四 最短路练习
    [kuangbin带你飞]专题三 Dancing Links
    [kuangbin带你飞]专题二 搜索进阶
    [kuangbin带你飞]专题一 简单搜索
    常用算法模板
    ACM程序设计选修课——Problem E:(ds:图)公路村村通(Prim)
    HDU——5667Sequence(矩阵快速幂+费马小定理应用)
  • 原文地址:https://www.cnblogs.com/wangfengju/p/6172557.html
Copyright © 2011-2022 走看看