nginx服务器给客户端发送响应时,包括http响应头部、http响应包体内容。可以调用http框架提供的两个函数ngx_http_send_header,ngx_http_output_filter,分别用于给客户端发送http响应头部、http响应包体。这两个函数会调用各个过滤器模块,对将要发送给客户端的响应头部、响应包体进行过滤处理。例如: 是否需要添加http响应头部字段、是否需要断点续传、是否只发送一个区间块的数据、响应包体是否需要压缩等。
一、过滤器模块链表创建
nginx服务器有一个全局的变量ngx_http_top_header_filter,这是一个函数指针变量。 也是http响应头部过滤链表的头部。对于每一个过滤模块,都会有一个静态的变量ngx_http_next_header_filter,同样这也是一个函数指针变量,指向过滤链表的下一个节点。例如:在由6个过滤器模块组成的http响应头部过滤链表内存布局如下:
从这张图可以看出,链表采用的是头插法。也就是说第一个过滤器模块变成了链表的尾节点,而最后一个过滤器模块变成了链表的首节点。同样的http响应包体组成的过滤链表内存布局和上图是类似的,都采用的是头插法。只不过http响应过滤包体链表头指针为一个全局的函数指针ngx_http_top_body_filter,每一个http过滤模块的静态变量ngx_http_next_body_filter则组成响应包体链表的next指针。
下面从代码角度分析下这个过滤链表是如何创建的。以http响应头部过滤链表的创建为例。
ngx_http_header_filter_module是第一个http响应头部过滤模块,在这个模块的ngx_http_header_filter_init函数中,将ngx_http_top_header_filter函数指针指向ngx_http_header_filter, 此时这个为链表的第一个节点。
static ngx_int_t ngx_http_header_filter_init(ngx_conf_t *cf)
{
ngx_http_top_header_filter = ngx_http_header_filter;
return NGX_OK;
}
ngx_http_chunked_filter_module是第二个http响应头部过滤模块,在这个模块的ngx_http_chunked_filter_init函数中将ngx_http_next_header_filter指向ngx_http_top_header_filter,也就是第一个http响应过滤头部模块,而ngx_http_top_header_filter指向本模块的回调ngx_http_chunked_header_filte,使用头部插入法构成一个链表。
static ngx_int_t ngx_http_chunked_filter_init(ngx_conf_t *cf)
{
ngx_http_next_header_filter = ngx_http_top_header_filter; //next指针指向之前的模块
ngx_http_top_header_filter = ngx_http_chunked_header_filter;//top指针指向当前这个模块的回调
return NGX_OK;
}
其它过滤器模块插入到链表的过程是类似的,这里就不再对每一个过滤模块如何插入到链表进行详细分析了, 没有这个必要。
二、常见http过滤模块的功能
过滤模块是比较简单的模块,对于每一个过滤模块,代码量也不过几百行而已。仔细分析很容易看懂,这里就不对这些过滤模块进行详细分析了。但需要总结下几个常见过滤模块是做什么的。
三、发送响应头部
ngx_http_send_header函数用于发送http响应头部,这个是http框架提供的接口,可以被http模块调用。看下这个函数的实现。
ngx_int_t ngx_http_send_header(ngx_http_request_t *r)
{
if (r->err_status)
{
r->headers_out.status = r->err_status;
r->headers_out.status_line.len = 0;
}
return ngx_http_top_header_filter(r);
}
这个函数会从http响应头部过滤器链表头部开始遍历,使用各个过滤器模块对响应头部进行处理。每一个过滤器模块如果处理完成后, 都会调用ngx_http_next_header_filter开始进入下一个过滤器模块进行处理, 最终把处理后的响应头部发送给客户端。而最后一个http响应头部过滤器链表节点的回调为:ngx_http_header_filter,看下这个函数的实现。
//1、统计http响应头部的长度
//2、开辟http响应头部的缓冲区空间
//3、将http响应头部的各个头部信息拼装到buf缓冲区中,然后将这个缓冲区的数据发送给客户端
static ngx_int_t ngx_http_header_filter(ngx_http_request_t *r)
{
/********1、以下是为了统计响应头部的总大小 **********/
/*********目的是为了开辟一个应答包头大小的缓冲区 **********/
len = sizeof("HTTP/1.x ") - 1 + sizeof(CRLF) - 1 + sizeof(CRLF) - 1;
//统计状态码字符串的长度
//status_line存放的是状态码的内容,例如:"302 Moved Temporarily"
if (r->headers_out.status_line.len)
{
len += r->headers_out.status_line.len;
status_line = &r->headers_out.status_line;
}
clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
//服务器的版本信息长度
if (r->headers_out.server == NULL)
{
len += clcf->server_tokens ? sizeof(ngx_http_server_full_string) - 1:
sizeof(ngx_http_server_string) - 1;
}
/********2、开辟一个buf结构,buf大小为应答包头的大小**********************/
b = ngx_create_temp_buf(r->pool, len);
if (b == NULL)
{
return NGX_ERROR;
}
/********3、以下操作将http应答包头的各个字段拷贝到开辟的缓冲区***********/
//拷贝HTTP1.1 200 OK状态行
/* "HTTP/1.x " */
b->last = ngx_cpymem(b->last, "HTTP/1.1 ", sizeof("HTTP/1.x ") - 1);
if (status_line)
{
b->last = ngx_copy(b->last, status_line->data, status_line->len);
}
*b->last++ = CR; *b->last++ = LF;
//拷贝应答包体长度字段
if (r->headers_out.content_length == NULL
&& r->headers_out.content_length_n >= 0)
{
b->last = ngx_sprintf(b->last, "Content-Length: %O" CRLF,r->headers_out.content_length_n);
}
//构造发送缓冲区
out.buf = b;
out.next = NULL;
/********4、发送http响应头部信息给客户端***********/
return ngx_http_write_filter(r, &out);
}
四、发送响应包体
ngx_http_output_filter函数用于向客户端发送http响应包体数据。这个是http框架提供的接口,可以被http模块调用。看下这个函数的实现:
ngx_int_t ngx_http_output_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
ngx_int_t rc;
ngx_connection_t *c;
//发送响应包体
rc = ngx_http_top_body_filter(r, in);
return rc;
}
这个函数会从http响应包体过滤器链表头部开始遍历,使用各个过滤器模块对响应包体进行处理。 每一个过滤器模块如果处理完成后, 都会调用ngx_http_next_body_filter开始进入下一个过滤器模块进行处理, 最终把处理后的响应包体发送给客户端。而最后一个http响应包体过滤器链表节点的回调为:ngx_http_write_filter,看下这个函数的实现。
ngx_http_write_filter这个函数会将响应包体发送给客户端,如果一次没法发送完成,则会保存未发送完成的数据到链表中,同时删除已经发送完成的链表数据。
现在假设应答包体链表由5个待发送的数据组成,则内存布局如下:
在一次发送过程中,只有一部分数据发送给了客户端浏览器,还有一部分数据没有发送完成,需要下一次才能发送给浏览器。则内存布局如下: 图中可以看出,已经发送完的数据缓冲区pos与last指针都指向缓冲区的末尾。需要注意的是第三个数据结点,这个结点的数据并没有全部发送完,pos指针指向待发送的位置。下一次发送时将从第三个节点的pos位置开始,把数据发送给客户端浏览器。
当再一次发送数据时,之前已经发送完的数据结点已经被删除了。将从第一个有效节点的pos位置开始发送剩余的数据给客户端浏览器,内存布局如下图:
ngx_int_t ngx_http_write_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
//计算out缓冲区占用字数数
for (cl = r->out; cl; cl = cl->next)
{
ll = &cl->next;
size += ngx_buf_size(cl->buf);
}
/* add the new chain to the existent one */
//将本次待发送的缓冲区加到out尾部,并计算出out总长度
for (ln = in; ln; ln = ln->next)
{
cl = ngx_alloc_chain_link(r->pool);
cl->buf = ln->buf;
*ll = cl;
ll = &cl->next;
//计算这个链表节点缓冲区数据大小
size += ngx_buf_size(cl->buf);
}
*ll = NULL;
//待发送的响应包体没有达到阈值,则暂时不发送
if (!last && !flush && in && size < (off_t) clcf->postpone_output)
{
return NGX_OK;
}
//需要延迟发送响应,用于限速功能,本次不发送的会,对客户端来讲,自然速度降低了
if (c->write->delayed)
{
c->buffered |= NGX_HTTP_WRITE_BUFFERED;
return NGX_AGAIN;
}
//判断是否需要限速
if (r->limit_rate)
{
limit = r->limit_rate * (ngx_time() - r->start_sec + 1)
- (c->sent - clcf->limit_rate_after);
//limit小于等于0,表示超发的字节数,需要限速
if (limit <= 0)
{
c->write->delayed = 1; //需要延迟发送响应
ngx_add_timer(c->write, (ngx_msec_t) (- limit * 1000 / r->limit_rate + 1));
c->buffered |= NGX_HTTP_WRITE_BUFFERED;
return NGX_AGAIN;
}
//计算本次最大可以发送多少数据给客户端
if (clcf->sendfile_max_chunk
&& (off_t) clcf->sendfile_max_chunk < limit)
{
limit = clcf->sendfile_max_chunk;
}
}
else
{
limit = clcf->sendfile_max_chunk;
}
//调用ngx_linux_sendfile_chain发送数据,返回值是未发送完的数据
chain = c->send_chain(c, r->out, limit);//ngx_writev_chain
//清空已经发送的缓冲
for (cl = r->out; cl && cl != chain; /* void */)
{
ln = cl;
cl = cl->next;
ngx_free_chain(r->pool, ln);
}
//保存未发送完成的数据,待下次事件触发后在发送给客户端
r->out = chain;
return NGX_OK;
}
到此过滤器模块已经分析完成了。过滤器模块还是比较简单的,代码量也不太,读者可以好好分析上图中的这6个过滤器模块的实现方式。下一篇文章将分析nginx是如何接收http请求包体的,如果不需要接收请求包体,那nginx又是如何丢弃http请求包体的。