Nginx 源码分析之 r Filter 与 U U pstream (第一章)

文章有点长分二章分享给大家、后续持续更新需要的朋友可以点击关注我哦。。。。

第一章 r Filter 模块

1.1 过滤模块简介

1. 执行时间和内容

过滤(filter)模块是过滤响应头和内容的模块,可以对回复的头和内容进行处理。它的处理

时间在获取回复内容之后,向用户发送响应之前。它的处理过程分为两个阶段,过滤 HTTP

回复的头部和主体,在这两个阶段可以分别对头部和主体进行修改。

在代码中有类似的函数:

ngx_http_top_header_filter(r);

ngx_http_top_body_filter(r, in);

就是分别对头部和主体进行过滤的函数。所有模块的响应内容要返回给客户端,都必须调用

这两个接口。

更多c/c++ Linux服务器高阶知识、电子书籍、视频等等请后台私信【架构】获取

知识点有C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等等。

Nginx 源码分析之 r Filter 与 U U pstream   (第一章)

Nginx 源码分析之 r Filter 与 U U pstream   (第一章)


2. 执行顺序

过滤模块的调用是有顺序的,它的顺序在编译的时候就决定了。控制编译的脚本位于

auto/modules 中,当你编译完 Nginx 以后,可以在 objs 目录下面看到一个 ngx_modules.c 的

文件。打开这个文件,有类似的代码:

ngx_module_t *ngx_modules[] = {

...

&ngx_http_write_filter_module,

&ngx_http_header_filter_module,

&ngx_http_chunked_filter_module,

&ngx_http_range_header_filter_module,

&ngx_http_gzip_filter_module,

&ngx_http_postpone_filter_module,

&ngx_http_ssi_filter_module,

&ngx_http_charset_filter_module,

&ngx_http_userid_filter_module,

&ngx_http_headers_filter_module,

&ngx_http_copy_filter_module,

&ngx_http_range_body_filter_module,

&ngx_http_not_modified_filter_module,

NULL

};

从 write_filter 到 not_modified_filter,模块的执行顺序是反向的。也就是说最早执行的

是 not_modified_filter,然后各个模块依次执行。所有第三方的模块只能加入到 copy_filter 和

headers_filter 模块之间执行。

Nginx 执行的时候是怎么按照次序依次来执行各个过滤模块呢?它采用了一种很隐晦的

方法,即通过局部的全局变量。比如,在每个 filter 模块,很可能看到如下代码:

static ngx_http_output_header_filter_pt

ngx_http_next_header_filter;

static ngx_http_output_body_filter_pt

ngx_http_next_body_filter;

...

ngx_http_next_header_filter = ngx_http_top_header_filter;

ngx_http_top_header_filter = ngx_http_example_header_filter;

ngx_http_next_body_filter = ngx_http_top_body_filter;

ngx_http_top_body_filter = ngx_http_example_body_filter;

ngx_http_top_header_filter 是一个全局变量。当编译进一个 filter 模块的时候,就被赋值

为当前 filter 模块的处理函数。而 ngx_http_next_header_filter 是一个局部全局变量,它保存

了编译前上一个 filter 模块的处理函数。所以整体看来,就像用全局变量组成的一条单向链

表。

每个模块想执行下一个过滤函数,只要调用一下 ngx_http_next_header_filter 这个局部

变量。而整个过滤模块链的入口,需要调用 ngx_http_top_header_filter 这个全局变量。

ngx_http_top_body_filter 的行为与 header fitler 类似。

响应头和响应体过滤函数的执行顺序如下所示:

Nginx 源码分析之 r Filter 与 U U pstream   (第一章)

这图只表示了 head_filter 和 body_filter 之间的执行顺序,在 header_filter 和 body_filter 处理

函数之间,在 body_filter 处理函数之间,可能还有其他执行代码。


3. 模块编译

Nginx 可以方便的加入第三方的过滤模块。在过滤模块的目录里,首先需要加入 config 文件,

文件的内容如下:

ngx_addon_name=ngx_http_example_filter_module

HTTP_AUX_FILTER_MODULES="$HTTP_AUX_FILTER_MODULES

ngx_http_example_filter_module"

NGX_ADDON_SRCS="$NGX_ADDON_SRCS

$ngx_addon_dir/ngx_http_example_filter_module.c"

说 明 把 这 个 名 为 ngx_http_example_filter_module 的 过 滤 模 块 加 入 ,

ngx_http_example_filter_module.c 是该模块的源代码。

注意 HTTP_AUX_FILTER_MODULES 这个变量与一般的内容处理模块不同。

1.2 过滤模块的分析

. 1. 相关结构体

ngx_chain_t 结构非常简单,是一个单向链表:

typedef struct ngx_chain_s ngx_chain_t;

struct ngx_chain_s {

ngx_buf_t *buf;

ngx_chain_t *next;

};

在过滤模块中,所有输出的内容都是通过一条单向链表所组成。这种单向链表的设计,正好

应和了 Nginx 流式的输出模式。每次 Nginx 都是读到一部分的内容,就放到链表,然后输出

出去。这种设计的好处是简单,非阻塞,但是相应的问题就是跨链表的内容操作非常麻烦,

如果需要跨链表,很多时候都只能缓存链表的内容。

单链表负载的就是 ngx_buf_t,这个结构体使用非常广泛,先让我们看下该结构体的代码:

struct ngx_buf_s {

u_char *pos; /* 当前 buffer 真实内容的起始位置 */

u_char *last; /* 当前 buffer 真实内容的结束位置 */

off_t file_pos; /* 在文件中真实内容的起始位置 */

off_t file_last; /* 在文件中真实内容的结束位置 */

u_char *start; /* buffer 内存的开始分配的位置 */

u_char *end; /* buffer 内存的结束分配的位置 */

ngx_buf_tag_t tag; /* buffer 属于哪个模块的标志 */

ngx_file_t *file; /* buffer 所引用的文件 *//* 用来引用替换过后的 buffer,以便当所有 buffer 输出以后,

* 这个影子 buffer 可以被释放。

*/

ngx_buf_t *shadow;

/* the buf's content could be changed */

unsigned temporary:1;

/*

* the buf's content is in a memory cache or in a read

only memory

* and must not be changed

*/

unsigned memory:1;

/* the buf's content is mmap()ed and must not be changed

*/

unsigned mmap:1;

unsigned recycled:1; /* 内存可以被输出并回收 */

unsigned in_file:1; /* buffer 的内容在文件中 */

/* 马上全部输出 buffer 的内容, gzip 模块里面用得比较多 */

unsigned flush:1;

/* 基本上是一段输出链的最后一个 buffer 带的标志,标示可以输

出,

* 有些零长度的 buffer 也可以置该标志

*/

unsigned sync:1;

/* 所有请求里面最后一块 buffer,包含子请求 */

unsigned last_buf:1;

/* 当前请求输出链的最后一块 buffer */

unsigned last_in_chain:1;

/* shadow 链里面的最后 buffer,可以释放 buffer 了 */

unsigned last_shadow:1;

/* 是否是暂存文件 */

unsigned temp_file:1;

/* 统计用,表示使用次数 */

/* STUB */ int num;

};

一般 buffer 结构体可以表示一块内存,内存的起始和结束地址分别用 start 和 end 表示,pos

和 last 表示实际的内容。如果内容已经处理过了,pos 的位置就可以往后移动。如果读取到

新的内容,last 的位置就会往后移动。所以 buffer 可以在多次调用过程中使用。如果 last 等

于 end,就说明这块内存已经用完了。如果 pos 等于 last,说明内存已经处理完了。下面是

一个简单的示意图,说明 buffer 中指针的用法:

Nginx 源码分析之 r Filter 与 U U pstream   (第一章)

2. 响应头过滤函数

响应头过滤函数主要的用处就是处理 HTTP 响应的头,可以根据实际情况对于响应头进行修

改或者添加删除。响应头过滤函数先于响应体过滤函数,而且只调用一次,所以一般可作过

滤模块的初始化工作。

响应头过滤函数的入口只有一个:

ngx_int_t

ngx_http_send_header(ngx_http_request_t *r)

{

...

return ngx_http_top_header_filter(r);

}

该函数向客户端发送回复的时候调用,然后按前一节所述的执行顺序。该函数的返回值一般

是 NGX_OK,NGX_ERROR 和 NGX_AGAIN,分别表示处理成功,失败和未完成。可以把 HTTP

响应头的存储方式想象成一个 hash 表,在 Nginx 内部可以很方便地查找和修改各个响应头

部,ngx_http_header_filter_module 过滤模块把所有的 HTTP 头组合成一个完整的 buffer,最

终 ngx_http_write_filter_module 过滤模块把 buffer 输出。

按照前一节过滤模块的顺序,依次讲解如下:

Nginx 源码分析之 r Filter 与 U U pstream   (第一章)

Nginx 源码分析之 r Filter 与 U U pstream   (第一章)

3. 响应体过滤函数

响应体过滤函数是过滤响应主体的函数。ngx_http_top_body_filter 这个函数每个请求可能会

被执行多次,它的入口函数是 ngx_http_output_filter,比如:

ngx_int_t

ngx_http_output_filter(ngx_http_request_t *r, ngx_chain_t *in)

{

ngx_int_t rc;

ngx_connection_t *c;

c = r->connection;

rc = ngx_http_top_body_filter(r, in);

if (rc == NGX_ERROR) {

/* NGX_ERROR may be returned by any filter */

c->error = 1;

}

return rc;

}

ngx_http_output_filter 可以被一般的静态处理模块调用,也有可能是在 upstream 模块里面被

调用,对于整个请求的处理阶段来说,他们处于的用处都是一样的,就是把响应内容过滤,

然后发给客户端。具体模块的响应体过滤函数的格式类似这样:

static int

ngx_http_example_body_filter(ngx_http_request_t *r, ngx_chain_t

*in)

{

...

return ngx_http_next_body_filter(r, in);

}

该函数的返回值一般是 NGX_OK,NGX_ERROR 和 NGX_AGAIN,分别表示处理成功,失败和

未完成。

4. 主要功能介绍

响应的主体内容就存于单链表 in,链表一般不会太长,有时 in 参数可能为 NULL。in 中存有

buf 结构体中,对于静态文件,这个 buf 大小默认是 32K;对于反向代理的应用,这个 buf 可

能是 4k 或者 8k。为了保持内存的低消耗,Nginx 一般不会分配过大的内存,处理的原则是

收到一定的数据,就发送出去。一个简单的例子,可以看看 Nginx 的 chunked_filter 模块,在

没有 content-length 的情况下,chunk 模块可以流式(stream)的加上长度,方便浏览器接收

和显示内容。

在响应体过滤模块中,尤其要注意的是 buf 的标志位,完整描述可以在“相关结构体”这个节

中看到。如果 buf 中包含 last 标志,说明是最后一块 buf,可以直接输出并结束请求了。如

果有 flush 标志,说明这块 buf 需要马上输出,不能缓存。如果整块 buffer 经过处理完以后,

没有数据了,你可以把 buffer 的 sync 标志置上,表示只是同步的用处。

当所有的过滤模块都处理完毕时,在最后的 write_fitler 模块中,Nginx 会将 in 输出链拷贝到

r->out 输出链的末尾,然后调用 sendfile 或者 writev 接口输出。由于 Nginx 是非阻塞的 socket

接口,写操作并不一定会成功,可能会有部分数据还残存在 r->out。在下次的调用中,Nginx

会继续尝试发送,直至成功。

5. 发出子请求

Nginx 过滤模块一大特色就是可以发出子请求,也就是在过滤响应内容的时候,你可以发送

新的请求,Nginx 会根据你调用的先后顺序,将多个回复的内容拼接成正常的响应主体。一

个简单的例子可以参考 addition 模块。

Nginx 是如何保证父请求和子请求的顺序呢?当 Nginx 发出子请求时,就会调用

ngx_http_subrequest 函数,将子请求插入父请求的 r->postponed 链表中。子请求会在主请求

执行完毕时获得依次调用。子请求同样会有一个请求所有的生存期和处理过程,也会进入过

滤模块流程。

关键点是在 postpone_filter 模块中,它会拼接主请求和子请求的响应内容。r->postponed 按

次序保存有父请求和子请求,它是一个链表,如果前面一个请求未完成,那后一个请求内容

就不会输出。当前一个请求完成时并输出时,后一个请求才可输出,当所有的子请求都完成

时,所有的响应内容也就输出完毕了。

6. 一些优化措施

Nginx 过滤模块涉及到的结构体,主要就是 chain 和 buf,非常简单。在日常的过滤模块中,

这两类结构使用非常频繁,Nginx 采用类似 freelist 重复利用的原则,将使用完毕的 chain 或

者 buf 结构体,放置到一个固定的空闲链表里,以待下次使用。

比如,在通用内存池结构体中,pool->chain 变量里面就保存着释放的 chain。而一般的 buf

结构体,没有模块间公用的空闲链表池,都是保存在各模块的缓存空闲链表池里面。对于 buf

结构体,还有一种 busy 链表,表示该链表中的 buf 都处于输出状态,如果 buf 输出完毕,这

些 buf 就可以释放并重复利用了。

Nginx 源码分析之 r Filter 与 U U pstream   (第一章)

7. 过滤内容的缓存

由于 Nginx 设计流式的输出结构,当我们需要对响应内容作全文过滤的时候,必须缓存部分

的 buf 内容。该类过滤模块往往比较复杂,比如 sub,ssi,gzip 等模块。这类模块的设计非

常灵活,我简单讲一下设计原则:

1. 输入链 in 需要拷贝操作,经过缓存的过滤模块,输入输出链往往已经完全不一样了,

所以需要拷贝,通过 ngx_chain_add_copy 函数完成。

2. 一般有自己的 free 和 busy 缓存链表池,可以提高 buf 分配效率。

3. 如果需要分配大块内容,一般分配固定大小的内存卡,并设置 recycled 标志,表示可以

重复利用。

4. 原有的输入 buf 被替换缓存时,必须将其 buf->pos 设为 buf->last,表明原有的 buf 已经

被输出完毕。或者在新建立的 buf,将 buf->shadow 指向旧的 buf,以便输出完毕时及时

释放旧的 buf。


分享到:


相關文章: