zoukankan      html  css  js  c++  java
  • 套接字I/O超时设置方法和用select实现超时

    注:如无特殊说明,sockfd 原始状态都是阻塞的。


    一、使用alarm 函数设置超时

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     
    void handler(int sig)
    {
    }
    signal(SIGALRM, handler);

    alarm(5);
    int ret = read(fd, buf, sizeof(buf));
    if (ret == -1 && errno == EINTR)
        errno = ETIMEOUT;
    else if (ret >= 0)
        alarm(0);
    .................

    程序大概框架如上所示,如果read在5s内被SIGALRM信号中断而返回,则表示超时,否则未超时已读取到数据,取消闹钟。但这种方法不常用,因为有时可能在其他地方使用了alarm会造成混乱。


    二、使用套接字选项SO_SNDTIMEO、SO_RCVTIMEO

     C++ Code 
    1
    2
    3
    4
    5
    6
     
    struct timeval timeout = {3,0}; 
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO(char *)&timeout, sizeof(struct timeval));

    int ret = read(sock, buf, sizeof(buf));
    if (ret == -1 && errno == EWOULDBLOCK)
        errno = ETIMEOUT;
    ..........

    即使用setsockopt 函数进行设置,但这种方法可移植性比较差,不是每种系统实现都有这些选项。


    三、使用select 实现超时

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

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
     
    /*************************************************************************
        > File Name: sysutil.c
        > Author: Simba
        > Mail: dameng34@163.com
        > Created Time: Sat 02 Mar 2013 10:53:06 PM CST
     ************************************************************************/

    #include "sysutil.h"

    /* 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将不再阻塞,测试代码可以这样写:
     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
     
    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 函数,客户端程序如下:

     C++ Code 
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
     
    #include "sysutil.h"

    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("127.0.0.1");


        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;
    }

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

    simba@ubuntu:~/Documents/code/linux_programming/UNP/socket$ ./echocli_timeout 
    connect_timeout: Connection refused

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


    在这里可以粗略说下tcp connect 的机制,connect 只是完成发送 syn 的过程,后续的两次握手由协议栈完成。如果 fd 是 阻塞的,则 connect 会一直

    等到超时或者连接成功返回;如果 fd 是非阻塞的,则 connect 会立刻返回,但此时协议栈是否已经完成连接要判断下返回值和 errno;无论 fd 阻塞还

    是非阻塞,如果没有设置超时,则当重传 syn 次数达到 sysctl net.ipv4.tcp_syn_retries  时才超时结束,重传 syn 的时间采取数退避的方式,假设 

    syn_retries 为5, 则分别为 1, 2, 4, 8, 16, 32 ... 即在目标 ip 不可达时要几十秒才 timeout(如果是ip 可达,但没有对应的监听端口,则在一次重试

    后,对端机器会发送reset 标志,连接结束,耗时 1s 多),故:

    如果是非阻塞方式,按照stevens 建议,如上面的做法即可;

    如果是阻塞方式,可以用 setsockopt 设置 SO_SNDTIMEO 即可。


    参考:

    《Linux C 编程一站式学习》

    《TCP/IP详解 卷一》

    《UNP》

  • 相关阅读:
    Python编码风格
    CI持续集成—记一次jenkins安装及ldap配置
    mysql主从同步失败处理
    redis优化
    sysbench
    Ubuntu12.04 Installation and Subversion(svn)
    MySQL 5.7 CLUSTER NDB 7.5 创建磁盘表 undo logfile group tablespace 内存表修改为磁盘表
    Java异步调用Future对象
    Java宝典(二)
    Java宝典(一)
  • 原文地址:https://www.cnblogs.com/alantu2018/p/8472954.html
Copyright © 2011-2022 走看看