很久沒更新文章了,今天突然想到這個問題,打算深入理解一下。我們知道建立tcp連接的代價是比較昂貴的,三次握手,慢開始,或者建立一個連接只爲了傳少量數據。這時候如果能保存連接,那會大大提高效率。下面我們通過源碼來看看keep-alive的原理。本文分成兩個部分
- http層的keep-alive
- tcp層的keep-alive
1 http層的keep-alive
最近恰好在看nginx1.17.9,我們就通過nginx來分析。我們先來看一下nginx的配置。
keepalive_timeout timeout;
keepalive_requests number;
上面兩個參數告訴nginx,如果客戶端設置了connection:keep-alive頭。nginx會保持這個連接多久,另外nginx還支持另外一個限制,就是這個長連接上最多可以處理多少個請求。達到閾值後就斷開連接。我們首先從nginx解析http報文開始。
ngx_http_request.c
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。
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 。
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函數。
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關閉連接時的邏輯。
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。
rev->handler = ngx_http_keepalive_handler;
ngx_add_timer(rev, clcf->keepalive_timeout);
nginx會設置一個定時器,過期時間是clcf->keepalive_timeout。過期後回調函數是ngx_http_keepalive_handler。
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代碼裏提供的配置。
// 多久沒有收到數據就發起探測包
#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提供的默認值。下面再看看閾值。
#define MAX_TCP_KEEPIDLE 32767
#define MAX_TCP_KEEPINTVL 32767
#define MAX_TCP_KEEPCNT 127
這三個配置和上面三個一一對應。是上面三個配置的閾值。我們一般通過setsockopt函數來設置keep-alive。所以來看一下tcp層tcp_setsockopt的實現。下面只摘取其中一個配置。其他的是類似的。
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
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
// 多長時間沒有收到數據包
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;
}
}