从内核角度看怎么设置connect超时

Linux内核那些事

共 3614字,需浏览 8分钟

 ·

2021-03-29 14:39

我们在编写网络程序时,通常需要连接其他服务端(如微服务之间的通信),这时就需要通过调用 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 值的方式不一定有效。


浏览 44
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报