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

通过源码理解http层和tcp层的keep-alive

作者头像
theanarkh
发布2020-06-19 11:02:30
8050
发布2020-06-19 11:02:30
举报
文章被收录于专栏:原创分享原创分享

很久没更新文章了,今天突然想到这个问题,打算深入理解一下。我们知道建立tcp连接的代价是比较昂贵的,三次握手,慢开始,或者建立一个连接只为了传少量数据。这时候如果能保存连接,那会大大提高效率。下面我们通过源码来看看keep-alive的原理。本文分成两个部分

  1. http层的keep-alive
  2. tcp层的keep-alive

1 http层的keep-alive

最近恰好在看nginx1.17.9,我们就通过nginx来分析。我们先来看一下nginx的配置。

代码语言:javascript
复制
keepalive_timeout timeout;
keepalive_requests number;

上面两个参数告诉nginx,如果客户端设置了connection:keep-alive头。nginx会保持这个连接多久,另外nginx还支持另外一个限制,就是这个长连接上最多可以处理多少个请求。达到阈值后就断开连接。我们首先从nginx解析http报文开始。 ngx_http_request.c

代码语言:javascript
复制
ngx_http_read_request_header(r);
// 解析http请求行,r->header_in的内容由ngx_http_read_request_header设置
rc = ngx_http_parse_request_line(r, r->header_in);
// 解析完一个http头,开始处理
ngx_http_process_request_headers(rev);

上面两句代码是解析http报文头,比如解析到connection:keep-alive。那么ngx_http_read_request_header函数就会解析出这个字符串,然后保存到r->header_in。

代码语言:javascript
复制
ngx_http_header_t  ngx_http_headers_in[] = {
    { 
        ngx_string("Connection"), 
        offsetof(ngx_http_headers_in_t, connection),
        ngx_http_process_connection 
    }
    ...
},
static void ngx_http_process_request_headers(ngx_event_t *rev) {
    hh = ngx_hash_find(&cmcf->headers_in_hash, h->hash, h->lowcase_key, h->key.len);

    if (hh && hh->handler(r, h, hh->offset) != NGX_OK) {
         break;
     }
}

上面的代码大致就是根据刚才解析到的Connection:keep-alive字符串,通过Connection为key从ngx_http_headers_in数组中找到对应的处理函数。然后执行。我们看看ngx_http_process_connection 。

代码语言:javascript
复制
static ngx_int_t
ngx_http_process_connection(ngx_http_request_t *r, ngx_table_elt_t *h,
    ngx_uint_t offset)
{
    if (ngx_strcasestrn(h->value.data, "close", 5 - 1)) {
        r->headers_in.connection_type = NGX_HTTP_CONNECTION_CLOSE;

    } else if (ngx_strcasestrn(h->value.data, "keep-alive", 10 - 1)) {
        r->headers_in.connection_type = NGX_HTTP_CONNECTION_KEEP_ALIVE;
    }

    return NGX_OK;
}

非常简单,就是判断value的值是什么,我们假设这里是keep-alive,那么nginx会设置connection_type为NGX_HTTP_CONNECTION_KEEP_ALIVE。接着nginx处理完http头后,调用ngx_http_process_request函数,该函数会调用ngx_http_handler函数。

代码语言:javascript
复制
void
ngx_http_handler(ngx_http_request_t *r) {
     switch (r->headers_in.connection_type) {
        case 0:
            r->keepalive = (r->http_version > NGX_HTTP_VERSION_10);
            break;

        case NGX_HTTP_CONNECTION_CLOSE:
            r->keepalive = 0;
            break;

        case NGX_HTTP_CONNECTION_KEEP_ALIVE:
            r->keepalive = 1;
            break;
        }
}

我们看到这时候connection_type的值是NGX_HTTP_CONNECTION_KEEP_ALIVE,nginx会设置keepalive字段为1。看完设置,我们看什么时候会使用这个字段。我们看nginx处理完一个http请求后,调用ngx_http_finalize_connection关闭连接时的逻辑。

代码语言:javascript
复制
 if (!ngx_terminate
         && !ngx_exiting
         && r->keepalive
         && clcf->keepalive_timeout > 0)
    {
        ngx_http_set_keepalive(r);
        return;
    }

我们知道这时候r->keepalive是1,clcf->keepalive_timeout就是文章开头提到的nginx配置的。接着进入ngx_http_set_keepalive。

代码语言:javascript
复制
rev->handler = ngx_http_keepalive_handler;
ngx_add_timer(rev, clcf->keepalive_timeout);

nginx会设置一个定时器,过期时间是clcf->keepalive_timeout。过期后回调函数是ngx_http_keepalive_handler。

代码语言:javascript
复制
static void
ngx_http_keepalive_handler(ngx_event_t *rev) {
    if (rev->timedout || c->close) {
        ngx_http_close_connection(c);
        return;
    }
}

我们看到nginx会通过ngx_http_close_connection关闭请求。这就是nginx中关于keep-alive的逻辑。

2 tcp中的keep-alive

相比应用层的长连接,tcp层提供的功能更多。我们看linux2.6.13.1代码里提供的配置。

代码语言:javascript
复制
// 多久没有收到数据就发起探测包
#define TCP_KEEPALIVE_TIME    (120*60*HZ) /* two hours */
// 探测次数
#define TCP_KEEPALIVE_PROBES    9       /* Max of 9 keepalive probes    */
// 没隔多久探测一次
#define TCP_KEEPALIVE_INTVL    (75*HZ)

这是linux提供的默认值。下面再看看阈值。

代码语言:javascript
复制
#define MAX_TCP_KEEPIDLE    32767
#define MAX_TCP_KEEPINTVL    32767
#define MAX_TCP_KEEPCNT        127

这三个配置和上面三个一一对应。是上面三个配置的阈值。我们一般通过setsockopt函数来设置keep-alive。所以来看一下tcp层tcp_setsockopt的实现。下面只摘取其中一个配置。其他的是类似的。

代码语言:javascript
复制
    case TCP_KEEPIDLE:
        if (val < 1 || val > MAX_TCP_KEEPIDLE)
            err = -EINVAL;
        else {
            tp->keepalive_time = val * HZ;
            /*
                tcp_time_stamp是当前时间,tp->rcv_tstamp是上次收到数据包的时间,
                相减得到多长时间没有收到数据包
            */
            __u32 elapsed = tcp_time_stamp - tp->rcv_tstamp;
            // 比如设置一分钟,那么有20秒没有收到了。则40秒后开启探测。
            if (tp->keepalive_time > elapsed)
                elapsed = tp->keepalive_time - elapsed;
            else
                // 直接达到超时时间了,直接开始探测
                elapsed = 0;
            // 开启一个定时器
            tcp_reset_keepalive_timer(sk, elapsed);
        }
        break;

我们看tcp_reset_keepalive_timer

代码语言:javascript
复制
void tcp_reset_keepalive_timer (struct sock *sk, unsigned long len)
{
    init_timer(&sk->sk_timer);
    sk->sk_timer.function   = &tcp_keepalive_timer;
    sk->sk_timer.data   = (unsigned long)sk;
    sk_reset_timer(sk, &sk->sk_timer, jiffies + len);
}

超时处理函数是tcp_keepalive_timer

代码语言:javascript
复制
    // 多长时间没有收到数据包
    elapsed = tcp_time_stamp - tp->rcv_tstamp;
    /*
        keepalive_time_when(tp)) = tp->keepalive_time ? : sysctl_tcp_keepalive_time;
        如果用户没有设置则取默认值
        如果elapsed > keepalive_time_when(tp)说明达到发送探测包的条件了
    */
    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;
        }
    }
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-06-13,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1 http层的keep-alive
  • 2 tcp中的keep-alive
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档