本文档是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模块,回馈社区。本文的官方版本并没有全部完成,依然处于活跃更新的状态,中文版本会持续保持跟踪并持续更新。
每个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)进行初始化。
使用下面的函数向数组添加元素:
如果现有内存无法满足新元素的需要,数组会分配新的内存并将现有元素复制过去。新分配的内存一般是原有内存的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):