SO_SNDTIMEO对connect的影响

今天在写代码的时候遇到这么一个问题: 如何设定connect的超时? 因为最近UNP1一直放在手边, 正好查阅一下, 上面提到有关设置超时的方法有3种:

  1. 信号. 推荐单进程模型使用, 因为多进程处理信号比较麻烦
  2. 使用setsockopt来设定socket的SO_RCVTIMEOSO_SNDTIMEO. 明确提到只能用于读写操作, 对connect无效.
  3. 使用非阻塞的connect操作.

针对这个问题, stackoverflow上有这么个讨论, 看到里面提到了很多种方法.

发现手头的代码基本都是使用上述第二种方法来实现connect超时的, 顿时觉得很奇怪. 问了下同事, 回答说是可以的, 可以try一下;) 好吧, 抱着好奇的心态, 我们来try一下.

准备工作

环境的准备工作还是非常重要的, 一个好的(真实)的环境可以极大的帮助编码以及测试. 如果有真实的环境那是再好不过, 但是现实往往是残酷的. 通常为了准备环境, 需要花费比较多的时间. 还好, 再次感谢vagrant这个好用的工具. 我们可以轻松创建两个处于同一网段的虚拟机(用了ubuntu发行版).

我们使用private_network来设定一下两台机器的ip分别为192.168.10.101192.168.10.102.

测试代码

既然已经要写程序了, 因此就写一个把几种情况的都测试了的程序吧. 下面是对应的代码:

connect.c
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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <errno.h>
#include <string.h>
#include <sys/time.h>
#include <fcntl.h>

typedef int (*my_connect_func) (struct sockaddr_in *, int);

int
my_connect0(struct sockaddr_in *addr, int sec)
{
    int sockfd;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    if (connect(sockfd, (struct sockaddr *)addr, sizeof(struct sockaddr_in)) < 0) {
        close(sockfd);
        return -1;
    }

    close(sockfd);

    return 0;
}

int
my_connect1(struct sockaddr_in *addr, int sec)
{
    int sockfd;
    struct timeval tv;

    tv.tv_sec  = sec;
    tv.tv_usec = 0;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));

    if (connect(sockfd, (struct sockaddr *)addr, sizeof(struct sockaddr_in)) < 0) {
        close(sockfd);
        return -1;
    }

    close(sockfd);

    return 0;
}

int
my_connect2(struct sockaddr_in *addr, int sec)
{
    int sockfd, ret, error;
    unsigned int len;
    struct timeval tv;
    fd_set rset, wset;

    error      = 0;
    tv.tv_sec  = sec;
    tv.tv_usec = 0;

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    fcntl(sockfd, F_SETFL, O_NONBLOCK);

    ret = connect(sockfd, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
    if (ret < 0) {
        if (errno != EINPROGRESS) {
            close(sockfd);
            return -1;
        }
    }

    if (ret == 0) {
        /* success */
        close(sockfd);
        return 0;
    }

    /* in progress */
    FD_ZERO(&rset);
    FD_SET(sockfd, &rset);
    wset = rset;

    ret = select(sockfd + 1, &rset, &wset, NULL, &tv);
    if (ret == 0) {
        /* no readable or writable socket, timeout */
        close(sockfd);
        errno = ETIMEDOUT;
        return -1;
    }
    else if (ret < 0) {
        /* select error */
        close(sockfd);
        return -1;
    }

    if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
        /* check is there any error */
        len = sizeof(error);
        getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len);
    }

    if (error) {
        /* connect error */
        close(sockfd);
        errno = error;
        return -1;
    }

    close(sockfd);
    return 0;
}

int
main(int argc, char *argv[])
{
    int                  type, sec;
    const char          *ip;
    struct sockaddr_in   addr;
    my_connect_func      my_connect;

    if (argc < 4) {
        printf("usage: connect <ip> <type> <seconds>\n");
        return -1;
    }

    ip   = argv[1];
    type = atoi(argv[2]);
    sec  = atoi(argv[3]);

    switch (type) {
        case 0:
            my_connect = my_connect0;
            break;
        case 1:
            my_connect = my_connect1;
            break;
        case 2:
            my_connect = my_connect2;
            break;
        default:
            printf("unknow");
            return -1;
    }

    bzero(&addr, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port   = htons(11111);
    inet_pton(AF_INET, ip, &addr.sin_addr);

    if (my_connect(&addr, sec) < 0) {
        printf("connect failed: %s\n", strerror(errno));
    }
    else {
        printf("connect success\n");
    }

    return 0;
}

汗, 代码还蛮长的. 主要是连接对应机器的11111端口. 三个connect函数分别测试:

  1. 基本的connect
  2. 设置了SO_SNDTIMEO的connect
  3. 使用nonblock的connect(主要参考了UNP1)

尝试

准备工作做好, 接下来的工作就是非常的轻松与愉快了~ 我们把101看成是client, 执行我们的测试程序, 而把102看成是server. 首先在102上执行nc -l 11111 -k来监听. 我们在101上依次测试三个方法, 发现连接都是正常的. 好, 接下来问题来了, 我们如何产生超时呢? 一个简单的办法是使用iptables. 使用下面这个语句来drop对应的包从而达到超时的目的:

iptables
1
2
# in server 102
sudo iptables -A INPUT -p tcp --dport 11111 -s 192.168.10.101 -j DROP

使用time函数来看看对应的情况:

result
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
## normal connect
v@p:/vagrant$ time ./connect 192.168.10.102 0 10
connect failed: Connection timed out

real    1m3.124s
user    0m0.000s
sys     0m0.000s

## setsockopt
v@p:/vagrant$ time ./connect 192.168.10.102 1 10
connect failed: Operation now in progress

real    0m9.999s
user    0m0.000s
sys     0m0.000s

## nonblock
v@p:/vagrant$ time ./connect 192.168.10.102 2 10
connect failed: Connection timed out

real    0m10.012s
user    0m0.000s
sys     0m0.000s

发现通过设定SO_SNDTIMEO确实可以设置connect的超时. 值得一看的错误的提示;)

原因

说实话, 这个结果还是蛮让我吃惊的. 看一下有关的帮助, man 7 socket:

SO_RCVTIMEO and SO_SNDTIMEO

Specify the receiving or sending timeouts until reporting an error. The argument is a struct timeval. If an input or output function blocks for this period of time, and data has been sent or received, the return value of that function will be the amount of data transferred; if no data has been trans‐ ferred and the timeout has been reached then -1 is returned with errno set to EAGAIN or EWOULDBLOCK, or EINPROGRESS (for connect(2)) just as if the socket was specified to be nonblocking. If the timeout is set to zero (the default) then the operation will never timeout. Timeouts only have effect for system calls that perform socket I/O (e.g., read(2), recvmsg(2), send(2), sendmsg(2)); timeouts have no effect for select(2), poll(2), epoll_wait(2), and so on.

貌似看不出来什么.

那为什么在Linux中通过设置SO_SNDTIMEO可以达到设置connect超时的目的呢, 后来搜到了这篇blog, 博主提到了linux中的相关实现. 结合stackoverflow上的讨论, 看起来是这个方法只适用于Linux系统.

其他

但看实现, 肯定是第二种方式更加简单, 但是如果要写更加通用的程序, 肯定是使用非阻塞的方式. 对于这点, 我们可以简单分析一下nginx中的处理, 给自己一个看nginx代码的机会~

nginx对于connect的处理代码主要位于event/ngx_event_connect.c中, 基本也是上述的逻辑:

  1. 创建socket
  2. 非阻塞
  3. 加入事件

nginx中socket相关错误处理主要是放到recv和send的处理中, 具体的功能实现可以参考os/unix/ngx_recv.cos/unix/ngx_send.c两个文件, 这里就不多说了.

继续学习, 多尝试~

Comments