从内核角度看怎么设置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 超时时间,如果是非阻塞会返回0
if ((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
值的方式不一定有效。