通過源碼理解http層和tcp層的keep-alive
很久沒更新文章了,今天突然想到這個(gè)問題,打算深入理解一下。我們知道建立tcp連接的代價(jià)是比較昂貴的,三次握手,慢開始,或者建立一個(gè)連接只為了傳少量數(shù)據(jù)。這時(shí)候如果能保存連接,那會(huì)大大提高效率。下面我們通過源碼來看看keep-alive的原理。本文分成兩個(gè)部分
http層的keep-alive
tcp層的keep-alive
1 http層的keep-alive
最近恰好在看nginx1.17.9,我們就通過nginx來分析。我們先來看一下nginx的配置。
keepalive_timeout?timeout;
keepalive_requests?number;
上面兩個(gè)參數(shù)告訴nginx,如果客戶端設(shè)置了connection:keep-alive頭。nginx會(huì)保持這個(gè)連接多久,另外nginx還支持另外一個(gè)限制,就是這個(gè)長連接上最多可以處理多少個(gè)請(qǐng)求。達(dá)到閾值后就斷開連接。我們首先從nginx解析http報(bào)文開始。
ngx_http_request.c
ngx_http_read_request_header(r);
//?解析http請(qǐng)求行,r->header_in的內(nèi)容由ngx_http_read_request_header設(shè)置
rc?=?ngx_http_parse_request_line(r,?r->header_in);
//?解析完一個(gè)http頭,開始處理
ngx_http_process_request_headers(rev);
上面兩句代碼是解析http報(bào)文頭,比如解析到connection:keep-alive。那么ngx_http_read_request_header函數(shù)就會(huì)解析出這個(gè)字符串,然后保存到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;
?????}
}
上面的代碼大致就是根據(jù)剛才解析到的Connection:keep-alive字符串,通過Connection為key從ngx_http_headers_in數(shù)組中找到對(duì)應(yīng)的處理函數(shù)。然后執(zhí)行。我們看看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的值是什么,我們假設(shè)這里是keep-alive,那么nginx會(huì)設(shè)置connection_type為NGX_HTTP_CONNECTION_KEEP_ALIVE。接著nginx處理完http頭后,調(diào)用ngx_http_process_request函數(shù),該函數(shù)會(huì)調(diào)用ngx_http_handler函數(shù)。
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;
????????}
}
我們看到這時(shí)候connection_type的值是NGX_HTTP_CONNECTION_KEEP_ALIVE,nginx會(huì)設(shè)置keepalive字段為1。看完設(shè)置,我們看什么時(shí)候會(huì)使用這個(gè)字段。我們看nginx處理完一個(gè)http請(qǐng)求后,調(diào)用ngx_http_finalize_connection關(guān)閉連接時(shí)的邏輯。
?if?(!ngx_terminate
?????????&&?!ngx_exiting
?????????&&?r->keepalive
?????????&&?clcf->keepalive_timeout?>?0)
????{
????????ngx_http_set_keepalive(r);
????????return;
????}
我們知道這時(shí)候r->keepalive是1,clcf->keepalive_timeout就是文章開頭提到的nginx配置的。接著進(jìn)入ngx_http_set_keepalive。
rev->handler?=?ngx_http_keepalive_handler;
ngx_add_timer(rev,?clcf->keepalive_timeout);
nginx會(huì)設(shè)置一個(gè)定時(shí)器,過期時(shí)間是clcf->keepalive_timeout。過期后回調(diào)函數(shù)是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會(huì)通過ngx_http_close_connection關(guān)閉請(qǐng)求。這就是nginx中關(guān)于keep-alive的邏輯。
2 tcp中的keep-alive
相比應(yīng)用層的長連接,tcp層提供的功能更多。我們看linux2.6.13.1代碼里提供的配置。
//?多久沒有收到數(shù)據(jù)就發(fā)起探測包
#define?TCP_KEEPALIVE_TIME????(120*60*HZ)?/*?two?hours?*/
//?探測次數(shù)
#define?TCP_KEEPALIVE_PROBES????9???????/*?Max?of?9?keepalive?probes????*/
//?沒隔多久探測一次
#define?TCP_KEEPALIVE_INTVL????(75*HZ)
這是linux提供的默認(rèn)值。下面再看看閾值。
#define?MAX_TCP_KEEPIDLE????32767
#define?MAX_TCP_KEEPINTVL????32767
#define?MAX_TCP_KEEPCNT????????127
這三個(gè)配置和上面三個(gè)一一對(duì)應(yīng)。是上面三個(gè)配置的閾值。我們一般通過setsockopt函數(shù)來設(shè)置keep-alive。所以來看一下tcp層tcp_setsockopt的實(shí)現(xiàn)。下面只摘取其中一個(gè)配置。其他的是類似的。
????case?TCP_KEEPIDLE:
????????if?(val?1?||?val?>?MAX_TCP_KEEPIDLE)
????????????err?=?-EINVAL;
????????else?{
????????????tp->keepalive_time?=?val?*?HZ;
????????????/*
????????????????tcp_time_stamp是當(dāng)前時(shí)間,tp->rcv_tstamp是上次收到數(shù)據(jù)包的時(shí)間,
????????????????相減得到多長時(shí)間沒有收到數(shù)據(jù)包
????????????*/
????????????__u32?elapsed?=?tcp_time_stamp?-?tp->rcv_tstamp;
????????????//?比如設(shè)置一分鐘,那么有20秒沒有收到了。則40秒后開啟探測。
????????????if?(tp->keepalive_time?>?elapsed)
????????????????elapsed?=?tp->keepalive_time?-?elapsed;
????????????else
????????????????//?直接達(dá)到超時(shí)時(shí)間了,直接開始探測
????????????????elapsed?=?0;
????????????//?開啟一個(gè)定時(shí)器
????????????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);
}
超時(shí)處理函數(shù)是tcp_keepalive_timer
????//?多長時(shí)間沒有收到數(shù)據(jù)包
????elapsed?=?tcp_time_stamp?-?tp->rcv_tstamp;
????/*
????????keepalive_time_when(tp))?=?tp->keepalive_time???:?sysctl_tcp_keepalive_time;
????????如果用戶沒有設(shè)置則取默認(rèn)值
????????如果elapsed?>?keepalive_time_when(tp)說明達(dá)到發(fā)送探測包的條件了
????*/
????if?(elapsed?>=?keepalive_time_when(tp))?{
????????//?再判斷探測次數(shù)是否也達(dá)到閾值了,是則發(fā)送重置包斷開連接
????????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;
????????}
????}
支持
如果你覺得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
關(guān)注我的官網(wǎng)?https://muyiy.cn,讓我們成為長期關(guān)系
關(guān)注公眾號(hào)「高級(jí)前端進(jìn)階」,公眾號(hào)后臺(tái)回復(fù)「面試題」 送你高級(jí)前端面試題,回復(fù)「加群」加入面試互助交流群
