首页 运维 Web技术 查看内容

细节详解Nginx编写HTTP模块

2019-11-29 09:36 |来自: 互联网 2212 0

摘要: 一、如何调用HTTP模块在开发HTTP模块前,首先需要了解典型的HTTP模块是如何介入Nginx处理用户请求流 程的。图1是一个简化的时序图,这里省略了许多异步调用,忽略了多个不同的HTTP处理阶段,仅标识了在一个典型请求的处理过程中主要模块被调用的流程,以此帮助读者理解HTTP模块如何处理用户请求。完整 ...
关键词: ngx list 模块 内存 数组 HTTP 元素 数据结构 Nginx


一、如何调用HTTP模块

在开发HTTP模块前,首先需要了解典型的HTTP模块是如何介入Nginx处理用户请求流 程的。图1是一个简化的时序图,这里省略了许多异步调用,忽略了多个不同的HTTP处理阶段,仅标识了在一个典型请求的处理过程中主要模块被调用的流程,以此帮助读者理解HTTP模块如何处理用户请求。完整的流程将在后续中详细介绍。

图1-Nginx HTTP模块调用的简化流程

从图1中看到,worker进程会在一个for循环语句里反复调用事件模块检测网络事件。 当事件模块检测到某个客户端发起的TCP请求时(接收到SYN包),将会为它建立TCP连接,成功建立连接后根据nginx.conf文件中的配置会交由HTTP框架处理。HTTP框架会试图接收完整的HTTP头部,并在接收到完整的HTTP头部后将请求分发到具体的HTTP模块中处理。这种分发策略是多样化的,其中最常见的是根据请求的URI和nginx.conf里location配置项的匹配度来决定如何分发。

HTTP模块在处理请求的结束时,大多会向客户端发送响应,此时会自动地依次调用所有的HTTP过滤模块,每个过滤模块可以根据配置文件决定自己的行为。例如,gzip过滤模块根据配置文件中的gzip on|off来决定是否压缩响应。HTTP处理模块在返回时会将控制权交还给HTTP框架,如果在返回前设置了subrequest,那么HTTP框架还会继续异步地调用适合的HTTP模块处理子请求。

开发HTTP模块时,首先要注意的就是HTTP框架到具体的HTTP模块间数据流的传递,以及开发的HTTP模块如何与诸多的过滤模块协同工作。下面正式进入HTTP模块的开发环节。

二、准备工作

Nginx模块需要使用C(或者C++)语言编写代码来实现,每个模块都要有自己的名字。

按照Nginx约定俗成的命名规则,我们把第一个HTTP模块命名ngx_http_mytest_module。由于第一个模块非常简单,一个C源文件就可以完成,所以这里按照官方惯例,将唯一的源代 码文件命名为ngx_http_mytest_module.c。

实际上,我们还需要定义一个名称,以便在编译前的configure命令执行时显示是否执行成功(即configure脚本执行时的ngx_addon_name变量)。为方便理解,仍然使用同一个模块 名来表示,如ngx_http_mytest_module。

为了让HTTP模块正常工作,首先需要把它编译进Nginx。其次需要设定模块如何在运行中生效,比如在图1描述的典型方式中,配置文件中的location块决定了匹配某种URI的请求将会由相应的HTTP模块处理,因此,运行时HTTP框架会在接收完毕HTTP请求的头部后,将请求的URI与配置文件中的所有location进行匹配(事实上会优先匹配虚拟主机),匹配后再根据location{}内的配置项选择HTTP模块来调用。这是一种最典型的HTTP模块调用方式。

既然有典型的调用方式,自然也有非典型的调用方式,比如ngx_http_access_module模 块,它是根据IP地址决定某个客户端是否可以访问服务的,因此,这个模块需要在 NGX_HTTP_ACCESS_PHASE阶段生效, 它会比mytest模块更早地介入请求的处理中,同时它的流程与图1中的不同,它可以对所有请求产生作用。也就是说,任何HTTP请求都会调用ngx_http_access_module模块处理,只是该模块会根据它感兴趣的配置项及所在的配置块来决定行为方式,这与mytest模块不同,在mytest模块中,只有在配置了location/uri{mytest;}后,HTTP框架才会在某个请求匹配了/uri后调用它处理请求。如果某个匹配了URI请求的location中没有配置mytest配置项,mytest模块依然是不会被调用的。

为了做到跨平台,Nginx定义、封装了一些基本的数据结构。由于Nginx对内存分配比较“吝啬”(只有保证低内存消耗,才可能实现十万甚至百万级别的同时并发连接数),所以这些Nginx数据结构天生都是尽可能少占用内存。后续介绍将要用到的Nginx定义的几个基本数据结构和方法。

1. 整型的封装

Nginx使用ngx_int_t封装有符号整型,使用ngx_uint_t封装无符号整型。Nginx各模块的变量定义都是如此使用的,建议读者沿用Nginx的习惯,以此替代int和unsinged int。在Linux平台下,Nginx对ngx_int_t和ngx_uint_t的定义如下:

typedef intptr_t ngx_int_t; typedef uintptr_t ngx_uint_t; 

2. ngx_str_t数据结构

在Nginx的领域中,ngx_str_t结构就是字符串。ngx_str_t的定义如下:

typedef struct { 
size_t len;
u_char *data;
} ngx_str_t;

ngx_str_t只有两个成员,其中data指针指向字符串起始地址,len表示字符串的有效长度。注意,ngx_str_t的data成员指向的并不是普通的字符串,因为这段字符串未必会以'0'作为结尾,所以使用时必须根据长度len来使用data成员。比较method_name时必须如下这样使用:

if (0 == ngx_strncmp( 
r->method_name.data,
"PUT",
r->method_name.len)
)
{...}

这里,ngx_strncmp其实就是strncmp函数,为了跨平台Nginx习惯性地对其进行了名称上的封装,下面看一下它的定义:

#define ngx_strncmp(s1, s2, n) strncmp((const char *) s1, (const char *) s2, n) 

任何试图将ngx_str_t的data成员当做字符串来使用的情况,都可能导致内存越界!Nginx使用ngx_str_t可以有效地降低内存使用量。例如,用户请求“GET/testa=1 http/1.1rn”存储到内存地址0x1d0b0110上,这时只需要把r->method_name设置{len=3,data=0x1d0b0110}就可以表示方法名“GET”,而不需要单独为method_name再分配内存冗余的存储字符串。

3.ngx_list_t数据结构

ngx_list_t是Nginx封装的链表容器,它在Nginx中使用得很频繁,例如HTTP的头部就是用 ngx_list_t来存储的。当然,C语言封装的链表没有C++或Java等面向对象语言那么容易理解。 先看一下ngx_list_t相关成员的定义:

typedef struct ngx_list_part_s ngx_list_part_t; struct ngx_list_part_s { 
void *elts;
ngx_uint_t nelts;
ngx_list_part_t *next;
};
typedef struct {
ngx_list_part_t *last;
ngx_list_part_t part;
size_t size;
ngx_uint_t nalloc;
ngx_pool_t *pool;
} ngx_list_t;

ngx_list_t描述整个链表,而ngx_list_part_t只描述链表的一个元素。这里要注意的是, ngx_list_t不是一个单纯的链表,为了便于理解,我们姑且称它为存储数组的链表,什么意思呢?抽象地说,就是每个链表元素ngx_list_part_t又是一个数组,拥有连续的内存,它既依赖于ngx_list_t里的size和nalloc来表示数组的容量,同时又依靠每个ngx_list_part_t成员中的nelts来表示数组当前已使用了多少容量。因此,ngx_list_t是一个链表容器,而链表中的元素又是一个数组。事实上,ngx_list_part_t数组中的元素才是用户想要存储的东西,ngx_list_t链表能够容纳的元素数量由ngx_list_part_t数组元素的个数与每个数组所能容纳的元素相乘得到。

这样设计有什么好处呢?

  • 链表中存储的元素是灵活的,它可以是任何一种数据结构。链表元素需要占用的内存由ngx_list_t管理,它已经通过数组分配好了。小块的内存使用链表访问效率是低下的,使用数组通过偏移量来直接访问内存则要高效得多。

下面详述每个成员的意义。

(1)ngx_list_t

  • part:链表的首个数组元素。last:指向链表的最后一个数组元素。size:前面讲过,链表中的每个ngx_list_part_t元素都是一个数组。因为数组存储的是某种类型的数据结构,且ngx_list_t是非常灵活的数据结构,所以它不会限制存储什么样的数据,只是通过size限制每一个数组元素的占用的空间大小,也就是用户要存储的一个数据所占用的字节数必须小于或等于size。nalloc:链表的数组元素一旦分配后是不可更改的。nalloc表示每个ngx_list_part_t数组的容量,即最多可存储多少个数据。pool:链表中管理内存分配的内存池对象。用户要存放的数据占用的内存都是由pool分配的,下文中会详细介绍。

(2)ngx_list_part_t

  • elts:指向数组的起始地址。nelts:表示数组中已经使用了多少个元素。当然,nelts必须小于ngx_list_t结构体中的nalloc。next:下一个链表元素ngx_list_part_t的地址。

事实上,ngx_list_t中的所有数据都是由ngx_pool_t类型的pool内存池分配的,它们通常都是连续的内存(在由一个pool内存池分配的情况下)。下面以图2为例来看一下ngx_list_t的内存分布情况。

图2- ngx_list_t的内存分布

图2中是由3个ngx_list_part_t数组元素组成的ngx_list_t链表可能拥有的一种内存分布结构,读者可以从这种较为常见的内存分布中看到ngx_list_t链表的用法。这里,pool内存池为其分配了连续的内存,最前端内存存储的是ngx_list_t结构中的成员,紧接着是第一个ngx_list_part_t结构占用的内存,然后是ngx_list_part_t结构指向的数组,它们一共占用size*nalloc字节,表示数组中拥有nalloc个大小为size的元素。其后面是第2个ngx_list_part_t结构以及它所指向的数组,依此类推。

对于链表,Nginx提供的接口包括:ngx_list_create接口用于创建新的链表,ngx_list_init接口用于初始化一个已有的链表,ngx_list_push接口用于添加新的元素,如下所示:

ngx_list_t ngx_list_create(ngx_pool_t pool, ngx_uint_t n, size_t size); static ngx_inline ngx_int_t 

ngx_list_init(ngx_list_t list, ngx_pool_t pool, ngx_uint_t n, size_t size); void ngx_list_push(ngx_list_t list);

调用ngx_list_create创建元素时,pool参数是内存池对象,size是每个元素的大小,n是每个链表数组可容纳元素的个数(相当于ngx_list_t结构中的nalloc成员)。

ngx_list_create返回新创建的链表地址,如果创建失败,则返回NULL空指针。ngx_list_create被调用后至少会创建一个数组(不会创建空链表),其中包含n个大小为size字节的连续内存块,也就是ngx_list_t结构中的part成员。

下面看一个简单的例子,我们首先建立一个链表,它存储的元素是ngx_str_t,其中每个链表数组中存储4个元素,代码如下所示:

ngx_list_t* testlist = ngx_list_create(r->pool, 4,sizeof(ngx_str_t)); if (testlist == NULL) { 
return NGX_ERROR;
}

ngx_list_init的使用方法与ngx_list_create非常类似,需要注意的是,这时链表数据结构已经创建好了,若ngx_list_init返回NGX_OK,则表示初始化成功,若返回NGX_ERROR,则表示失败。

调用ngx_list_push表示添加新的元素,传入的参数是ngx_list_t链表。正常情况下,返回的是新分配的元素首地址。如果返回NULL空指针,则表示添加失败。在使用它时通常先调用ngx_list_push得到返回的元素地址,再对返回的地址进行赋值。例如:

ngx_str_t* str = ngx_list_push(testlist); if (str == NULL) { 
return NGX_ERROR;
}
str->len= sizeof("Hello world");
str->data = "Hello world";

遍历链表时Nginx没有提供相应的接口,实际上也不需要。我们可以用以下方法遍历链表中的元素:

// part用于指向链表中的每一个 

ngx_list_part_t数组

ngx_list_part_t* part = &testlist.part; // 根据链表中的数据类型,把数组里的
elts转化为该类型使用

ngx_str_t* str = part->elts;

// i表示元素在链表的每个

ngx_list_part_t数组里的序号

for (i = 0; /* void */; i++) {

if (i >= part->nelts) {

if (part->next == NULL) {

// 如果某个

ngx_list_part_t数组的

next指针为空,

// 则说明已经遍历完链表

break;

}

// 访问下一个

ngx_list_part_t

part = part->next;

str = part->elts;

// 将

i序号置为

0,准备重新访问下一个数组

i = 0;

}

// 这里可以很方便地取到当前遍历到的链表元素

printf("list element: %*sn",str[i].len, str[i].data); }

4 .ngx_table_elt_t数据结构

ngx_table_elt_t数据结构如下所示:

typedef struct { 

ngx_uint_t hash;

ngx_str_t key;

ngx_str_t value;

u_char *lowcase_key;

} ngx_table_elt_t;

可以看到,ngx_table_elt_t就是一个key/value对,ngx_str_t类型的key、value成员分别存储的是名字、值字符串。hash成员表明ngx_table_elt_t也可以是某个散列表数据结构(ngx_hash_t类型)中的成员。ngx_uint_t类型的hash成员可以在ngx_hash_t中更快地找到相同key的ngx_table_elt_t数据。lowcase_key指向的是全小写的key字符串。

显而易见,ngx_table_elt_t是为HTTP头部“量身订制”的,其中key存储头部名称(如Content-Length),value存储对应的值(如“1024”),lowcase_key是为了忽略HTTP头部名称的大小写(例如,有些客户端发来的HTTP请求头部是content-length,Nginx希望它与大小写敏感的Content-Length做相同处理,有了全小写的lowcase_key成员后就可以快速达成目的了),hash用于快速检索头部。

5. ngx_buf_t数据结构

缓冲区ngx_buf_t是Nginx处理大数据的关键数据结构,它既应用于内存数据也应用于磁盘数据。下面主要介绍ngx_buf_t结构体本身,下面来看一下相关代码:

typedef struct ngx_buf_s ngx_buf_t; typedef void * ngx_buf_tag_t; struct ngx_buf_s { 
/*pos通常是用来告诉使用者本次应该从
pos这个位置开始处理内存中的数据,这样设置是因为同一个ngx_buf_t可能被多次反复处理。当然,
pos的含义是由使用它的模块定义的
*/
u_char *pos;
/*last通常表示有效的内容到此为止,注意,
pos与
last之间的内存是希望
nginx处理的内容
*/
u_char *last;
/*处理文件时,
file_pos与
file_last的含义与处理内存时的
pos与last相同,
file_pos表示将要处理的文件位置,
file_last表示截止的文件位置
*/
off_t file_pos;
off_t file_last;
// 如果
ngx_buf_t缓冲区用于内存,那么
start指向这段内存的起始地址
u_char *start;
// 与
start成员对应,指向缓冲区内存的末尾u_char *end;
/*表示当前缓冲区的类型,例如由哪个模块使用就指向这个模块
ngx_module_t变量的地址
*/
ngx_buf_tag_t tag;
// 引用的文件
ngx_file_t *file;
/*当前缓冲区的影子缓冲区,该成员很少用到,仅仅在
12.8节描述的使用缓冲区转发上游服务器的响应时才使用了
shadow成员,这是因为
Nginx太节约内存了,分配一块内存并使用
ngx_buf_t表示接收到的上游服务器响应后,在向下游客户端转发时可能会把这块内存存储到文件中,也可能直接向下游发送,此时
Nginx绝不会重新复制一份内存用于新的目的,而是再次建立一个ngx_buf_t结构体指向原内存,这样多个
ngx_buf_t结构体指向了同一块内存,它们之间的关系就通过
shadow成员来引用。这种设计过于复杂,通常不建议使用
*/
ngx_buf_t *shadow;
// 临时内存标志位,为
1时表示数据在内存中且这段内存可以修改
unsigned temporary:1;
// 标志位,为
1时表示数据在内存中且这段内存不可以被修改
unsigned memory:1;
// 标志位,为
1时表示这段内存是用mmap系统调用映射过来的,不可以被修改
unsigned mmap:1;
// 标志位,为
1时表示可回收
unsigned recycled:1;
// 标志位,为
1时表示这段缓冲区处理的是文件而不是内存
unsigned in_file:1;
// 标志位,为
1时表示需要执行
flush操作unsigned flush:1;
/*标志位,对于操作这块缓冲区时是否使用同步方式,需谨慎考虑,这可能会阻塞
Nginx进程,
Nginx中所有操作几乎都是异步的,这是它支持高并发的关键。有些框架代码在
sync为
1时可能会有阻塞的方式进行
I/O操作,它的意义视使用它的
Nginx模块而定
*/
unsigned sync:1;
/*标志位,表示是否是最后一块缓冲区,因为
ngx_buf_t可以由ngx_chain_t链表串联起来,因此,当
last_buf为
1时,表示当前是最后一块待处理的缓冲区
*/
unsigned last_buf:1;
// 标志位,表示是否是
ngx_chain_t中的最后一块缓冲区
unsigned last_in_chain:1;
/*标志位,表示是否是最后一个影子缓冲区,与
shadow域配合使用。通常不建议使用它
*/
unsigned last_shadow:1;
// 标志位,表示当前缓冲区是否属于临时文件
unsigned temp_file:1;};

ngx_buf_t是一种基本数据结构,本质上它提供的仅仅是一些指针成员和标志位。对于HTTP模块来说,需要注意HTTP框架、事件框架是如何设置和使用pos、last等指针以及如何处理这些标志位的,上述说明只是最常见的用法。(如果我们自定义一个ngx_buf_t结构体,不应当受限于上述用法,而应该根据业务需求自行定义)

6.ngx_chain_t数据结构

ngx_chain_t是与ngx_buf_t配合使用的链表数据结构,下面看一下它的定义:

typedef struct ngx_chain_s ngx_chain_t; struct ngx_chain_s { 
ngx_buf_t *buf;
ngx_chain_t *next;
};

buf指向当前的ngx_buf_t缓冲区,next则用来指向下一个ngx_chain_t。如果这是最后一个ngx_chain_t,则需要把next置为NULL。

在向用户发送HTTP包体时,就要传入ngx_chain_t链表对象,注意,如果是最后一个ngx_chain_t,那么必须将next置为NULL,否则永远不会发送成功,而且这个请求将一直不会结束(Nginx框架的要求)。

内容过多已经整理在PDF,需要的私信《PDF》 喜欢的记得关注收藏,感谢你的阅读支持。

本文出处: https://www.toutiao.com/a6731962402004288014/
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系 [邮箱地址] 删除

路过

雷人

握手

鲜花

鸡蛋

最新评论

返回顶部