从内核角度看怎么设置connect超时
我们在编写网络程序时,通常需要连接其他服务端(如微服务之间的通信),这时就需要通过调用 connect 函数来连接服务端。但我们发现 connect 函数并没有提供超时的设置,而在 Linux 系统中,connect 的默认超时时间为75秒。所以,在连接不上服务端的情况下,我们需要等待75秒,这对我们不能接受的。
通过 SO_SNDTIMEO 设置 connect 超时时间
虽然 connect 系统调用没有提供超时的设置,但我们通过查阅 Linux 内核代码可以发现,connect 系统调用的超时时间可以通过 SO_SNDTIMEO 参数来设定的,而 SO_SNDTIMEO 参数可以通过 setsockopt 系统调用来设置,如下代码:
struct timeval tv;tv.tv_sec = 1; /* 设置1秒超时 */tv.tv_usec = 0;setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
一般来说,SO_SNDTIMEO 参数是用来设置 socket 的发送超时时间,为什么在 Linux 中还能设置 connect 的超时时间呢?我们来查看一下 connect 系统调用的实现:
// 调用链: connect() -> sys_connect() -> inet_stream_connect()int inet_stream_connect(struct socket *sock, struct sockaddr * uaddr,int addr_len, int flags){struct sock *sk = sock->sk;int err;long timeo;lock_sock(sk);...switch (sock->state) {...case SS_UNCONNECTED:...err = -EINPROGRESS;break;}timeo = sock_sndtimeo(sk, flags & O_NONBLOCK); // 获取 connect 超时时间,如果是非阻塞会返回0if ((1<<sk->state)&(TCPF_SYN_SENT|TCPF_SYN_RECV)) {// 如果 socket 设置了非阻塞或者 connect 超时了// 跳到 out 处执行, 并且返回 EINPROGRESS 错误if (!timeo || !inet_wait_for_connect(sk, timeo))goto out;err = sock_intr_errno(timeo);if (signal_pending(current))goto out;}if (sk->state == TCP_CLOSE)goto sock_error;sock->state = SS_CONNECTED;err = 0;out:release_sock(sk);return err;...}
在 inet_stream_connect 函数中,首先调用了 sock_sndtimeo 获取 socket 的 SO_SNDTIMEO 的值,我们来看看 sock_sndtimeo 函数的实现:
static inline long sock_sndtimeo(struct sock *sk, int noblock){return noblock ? 0 : sk->sndtimeo; // 获取socket的SO_SNDTIMEO的值,如果socket被设置了非阻塞,那么返回0}
sock_sndtimeo 函数只是简单的从 socket 对象中获取 sndtimeo 字段的值,如果 socket 被设置了非阻塞,那么就返回0。
我们接着分析 inet_stream_connect 函数,在获取到 SO_SNDTIMEO 的值后,就调用 inet_wait_for_connect 函数等待 socket 连接返回。返回三种情况:
- 连接成功了。 
- 连接超时了。 
- 连接被中断了。 
如果连接成功,connect 会返回0;如果连接超时,connect 会返回 EINPROGRESS 错误;如果连接被中断,connect 会返回 EINTR 错误。
通过非阻塞与多路复用IO设置 connect 超时时间
从上面的分析可以看到,当把 socket 设置为非阻塞时,connect 系统调用会立刻返回 EINPROGRESS 错误,这时我们可以把 socket 添加到多路复用 IO 中进行监听,并且设置多路复用 IO 的超时时间即可达到设置 connect 超时时间的目的,如下代码:
int connect_timeout(int sockfd, struct sockaddr *serv_addr, int addrlen, int timeout){int flags = fcntl(sockfd, F_GETFL, 0);fcntl(sockfd, F_SETFL, flags|O_NONBLOCK); // 设置为非阻塞int n = connect(sockfd, serv_addr, sizeof(*serv_addr)); // 连接服务端if (n < 0) {if (errno != EINPROGRESS && errno != EWOULDBLOCK)return -1;struct timeval tv;fd_set wset;tv.tv_sec = timeout/1000;tv.tv_usec = (timeout - tv.tv_sec*1000)*1000;FD_ZERO(&wset);FD_SET(sockfd, &wset); // 把socket添加到select中进行监听n = select(sockfd + 1, NULL, &wset, NULL, &tv);if (n < 0) {return -1; // 出错} else if (0 == n) {return 0; // 超时}}fcntl(fd,F_SETFL,flags & ~O_NONBLOCK); // 恢复为阻塞模式return 1;}
connect_timeout 函数实现了有超时机制的 connect,其主要步骤有:
- 通过调用 - fcntl函数把 socket 设置为非阻塞。
- 调用 - connect函数进行连接服务端。
- 如果 - connect函数返回- EINPROGRESS或者- EWOULDBLOCK错误,表示连接还没有建立,所以此时把 socket 添加到- select中进行监听,并且设置- select的超时时间。
- 判断 - select的返回值,如果返回值大于0,表示连接成功;如果返回值小于0,表示连接出错;如果反正等于0,表示连接超时。
- 最后把 socket 恢复到阻塞模式。 
这种设置 connect 的超时时间的方式比前面设置 SO_SNDTIMEO 值的方式更为通用,因为在非 Linux 系统中,设置 SO_SNDTIMEO 值的方式不一定有效。
