| 关键词: 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 { ngx_str_t只有两个成员,其中data指针指向字符串起始地址,len表示字符串的有效长度。注意,ngx_str_t的data成员指向的并不是普通的字符串,因为这段字符串未必会以'0'作为结尾,所以使用时必须根据长度len来使用data成员。比较method_name时必须如下这样使用: if (0 == ngx_strncmp( 这里,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 { 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数组元素的个数与每个数组所能容纳的元素相乘得到。 这样设计有什么好处呢?
下面详述每个成员的意义。 (1)ngx_list_t
(2)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_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) { 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) { 遍历链表时Nginx没有提供相应的接口,实际上也不需要。我们可以用以下方法遍历链表中的元素: // part用于指向链表中的每一个 4 .ngx_table_elt_t数据结构 ngx_table_elt_t数据结构如下所示: typedef struct { 可以看到,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 { 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 { buf指向当前的ngx_buf_t缓冲区,next则用来指向下一个ngx_chain_t。如果这是最后一个ngx_chain_t,则需要把next置为NULL。 在向用户发送HTTP包体时,就要传入ngx_chain_t链表对象,注意,如果是最后一个ngx_chain_t,那么必须将next置为NULL,否则永远不会发送成功,而且这个请求将一直不会结束(Nginx框架的要求)。
|
| 本文出处: https://www.toutiao.com/a6731962402004288014/ |
|
声明:文章版权归原作者所有 部分文章转自互联网 如有侵权请联系
[邮箱地址] 删除
|