http keep-alive 进化史
- 在http1.0之前,是没有[Connection: keep-alive]头的,也就是说那个时候每个http请求都会打开一个tpc连接,这样是相当消耗服务端资源(内存,cpu)。
- 在http1.0时,工程师们绝对http不够持久,浪费资源,遇到密集的http请求时性能低下,于是有些浏览器在请求时,在header里增加了一个非标准的Connection关键字,当[Connection: keep-alive]时,就表示客户端要求服务端的tcp建立后不要立即关闭,而是复用该tcp连接,当[Connection: close]时,就和以前一样了。
- 在http1.1及以后,W3C组织(http协议制定者)觉得这个头很有用,就纳入了http标准中,并且要求http客户端和服务端默认支持keep-alive。
keep-alive实现原理
这里需要注意的是,http keep-alive实现了对同一tcp连接的复用,即可以在一个TCP连接中发送多个HTTP请求,这种技术叫做HTTP复用(HTTP Multiplexing),与TCP连接复用是完全不同的技术,不要混淆了。
还有就是HTTP复用(对同一tcp连接的复用)只是针对同一个客户端来说的,对于不同的客户端是无法做到的,要想实现就需要TCP复用技术了。
实现原理图如下:
可以看到,要想实现keep-alive,客户端和服务端都需要做相应的支持!!!!
针对 keep-alive 网络抓包分析
环境:
客户端=》火狐浏览器
ajax 请求
$.ajax({
contentType: 'application/json; charset=utf-8;',
url: URL,
timeout: timeOut,
data: JSON.stringify(data),
method: "POST",
dataType: 'json',
success: function (response) {
window.rsp_data = response;
// JSON-Formatter: https://github.com/mohsen1/json-formatter-js
var formatter = new JSONFormatter(response, exp, {
hoverPreviewEnabled: true,
hoverPreviewArrayCount: 100,
hoverPreviewFieldCount: 5,
theme: '',
animateOpen: true,
animateClose: true
});
$('#area_callback')
.html(formatter.render());
console.log(response);
// To the bottom.
/*$("html, body").animate({ scrollTop: $(document).height()-$(window).height() }, 500);*/
},
error: function (xhr, status, err) {
console.log(xhr)
console.log(status)
console.log(err)
}
});
服务端=》nginx
配置如下:
#keepalive_disable msie6;
keepalive_timeout 65;#表示一个tcp连接空闲65s后就会被关闭
#keepalive_requests 1000;#表示一个tcp连接处理1000个请求后就会被关闭
抓包如下:
【火狐浏览器抓包】
明显看到tcp连接被服用了,并且可以观察到keepalive_timeout的含义如下:
- 在keepalive_timeout时间内,若有新的请求进来,使用了该连接,timeout时间将会被刷新,重新计数;
- 在keepalive_timeout时间内,若一直没有新的请求,一直处理空闲状态,则在超时后,nginx会主动断开该连接,即发送FIN包。
【keepalive_requests 含义也类似,有兴趣的同学可以自己试验下】。
另外,可以看到火狐浏览器的心跳包是10s一个,但是用谷歌浏览器测试,就是45s一个,可见不同浏览器的心跳包间隔是不同的,如下:
【谷歌浏览器抓包】
对比就会发现,服务端socket空闲到达keepalive_timeout,主动发了FIN包后,二者的行为也不同,火狐的是发出ACK包后紧跟着也发出FIN包,而谷歌的就不会,而是继续发送心跳包,直到服务端发出RST包。
小疑问: 使用keep-alive后,客户端是如何知道服务端数据传输完成了呢?
-
用户在代码里主动计算返回资源(HTTP Response Body)大小(单位:B),并设置
Content-Length: <length>
header中;
-
交给http服务软件(nginx,apache)处理,HTTP头部使用
Transfer-Encoding: chunked
来代替Content-Length,他表示数据是分块传输的,最后一块是空的,表示数据传输完了
-
等待服务端主动关闭socket
不过测试中,如果设置了Content-Length,就会导致keep-alive失效,因为浏览器会主动关闭socket,【火狐,谷歌都这样,猜测这个行为没有纳入http协议中,但是已经作为浏览器业界的默认行为了】抓包如下:
nginx 对keep-alive的实现原理
首先去官网扒拉了下,还真发现有相关解释:
于是下载了nginx一份源码,顺着该文档追查了下:
1:目录如下,先跑到http目录下看就好
2:在ngx_http_process_request_headers函数里,
//解析http头
if (rc == NGX_OK) {
.....
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) {
return;
}
......
}
//解析完http头的动作
if (rc == NGX_HTTP_PARSE_HEADER_DONE) {
/* a whole header has been parsed successfully */
ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"http header done");
r->request_length += r->header_in->pos - r->header_name_start;
r->http_state = NGX_HTTP_PROCESS_REQUEST_STATE;
rc = ngx_http_process_request_header(r);
if (rc != NGX_OK) {
return;
}
ngx_http_process_request(r);
return;
}
3:在ngx_http_process_request函数里,会调用ngx_http_handler函数,该函数如下:
void
ngx_http_handler(ngx_http_request_t *r)
{
......
if (!r->internal) {
switch (r->headers_in.connection_type) {
case 0:
r->keepalive = (r->http_version > NGX_HTTP_VERSION_10);//http1.1默认支持keep-alive
break;
case NGX_HTTP_CONNECTION_CLOSE:
r->keepalive = 0;
break;
case NGX_HTTP_CONNECTION_KEEP_ALIVE:
r->keepalive = 1;
break;
}
r->lingering_close = (r->headers_in.content_length_n > 0
|| r->headers_in.chunked);
r->phase_handler = 0;
} else {
cmcf = ngx_http_get_module_main_conf(r, ngx_http_core_module);
r->phase_handler = cmcf->phase_engine.server_rewrite_index;
}
......
}
到这里,Connection:keep-alive解析完毕;
4:ngx_http_finalize_connection函数中:
//根据条件是否执行ngx_http_set_keepalive函数
if (!ngx_terminate
&& !ngx_exiting
&& r->keepalive
&& clcf->keepalive_timeout > 0)//可见要想nginx支持keep-alive,配置文件里必须有keepalive_timeout > 0
{
ngx_http_set_keepalive(r);
return;
}
//关闭该请求,满足执行ngx_http_set_keepalive函数,就不会走到这里了
ngx_http_close_request(r, 0);
5:ngx_http_set_keepalive函数就是根据步骤3的解析结果进行处理了
.......
r->keepalive = 0;
ngx_http_free_request(r, 0);
......
ngx_add_timer(rev, clcf->keepalive_timeout);//添加一个定时任务,并且每个请求都会重新对该定时任务刷新
6:搜索Connection
关键字,找到了ngx_http_headers_in数组,是用于配置解析http header头的:
ngx_http_header_t ngx_http_headers_in[] = {
{ ngx_string("Host"), offsetof(ngx_http_headers_in_t, host),
ngx_http_process_host },
{ ngx_string("Connection"), offsetof(ngx_http_headers_in_t, connection),
ngx_http_process_connection },
{ ngx_string("If-Modified-Since"),
offsetof(ngx_http_headers_in_t, if_modified_since),
ngx_http_process_unique_header_line },
...
}
7:搜索ngx_http_headers_in
关键字,找到了ngx_http_init_headers_in_hash函数,该函数的作用就是将ngx_http_headers_in内容放到hash中:
static ngx_int_t
ngx_http_init_headers_in_hash(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf)
{
......
for (header = ngx_http_headers_in; header->name.len; header++) {
hk = ngx_array_push(&headers_in);
if (hk == NULL) {
return NGX_ERROR;
}
hk->key = header->name;
hk->key_hash = ngx_hash_key_lc(header->name.data, header->name.len);
hk->value = header;
}
hash.hash = &cmcf->headers_in_hash;
hash.key = ngx_hash_key_lc;
hash.max_size = 512;
hash.bucket_size = ngx_align(64, ngx_cacheline_size);
hash.name = "headers_in_hash";
hash.pool = cf->pool;
hash.temp_pool = NULL;
......
}
对ngx_http_init_headers_in_hash的调用链进行追踪,会发现有一个ngx_http_block的函数调用,这个函数就牛逼了,所有的http配置都是它在管理。。。,这里就不深究了,有兴趣的可以看看一张脑图说清 Nginx 的主流程。
8:接着说ngx_http_headers_in
,Connection头的处理函数是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;
}
参考资料
1:nginx 官方文档
2:Nginx 的配置解析流程
3:Http协议之Content-Length
4:HTTP复用与TCP复用
5:Nginx 对 Connection 头的处理过程
6:Nginx使用keep-alive复用tcp连接
7:nginx长连接keep-alive与pipeline
8:理解http层和tcp层的keep-alive