简介
NGINX 开发指南(Part 1)
本文档是nginx官方文档“Developer Guide”(https://nginx.org/en/docs/dev/development_guide.html)的中文版本,由白山云科技(http://www.baishancloud.com)NGINX开发团队负责翻译。官方文档是HTML页面发布的,我们翻译的时候转成了Markdown,以方便编辑。同时也一并保留了英文的Markdown版本:https://github.com/baishancloud/nginx-development-guide/blob/master/en.md。希望此中文版文档能为广大的nginx以及开源爱好者提供入门指导,开发出优秀的nginx模块,回馈社区。本文的官方版本并没有全部完成,依然处于活跃更新的状态,中文版本会持续保持跟踪并持续更新。
代码结构
auto — 编译脚本
src
unix
win32
modules — 其他HTTP模块
v2 — HTTP/2模块
modules — 具体事件机制模块:epoll,kqueue,select等
core — 基础数据结构和函数 — 字符串,数组,日志,内存池等
event — 事件机制核心模块
http — HTTP核心模块和公共代码
mail — 邮件协议模块
os — 平台相关代码
stream — 流模块
头文件
每个nginx文件都应该在开头包含如下两个头文件:
除此之外,HTTP相关的代码还要包含:
邮件模块的代码应该包含:
Stream模块的代码应该包含:
整数
一般情况下,nginx代码使用如下两个整数类型:ngx_int_t 和 ngx_uint_t,分别用typedef定义成了intptr_t 和 uintptr_t。
常用返回值
nginx中的大多数函数使用如下类型的返回值:
NGX_OK — 处理成功
NGX_ERROR — 处理失败
NGX_AGAIN — 处理未完成,函数需要被再次调用
NGX_DECLINED — 处理被拒绝,例如相关功能在配置文件中被关闭。不要将此当成错误。
NGX_BUSY — 资源不可用
NGX_DONE — 处理完成或者在他处继续处理。也可以作为处理成功使用。
NGX_ABORT — 函数终止。也可以作为处理出错的返回值。
错误处理
为了获取最近一次系统错误码,nginx提供了ngx_errno宏。该宏被映射到了POSIX平台的errno变量上,而在Windows平台中,则变为对GetLastError()的函数调用。为了获取最近一次socket错误码,nginx提供了ngx_socket_errno宏。同样,在POSIX平台上该宏被映射为errno变量,而在Windows环境中则是对WSAGetLastError()进行调用。考虑到对性能的影响,ngx_errno和ngx_socket_errno不应该被连续访问。如果有连续、频繁访问的需要,则应该将错误码的值存储到类型为ngx_err_t的本地变量中,然后使用本地变量进行访问。如果需要设置错误码,可以使用ngx_set_errno(errno)和ngx_set_socket_errno(errno)这两个宏。
ngx_errno和ngx_socket_errno变量可以在调用日志相关函数ngx_log_error()和ngx_log_debugX()的时候使用,这样具体的错误文本就会被添加到日志输出中。
一个使用ngx_errno的例子:
字符串
概述
nginx使用无符号的char类型指针来表示C字符串:u_char *。
nginx字符串类型ngx_str_t的定义如下所示:
结构体成员len存放字符串的长度,成员data指向字符串本身数据。在ngx_str_t中存放的字符串,对于超出len长度的部分可以是NULL结尾('\0'——译者注),也可以不是。在大多数情况是不以NULL结尾的。然而,在nginx的某些代码中(例如解析配置的时候),ngx_str_t中的字符串是以NULL结尾的,这种情况会使得字符串比较变得更加简单,也使得使用系统调用的时候更加容易。
nginx提供了一系列关于字符串处理的函数。它们在src/core/ngx_string.h文件中定义。其中的一部分就是对C库中字符串函数的封装:
ngx_strcmp()
ngx_strncmp()
ngx_strstr()
ngx_strlen()
ngx_strchr()
ngx_memcmp()
ngx_memset()
ngx_memcpy()
ngx_memmove()
还有一些nginx特有的字符串函数:
ngx_memzero() 内存清0
ngx_cpymem() 和ngx_memcpy()行为类似,不同的是该函数返回的是copy后的最终目的地址,这在需要连续拼接多个字符串的场景下很方便。
ngx_movemem() 和ngx_memmove()的行为类似,不同的是该函数返回的是move后的最终目的地址。
ngx_strlchr() 在字符串中查找一个特定字符,字符串由两个指针界定。
最后是一些大小写转换和字符串比较的函数:
ngx_tolower()
ngx_toupper()
ngx_strlow()
ngx_strcasecmp()
ngx_strncasecmp()
格式化
nginx提供了一些格式化字符串的函数。以下这些函数支持nginx特有的类型:
ngx_sprintf(buf, fmt, ...)
ngx_snprintf(buf, max, fmt, ...)
ngx_slpintf(buf, last, fmt, ...)
ngx_vslprint(buf, last, fmt, args)
ngx_vsnprint(buf, max, fmt, args)
这些函数支持的全部格式化选项定义在src/core/ngx_string.c文件中,以下是其中的一部分:
'u'修饰符将类型指明为无符号,'X'和'x'则将输出转换为16进制。
例如:
数值转换
nginx实现了若干用于数值转换的函数:
ngx_atoi(line, n) — 将一个指定长度的字符串转换为一个正整数,类型为ngx_int_t。出错返回NGX_ERROR。
ngx_atosz(line, n) — 同上,转换类型为ssize_t
ngx_atoof(line, n) — 同上,转换类型为off_t
ngx_atotm(line, n) — 同上,转换类型为time_t
ngx_atofp(line, n, point) — 将一个固定长度的定点小数字符串转换为ngx_int_t类型的正整数。转换结果会左移point指定的10进制位数。字符串中的定点小数不能含有多过point参数指定的小数位。出错返回NGX_ERROR。举例:ngx_atofp("10.5", 4, 2) 返回1050
ngx_hextoi(line, n) — 将表示16进制正整数的字符串转换为ngx_int_t类型的整数。出错返回NGX_ERROR。
正则表达式
nginx中的正则表达式接口是对PCRE库的封装。相关的头文件是src/core/ngx_regex.h。
要使用正则表达式进行字符串匹配,首先需要对正则表达式进行编译,这通常是在配置解析阶段处理的。需要注意的是,因为PCRE的支持是可选的,因此所有使用正则相关接口的代码都需要用NGX_PCRE括起来:
编译成功之后,结构体ngx_regex_compile_t的captures和named_captures成员分别会被填上正则表达式中全部以及命名捕获的数量。
然后,编译过的正则表达式就可以用来进行字符串匹配:
ngx_regex_exec()的参数有:编译了的正则表达式re,待匹配的字符串s,可选的用于存放发现的捕获和其大小的整数数组。捕获数组的大小必须是3的倍数,这是PCRE库的API要求的。在上面例子中,该数组的大小是通过总捕获数加上字符串自身来计算得出的。
现在,如果成功匹配,则可以对捕获进行访问:
ngx_regex_exec_array()函数接受ngx_regex_elt_t元素的数组(其实就是多个编译好的正则表达式以及对应的名字),一个待匹配字符串以及一个log。该函数会对待匹配字符串逐一应用数组中的正则表达式,直到匹配成功或者无一匹配。存在成功的匹配则返回NGX_OK,否则返回NGX_DECLINED,出错返回NGX_ERROR。
时间
结构体 ngx_time_t 将GMT格式的时间表示分割成秒和毫秒:
ngx_tm_t 是 struct tm 的一个别名,用在 UNIX 平台和Windows上的SYSTEMTIME。
为了获取当前时间,通常只需要访问一个可用的全局变量,表示所需格式的缓存时间值。ngx_current_msec 变量保存着自Epoch以来的毫秒数,并截成ngx_msec_t。
以下是可用的字符串表示:
ngx_cached_err_log_time — 用在 error log: "1970/09/28 12:00:00"
ngx_cached_http_log_time — 用在 HTTP access log: "28/Sep/1970:12:00:00 +0600"
ngx_cached_syslog_time — 用在 syslog: "Sep 28 12:00:00"
ngx_cached_http_time — 用在 HTTP headers: "Mon, 28 Sep 1970 06:00:00 GMT"
ngx_cached_http_log_iso8601 — ISO 8601 标准格式: "1970-09-28T12:00:00+06:00"
宏 ngx_time() 和 ngx_timeofday() 返回当前时间的秒,是访问缓存时间值的首选方式。
为了明确获取时间,可以使用ngx_gettimeofday(),它会更新参数(指向struct timeval)。当nginx从系统调用回到事件循环体时,时间总是会更新。如果想立即更新时间,调用 ngx_time_update() 或 ngx_time_sigsafe_up date() (如果在信号处理上下文需要用到)。
以下函数将 time_t 转换成可分解的时间表示形式,对于libc前缀的那些,可以使用 ngx_tm_t 或者 struct tm。
ngx_gmtime(), ngx_libc_gmtime() — 结果时间是 UTC
ngx_localtime(), ngx_libc_localtime() — 结果时间是相对时区
ngx_http_time(buf, time) 返回用于适合 HTTP headers(比如 "Mon, 28 Sep 1970 06:00:00 GMT")的字符串表示。另一种可能转变通过 ngx_http_cookie_time(buf, time) 提供,用于生成适合HTTP cookies ("Thu, 3 1-Dec-37 23:55:55 GMT") 的格式。
容器
数组
表示nginx数组(array)的结构体ngx_array_t定义如下:
数组的元素可以通过elts成员获取。元素的个数存放在nelts成员里。size成员记录单个元素的大小,size成员是在数组初始化的时候设置的。
数组可以使用调用ngx_array_create(pool, n, size)来创建,其所需内存在提供的pool中。一个已经分配过内存的数组对象,可以调用ngx_array_init(array, pool, n, size)进行初始化。
使用下面的函数向数组添加元素:
ngx_array_push(a) 向数组末尾添加一个元素并返回其指针
ngx_array_push_n(a, n) 向数组末尾添加n个元素并返回指向其中第一个元素的指针
如果现有内存无法满足新元素的需要,数组会分配新的内存并将现有元素复制过去。新分配的内存一般是原有内存的2倍大。
列表
nginx中的列表(List)由一系列的数组组成,并为可能插入大量item进行了优化。列表类型定义如下:
实际的item存放在列表部件结构中,定义如下:
使用之前,列表必须通过ngx_list_init(list, pool, n, size)初始化,或者通过ngx_list_create(pool, n, size)创建。两个方式都需要指定单一条目的大小以及每个列表部件中item的数量。ngx_list_push(list)函数用来向列表添加一个item。遍历item是通过直接访问列表成员实现的,参考以下示例:
nginx中列表的主要用途是处理HTTP中输入和输出的头部。
列表不支持删除item。然而,如果需要的话,可以将item标识成missing而不是真正的删除他们。例如,HTTP的输出头部——以ngx_table_elt_t对象存储——可以通过将ngx_table_elt_t结构的hash成员设置成0来将其标识为missing。这样一来,该HTTP头部就不会被遍历到。
队列
nginx里的队列是一个双向链表,每个节点定义如下:
头部队列节点没有连接任何数据。使用之前,列表头部要先调用 ngx_queue_init(q) 以初始化。队列支持如下操作:
ngx_queue_insert_head(h, x), ngx_queue_insert_tail(h, x) — 插入新节点
ngx_queue_remove(x) — 删除队列节点
ngx_queue_split(h, q, n) — 从a节点切割,队列尾部起将变成新的独立的队列
ngx_queue_add(h, n) — 将队列n加到队列h
ngx_queue_head(h), ngx_queue_last(h) — 返回首或尾队列节点
ngx_queue_sentinel(h) - 返回队列哨兵用来结束迭代
ngx_queue_data(q, type, link) — 返回指向queue的data字段的起始地址,根据它的queue字段的偏移量
例子:
红黑树
头文件 src/core/ngx_rbtree.h 提供了访问红黑树的定义。
为了处理整个树,需要两个节点:root 和 sentinel。通常他们被添加到某些自定义的结构中,这样就能将数据组织到树中,其中包含指向数据的边接。
初始化树:
insert_value_function是一个负责遍历树并将新值插入正确位置的函数。 例如,ngx_str_rbtree_insert_value函数旨在处理ngx_str_t类型。
第一个参数是树中插入的节点,第二个是新创建的用来添加的节点,最后一个是树的sentinel。
遍历非常简单明了,用下面的轮询函数模式作为演示。
compare() 是一个返回较小,相等或较大的经典函数。为了更快的查找,并且避免比较太大的对象,整型的hash字段就派上用场了。
为了添加节点到树,需要分配新节点,初始化它,然后调用 ngx_rbtree_insert():
删除节点:
哈希
哈希表定义在 src/core/ngx_hash.h,支持精确和通配符匹配。后者需要额外的处理,放在下面的章节专门描述。
初始化哈希时,我们需要提前知道元素的个数,以便nginx能更好的优化哈希表。max_size 和 bucket_size 这两参数需要配置。细节详见官方提供的文档。通常这两参数会做成用户可配置的。哈希初始化的设置放在ngx_hash_init_t类型的存储中。而哈希表本身的类型是 ngx_hash_t。
key是一个指向能根据字符串创建整型的函数的指针。nginx提供了两个通用的函数:ngx_hash_key(data, len) 和 ngx_hash_key_lc(data, len)。后者将字符串转为小写,这需要这个字符串是可写的。如果不想这样,NGX_HASH_READONLY_KEY 标记可以传给这个函数,然后初始化数组键(见下文)。
哈希keys保存在ngx_hash_keys_arrays_t里,然后通过 ngx_hash_keys_array_init(arr, type) 初始化。
第二个参数可以是NGX_HASH_SMALL或者NGX_HASH_LARGE,用于控制哈希表的预分配。如果你想hash包含更多的无素,请用NGX_HASH_LARGE。
ngx_hash_add_key(keys_array, key, value, flags) 函数用于将key添加到hash keys array:
现在就可能通过调用 ngx_hash_init(hinit, key_names, nelts) 来完成hash表的创建:
这样是有可能错误的,如果max_size或者bucket_size不足够大的话。当hash创建了之后, ngx_hash_find(hash, key, name, len) 函数可用来查找无素:
通配符匹配
为了创建能运行通配符的hash,需要用 ngx_hash_combined_t 类型。它包含了上面提到的hash类型,还有两个额外的keys arrays:dns_wc_head 和 dns_wc_tail。它的基本的初始化类似于普通hash。
可以使用 NGX_HASH_WILDCARD_KEY 标记来添加通配符的key。
这个函数重新组织通配符和添加keys到对应的数组。详细用法和匹配算法参考map模块。
根据添加keys的内容,你可能需要初始化三个keys arrays:一个用于前面提到的精确数组,另外两个用于从头或尾的模糊匹配:
keys 数组需要先排序,然后初始化后的结果必须添加到合并hash。dns_wc_tail 也是类似的操作。
查找合并hash通过 ngx_hash_find_combined(chash, key, name, len):
内存管理
堆
nginx提供以下的函数用于从系统堆分配内存:
ngx_alloc(size, log) — 从系统堆分配内存。这个封装了malloc(),并且带有log。分配错误和调试信息都会记录到log。
ngx_calloc(size, log) — 和 ngx_alloc() 一样,但是将分配后的内存填充为0。
ngx_memalign(alignment, size, log) — 从系统堆分配可对齐的内存。如果平台提供了posix_memalign(),就用它做为封装。否则返回调用传递最大对齐值参数的ngx_alloc()。
ngx_free(p) — 释放内存。这是free()的封装。
内存池
大部份nginx分配使用内存池完成。在内存池分配的内存会在内存池销毁时自动释放。这样就提供了更好的分配性能,并且控制内存变的更简单。
内存池是通过在内部连续的内存块分配对象的。当一个块满时,新的块会被分配并且加入到该池的内存块列表。当块装不了一个大的分配时,分配会交给系统,然后返回指向存到该内存池,以后以后释放。
nginx 内存池类型为 ngx_pool_t。支持以下操作:
ngx_create_pool(size, log) — 根据块大小创建内存池。返回pool对象也是在里面内存池里创建的。
ngx_destroy_pool(pool) — 销毁整个内存池,包括pool对象自己。
ngx_palloc(pool, size) — 从内存池分配对齐的内存。
ngx_pcalloc(pool, size) — 从内存池分配对齐的内存并且置为0。
ngx_pnalloc(pool, size) — 从内存池分配没对齐的内存。大部份用于分配字符串。
ngx_pfree(pool, p) — 释放前面内存池中分配的内存。只对那些由系统分配的内存才会释放。
因为链 ngx_chain_t 在nginx经常使用,所以nginx内存池提供了一种方式来复用它们。ngx_pool_t 的 chain 字段保留了原先已经分配的列表用来复用。 为了有效分配内存池中的chain,应当使用 ngx_alloc_chain_link(pool) 函数。该函数查找内存池中空闲的chain,只有当为空时才分配一个新的。使用ngx_free_chain(pool, cl) 可以回收chain。
cleanup handler可以注册在pool里。cleanup handler 是一个带有参数的回调,在内存池销毁时调用。内存池通常在特定的nginx对象(比如HTTP请求),并且在对象的生命周期结束时销毁,以释放对象自己。注册内存池cleanup可以方便地释放资源,关闭文件描述符,或者做最后的关联在对象上的数据的调整。
通过调用ngx_pool_cleanup_add(pool, size)注册pool cleanup,它将返回 ngx_pool_cleanup_t 类型的指针,调用者会设置它。size 参数用分配cleanup上下文的大小。
共享内存
nginx用共享内存在进程之间共享公共的数据。函数 ngx_shared_memory_add(cf, name, size, tag) 添加新的共享内存实体到cycle。该函数接收 name 和 zone的大小。每个共享内存必须有唯一的名称。如果提供的名称存在,并且tag值也匹配,则会复用旧的zone实体。tag不匹配会被认为错误。通常模块地址会被当作tag的值,这样在模块里就能通过name来复用共享内存。
以下是 ngx_shm_zone_t 的字段:
init — 初始化回调函数,在实际的共享内存映射后调用。
data — data 上下文,传递给初始化回调函数。
noreuse — 村记。禁止复用从旧的cycle里的共享内存。
tag — 共享内存tag。
shm — 类型为 ngx_shm_t 的特定平台对象,有以下几个字段:
addr — 映射的共享内存地址,初始为NULL
size — 共享内存大小
name — 共享内存名称
log — 共享内存log
exists — 标记。表示共享内存继承自主进程 (Windows特定)
共享内存zone实体会在ngx_init_cycle()解析配置后在映射到实际的内存。对POSIX系统,mmap() 系统调用用来创建匿名共享映射。对Windows,使用CreateFileMapping()/MapViewOfFileEx()对。
nginx提供了 ngx_slab_pool_t 来分配共享内存。对每个zone,slab pool会自动创建用来分配内存。这个池在共享zone的开头,并且通过表达式 (ngx_slab_pool_t *) shm_zone->shm.addr 访问。共享内存的分配通过调用 ngx_slab_alloc(pool, size)/ngx_slab_c alloc(pool, size) 函数完成,内存通过调用 ngx_slab_free(pool, p) 释放。
slab pool 将共享zone分成多个页。每个页被用于分配同样大小的对象。大小推荐为2的次方,并且不小于8。其它值被四舍五入。对每个页,bitmask被用来表示哪些块是已经使用的和哪些是空闲的。对大小超过半页(通常是2048字节),将按完整的页大小分配。
为了保护数据不会并发访问,需要有 ngx_slab_pool_t 的 mutex 字段。mutex 在分配和释放内存里被使用。然后它也可以用来保护其它分配自共享内存的数据。调用 ngx_shmtx_lock(&shpool->mutex) 锁住,调用 ngx_shmtx_unlock(&shpool->mutex) 解锁。
后续章节请点击“阅读原文”获取或留意本公众号推送。
HP-Socket:高性能 TCP/UDP/HTTP 通信框架 | 软件推介