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

Posted linux大本营

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从linux内核角度看怎么设置connect超时相关的知识,希望对你有一定的参考价值。

linux服务器开发相关视频解析:

linux多线程之epoll原理剖析与reactor原理及应用
10道经典面试题的剖析, 技术方向如何决定职业方向

c/c++ linux服务器开发免费学习地址:c/c++ linux后台服务器高级架构师

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

以上是关于从linux内核角度看怎么设置connect超时的主要内容,如果未能解决你的问题,请参考以下文章

从内核角度看TCP三次握手

从内核角度看网络包发送流程

从内核角度看网络包发送流程

聊聊Netty那些事儿之从内核角度看IO模型

Linux 设备模型之 (kobjectkset 和 Subsystem)

聊聊Netty那些事儿之从内核角度看IO模型