深入理解Nginx:模块开发与架构解析(第2版)
上QQ阅读APP看书,第一时间看更新

3.2 准备工作

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(3.3节会探讨编译新增模块的两种方式)。其次需要设定模块如何在运行中生效,比如在图3-1描述的典型方式中,配置文件中的location块决定了匹配某种URI的请求将会由相应的HTTP模块处理,因此,运行时HTTP框架会在接收完毕HTTP请求的头部后,将请求的URI与配置文件中的所有location进行匹配(事实上会优先匹配虚拟主机,第11章会详细说明该流程),匹配后再根据location{}内的配置项选择HTTP模块来调用。这是一种最典型的HTTP模块调用方式。3.4节将解释HTTP模块定义嵌入方式时用到的数据结构,3.5节将定义我们的第一个HTTP模块,3.6节中介绍如何使用上述模块调用方式来处理请求。

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

为了做到跨平台,Nginx定义、封装了一些基本的数据结构。由于Nginx对内存分配比较“吝啬”(只有保证低内存消耗,才可能实现十万甚至百万级别的同时并发连接数),所以这些Nginx数据结构天生都是尽可能少占用内存。下面介绍本章中将要用到的Nginx定义的几个基本数据结构和方法,在第7章还会介绍一些复杂的容器,读者可以从中体会到如何才能有效地利用内存。

3.2.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;

3.2.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成员。例如,在3.7.2节中,我们会看到r->method_name就是一个ngx_str_t类型的变量,比较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.1\r\n”存储到内存地址0x1d0b0110上,这时只需要把r->method_name设置为{len=3,data=0x1d0b0110}就可以表示方法名“GET”,而不需要单独为method_name再分配内存冗余的存储字符串。

3.2.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内存池分配的情况下)。下面以图3-2为例来看一下ngx_list_t的内存分布情况。

图3-2 ngx_list_t的内存分布

图3-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参数是内存池对象(参见3.7.2节),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: %*s\n",str[i].len, str[i].data);
}

3.2.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用于快速检索头部(它的用法在3.6.3节中进行详述)。

3.2.5 ngx_buf_t数据结构

缓冲区ngx_buf_t是Nginx处理大数据的关键数据结构,它既应用于内存数据也应用于磁盘数据。下面主要介绍ngx_buf_t结构体本身,而描述磁盘文件的ngx_file_t结构体则在3.8.1节中说明。下面来看一下相关代码:

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的案例参见3.7.2节。ngx_buf_t是一种基本数据结构,本质上它提供的仅仅是一些指针成员和标志位。对于HTTP模块来说,需要注意HTTP框架、事件框架是如何设置和使用pos、last等指针以及如何处理这些标志位的,上述说明只是最常见的用法。(如果我们自定义一个ngx_buf_t结构体,不应当受限于上述用法,而应该根据业务需求自行定义。例如,在13.7节中用一个ngx_buf_t缓冲区转发上下游TCP流时,pos会指向将要发送到下游的TCP流起始地址,而last会指向预备接收上游TCP流的缓冲区起始地址。)

3.2.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框架的要求)。