前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >通过linux源码分析nodejs的keep-alive

通过linux源码分析nodejs的keep-alive

作者头像
theanarkh
发布2020-06-28 15:54:53
1K0
发布2020-06-28 15:54:53
举报
文章被收录于专栏:原创分享原创分享原创分享

之前已经分析过了keep-alive,最近在使用nodejs的keep-alive的时候发现了遗漏了一个内容。本文进行一个补充说明。我们先看一下nodejs中keep-alive的使用。

enable:是否开启keep-alive,linux下默认是不开启的。 initialDelay:多久没有收到数据包就开始发送探测包。 接着我们看看这个api在libuv中的实现。

int uv__tcp_keepalive(int fd, int on, unsigned int delay) {
  if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)))
    return UV__ERR(errno);
// linux定义了这个宏
#ifdef TCP_KEEPIDLE
  /*
      on是1才会设置,所以如果我们先开启keep-alive,并且设置delay,
      然后关闭keep-alive的时候,是不会修改之前修改过的配置的。
      因为这个配置在keep-alive关闭的时候是没用的
  */
  if (on && setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &delay, sizeof(delay)))
    return UV__ERR(errno);
#endif

  return 0;
}

我们看到libuv调用了同一个系统函数两次。我们分别看一下这个函数的意义。参考linux2.6.13.1的代码。

// net\socket.c
asmlinkage long sys_setsockopt(int fd, int level, int optname, char __user *optval, int optlen)
{
    int err;
    struct socket *sock;

    if ((sock = sockfd_lookup(fd, &err))!=NULL)
    {
        ...
        if (level == SOL_SOCKET)
            err=sock_setsockopt(sock,level,optname,optval,optlen);
        else
            err=sock->ops->setsockopt(sock, level, optname, optval, optlen);
        sockfd_put(sock);
    }
    return err;
}

当level是SOL_SOCKET代表修改的socket层面的配置。IPPROTO_TCP是修改tcp层的配置(该版本代码里是SOL_TCP)。我们先看SOL_SOCKET层面的。

// net\socket.c -> net\core\sock.c -> net\ipv4\tcp_timer.c
int sock_setsockopt(struct socket *sock, int level, int optname,
            char __user *optval, int optlen) {
    ...
    case SO_KEEPALIVE:

            if (sk->sk_protocol == IPPROTO_TCP)
                tcp_set_keepalive(sk, valbool);
            // 设置SOCK_KEEPOPEN标记位1
            sock_valbool_flag(sk, SOCK_KEEPOPEN, valbool);
            break;
    ...
}

sock_setcsockopt首先调用了tcp_set_keepalive函数,然后给对应socket的SOCK_KEEPOPEN字段打上标记(0或者1表示开启还是关闭)。接下来我们看tcp_set_keepalive

void tcp_set_keepalive(struct sock *sk, int val)
{
    if ((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN))
        return;
    /*
        如果val是1并且之前是0(没开启)那么就开启计时,超时后发送探测包,
        如果之前是1,val又是1,则忽略,所以重复设置是无害的
    */
    if (val && !sock_flag(sk, SOCK_KEEPOPEN))
        tcp_reset_keepalive_timer(sk, keepalive_time_when(tcp_sk(sk)));
    else if (!val)
        // val是0表示关闭,则清除定时器,就不发送探测包了
        tcp_delete_keepalive_timer(sk);
}

我们看看超时后的逻辑。

// 多久没有收到数据包则发送第一个探测包
static inline int keepalive_time_when(const struct tcp_sock *tp)
{
    // 用户设置的(TCP_KEEPIDLE)和系统默认的
    return tp->keepalive_time ? : sysctl_tcp_keepalive_time;
}
// 隔多久发送一个探测包
static inline int keepalive_intvl_when(const struct tcp_sock *tp)
{
    return tp->keepalive_intvl ? : sysctl_tcp_keepalive_intvl;
}

static void tcp_keepalive_timer (unsigned long data)
{
...
// 多久没有收到数据包了
elapsed = tcp_time_stamp - tp->rcv_tstamp;
    // 是否超过了阈值
    if (elapsed >= keepalive_time_when(tp)) {
        // 发送的探测包个数达到阈值,发送重置包
        if ((!tp->keepalive_probes && tp->probes_out >= sysctl_tcp_keepalive_probes) ||
             (tp->keepalive_probes && tp->probes_out >= tp->keepalive_probes)) {
            tcp_send_active_reset(sk, GFP_ATOMIC);
            tcp_write_err(sk);
            goto out;
        }
        // 发送探测包,并计算下一个探测包的发送时间(超时时间)
        tcp_write_wakeup(sk)
            tp->probes_out++;
            elapsed = keepalive_intvl_when(tp);
    } else {
        /*
            还没到期则重新计算到期时间,收到数据包的时候应该会重置定时器,
            所以执行该函数说明的确是超时了,按理说不会进入这里。
        */
        elapsed = keepalive_time_when(tp) - elapsed;
    }

    TCP_CHECK_TIMER(sk);
    sk_stream_mem_reclaim(sk);

resched:
    // 重新设置定时器
    tcp_reset_keepalive_timer (sk, elapsed);
...

所以在SOL_SOCKET层面是设置是否开启keep-alive机制。如果开启了,就会设置定时器,超时的时候就会发送探测包。

对于定时发送探测包这个逻辑,tcp层定义了三个配置。 1 多久没有收到数据包,则开始发送探测包。 2 开始发送,探测包之前,如果还是没有收到数据(这里指的是有效数据,因为对端会回复ack给探测包),每隔多久,再次发送探测包。 3 发送多少个探测包后,就断开连接。 但是我们发现,SOL_SOCKET只是设置了是否开启探测机制,并没有定义上面三个配置的值,所以系统会使用默认值进行心跳机制(如果我们设置了开启keep-alive的话)。这就是为什么libuv调了两次setsockopt函数。第二次的调用设置了就是上面三个配置中的第一个(后面两个也可以设置,不过libuv没有提供接口,可以自己调用setsockopt设置)。那么我们来看一下libuv的第二次调用setsockopt是做了什么。我们直接看tcp层的实现。

// net\ipv4\tcp.c
int tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval,int optlen)
{
    ...
    case TCP_KEEPIDLE:
        // 修改多久没有收到数据包则发送探测包的配置
        tp->keepalive_time = val * HZ;
            // 是否开启了keep-alive机制
            if (sock_flag(sk, SOCK_KEEPOPEN) &&
                !((1 << sk->sk_state) &
                  (TCPF_CLOSE | TCPF_LISTEN))) {
                // 当前时间减去上次收到数据包的时候,即多久没有收到数据包了
                __u32 elapsed = tcp_time_stamp - tp->rcv_tstamp;
                // 算出还要多久可以发送探测包,还是可以直接发(已经触发了)
                if (tp->keepalive_time > elapsed)
                    elapsed = tp->keepalive_time - elapsed;
                else
                    elapsed = 0;
                // 设置定时器
                tcp_reset_keepalive_timer(sk, elapsed);
            }   
        ...
}

该函数首先修改配置,然后判断是否开启了keep-alive的机制,如果开启了,则重新设置定时器,超时的时候就会发送探测包。

总结:keep-alive有两个层面的内容,第一个是是否开启,第二个是开启后,使用的配置。nodejs的setKeepAlive就是做了这两件事情。只不过他只支持修改一个配置。另外测试发现,window下,调用setKeepAlive设置的initialDelay,会修改两个配置。分别是多久没有数据包就发送探测包,隔多久发送一次这两个配置。但是linux下只会修改多久没有数据包就发送探测包这个配置。

最后我们看看linux下的默认配置。

include <stdio.h>
#include <netinet/tcp.h>     

int main(int argc, const char *argv[])
{
    int sockfd;
    int optval;
    socklen_t optlen = sizeof(optval);

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    getsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &optval, &optlen);
    printf("默认是否开启keep-alive:%d \n", optval);

    getsockopt(sockfd, SOL_TCP, TCP_KEEPIDLE, &optval, &optlen);
    printf("多久没有收到数据包则发送探测包:%d seconds \n", optval);

    getsockopt(sockfd, SOL_TCP, TCP_KEEPINTVL, &optval, &optlen);
    printf("多久发送一次探测包:%d seconds \n", optval);

    getsockopt(sockfd, SOL_TCP, TCP_KEEPCNT, &optval, &optlen);
    printf("最多发送几个探测包就断开连接:%d \n", optval);

    return 0;
}

执行结果

我们看到linux下默认是不开启keep-alive的。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程杂技 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档