tcp与http都支持keepalive机制,但两者是不同的。先看下tcp的keepalive机制。当客户端与服务器建立了tcp连接后,如果客户端一直不发送数据, 或者隔很长时间才发送一次数据。当连接很久没有数据报文传输时,服务器如何去确定对方还在线。到底是掉线了还是确实没有数据传输,连接还需不需要保持,这种情况在TCP协议设计中是需要考虑的。TCP协议通过一种巧妙的方式去解决这个问题,当超过一段时间之后,TCP自动发送一个数据为空的报文给对方, 如果对方回应了这个报文,说明对方还在线,连接可以继续保持,如果对方没有报文返回并且重试了多次之后则认为连接丢失,没有必要保持连接。这个过程相当于服务器向客户端发送心跳包,确认客户端是否还在线。
而http的keepalive机制为: 通常客户端浏览器要展现一张完整的页面需要很多个请求才能完成,如图片,js,CSS等。如果每一个HTTP请求都需要新建并断开一个TCP,这个开销是完全没有必要的。开启HTTP Keep-Alive之后,能复用已有的TCP链接, 当一个请求已经响应完毕,服务器端没有立即关闭TCP连接,而是等待一段时间继续接收浏览器可能发送过来的第二个请求,通常浏览器在第一个请求返回之后会立即发送第二个请求。因此在一个tcp连接上可以存在多个http请求,当然这个如果客户端一直不发送新的http请求,超过一段时间后,nginx服务器还是会关闭这个TCP长连接。
一、keealive的请求头部解析
在http1.0协议里,客户端通过发送connection: keep-alive的请求头来实现与服务器之间的长连接。 http1.1默认支持keepalive, 但通过请求头connection: close可明确要求不进行长连接保持。先看下如何设置keepalive。
//http请求头部常用头部对应的处理函数
ngx_http_header_t ngx_http_headers_in[] =
{
{ ngx_string("Connection"), offsetof(ngx_http_headers_in_t, connection),ngx_http_process_connection },
};
//处理http请求头部的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;}在解析http头部的connection字段后,将会设置TCP的连接类型,是打开长连接还是关闭长连接。然后会设置到请求对象的keepalive中。对于keepalive只用于服务器与客户端保持TCP长连接,而nginx内部操作,例如内部跳转, 命名location跳转、子请求等是不需要用到keepalive的。
//调用各个http模块协同处理这个请求void ngx_http_handler(ngx_http_request_t *r){//不需要进行内部跳转。keepalive机制是在客户端和nginx服务器之间才需要关注。对于内部跳转则不会用到//keepalive机制 if (!r->internal) { switch (r->headers_in.connection_type) { case 0: r->keepalive = (r->http_version > NGX_HTTP_VERSION_10);//http1.1版本默认开启keepalive break; case NGX_HTTP_CONNECTION_CLOSE: r->keepalive = 0;//关闭长连接 break; case NGX_HTTP_CONNECTION_KEEP_ALIVE: r->keepalive = 1; //打开长连接 break; }}}二、keepalive的设置
当一个http请求完成后, 这个时候引用计数为0了,会释放这个http请求,但到底要不要释放tcp连接,是由keepalive机制与延迟关闭机制决定的。keepalive相比延迟关闭,优先级更高。来看下ngx_http_finalize_connection函数的实现。
//释放http请求与连接keepalive_timeout可以在nginx.conf配置文件中,通过keepalive_timeout指令设置,默认情况下超时时间就是75秒。如果超过这个时间都还没有收到来自客户端新的http请求,则会关闭这个tcp连接,keepalive功能也就结束了。 ngx_http_set_keepalive函数则是正式开始keepalive的处理,函数有点长,实现了pipeline长连接与普通长连接。先来看下pipeline的实现。
static void ngx_http_finalize_connection(ngx_http_request_t *r)
{
//执行到这里,引用计数为1,则要准备结束请求了
//keepalive为1表示请求需要释放,但tcp连接还是用复用的
if (!ngx_terminate
&& !ngx_exiting
&& r->keepalive
&& clcf->keepalive_timeout > 0)
{
ngx_http_set_keepalive(r);
return;
}
}
三、pipeline处理
那么什么是pipeline呢?pipeline其实就是流水线作业,它可以看作为keepalive的一种升华,因为pipeline也是基于长连接的,目的就是利用一个连接做多次请求。如果客户端要提交多个请求,对于keepalive来说,那么第二个请求,必须要等到第一个请求的响应接收完全后,才能发起,这和TCP的停止等待协议是一样的,得到两个响应的时间至少为2*RTT。而对pipeline来说,客户端不必等到第一个请求处理完后,就可以马上发起第二个请求。得到两个响应的时间可能能够达到1*RTT。nginx是直接支持pipeline的,但是,nginx对pipeline中的多个请求的处理却不是并行的,依然是一个请求接一个请求的处理,只是在处理第一个请求的时候,客户端就可以发起第二个请求。这样,nginx利用pipeline减少了处理完一个请求后,等待第二个请求的请求行与请求头部的时间。其实nginx的做法很简单,前面说到,nginx在读取数据时,会将读取的数据放到一个buffer里面,所以,如果nginx在处理完前一个请求后,如果发现buffer里面还有数据,就认为剩下的数据是下一个请求的开始,然后接下来处理下一个请求,否则就设置keepalive。
来看下nginx服务器是如何处理pipeline的?
//设置keepalive过程(1) nginx服务器是怎么知道需要进行pipeline处理呢? 条件就是这个b->pos < b->last。 多个http请求可以复用同一个tcp连接,客户端浏览器在发出第一个http请求时,可以不需要等待收到服务器的响应,可以立马发出第二个http请求。在这种请求下,nginx服务器收到第一个http请求时,是有可能也接收到了第二个http请求的头部信息,并保存到http请求对象的header_in缓冲区中。看下这个接收过程, 函数只负责从内核读取数据到缓冲区,并没有限制只读取第一个http请求的数据,如果有第二个http请求,也会把第二个http请求头部也读取到缓冲区。
static void ngx_http_set_keepalive(ngx_http_request_t *r)
{
//在接收到来自客户端的连接请求时,同时也接收到了同一个tcp连接上的第2个http请求头部。
//因此在处理完第一个请求时,pos不等于last,说明接收到了第二个http请求头部,接下来要立马处理第二个请求
if (b->pos < b->last)
{
//在接收来自客户端的请求行、或者请求头部时。如果连接对象的buffer缓冲区不能够存放所有的请求行,请求头。
//则http请求对象自己会开辟新的空间。因此在请求结束时,需要把请求对象开辟的空间加入到空闲表中。
//这样在这个连接上有新的http请求到来时,可以复用上一个请求对象开辟的空间
if (b != c->buffer)
{
if (hc->free == NULL)
{
hc->free = ngx_palloc(c->pool, cscf->large_client_header_buffers.num * sizeof(ngx_buf_t *));
}
//将在用表移动到空闲表
for (i = 0; i < hc->nbusy - 1; i++)
{
f = hc->busy[i];
hc->free[hc->nfree++] = f;
f->pos = f->start;
f->last = f->start;
}
hc->busy[0] = b;
hc->nbusy = 1;
}
}
}
static ssize_t ngx_http_read_request_header(ngx_http_request_t *r)那这个b != c->buffer又怎么理解呢? 默认情况下 http请求结构的 header_in缓冲区是等于连接对象的 buffer缓冲区的。 看下面这个函数就知道了。刚建立http请求时,两个缓冲区都指向同一个内存空间。
{
//事件就绪,也就是从epoll_wait中返回后,从内核缓冲区中读取内容到应用层缓冲区header_in
if (rev->ready)
{
//ngx_unix_recv
n = c->recv(c, r->header_in->last, r->header_in->end - r->header_in->last);
}
}
//首次建立tcp连接后,读事件的回调,用于创建一个ngx_http_request_t对象那 什么情况下http请求结构的header_in缓冲区会不等于连接对象的buffer缓冲区呢? nginx服务器在接收到来自客户端的http请求行或者请求头部时,如果接受缓冲区不能够存放请求行或者请求头时,是会开辟一个新的缓冲区,并把这个缓冲区加入到在用缓冲区数组busy中。通常情况下nginx服务器的默认缓冲区是足够存放一个http请求行或者请求头部的, 之所以缓冲区不够,大部分情况下是接收到了多个http请求头部。这种情况下将会执行pipeline处理
static void ngx_http_init_request(ngx_event_t *rev)
{
//刚建立的请求,默认情况下这两个缓冲区是相等的
if (r->header_in == NULL)
{
r->header_in = c->buffer;
}
}
//开辟一个大的缓冲区,并把旧缓冲区的数据拷贝到新缓冲区中为什么要把busy这个在用缓冲区数据移动到空闲缓冲区数组呢? 为什么不直接释放这些空间呢? 还是因为pipeline的原因,因为除了当前要结束的这个http请求外,缓冲区中还存放了其它的http请求。当这个http请求结束时,可以立即处理剩余的http请求。而如果不是pipeline,则会关闭这些缓冲区的, 这种情况下是一个普通的TCP长连接,客户端什么时候会再次发送http请求,以及是否真的还会在发送http请求是不可知的, nginx将会释放这些资源。
//解析请求行时,request_line = 1, 解析请求头时,request_line = 0
static ngx_int_t ngx_http_alloc_large_header_buffer(ngx_http_request_t *r, ngx_uint_t request_line)
{
/* 检查给该请求分配的请求头缓冲区个数是否已经超过限制,默认最大个数为4个 */
if (hc->busy == NULL)
{
hc->busy = ngx_palloc(r->connection->pool, cscf->large_client_header_buffers.num * sizeof(ngx_buf_t *));
}
/* 如果还没有达到最大分配数量,则分配一个新的大缓冲区 */
b = ngx_create_temp_buf(r->connection->pool, cscf->large_client_header_buffers.size);
/* 将从空闲队列取得的或者新分配的缓冲区加入已使用队列 */
hc->busy[hc->nbusy++] = b;
//header_in指向这个新的缓冲区, 连接对象的缓冲区仍然为旧的缓冲区,大小没有改变
r->header_in = b;
}
(2)接下来要释放一个http请求了,但请求本身这个对象没有释放, 这样在这个tcp连接上的已经接收到的其它请求可以复用这个http请求对象。同时也会设置keepalive的超时时间,超过这个时间后会真正关闭这个tcp连接。
//设置keepalive过程(3)在pipeline这种情况下,当前http请求已经结束了。那nginx如何处理这个tcp连接上的其它http请求呢? nginx服务器将会设置读事件ngx_event_t的接收回调handler为: ngx_http_init_request ,并立马把读事件加入到post队列中。因此这个读事件就会被立即调用,从而开始处理这个tcp连接上的其它http请求。
static void ngx_http_set_keepalive(ngx_http_request_t *r)
{
//释放请求的内容空间
ngx_http_free_request(r, 0);
//将读事件注册到红黑树实现的定时器中,超时时间由keepalive_timeout命令设置,默认为75秒。
//如果超时时间到后都还没有再收到来自客户端的http请求,则会关闭连接。
ngx_add_timer(rev, clcf->keepalive_timeout);
ngx_handle_read_event(rev, 0);
//接收来自客户端请求时,还不需要向客户端写入数据,因此把写回调设置为不做任何事情
wev = c->write;
wev->handler = ngx_http_empty_handler;
}
static void ngx_http_set_keepalive(ngx_http_request_t *r)看下 ngx_http_init_request这个函数怎么在这个tcp连接上 重新创建一个http请求。对于pipeline,是不会创建一个新的http请求的,而是复用已经结束的http请求。也可以看出不管是在这个tcp连接上新建立的http请求,还是当前请求结束后重新建立了一个新的http请求,都会调用 ngx_http_init_request这个函数进行处理。如果对这个函数不熟悉,可以参考ngxin请求行与请求头处理这篇文章
{
//该请求结束了,设置接收回调为ngx_http_init_request。在这个tcp连接上的下一个请求到来时,
//重新开始对一个新请求进行处理
if (b->pos < b->last)
{
//设置为串行请求
hc->pipeline = 1;
//设置读事件的回调,并加入到post事件队列
rev->handler = ngx_http_init_request;
ngx_post_event(rev, &ngx_posted_events);
return;
}
}
static void ngx_http_init_request(ngx_event_t *rev)到此对pipeline这种长连接的处理过程已经分析完成了,下面将分析下非pipeline情况下的长连接的处理过程。暂且称非pipeline的长连接为普通长连接吧!
{
r = hc->request;
if (r)
{
ngx_memzero(r, sizeof(ngx_http_request_t));
//串行请求时值为1,表示旧请求结束后,内部资源已经释放了,但请求本身没有释放。
//这样的话,在这个tcp连接上的新请求可以使用旧请求的对象。
//配和ngx_http_set_keepalive函数就理解这部分的实现了
r->pipeline = hc->pipeline;
//串行请求时,在用缓冲区加入到了空闲缓冲区数组中。配和ngx_http_set_keepalive函数帮助理解
if (hc->nbusy)
{
r->header_in = hc->busy[0];
}
}
}
四、普通长连接处理
如果不是pipeline这种串行请求,因此会尽可能的释放该请求空间。因为这个tcp连接上什么时候会有客户端发来新的http请求,以及是否真的会有新的http请求是不确定的。nginx秉承一个能尽量减少资源占用就减少资源的原则,会把这个http请求内部开辟的资源给释放,同时也把请求对象本身也真正的释放, 但tcp连接还是没有关闭,等超时没有收到来自客户端的http请求时在关闭。以此同时将会设置读事件ngx_event_t的接收回调handler为: ngx_http_keepalive_handler,用来处理在这个tcp连接上的其它http请求(这里不像pipeline, 当前http请求结束了,但此时这个tcp连接上并没有其它的http请求存在)。
static void ngx_http_set_keepalive(ngx_http_request_t *r)现在来看是ngx_http_keepalive_handler这个函数的处理过程。函数内部会开辟一些必要的缓冲区外,最终还是回调用ngx_http_init_request这个函数重新开始一个新的http请求。
{
//释放资源
if (hc->busy)
{
for (i = 0; i < hc->nbusy; i++)
{
ngx_pfree(c->pool, hc->busy[i]->start);
hc->busy[i] = NULL;
}
}
//设置读事件回调,用于处理来自客户端的新的http请求
rev->handler = ngx_http_keepalive_handler;
}
//http请求关闭后,tcp连接并没有关闭。这个函数用于在这个tcp连接上接收新的http请求可以看出,在同一个tcp连接下,不管是第一次建立的请求,还是之后重新建立的请求,最终都会调用ngx_http_init_request这个函数。由这个函数负责对请求的初始化操作。由此也可以知道,多个http请求是可以复用同一个tcp连接的。没有必要客户端发起一个http请求就建立一个tcp连接,太浪费资源了。到此keepalive机制与pipeline已经分析完成了, 因为keepalive机制和在同一个tcp连接上重新建立一个新的http请求有关,也与关闭一个http请求有关,跨度比较大,看了也比较混乱。如果对http请求初始化过程还不是很清楚,可以参考前面的文章。下一篇文章将会对nginx的延迟关闭功能进行分析。
static void ngx_http_keepalive_handler(ngx_event_t *rev)
{
//开辟接收请求行、请求头缓冲区。因为非pipeline情况下,释放之前的一个请求时,
//也把连接对象的buffer缓冲区也给释放了。因此新的请求到来时,需要重新开辟空间
b->pos = ngx_palloc(c->pool, size);
//调用这个函数,重新开始一个新的http请求处理
ngx_http_init_request(rev);
}