开发指南
介绍
代码布局
-
auto
— 构建脚本 -
src
-
core
— 基本类型和函数 — string, array, log, pool 等 -
event
— 事件核心-
modules
— 事件通知模块:epoll
,kqueue
,select
等
-
-
http
— 核心 HTTP 模块和通用代码-
modules
— 其他 HTTP 模块 -
v2
— HTTP/2
-
-
mail
— Mail 模块 -
os
— 平台特定代码-
unix
-
win32
-
-
stream
— Stream 模块
-
包含文件
以下两个 #include
语句必须出现在每个 nginx 文件的开头
#include <ngx_config.h> #include <ngx_core.h>
除此之外,HTTP 代码应该包含
#include <ngx_http.h>
Mail 代码应该包含
#include <ngx_mail.h>
Stream 代码应该包含
#include <ngx_stream.h>
整型
通常,nginx 代码使用两种整型:ngx_int_t
和 ngx_uint_t
,它们分别是 intptr_t
和 uintptr_t
的 typedef。
常见返回码
nginx 中的大多数函数返回以下代码
-
NGX_OK
— 操作成功。 -
NGX_ERROR
— 操作失败。 -
NGX_AGAIN
— 操作未完成;再次调用该函数。 -
NGX_DECLINED
— 操作被拒绝,例如,因为在配置中已禁用。这绝不是一个错误。 -
NGX_BUSY
— 资源不可用。 -
NGX_DONE
— 操作完成或在其他地方继续。也用作替代的成功代码。 -
NGX_ABORT
— 函数已中止。也用作替代的错误代码。
错误处理
ngx_errno
宏返回最后一个系统错误码。在 POSIX 平台上它映射到 errno
,在 Windows 上映射到 GetLastError()
调用。ngx_socket_errno
宏返回最后一个套接字错误号。与 ngx_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
的示例
ngx_int_t ngx_my_kill(ngx_pid_t pid, ngx_log_t *log, int signo) { ngx_err_t err; if (kill(pid, signo) == -1) { err = ngx_errno; ngx_log_error(NGX_LOG_ALERT, log, err, "kill(%P, %d) failed", pid, signo); if (err == NGX_ESRCH) { return 2; } return 1; } return 0; }
字符串
概述
对于 C 字符串,nginx 使用无符号字符类型指针 u_char *
。
nginx 字符串类型 ngx_str_t
定义如下
typedef struct { size_t len; u_char *data; } ngx_str_t;
len
字段保存字符串长度,data
字段保存字符串数据。存储在 ngx_str_t
中的字符串可能在 len
字节后以 null 结尾,也可能不以 null 结尾。在大多数情况下不会。然而,在代码的某些部分(例如,解析配置时),已知 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()
— 用零填充内存。 -
ngx_explicit_memzero()
— 功能与ngx_memzero()
相同,但此调用不会被编译器的死存储消除优化移除。此函数可用于清除敏感数据,如密码和密钥。 -
ngx_cpymem()
— 功能与ngx_memcpy()
相同,但返回最终目标地址。这对于连续附加多个字符串很方便。 -
ngx_movemem()
— 功能与ngx_memmove()
相同,但返回最终目标地址。 -
ngx_strlchr()
— 在由两个指针限定的字符串中搜索字符。
以下函数执行大小写转换和比较
-
ngx_tolower()
-
ngx_toupper()
-
ngx_strlow()
-
ngx_strcasecmp()
-
ngx_strncasecmp()
以下宏简化了字符串初始化
-
ngx_string(text)
— 用于从 C 字符串字面量text
初始化ngx_str_t
类型的静态初始化器 -
ngx_null_string
— 用于初始化ngx_str_t
类型的静态空字符串初始化器 -
ngx_str_set(str, text)
— 使用 C 字符串字面量text
初始化ngx_str_t *
类型的字符串str
-
ngx_str_null(str)
— 使用空字符串初始化ngx_str_t *
类型的字符串str
格式化
以下格式化函数支持 nginx 特定类型
-
ngx_sprintf(buf, fmt, ...)
-
ngx_snprintf(buf, max, fmt, ...)
-
ngx_slprintf(buf, last, fmt, ...)
-
ngx_vslprintf(buf, last, fmt, args)
-
ngx_vsnprintf(buf, max, fmt, args)
这些函数支持的完整格式化选项列表在 src/core/ngx_string.c
中。其中一些是
-
%O
—off_t
-
%T
—time_t
-
%z
—ssize_t
-
%i
—ngx_int_t
-
%p
—void *
-
%V
—ngx_str_t *
-
%s
—u_char *
(以 null 结尾) -
%*s
—size_t + u_char *
可以在大多数类型前加上 u
使其成为无符号类型。要将输出转换为十六进制,请使用 X
或 x
。
例如
u_char buf[NGX_INT_T_LEN]; size_t len; ngx_uint_t n; /* set n here */ len = ngx_sprintf(buf, "%ui", n) — buf;
数值转换
nginx 中实现了几个用于数值转换的函数。前四个函数各自将给定长度的字符串转换为指定类型的正整数。错误时返回 NGX_ERROR
。
-
ngx_atoi(line, n)
—ngx_int_t
-
ngx_atosz(line, n)
—ssize_t
-
ngx_atoof(line, n)
—off_t
-
ngx_atotm(line, n)
—time_t
还有两个额外的数值转换函数。与前四个类似,它们在错误时返回 NGX_ERROR
。
-
ngx_atofp(line, n, point)
— 将给定长度的定点浮点数转换为ngx_int_t
类型的正整数。结果向左移位point
个小数位。数字的字符串表示预期小数点后不超过points
位。例如,ngx_atofp("10.5", 4, 2)
返回1050
。 -
ngx_hextoi(line, n)
— 将正整数的十六进制表示转换为ngx_int_t
。
正则表达式
nginx 中的正则表达式接口是 PCRE 库的包装。相应的头文件是 src/core/ngx_regex.h
。
要使用正则表达式进行字符串匹配,首先需要对其进行编译,这通常在配置阶段完成。请注意,由于 PCRE 支持是可选的,所有使用该接口的代码都必须被外围的 NGX_PCRE
宏保护
#if (NGX_PCRE) ngx_regex_t *re; ngx_regex_compile_t rc; u_char errstr[NGX_MAX_CONF_ERRSTR]; ngx_str_t value = ngx_string("message (\\d\\d\\d).*Codeword is '(?<cw>\\w+)'"); ngx_memzero(&rc, sizeof(ngx_regex_compile_t)); rc.pattern = value; rc.pool = cf->pool; rc.err.len = NGX_MAX_CONF_ERRSTR; rc.err.data = errstr; /* rc.options can be set to NGX_REGEX_CASELESS */ if (ngx_regex_compile(&rc) != NGX_OK) { ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, "%V", &rc.err); return NGX_CONF_ERROR; } re = rc.regex; #endif
编译成功后,ngx_regex_compile_t
结构中的 captures
和 named_captures
字段分别包含正则表达式中找到的所有捕获和命名捕获的数量。
编译后的正则表达式可以用于与字符串进行匹配
ngx_int_t n; int captures[(1 + rc.captures) * 3]; ngx_str_t input = ngx_string("This is message 123. Codeword is 'foobar'."); n = ngx_regex_exec(re, &input, captures, (1 + rc.captures) * 3); if (n >= 0) { /* string matches expression */ } else if (n == NGX_REGEX_NO_MATCHED) { /* no match was found */ } else { /* some error */ ngx_log_error(NGX_LOG_ALERT, log, 0, ngx_regex_exec_n " failed: %i", n); }
ngx_regex_exec()
的参数是被编译的正则表达式 re
,待匹配的字符串 input
,一个可选的整数数组用于存放找到的任何 captures
,以及数组的 size
。captures
数组的大小必须是三的倍数,这是 PCRE API 所要求的。在示例中,大小由捕获总数加上匹配字符串本身的一个(捕获)计算得出。
如果存在匹配项,可以按如下方式访问捕获
u_char *p; size_t size; ngx_str_t name, value; /* all captures */ for (i = 0; i < n * 2; i += 2) { value.data = input.data + captures[i]; value.len = captures[i + 1] — captures[i]; } /* accessing named captures */ size = rc.name_size; p = rc.names; for (i = 0; i < rc.named_captures; i++, p += size) { /* capture name */ name.data = &p[2]; name.len = ngx_strlen(name.data); n = 2 * ((p[0] << 8) + p[1]); /* captured value */ value.data = &input.data[captures[n]]; value.len = captures[n + 1] — captures[n]; }
ngx_regex_exec_array()
函数接受 ngx_regex_elt_t
元素的数组(它们只是带有相关名称的已编译正则表达式),待匹配的字符串,以及一个日志。该函数将数组中的表达式应用于字符串,直到找到匹配项或没有更多表达式为止。找到匹配项时返回 NGX_OK
,否则返回 NGX_DECLINED
,错误时返回 NGX_ERROR
。
时间
ngx_time_t
结构体表示时间,包含秒、毫秒和 GMT 偏移量三个独立类型
typedef struct { time_t sec; ngx_uint_t msec; ngx_int_t gmtoff; } ngx_time_t;
ngx_tm_t
结构体在 UNIX 平台上是 struct tm
的别名,在 Windows 上是 SYSTEMTIME
的别名。
要获取当前时间,通常只需访问可用的全局变量之一,这些变量以所需格式表示缓存的时间值。
可用的字符串表示形式有
-
ngx_cached_err_log_time
— 用于错误日志条目:"1970/09/28 12:00:00"
-
ngx_cached_http_log_time
— 用于 HTTP 访问日志条目:"28/Sep/1970:12:00:00 +0600"
-
ngx_cached_syslog_time
— 用于 syslog 条目:"Sep 28 12:00:00"
-
ngx_cached_http_time
— 用于 HTTP 头部:"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_update()
。
以下函数将 time_t
转换为指定的分解时间表示。每对函数中的第一个函数将 time_t
转换为 ngx_tm_t
,第二个函数(带 _libc_
中缀)转换为 struct tm
-
ngx_gmtime(), ngx_libc_gmtime()
— 表示为 UTC 的时间 -
ngx_localtime(), ngx_libc_localtime()
— 表示为本地时区的时间
ngx_http_time(buf, time)
函数返回适合用于 HTTP 头部(例如 "Mon, 28 Sep 1970 06:00:00 GMT"
)的字符串表示。ngx_http_cookie_time(buf, time)
函数返回适合用于 HTTP cookie("Thu, 31-Dec-37 23:55:55 GMT"
)的字符串表示。
数据结构
数组
nginx 数组类型 ngx_array_t
定义如下
typedef struct { void *elts; ngx_uint_t nelts; size_t size; ngx_uint_t nalloc; ngx_pool_t *pool; } ngx_array_t;
数组的元素在 elts
字段中可用。nelts
字段保存元素数量。size
字段保存单个元素的大小,并在数组初始化时设置。
使用 ngx_array_create(pool, n, size)
调用在内存池中创建一个数组,使用 ngx_array_init(array, pool, n, size)
调用初始化一个已经分配好的数组对象。
ngx_array_t *a, b; /* create an array of strings with preallocated memory for 10 elements */ a = ngx_array_create(pool, 10, sizeof(ngx_str_t)); /* initialize string array for 10 elements */ ngx_array_init(&b, pool, 10, sizeof(ngx_str_t));
使用以下函数向数组添加元素
-
ngx_array_push(a)
添加一个尾部元素并返回其指针 -
ngx_array_push_n(a, n)
添加n
个尾部元素并返回第一个元素的指针
如果当前分配的内存不足以容纳新元素,则会分配一个新的内存块,并将现有元素复制到其中。新的内存块通常是现有内存块的两倍大。
s = ngx_array_push(a); ss = ngx_array_push_n(&b, 3);
列表
在 nginx 中,列表是一系列数组,优化用于插入大量潜在的项目。ngx_list_t
列表类型定义如下
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;
实际项目存储在列表部分中,其定义如下
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; };
在使用之前,必须通过调用 ngx_list_init(list, pool, n, size)
初始化列表,或通过调用 ngx_list_create(pool, n, size)
创建列表。这两个函数都接受单个项目的大小以及每个列表部分的项数作为参数。要向列表添加项目,请使用 ngx_list_push(list)
函数。要迭代项目,请直接访问列表字段,如示例所示
ngx_str_t *v; ngx_uint_t i; ngx_list_t *list; ngx_list_part_t *part; list = ngx_list_create(pool, 100, sizeof(ngx_str_t)); if (list == NULL) { /* error */ } /* add items to the list */ v = ngx_list_push(list); if (v == NULL) { /* error */ } ngx_str_set(v, "foo"); v = ngx_list_push(list); if (v == NULL) { /* error */ } ngx_str_set(v, "bar"); /* iterate over the list */ part = &list->part; v = part->elts; for (i = 0; /* void */; i++) { if (i >= part->nelts) { if (part->next == NULL) { break; } part = part->next; v = part->elts; i = 0; } ngx_do_smth(&v[i]); }
列表主要用于 HTTP 输入和输出头部。
列表不支持项目移除。然而,在需要时,项目可以在内部标记为缺失,而无需实际从列表中移除。例如,要将 HTTP 输出头部(存储为 ngx_table_elt_t
对象)标记为缺失,请将 ngx_table_elt_t
中的 hash
字段设置为零。以这种方式标记的项目在迭代头部时会被明确跳过。
队列
在 nginx 中,队列是一个侵入式双向链表,每个节点定义如下
typedef struct ngx_queue_s ngx_queue_t; struct ngx_queue_s { ngx_queue_t *prev; ngx_queue_t *next; };
队列头部节点不与任何数据链接。在使用之前,使用 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)
— 在一个节点处分割队列,将队列尾部放在一个单独的队列中返回 -
ngx_queue_add(h, n)
— 将第二个队列添加到第一个队列 -
ngx_queue_head(h)
,ngx_queue_last(h)
— 获取第一个或最后一个队列节点 -
ngx_queue_sentinel(h)
- 获取一个队列哨兵对象,用于结束迭代 -
ngx_queue_data(q, type, link)
— 获取队列节点数据结构的起始引用,考虑队列字段在其内部的偏移量
一个示例
typedef struct { ngx_str_t value; ngx_queue_t queue; } ngx_foo_t; ngx_foo_t *f; ngx_queue_t values, *q; ngx_queue_init(&values); f = ngx_palloc(pool, sizeof(ngx_foo_t)); if (f == NULL) { /* error */ } ngx_str_set(&f->value, "foo"); ngx_queue_insert_tail(&values, &f->queue); /* insert more nodes here */ for (q = ngx_queue_head(&values); q != ngx_queue_sentinel(&values); q = ngx_queue_next(q)) { f = ngx_queue_data(q, ngx_foo_t, queue); ngx_do_smth(&f->value); }
红黑树
src/core/ngx_rbtree.h
头文件提供了对红黑树有效实现的访问。
typedef struct { ngx_rbtree_t rbtree; ngx_rbtree_node_t sentinel; /* custom per-tree data here */ } my_tree_t; typedef struct { ngx_rbtree_node_t rbnode; /* custom per-node data */ foo_t val; } my_node_t;
要处理整个树,需要两个节点:根节点和哨兵节点。通常,它们被添加到自定义结构中,允许您将数据组织成一棵树,其中叶子包含指向您的数据的链接或嵌入您的数据。
初始化树
my_tree_t root; ngx_rbtree_init(&root.rbtree, &root.sentinel, insert_value_function);
要遍历树并插入新值,使用“insert_value
”函数。例如,ngx_str_rbtree_insert_value
函数处理 ngx_str_t
类型。其参数是指向插入操作的根节点、待添加的新创建节点以及树的哨兵节点的指针。
void ngx_str_rbtree_insert_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel)
遍历非常简单,可以使用以下查找函数模式进行演示
my_node_t * my_rbtree_lookup(ngx_rbtree_t *rbtree, foo_t *val, uint32_t hash) { ngx_int_t rc; my_node_t *n; ngx_rbtree_node_t *node, *sentinel; node = rbtree->root; sentinel = rbtree->sentinel; while (node != sentinel) { n = (my_node_t *) node; if (hash != node->key) { node = (hash < node->key) ? node->left : node->right; continue; } rc = compare(val, node->val); if (rc < 0) { node = node->left; continue; } if (rc > 0) { node = node->right; continue; } return n; } return NULL; }
compare()
函数是一个经典的比较器函数,返回小于、等于或大于零的值。为了加快查找速度并避免比较可能很大的用户对象,使用了整数哈希字段。
要向树中添加节点,分配一个新节点,初始化它并调用 ngx_rbtree_insert()
my_node_t *my_node; ngx_rbtree_node_t *node; my_node = ngx_palloc(...); init_custom_data(&my_node->val); node = &my_node->rbnode; node->key = create_key(my_node->val); ngx_rbtree_insert(&root->rbtree, node);
要移除节点,调用 ngx_rbtree_delete()
函数
ngx_rbtree_delete(&root->rbtree, node);
哈希表
哈希表函数在 src/core/ngx_hash.h
中声明。支持精确匹配和通配符匹配。后者需要额外的设置,将在下面的单独章节中描述。
在初始化哈希表之前,需要知道它将容纳的元素数量,以便 nginx 能够对其进行优化构建。需要配置两个参数:max_size
和 bucket_size
,详细信息请参阅另一份文档。它们通常可由用户配置。哈希初始化设置存储在 ngx_hash_init_t
类型中,哈希表本身是 ngx_hash_t
类型。
ngx_hash_t foo_hash; ngx_hash_init_t hash; hash.hash = &foo_hash; hash.key = ngx_hash_key; hash.max_size = 512; hash.bucket_size = ngx_align(64, ngx_cacheline_size); hash.name = "foo_hash"; hash.pool = cf->pool; hash.temp_pool = cf->temp_pool;
key
是一个指向函数的指针,该函数从字符串创建哈希整数键。有两个通用的键创建函数:ngx_hash_key(data, len)
和 ngx_hash_key_lc(data, len)
。后者将字符串转换为全小写字符,因此传递的字符串必须是可写的。如果不可写,请将 NGX_HASH_READONLY_KEY
标志传递给初始化键数组的函数(见下文)。
哈希键存储在 ngx_hash_keys_arrays_t
中,并使用 ngx_hash_keys_array_init(arr, type)
进行初始化:第二个参数(type
)控制为哈希预分配的资源量,可以是 NGX_HASH_SMALL
或 NGX_HASH_LARGE
。如果您预计哈希将包含数千个元素,则后者是合适的。
ngx_hash_keys_arrays_t foo_keys; foo_keys.pool = cf->pool; foo_keys.temp_pool = cf->temp_pool; ngx_hash_keys_array_init(&foo_keys, NGX_HASH_SMALL);
要将键插入哈希键数组,请使用 ngx_hash_add_key(keys_array, key, value, flags)
函数
ngx_str_t k1 = ngx_string("key1"); ngx_str_t k2 = ngx_string("key2"); ngx_hash_add_key(&foo_keys, &k1, &my_data_ptr_1, NGX_HASH_READONLY_KEY); ngx_hash_add_key(&foo_keys, &k2, &my_data_ptr_2, NGX_HASH_READONLY_KEY);
要构建哈希表,调用 ngx_hash_init(hinit, key_names, nelts)
函数
ngx_hash_init(&hash, foo_keys.keys.elts, foo_keys.keys.nelts);
如果 max_size
或 bucket_size
参数不够大,函数将失败。
构建哈希表后,使用 ngx_hash_find(hash, key, name, len)
函数查找元素
my_data_t *data; ngx_uint_t key; key = ngx_hash_key(k1.data, k1.len); data = ngx_hash_find(&foo_hash, key, k1.data, k1.len); if (data == NULL) { /* key not found */ }
通配符匹配
要创建支持通配符的哈希表,使用 ngx_hash_combined_t
类型。它包含上面描述的哈希类型,并有两个额外的键数组:dns_wc_head
和 dns_wc_tail
。基本属性的初始化类似于常规哈希表
ngx_hash_init_t hash ngx_hash_combined_t foo_hash; hash.hash = &foo_hash.hash; hash.key = ...;
可以使用 NGX_HASH_WILDCARD_KEY
标志添加通配符键
/* k1 = ".example.org"; */ /* k2 = "foo.*"; */ ngx_hash_add_key(&foo_keys, &k1, &data1, NGX_HASH_WILDCARD_KEY); ngx_hash_add_key(&foo_keys, &k2, &data2, NGX_HASH_WILDCARD_KEY);
该函数识别通配符并将键添加到相应的数组中。有关通配符语法和匹配算法的说明,请参阅 map 模块文档。
根据添加的键的内容,您可能需要初始化多达三个键数组:一个用于精确匹配(如上所述),另外两个用于启用从字符串头部或尾部开始的匹配
if (foo_keys.dns_wc_head.nelts) { ngx_qsort(foo_keys.dns_wc_head.elts, (size_t) foo_keys.dns_wc_head.nelts, sizeof(ngx_hash_key_t), cmp_dns_wildcards); hash.hash = NULL; hash.temp_pool = pool; if (ngx_hash_wildcard_init(&hash, foo_keys.dns_wc_head.elts, foo_keys.dns_wc_head.nelts) != NGX_OK) { return NGX_ERROR; } foo_hash.wc_head = (ngx_hash_wildcard_t *) hash.hash; }
键数组需要进行排序,并且初始化结果必须添加到组合哈希中。dns_wc_tail
数组的初始化方式类似。
组合哈希中的查找由 ngx_hash_find_combined(chash, key, name, len)
处理
/* key = "bar.example.org"; — will match ".example.org" */ /* key = "foo.example.com"; — will match "foo.*" */ hkey = ngx_hash_key(key.data, key.len); res = ngx_hash_find_combined(&foo_hash, hkey, key.data, key.len);
内存管理
堆
要从系统堆分配内存,请使用以下函数
-
ngx_alloc(size, log)
— 从系统堆分配内存。这是malloc()
的一个包装器,支持日志记录。分配错误和调试信息会记录到log
中。 -
ngx_calloc(size, log)
— 像ngx_alloc()
一样从系统堆分配内存,但在分配后用零填充内存。 -
ngx_memalign(alignment, size, log)
— 从系统堆分配对齐的内存。在提供posix_memalign()
函数的平台上,这是它的一个包装器。否则,实现会退回到提供最大对齐的ngx_alloc()
。 -
ngx_free(p)
— 释放已分配的内存。这是free()
的一个包装器
内存池
大多数 nginx 分配在内存池中完成。在 nginx 内存池中分配的内存会在内存池销毁时自动释放。这提供了良好的分配性能,并使内存控制变得容易。
内存池在内部以连续的内存块分配对象。一旦一个块满了,就会分配一个新的块并将其添加到内存池的内存块列表中。当请求的分配太大而无法放入一个块时,请求会转发到系统分配器,返回的指针会存储在内存池中,以便后续释放。
nginx 内存池的类型是 ngx_pool_t
。支持以下操作
-
ngx_create_pool(size, log)
— 创建具有指定块大小的内存池。返回的内存池对象也在此内存池中分配。size
应至少为NGX_MIN_POOL_SIZE
,并且是NGX_POOL_ALIGNMENT
的倍数。 -
ngx_destroy_pool(pool)
— 释放所有内存池内存,包括内存池对象本身。 -
ngx_palloc(pool, size)
— 从指定的内存池分配对齐的内存。 -
ngx_pcalloc(pool, size)
— 从指定的内存池分配对齐的内存并用零填充。 -
ngx_pnalloc(pool, size)
— 从指定的内存池分配非对齐内存。主要用于分配字符串。 -
ngx_pfree(pool, p)
— 释放先前在指定内存池中分配的内存。只有转发到系统分配器的请求所产生的分配才能被释放。
u_char *p; ngx_str_t *s; ngx_pool_t *pool; pool = ngx_create_pool(1024, log); if (pool == NULL) { /* error */ } s = ngx_palloc(pool, sizeof(ngx_str_t)); if (s == NULL) { /* error */ } ngx_str_set(s, "foo"); p = ngx_pnalloc(pool, 3); if (p == NULL) { /* error */ } ngx_memcpy(p, "foo", 3);
链链接(ngx_chain_t
)在 nginx 中被积极使用,因此 nginx 内存池实现提供了一种重用它们的方法。ngx_pool_t
的 chain
字段维护了一个已分配链接的列表,这些链接可供重用。为了在内存池中高效分配链链接,请使用 ngx_alloc_chain_link(pool)
函数。该函数会在内存池列表中查找一个空闲链链接,如果列表为空,则分配一个新的链链接。要释放链接,请调用 ngx_free_chain(pool, cl)
函数。
可以在内存池中注册清理处理程序。清理处理程序是一个回调函数,带有一个参数,在内存池销毁时被调用。内存池通常与特定的 nginx 对象(如 HTTP 请求)绑定,并在对象生命周期结束时被销毁。注册内存池清理是一种释放资源、关闭文件描述符或对与主对象关联的共享数据进行最终调整的便捷方式。
要注册内存池清理,请调用 ngx_pool_cleanup_add(pool, size)
,它返回一个 ngx_pool_cleanup_t
指针,供调用者填充。使用 size
参数为清理处理程序分配上下文。
ngx_pool_cleanup_t *cln; cln = ngx_pool_cleanup_add(pool, 0); if (cln == NULL) { /* error */ } cln->handler = ngx_my_cleanup; cln->data = "foo"; ... static void ngx_my_cleanup(void *data) { u_char *msg = data; ngx_do_smth(msg); }
共享内存
nginx 使用共享内存来在进程之间共享公共数据。ngx_shared_memory_add(cf, name, size, tag)
函数将一个新的共享内存条目 ngx_shm_zone_t
添加到周期中。该函数接收区域的 name
和 size
。每个共享区域必须具有唯一的名称。如果已存在具有提供的 name
和 tag
的共享区域条目,则重用现有区域条目。如果具有相同名称的现有条目具有不同的 tag,函数将失败并报错。通常,模块结构的地址作为 tag
传递,这使得在同一个 nginx 模块内按名称重用共享区域成为可能。
共享内存条目结构体 ngx_shm_zone_t
包含以下字段
-
init
— 初始化回调函数,在共享区域映射到实际内存后调用 -
data
— 数据上下文,用于向init
回调函数传递任意数据 -
noreuse
— 禁用从旧周期重用共享区域的标志 -
tag
— 共享区域标签 -
shm
— 平台特定对象,类型为ngx_shm_t
,至少包含以下字段-
addr
— 映射的共享内存地址,初始为 NULL -
size
— 共享内存大小 -
name
— 共享内存名称 -
log
— 共享内存日志 -
exists
— 标志,指示共享内存是否从主进程继承(Windows 特定)
-
共享区域条目在配置解析后,在 ngx_init_cycle()
中映射到实际内存。在 POSIX 系统上,使用 mmap()
系统调用创建共享匿名映射。在 Windows 上,使用 CreateFileMapping()
/ MapViewOfFileEx()
对。
为了在共享内存中进行分配,nginx 提供了 slab 内存池 ngx_slab_pool_t
类型。每个 nginx 共享区域中会自动创建一个用于分配内存的 slab 内存池。该内存池位于共享区域的开头,可以通过表达式 (ngx_slab_pool_t *) shm_zone->shm.addr
访问。要在共享区域中分配内存,请调用 ngx_slab_alloc(pool, size)
或 ngx_slab_calloc(pool, size)
。要释放内存,请调用 ngx_slab_free(pool, p)
。
Slab 内存池将所有共享区域划分为页面。每个页面用于分配相同大小的对象。指定的大小必须是 2 的幂,且大于最小大小 8 字节。不符合的值会被向上取整。每个页面都有一个位掩码,用于跟踪哪些块正在使用以及哪些块可用于分配。对于大于半页(通常为 2048 字节)的大小,一次分配整个页面
为了保护共享内存中的数据免受并发访问,使用 ngx_slab_pool_t
的 mutex
字段中可用的互斥锁。互斥锁最常由 slab 内存池在分配和释放内存时使用,但它也可用于保护在共享区域中分配的任何其他用户数据结构。要锁定或解锁互斥锁,分别调用 ngx_shmtx_lock(&shpool->mutex)
或 ngx_shmtx_unlock(&shpool->mutex)
。
ngx_str_t name; ngx_foo_ctx_t *ctx; ngx_shm_zone_t *shm_zone; ngx_str_set(&name, "foo"); /* allocate shared zone context */ ctx = ngx_pcalloc(cf->pool, sizeof(ngx_foo_ctx_t)); if (ctx == NULL) { /* error */ } /* add an entry for 64k shared zone */ shm_zone = ngx_shared_memory_add(cf, &name, 65536, &ngx_foo_module); if (shm_zone == NULL) { /* error */ } /* register init callback and context */ shm_zone->init = ngx_foo_init_zone; shm_zone->data = ctx; ... static ngx_int_t ngx_foo_init_zone(ngx_shm_zone_t *shm_zone, void *data) { ngx_foo_ctx_t *octx = data; size_t len; ngx_foo_ctx_t *ctx; ngx_slab_pool_t *shpool; value = shm_zone->data; if (octx) { /* reusing a shared zone from old cycle */ ctx->value = octx->value; return NGX_OK; } shpool = (ngx_slab_pool_t *) shm_zone->shm.addr; if (shm_zone->shm.exists) { /* initialize shared zone context in Windows nginx worker */ ctx->value = shpool->data; return NGX_OK; } /* initialize shared zone */ ctx->value = ngx_slab_alloc(shpool, sizeof(ngx_uint_t)); if (ctx->value == NULL) { return NGX_ERROR; } shpool->data = ctx->value; return NGX_OK; }
日志记录
对于日志记录,nginx 使用 ngx_log_t
对象。nginx 日志记录器支持多种输出类型
- stderr — 记录到标准错误 (stderr)
- file — 记录到文件
- syslog — 记录到 syslog
- memory — 为了开发目的记录到内部内存存储;内存以后可以通过调试器访问
一个日志记录器实例可以是一个日志记录器链,通过 next
字段相互链接。在这种情况下,每条消息会写入链中的所有日志记录器。
对于每个日志记录器,严重级别控制哪些消息被写入日志(只记录分配了该级别或更高严重级别的事件)。支持以下严重级别
-
NGX_LOG_EMERG
-
NGX_LOG_ALERT
-
NGX_LOG_CRIT
-
NGX_LOG_ERR
-
NGX_LOG_WARN
-
NGX_LOG_NOTICE
-
NGX_LOG_INFO
-
NGX_LOG_DEBUG
对于调试日志记录,也会检查调试掩码。调试掩码有
-
NGX_LOG_DEBUG_CORE
-
NGX_LOG_DEBUG_ALLOC
-
NGX_LOG_DEBUG_MUTEX
-
NGX_LOG_DEBUG_EVENT
-
NGX_LOG_DEBUG_HTTP
-
NGX_LOG_DEBUG_MAIL
-
NGX_LOG_DEBUG_STREAM
通常,日志记录器由现有的 nginx 代码根据 error_log
指令创建,并且在周期、配置、客户端连接和其他对象的几乎每个处理阶段都可用。
Nginx 提供以下日志记录宏
-
ngx_log_error(level, log, err, fmt, ...)
— 错误日志记录 -
ngx_log_debug0(level, log, err, fmt)
,ngx_log_debug1(level, log, err, fmt, arg1)
等 — 调试日志记录,最多支持八个格式化参数
日志消息在栈上大小为 NGX_MAX_ERROR_STR
(当前为 2048 字节)的缓冲区中格式化。消息前面会加上严重级别、进程 ID (PID)、连接 ID(存储在 log->connection
中)和系统错误文本。对于非调试消息,也会调用 log->handler
向日志消息前面添加更具体的信息。HTTP 模块将 ngx_http_log_error()
函数设置为日志处理程序,以记录客户端和服务器地址、当前动作(存储在 log->action
中)、客户端请求行、服务器名称等。
/* specify what is currently done */ log->action = "sending mp4 to client"; /* error and debug log */ ngx_log_error(NGX_LOG_INFO, c->log, 0, "client prematurely closed connection"); ngx_log_debug2(NGX_LOG_DEBUG_HTTP, mp4->file.log, 0, "mp4 start:%ui, length:%ui", mp4->start, mp4->length);
上面的示例会产生如下的日志条目
2016/09/16 22:08:52 [info] 17445#0: *1 client prematurely closed connection while sending mp4 to client, client: 127.0.0.1, server: , request: "GET /file.mp4 HTTP/1.1" 2016/09/16 23:28:33 [debug] 22140#0: *1 mp4 start:0, length:10000
周期
周期对象存储由特定配置创建的 nginx 运行时上下文。其类型是 ngx_cycle_t
。当前周期由 ngx_cycle
全局变量引用,并在 nginx 工作进程启动时继承。每次重新加载 nginx 配置时,都会从新的 nginx 配置创建一个新周期;旧周期通常在新周期成功创建后被删除。
周期由 ngx_init_cycle()
函数创建,该函数接受前一个周期作为参数。该函数定位前一个周期的配置文件,并尽可能多地从前一个周期继承资源。nginx 启动时会创建一个名为“init cycle”的占位符周期,然后被根据配置构建的实际周期替换。
周期的成员包括
-
pool
— 周期内存池。为每个新周期创建。 -
log
— 周期日志。最初从旧周期继承,读取配置后设置为指向new_log
。 -
new_log
— 由配置创建的周期日志。它受根范围error_log
指令的影响。 -
connections
,connection_n
— 类型为ngx_connection_t
的连接数组,由事件模块在初始化每个 nginx 工作进程时创建。nginx 配置中的worker_connections
指令设置连接数connection_n
。 -
free_connections
,free_connection_n
— 当前可用连接的列表和数量。如果没有可用连接,nginx 工作进程将拒绝接受新客户端或连接到上游服务器。 -
files
,files_n
— 用于将文件描述符映射到 nginx 连接的数组。此映射由具有NGX_USE_FD_EVENT
标志的事件模块使用(当前是poll
和devpoll
)。 -
conf_ctx
— 核心模块配置数组。配置在读取 nginx 配置文件期间创建和填充。 -
modules
,modules_n
— 类型为ngx_module_t
的模块数组,包括当前配置加载的静态和动态模块。 -
listening
— 类型为ngx_listening_t
的监听对象数组。监听对象通常由不同模块的listen
指令通过调用ngx_create_listening()
函数添加。监听套接字基于监听对象创建。 -
paths
— 类型为ngx_path_t
的路径数组。路径由需要在特定目录上操作的模块通过调用ngx_add_path()
函数添加。nginx 在读取配置后会创建这些目录(如果不存在)。此外,可以为每个路径添加两个处理程序- 路径加载器 — 在启动或重新加载 nginx 后每 60 秒仅执行一次。通常,加载器会读取目录并将数据存储在 nginx 共享内存中。该处理程序由专用的 nginx 进程“nginx cache loader”调用。
- path manager — 定期执行。通常,该管理器会从目录中删除旧文件并更新 nginx 内存以反映这些更改。该处理程序从专用的“nginx 缓存管理器”进程中调用。
-
open_files
— 类型为ngx_open_file_t
的打开文件对象列表,通过调用函数ngx_conf_open_file()
创建。目前,nginx 使用此类打开文件进行日志记录。读取配置后,nginx 会打开open_files
列表中的所有文件,并将每个文件描述符存储在对象的fd
字段中。文件以追加模式打开,如果缺失则创建。列表中的文件在 nginx worker 收到重新打开信号(通常是USR1
)后由 worker 重新打开。在这种情况下,fd
字段中的描述符会更改为一个新值。 -
shared_memory
— 共享内存区域列表,每个区域通过调用ngx_shared_memory_add()
函数添加。共享区域映射到所有 nginx 进程中的相同地址范围,用于共享公共数据,例如 HTTP 缓存的内存树。
缓冲区
对于输入/输出操作,nginx 提供了缓冲区类型 ngx_buf_t
。通常,它用于存放要写入目标或从源读取的数据。缓冲区可以引用内存中的数据或文件中的数据,并且技术上缓冲区可能同时引用两者。缓冲区的内存是单独分配的,与缓冲区结构 ngx_buf_t
无关。
ngx_buf_t
结构包含以下字段
-
start
,end
— 为缓冲区分配的内存块的边界。 -
pos
,last
— 内存缓冲区的边界;通常是start
..end
的子范围。 -
file_pos
,file_last
— 文件缓冲区的边界,表示为相对于文件开头的偏移量。 -
tag
— 用于区分缓冲区的唯一值;由不同的 nginx 模块创建,通常用于缓冲区复用。 -
file
— 文件对象。 -
temporary
— 标志,表示缓冲区引用可写内存。 -
memory
— 标志,表示缓冲区引用只读内存。 -
in_file
— 标志,表示缓冲区引用文件中的数据。 -
flush
— 标志,表示缓冲区之前的所有数据都需要被刷新。 -
recycled
— 标志,表示缓冲区可以被复用,需要尽快使用。 -
sync
— 标志,表示缓冲区不包含数据或特殊的信号,例如flush
或last_buf
。默认情况下,nginx 认为此类缓冲区是错误条件,但此标志告诉 nginx 跳过错误检查。 -
last_buf
— 标志,表示该缓冲区是输出中的最后一个。 -
last_in_chain
— 标志,表示在请求或子请求中没有更多数据缓冲区。 -
shadow
— 指向另一个(“影子”)缓冲区的引用,该缓冲区与当前缓冲区相关,通常意味着当前缓冲区使用影子缓冲区中的数据。当当前缓冲区被使用后,影子缓冲区通常也会被标记为已使用。 -
last_shadow
— 标志,表示该缓冲区是引用特定影子缓冲区的最后一个缓冲区。 -
temp_file
— 标志,表示缓冲区位于临时文件中。
对于输入和输出操作,缓冲区以链的形式链接。一个链是由类型为 ngx_chain_t
的链节点组成的序列,定义如下:
typedef struct ngx_chain_s ngx_chain_t; struct ngx_chain_s { ngx_buf_t *buf; ngx_chain_t *next; };
每个链节点都持有对其缓冲区的引用和对其下一个链节点的引用。
使用缓冲区和链的示例
ngx_chain_t * ngx_get_my_chain(ngx_pool_t *pool) { ngx_buf_t *b; ngx_chain_t *out, *cl, **ll; /* first buf */ cl = ngx_alloc_chain_link(pool); if (cl == NULL) { /* error */ } b = ngx_calloc_buf(pool); if (b == NULL) { /* error */ } b->start = (u_char *) "foo"; b->pos = b->start; b->end = b->start + 3; b->last = b->end; b->memory = 1; /* read-only memory */ cl->buf = b; out = cl; ll = &cl->next; /* second buf */ cl = ngx_alloc_chain_link(pool); if (cl == NULL) { /* error */ } b = ngx_create_temp_buf(pool, 3); if (b == NULL) { /* error */ } b->last = ngx_cpymem(b->last, "foo", 3); cl->buf = b; cl->next = NULL; *ll = cl; return out; }
网络
连接
连接类型 ngx_connection_t
是围绕套接字描述符的封装。它包含以下字段
-
fd
— 套接字描述符 -
data
— 任意连接上下文。通常,它是指向基于连接构建的高级对象的指针,例如 HTTP 请求或 Stream 会话。 -
read
,write
— 连接的读事件和写事件。 -
recv
,send
,recv_chain
,send_chain
— 连接的 I/O 操作。 -
pool
— 连接池。 -
log
— 连接日志。 -
sockaddr
,socklen
,addr_text
— 远端套接字地址的二进制和文本形式。 -
local_sockaddr
,local_socklen
— 本地套接字地址的二进制形式。最初,这些字段是空的。使用ngx_connection_local_sockaddr()
函数获取本地套接字地址。 -
proxy_protocol_addr
,proxy_protocol_port
- PROXY 协议客户端地址和端口,如果连接启用了 PROXY 协议。 -
ssl
— 连接的 SSL 上下文。 -
reusable
— 标志,表示连接处于可复用状态。 -
close
— 标志,表示连接正在被处理以便关闭和复用。
nginx 连接可以透明地封装 SSL 层。在这种情况下,连接的 ssl
字段持有一个指向 ngx_ssl_connection_t
结构的指针,其中包含连接的所有 SSL 相关数据,包括 SSL_CTX
和 SSL
。`recv`、`send`、`recv_chain` 和 `send_chain` 处理程序也会被设置为支持 SSL 的函数。
nginx 配置中的 worker_connections
指令限制了每个 nginx worker 的连接数。所有连接结构在 worker 启动时预先创建,并存储在 cycle 对象的 connections
字段中。要检索连接结构,请使用 `ngx_get_connection(s, log)` 函数。它将套接字描述符 `s` 作为参数,该描述符需要被封装在一个连接结构中。
由于每个 worker 的连接数是有限的,nginx 提供了一种方式来获取当前正在使用的连接。要启用或禁用连接的复用,请调用 `ngx_reusable_connection(c, reusable)` 函数。调用 `ngx_reusable_connection(c, 1)` 会在连接结构中设置 `reuse` 标志,并将连接插入 cycle 的 `reusable_connections_queue` 中。当 `ngx_get_connection()` 发现 cycle 的 `free_connections` 列表中没有可用的连接时,它会调用 `ngx_drain_connections()` 来释放特定数量的可复用连接。对于每个此类连接,会设置 `close` 标志,并调用其读处理程序,该处理程序应该通过调用 `ngx_close_connection(c)` 来释放连接并使其可供复用。要退出连接可复用的状态,则调用 `ngx_reusable_connection(c, 0)`。HTTP 客户端连接是 nginx 中可复用连接的一个例子;它们在从客户端接收到第一个请求字节之前都被标记为可复用。
事件
事件
nginx 中的事件对象 ngx_event_t
提供了一种机制,用于通知特定事件的发生。
ngx_event_t
中的字段包括
-
data
— 用于事件处理程序中的任意事件上下文,通常是指向与事件相关的连接的指针。 -
handler
— 事件发生时调用的回调函数。 -
write
— 标志,表示一个写事件。没有此标志表示一个读事件。 -
active
— 标志,表示事件已注册以接收 I/O 通知,通常来自 `epoll`、`kqueue`、`poll` 等通知机制。 -
ready
— 标志,表示事件已收到 I/O 通知。 -
delayed
— 标志,表示由于速率限制导致 I/O 被延迟。 -
timer
— 红黑树节点,用于将事件插入到定时器树中。 -
timer_set
— 标志,表示事件定时器已设置但尚未到期。 -
timedout
— 标志,表示事件定时器已到期。 -
eof
— 标志,表示在读取数据时发生了 EOF (文件结束)。 -
pending_eof
— 标志,表示套接字上 pending EOF,即使在此之前可能还有一些数据可用。该标志通过 `EPOLLRDHUP` `epoll` 事件或 `EV_EOF` `kqueue` 标志传递。 -
error
— 标志,表示在读取(对于读事件)或写入(对于写事件)期间发生了错误。 -
cancelable
— 定时器事件标志,表示在 worker 关闭期间应忽略该事件。优雅的 worker 关闭会延迟,直到没有不可取消的定时器事件被调度。 -
posted
— 标志,表示事件已发布到队列。 -
queue
— 队列节点,用于将事件发布到队列。
I/O 事件
通过调用 `ngx_get_connection()` 函数获得的每个连接都有两个附加事件,`c->read` 和 `c->write`,用于接收套接字准备好进行读或写操作的通知。所有这些事件都工作在 Edge-Triggered (边缘触发)模式下,这意味着它们仅在套接字状态发生变化时触发通知。例如,对套接字进行部分读取不会使 nginx 再次发送读取通知,直到套接字上有更多数据到达。即使底层 I/O 通知机制本质上是 Level-Triggered (水平触发)(例如 `poll`、`select` 等),nginx 也会将通知转换为 Edge-Triggered。为了使 nginx 事件通知在不同平台上的所有通知系统之间保持一致,处理 I/O 套接字通知或在该套接字上调用任何 I/O 函数后,必须调用 `ngx_handle_read_event(rev, flags)` 和 `ngx_handle_write_event(wev, lowat)` 函数。通常,这些函数在每个读或写事件处理程序的末尾调用一次。
定时器事件
可以为事件设置一个定时器,以便在超时到期时发送通知。事件使用的定时器计算自过去某个未指定时间点以来的毫秒数,并截断为 `ngx_msec_t` 类型。其当前值可以通过 `ngx_current_msec` 变量获取。
函数 `ngx_add_timer(ev, timer)` 为事件设置超时,`ngx_del_timer(ev)` 删除先前设置的超时。全局超时红黑树 `ngx_event_timer_rbtree` 存储当前所有设置的超时。树中的键是 `ngx_msec_t` 类型,表示事件发生的时间。树结构支持快速的插入和删除操作,以及访问最接近到期的超时,nginx 使用这些信息来确定等待 I/O 事件以及处理到期超时事件需要等待多久。
投递事件
事件可以被 post (发布),这意味着其处理程序将在当前事件循环迭代中的稍后某个时间被调用。发布事件是简化代码和避免堆栈溢出的良好实践。已发布的事件保存在 post 队列中。宏 `ngx_post_event(ev, q)` 将事件 `ev` 发布到 post 队列 `q`。宏 `ngx_delete_posted_event(ev)` 从事件当前所在的队列中删除事件 `ev`。通常,事件被发布到 `ngx_posted_events` 队列,该队列在事件循环的后期处理——即所有 I/O 和定时器事件都已处理完毕之后。调用 `ngx_event_process_posted()` 函数来处理事件队列。它会不断调用事件处理程序,直到队列为空。这意味着已发布的事件处理程序可以在当前事件循环迭代中发布更多事件进行处理。
一个示例
void ngx_my_connection_read(ngx_connection_t *c) { ngx_event_t *rev; rev = c->read; ngx_add_timer(rev, 1000); rev->handler = ngx_my_read_handler; ngx_my_read(rev); } void ngx_my_read_handler(ngx_event_t *rev) { ssize_t n; ngx_connection_t *c; u_char buf[256]; if (rev->timedout) { /* timeout expired */ } c = rev->data; while (rev->ready) { n = c->recv(c, buf, sizeof(buf)); if (n == NGX_AGAIN) { break; } if (n == NGX_ERROR) { /* error */ } /* process buf */ } if (ngx_handle_read_event(rev, 0) != NGX_OK) { /* error */ } }
事件循环
除了 nginx master 进程外,所有 nginx 进程都执行 I/O 操作,因此都有一个事件循环。(nginx master 进程则大部分时间花在 `sigsuspend()` 调用中,等待信号到达。)nginx 事件循环实现在 `ngx_process_events_and_timers()` 函数中,该函数会重复调用,直到进程退出。
事件循环包含以下阶段
- 通过调用 `ngx_event_find_timer()` 找到最接近到期的超时。此函数找到定时器树中最左侧的节点,并返回直到该节点到期的毫秒数。
- 通过调用一个处理程序来处理 I/O 事件,该处理程序特定于由 nginx 配置选择的事件通知机制。该处理程序会等待至少一个 I/O 事件发生,但仅等到下一个超时到期。当读或写事件发生时,`ready` 标志会被设置,并调用事件的处理程序。对于 Linux,通常使用 `ngx_epoll_process_events()` 处理程序,它调用 `epoll_wait()` 来等待 I/O 事件。
- 通过调用 `ngx_event_expire_timers()` 使定时器到期。定时器树从最左侧的元素向右迭代,直到找到一个未到期的超时。对于每个到期节点,`timedout` 事件标志被设置,`timer_set` 标志被重置,并调用事件处理程序。
- 通过调用 `ngx_event_process_posted()` 处理已发布的事件。该函数会重复地从已发布的事件队列中移除第一个元素并调用该元素的处理程序,直到队列为空。这意味着已发布的事件处理程序可以在当前事件循环迭代中发布更多事件进行处理。
所有 nginx 进程也处理信号。信号处理程序只设置全局变量,这些变量在 `ngx_process_events_and_timers()` 调用之后进行检查。
进程
nginx 中有几种进程类型。进程类型保存在全局变量 `ngx_process` 中,是以下类型之一
-
NGX_PROCESS_MASTER
— master 进程,负责读取 NGINX 配置,创建 cycles,并启动和控制子进程。它不执行任何 I/O 操作,只响应信号。其 cycle 函数是 `ngx_master_process_cycle()`。 -
NGX_PROCESS_WORKER
— worker 进程,负责处理客户端连接。它由 master 进程启动,并响应 master 进程的信号和通道命令。其 cycle 函数是 `ngx_worker_process_cycle()`。可以有多个 worker 进程,数量由 `worker_processes` 指令配置。 -
NGX_PROCESS_SINGLE
— single 进程,仅在 `master_process off` 模式下存在,并且是该模式下唯一运行的进程。它创建 cycles(如同 master 进程)并处理客户端连接(如同 worker 进程)。其 cycle 函数是 `ngx_single_process_cycle()`。 -
NGX_PROCESS_HELPER
— helper 进程,目前有两种类型:cache manager 和 cache loader。两者的 cycle 函数都是 `ngx_cache_manager_process_cycle()`。
nginx 进程处理以下信号
-
NGX_SHUTDOWN_SIGNAL
(SIGQUIT
在大多数系统上) — 优雅关闭。收到此信号后,master 进程会向所有子进程发送关闭信号。当没有子进程剩余时,master 销毁 cycle pool 并退出。当 worker 进程收到此信号时,它会关闭所有监听套接字,并等待直到没有不可取消的事件被调度,然后销毁 cycle pool 并退出。当 cache manager 或 cache loader 进程收到此信号时,它会立即退出。当进程收到此信号时,变量 `ngx_quit` 被设置为1
,并在处理后立即重置。当 worker 进程处于关闭状态时,变量 `ngx_exiting` 被设置为1
。 -
NGX_TERMINATE_SIGNAL
(SIGTERM
在大多数系统上) — 终止。收到此信号后,master 进程会向所有子进程发送终止信号。如果子进程在 1 秒内没有退出,master 进程会发送SIGKILL
信号来杀死它。当没有子进程剩余时,master 进程销毁 cycle pool 并退出。当 worker 进程、cache manager 进程或 cache loader 进程收到此信号时,它会销毁 cycle pool 并退出。当收到此信号时,变量ngx_terminate
被设置为1
。 -
NGX_NOACCEPT_SIGNAL
(SIGWINCH
在大多数系统上) - 关闭所有 worker 和 helper 进程。收到此信号后,master 进程关闭其子进程。如果先前启动的新 nginx 二进制文件退出,旧 master 的子进程会再次启动。当 worker 进程收到此信号时,它会在 `debug_points` 指令设置的 debug 模式下关闭。 -
NGX_RECONFIGURE_SIGNAL
(SIGHUP
在大多数系统上) - 重新配置。收到此信号后,master 进程重新读取配置并基于新配置创建新的 cycle。如果新 cycle 成功创建,旧 cycle 将被删除,并启动新的子进程。同时,旧的子进程会收到 `NGX_SHUTDOWN_SIGNAL` 信号。在 single-process 模式下,nginx 会创建新的 cycle,但会保留旧 cycle,直到不再有与之关联的活跃连接的客户端。worker 和 helper 进程忽略此信号。 -
NGX_REOPEN_SIGNAL
(SIGUSR1
在大多数系统上) — 重新打开文件。master 进程将此信号发送给 worker,worker 重新打开所有与 cycle 相关的 `open_files`。 -
NGX_CHANGEBIN_SIGNAL
(SIGUSR2
在大多数系统上) — 更换 nginx 二进制文件。master 进程启动新的 nginx 二进制文件,并传入所有监听套接字的列表。这个文本格式的列表通过“NGINX”
环境变量传递,由用分号分隔的描述符编号组成。新的 nginx 二进制文件读取“NGINX”
变量,并将这些套接字添加到其 init cycle 中。其他进程忽略此信号。
尽管所有 nginx worker 进程都能够接收并正确处理 POSIX 信号,但 master 进程并不使用标准的 kill()
系统调用将信号传递给 worker 和 helper 进程。相反,nginx 使用进程间套接字对,它允许在所有 nginx 进程之间发送消息。然而,目前消息仅从 master 发送到其子进程。这些消息携带标准信号。
线程
可以将原本会阻塞 nginx worker 进程的任务卸载到单独的线程中执行。例如,可以将 nginx 配置为使用线程执行文件 I/O。另一个用例是库没有异步接口,因此无法正常与 nginx 一起使用。请记住,线程接口是现有异步处理客户端连接方法的辅助工具,绝不是替代方案。
为了处理同步,可以使用以下对 pthreads
原语的封装
-
typedef pthread_mutex_t ngx_thread_mutex_t;
-
ngx_int_t ngx_thread_mutex_create(ngx_thread_mutex_t *mtx, ngx_log_t *log);
-
ngx_int_t ngx_thread_mutex_destroy(ngx_thread_mutex_t *mtx, ngx_log_t *log);
-
ngx_int_t ngx_thread_mutex_lock(ngx_thread_mutex_t *mtx, ngx_log_t *log);
-
ngx_int_t ngx_thread_mutex_unlock(ngx_thread_mutex_t *mtx, ngx_log_t *log);
-
-
typedef pthread_cond_t ngx_thread_cond_t;
-
ngx_int_t ngx_thread_cond_create(ngx_thread_cond_t *cond, ngx_log_t *log);
-
ngx_int_t ngx_thread_cond_destroy(ngx_thread_cond_t *cond, ngx_log_t *log);
-
ngx_int_t ngx_thread_cond_signal(ngx_thread_cond_t *cond, ngx_log_t *log);
-
ngx_int_t ngx_thread_cond_wait(ngx_thread_cond_t *cond, ngx_thread_mutex_t *mtx, ngx_log_t *log);
-
nginx 并未为每个任务创建一个新线程,而是实现了线程池 (thread_pool)策略。可以为不同的目的配置多个线程池(例如,对不同的磁盘组执行 I/O)。每个线程池在启动时创建,并包含有限数量的线程来处理任务队列。任务完成后,会调用预定义的完成处理程序。
src/core/ngx_thread_pool.h
头文件包含相关定义
struct ngx_thread_task_s { ngx_thread_task_t *next; ngx_uint_t id; void *ctx; void (*handler)(void *data, ngx_log_t *log); ngx_event_t event; }; typedef struct ngx_thread_pool_s ngx_thread_pool_t; ngx_thread_pool_t *ngx_thread_pool_add(ngx_conf_t *cf, ngx_str_t *name); ngx_thread_pool_t *ngx_thread_pool_get(ngx_cycle_t *cycle, ngx_str_t *name); ngx_thread_task_t *ngx_thread_task_alloc(ngx_pool_t *pool, size_t size); ngx_int_t ngx_thread_task_post(ngx_thread_pool_t *tp, ngx_thread_task_t *task);
在配置时,希望使用线程的模块必须通过调用 ngx_thread_pool_add(cf, name)
获取线程池的引用。该函数会创建给定 name
的新线程池,如果已存在同名线程池,则返回对该池的引用。
要在运行时将 task
添加到指定的线程池 tp
的队列中,请使用 ngx_thread_task_post(tp, task)
函数。要在线程中执行函数,请使用 ngx_thread_task_t
结构传递参数并设置完成处理程序。
typedef struct { int foo; } my_thread_ctx_t; static void my_thread_func(void *data, ngx_log_t *log) { my_thread_ctx_t *ctx = data; /* this function is executed in a separate thread */ } static void my_thread_completion(ngx_event_t *ev) { my_thread_ctx_t *ctx = ev->data; /* executed in nginx event loop */ } ngx_int_t my_task_offload(my_conf_t *conf) { my_thread_ctx_t *ctx; ngx_thread_task_t *task; task = ngx_thread_task_alloc(conf->pool, sizeof(my_thread_ctx_t)); if (task == NULL) { return NGX_ERROR; } ctx = task->ctx; ctx->foo = 42; task->handler = my_thread_func; task->event.handler = my_thread_completion; task->event.data = ctx; if (ngx_thread_task_post(conf->thread_pool, task) != NGX_OK) { return NGX_ERROR; } return NGX_OK; }
模块
添加新模块
每个独立的 nginx 模块都位于一个单独的目录中,该目录至少包含两个文件:config
和模块源代码文件。config
文件包含 nginx 集成该模块所需的所有信息,例如
ngx_module_type=CORE ngx_module_name=ngx_foo_module ngx_module_srcs="$ngx_addon_dir/ngx_foo_module.c" . auto/module ngx_addon_name=$ngx_module_name
config
文件是一个 POSIX shell 脚本,可以设置和访问以下变量
-
ngx_module_type
— 要构建的模块类型。可能的值包括CORE
,HTTP
,HTTP_FILTER
,HTTP_INIT_FILTER
,HTTP_AUX_FILTER
,MAIL
,STREAM
, 或MISC
。 -
ngx_module_name
— 模块名称。要从一组源文件构建多个模块,请指定一个由空格分隔的名称列表。列表中的第一个名称表示动态模块的输出二进制文件名称。列表中的名称必须与源代码中使用的名称匹配。 -
ngx_addon_name
— 模块在 configure 脚本控制台输出中显示的名称。 -
ngx_module_srcs
— 用于编译模块的源文件列表,由空格分隔。可以使用$ngx_addon_dir
变量表示模块目录的路径。 -
ngx_module_incs
— 构建模块所需的包含路径 -
ngx_module_deps
— 模块依赖项列表,由空格分隔。通常是头文件列表。 -
ngx_module_libs
— 与模块链接的库列表,由空格分隔。例如,使用ngx_module_libs=-lpthread
来链接libpthread
库。可以使用以下宏来链接与 nginx 相同的库:LIBXSLT
,LIBGD
,GEOIP
,PCRE
,OPENSSL
,MD5
,SHA1
,ZLIB
, 和PERL
。 -
ngx_module_link
— 由构建系统设置的变量,对于动态模块设置为DYNAMIC
,对于静态模块设置为ADDON
,用于根据链接类型确定执行的不同操作。 -
ngx_module_order
— 模块加载顺序;对于HTTP_FILTER
和HTTP_AUX_FILTER
模块类型很有用。此选项的格式是由空格分隔的模块列表。列表中位于当前模块名称之后的所有模块将在全局模块列表中排在其后面,这确定了模块初始化的顺序。对于过滤器模块,稍后的初始化意味着更早的执行。以下模块通常用作参考。
ngx_http_copy_filter_module
为其他过滤器模块读取数据,并位于列表接近底部的位置,因此它是最早执行的模块之一。ngx_http_write_filter_module
将数据写入客户端套接字,并位于列表接近顶部的位置,是最后执行的模块。默认情况下,过滤器模块被放置在模块列表中
ngx_http_copy_filter
之前,以便过滤器处理程序在 copy filter 处理程序之后执行。对于其他模块类型,默认值为空字符串。
要将模块静态编译到 nginx 中,请使用 configure 脚本的 --add-module=/path/to/module
参数。要编译模块以便稍后动态加载到 nginx 中,请使用 --add-dynamic-module=/path/to/module
参数。
核心模块
模块是 nginx 的构建块,其大部分功能都作为模块实现。模块源文件必须包含一个类型为 ngx_module_t
的全局变量,其定义如下
struct ngx_module_s { /* private part is omitted */ void *ctx; ngx_command_t *commands; ngx_uint_t type; ngx_int_t (*init_master)(ngx_log_t *log); ngx_int_t (*init_module)(ngx_cycle_t *cycle); ngx_int_t (*init_process)(ngx_cycle_t *cycle); ngx_int_t (*init_thread)(ngx_cycle_t *cycle); void (*exit_thread)(ngx_cycle_t *cycle); void (*exit_process)(ngx_cycle_t *cycle); void (*exit_master)(ngx_cycle_t *cycle); /* stubs for future extensions are omitted */ };
省略的私有部分包含模块版本和签名,并使用预定义宏 NGX_MODULE_V1
填充。
每个模块都将其私有数据保存在 ctx
字段中,识别在 commands
数组中指定的配置指令,并可在 nginx 生命周期的特定阶段被调用。模块生命周期包括以下事件
- 配置指令处理程序在 master 进程上下文中按照它们在配置文件中出现的顺序被调用。
- 配置成功解析后,
init_module
处理程序在 master 进程上下文中被调用。init_module
处理程序在每次加载配置时都会在 master 进程中被调用。 - master 进程创建了一个或多个 worker 进程,并在每个 worker 进程中调用
init_process
处理程序。 - 当 worker 进程从 master 接收到 shutdown 或 terminate 命令时,它会调用
exit_process
处理程序。 - master 进程在退出前调用
exit_master
处理程序。
由于线程在 nginx 中仅用作带有自己 API 的辅助 I/O 功能,因此目前不会调用 init_thread
和 exit_thread
处理程序。也没有 init_master
处理程序,因为这会带来不必要的开销。
模块的 type
精确定义了存储在 ctx
字段中的内容。其值是以下类型之一
NGX_CORE_MODULE
NGX_EVENT_MODULE
NGX_HTTP_MODULE
NGX_MAIL_MODULE
NGX_STREAM_MODULE
NGX_CORE_MODULE
是最基本的模块类型,因此也是最通用、最底层的模块类型。其他模块类型在其之上实现,并提供了处理相应领域(如处理事件或 HTTP 请求)更方便的方式。
核心模块集包括 ngx_core_module
, ngx_errlog_module
, ngx_regex_module
, ngx_thread_pool_module
和 ngx_openssl_module
模块。HTTP 模块、stream 模块、mail 模块和 event 模块也是核心模块。核心模块的上下文定义为
typedef struct { ngx_str_t name; void *(*create_conf)(ngx_cycle_t *cycle); char *(*init_conf)(ngx_cycle_t *cycle, void *conf); } ngx_core_module_t;
其中 name
是模块名称字符串,create_conf
和 init_conf
分别是指向创建和初始化模块配置的函数的指针。对于核心模块,nginx 在解析新配置之前调用 create_conf
,并在所有配置成功解析后调用 init_conf
。典型的 create_conf
函数会为配置分配内存并设置默认值。
例如,一个简单的模块 ngx_foo_module
可能看起来像这样
/* * Copyright (C) Author. */ #include <ngx_config.h> #include <ngx_core.h> typedef struct { ngx_flag_t enable; } ngx_foo_conf_t; static void *ngx_foo_create_conf(ngx_cycle_t *cycle); static char *ngx_foo_init_conf(ngx_cycle_t *cycle, void *conf); static char *ngx_foo_enable(ngx_conf_t *cf, void *post, void *data); static ngx_conf_post_t ngx_foo_enable_post = { ngx_foo_enable }; static ngx_command_t ngx_foo_commands[] = { { ngx_string("foo_enabled"), NGX_MAIN_CONF|NGX_DIRECT_CONF|NGX_CONF_FLAG, ngx_conf_set_flag_slot, 0, offsetof(ngx_foo_conf_t, enable), &ngx_foo_enable_post }, ngx_null_command }; static ngx_core_module_t ngx_foo_module_ctx = { ngx_string("foo"), ngx_foo_create_conf, ngx_foo_init_conf }; ngx_module_t ngx_foo_module = { NGX_MODULE_V1, &ngx_foo_module_ctx, /* module context */ ngx_foo_commands, /* module directives */ NGX_CORE_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; static void * ngx_foo_create_conf(ngx_cycle_t *cycle) { ngx_foo_conf_t *fcf; fcf = ngx_pcalloc(cycle->pool, sizeof(ngx_foo_conf_t)); if (fcf == NULL) { return NULL; } fcf->enable = NGX_CONF_UNSET; return fcf; } static char * ngx_foo_init_conf(ngx_cycle_t *cycle, void *conf) { ngx_foo_conf_t *fcf = conf; ngx_conf_init_value(fcf->enable, 0); return NGX_CONF_OK; } static char * ngx_foo_enable(ngx_conf_t *cf, void *post, void *data) { ngx_flag_t *fp = data; if (*fp == 0) { return NGX_CONF_OK; } ngx_log_error(NGX_LOG_NOTICE, cf->log, 0, "Foo Module is enabled"); return NGX_CONF_OK; }
配置指令
ngx_command_t
类型定义了一个配置指令。每个支持配置的模块都提供了一个此类结构数组,描述如何处理参数以及调用哪些处理程序。
typedef struct ngx_command_s ngx_command_t; struct ngx_command_s { ngx_str_t name; ngx_uint_t type; char *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); ngx_uint_t conf; ngx_uint_t offset; void *post; };
使用特殊值 ngx_null_command
终止数组。name
是指令在配置文件中出现的名称,例如 "worker_processes" 或 "listen"。type
是一个标志位字段,指定指令接受的参数数量、其类型以及它出现的上下文。这些标志是
-
NGX_CONF_NOARGS
— 指令不接受参数。 -
NGX_CONF_1MORE
— 指令接受一个或多个参数。 -
NGX_CONF_2MORE
— 指令接受两个或多个参数。 -
NGX_CONF_TAKE1
..NGX_CONF_TAKE7
— 指令正好接受指定数量的参数。 -
NGX_CONF_TAKE12
,NGX_CONF_TAKE13
,NGX_CONF_TAKE23
,NGX_CONF_TAKE123
,NGX_CONF_TAKE1234
— 指令可以接受不同数量的参数。选项仅限于给定的数字。例如,NGX_CONF_TAKE12
表示它接受一个或两个参数。
指令类型的标志是
-
NGX_CONF_BLOCK
— 指令是一个块,也就是说,它可以包含在其开闭大括号内的其他指令,甚至可以实现自己的解析器来处理内部内容。 -
NGX_CONF_FLAG
— 指令接受布尔值,可以是on
或off
。
指令的上下文定义了它可能出现在配置中的位置
-
NGX_MAIN_CONF
— 在顶级上下文中。 -
NGX_HTTP_MAIN_CONF
— 在http
块中。 -
NGX_HTTP_SRV_CONF
— 在http
块内的server
块中。 -
NGX_HTTP_LOC_CONF
— 在http
块内的location
块中。 -
NGX_HTTP_UPS_CONF
— 在http
块内的upstream
块中。 -
NGX_HTTP_SIF_CONF
— 在http
块内server
块内的if
块中。 -
NGX_HTTP_LIF_CONF
— 在http
块内location
块内的if
块中。 -
NGX_HTTP_LMT_CONF
— 在http
块内的limit_except
块中。 -
NGX_STREAM_MAIN_CONF
— 在stream
块中。 -
NGX_STREAM_SRV_CONF
— 在stream
块内的server
块中。 -
NGX_STREAM_UPS_CONF
— 在stream
块内的upstream
块中。 -
NGX_MAIL_MAIN_CONF
— 在mail
块中。 -
NGX_MAIL_SRV_CONF
— 在mail
块内的server
块中。 -
NGX_EVENT_CONF
— 在event
块中。 -
NGX_DIRECT_CONF
— 由不创建上下文层次结构且只有一个全局配置的模块使用。此配置作为conf
参数传递给处理程序。
配置解析器使用这些标志在指令位置不正确时抛出错误,并调用带有适当配置指针的指令处理程序,以便同一指令在不同位置可以将值存储在不同的地方。
set
字段定义了一个处理指令并将解析值存储到相应配置中的处理程序。有一些函数执行常见的转换
-
ngx_conf_set_flag_slot
— 将字面字符串on
和off
分别转换为值为 1 或 0 的ngx_flag_t
类型。 -
ngx_conf_set_str_slot
— 将字符串存储为ngx_str_t
类型的值。 -
ngx_conf_set_str_array_slot
— 将值附加到字符串ngx_str_t
的数组ngx_array_t
中。如果数组不存在,则创建。 -
ngx_conf_set_keyval_slot
— 将键值对附加到键值对ngx_keyval_t
的数组ngx_array_t
中。第一个字符串成为键,第二个成为值。如果数组不存在,则创建。 -
ngx_conf_set_num_slot
— 将指令参数转换为ngx_int_t
类型的值。 -
ngx_conf_set_size_slot
— 将大小 (size) 转换为以字节表示的size_t
值。 -
ngx_conf_set_off_slot
— 将偏移 (offset) 转换为以字节表示的off_t
值。 -
ngx_conf_set_msec_slot
— 将时间 (time) 转换为以毫秒表示的ngx_msec_t
值。 -
ngx_conf_set_sec_slot
— 将时间 (time) 转换为以秒表示的time_t
值。 -
ngx_conf_set_bufs_slot
— 将提供的两个参数转换为一个ngx_bufs_t
对象,该对象包含缓冲区的数量和大小 (size)。 -
ngx_conf_set_enum_slot
— 将提供的参数转换为ngx_uint_t
值。在post
字段中传递的以 null 结尾的ngx_conf_enum_t
数组定义了可接受的字符串和相应的整数值。 -
ngx_conf_set_bitmask_slot
— 将提供的参数转换为ngx_uint_t
值。每个参数的掩码值进行按位或运算得到结果。在post
字段中传递的以 null 结尾的ngx_conf_bitmask_t
数组定义了可接受的字符串和相应的掩码值。 -
set_path_slot
— 将提供的参数转换为ngx_path_t
值并执行所有必需的初始化。详细信息请参阅 proxy_temp_path 指令的文档。 -
set_access_slot
— 将提供的参数转换为文件权限掩码。详细信息请参阅 proxy_store_access 指令的文档。
conf
字段定义了哪个配置结构会传递给指令处理程序。核心模块只有全局配置,并设置 NGX_DIRECT_CONF
标志来访问它。HTTP、Stream 或 Mail 等模块会创建配置层次结构。例如,模块的配置会为 server
、location
和 if
作用域创建。
-
NGX_HTTP_MAIN_CONF_OFFSET
—http
块的配置。 -
NGX_HTTP_SRV_CONF_OFFSET
—http
块内server
块的配置。 -
NGX_HTTP_LOC_CONF_OFFSET
—http
块内location
块的配置。 -
NGX_STREAM_MAIN_CONF_OFFSET
—stream
块的配置。 -
NGX_STREAM_SRV_CONF_OFFSET
—stream
块内server
块的配置。 -
NGX_MAIL_MAIN_CONF_OFFSET
—mail
块的配置。 -
NGX_MAIL_SRV_CONF_OFFSET
—mail
块内server
块的配置。
offset
定义了模块配置结构中某个字段的偏移量,该字段保存特定指令的值。典型用法是使用 offsetof()
宏。
post
字段有两个用途:它可以用于定义一个在主处理器完成之后调用的处理器,也可以用于向主处理器传递额外的数据。在前一种情况下,需要使用指向该处理器的指针来初始化 ngx_conf_post_t
结构体,例如
static char *ngx_do_foo(ngx_conf_t *cf, void *post, void *data); static ngx_conf_post_t ngx_foo_post = { ngx_do_foo };
post
参数是 ngx_conf_post_t
对象本身,而 data
是一个指向值的指针,该值由主处理器根据参数转换为适当的类型。
HTTP
连接
每个 HTTP 客户端连接都会经历以下阶段
-
ngx_event_accept()
接受一个客户端 TCP 连接。这个处理器是在监听套接字上收到读通知时被调用的。在此阶段会创建一个新的ngx_connection_t
对象来封装新接受的客户端套接字。每个 nginx 监听器都提供一个处理器来接收新的连接对象。对于 HTTP 连接来说,它是ngx_http_init_connection(c)
。 -
ngx_http_init_connection()
执行 HTTP 连接的早期初始化。在此阶段,会为该连接创建一个ngx_http_connection_t
对象,并将其引用存储在连接的data
字段中。稍后它将被 HTTP 请求对象替换。PROXY 协议解析器和 SSL 握手也会在此阶段启动。 -
ngx_http_wait_request_handler()
读取事件处理器在客户端套接字有数据可用时被调用。在此阶段,会创建一个 HTTP 请求对象ngx_http_request_t
并将其设置到连接的data
字段中。 -
ngx_http_process_request_line()
读取事件处理器读取客户端请求行。该处理器由ngx_http_wait_request_handler()
设置。数据被读取到连接的buffer
中。缓冲区的大小最初由指令 client_header_buffer_size 设置。整个客户端头部应该能容纳在这个缓冲区中。如果初始大小不足,则会分配一个更大的缓冲区,其容量由large_client_header_buffers
指令设置。 -
ngx_http_process_request_headers()
读取事件处理器,在ngx_http_process_request_line()
之后设置,用于读取客户端请求头部。 -
ngx_http_core_run_phases()
在请求头部完全读取和解析完成后被调用。此函数运行请求阶段从NGX_HTTP_POST_READ_PHASE
到NGX_HTTP_CONTENT_PHASE
。最后一个阶段旨在生成响应并沿过滤器链传递。响应不一定在此阶段发送给客户端。它可能保持缓冲状态,并在结束阶段发送。 -
ngx_http_finalize_request()
通常在请求生成所有输出或产生错误时被调用。在后一种情况下,会查找并使用适当的错误页面作为响应。如果此时响应尚未完全发送给客户端,则会激活一个 HTTP 写入器ngx_http_writer()
以完成发送剩余数据。 -
ngx_http_finalize_connection()
在完整响应已发送给客户端且请求可以销毁时被调用。如果客户端连接的 keepalive 功能已启用,则会调用ngx_http_set_keepalive()
,它会销毁当前请求并在该连接上等待下一个请求。否则,ngx_http_close_request()
会销毁请求和连接。
请求
对于每个客户端 HTTP 请求,都会创建一个 ngx_http_request_t
对象。该对象的一些字段是
-
connection
— 指向ngx_connection_t
客户端连接对象的指针。多个请求可以同时引用同一个连接对象——一个主请求及其子请求。请求删除后,可以在同一连接上创建新请求。请注意,对于 HTTP 连接,
ngx_connection_t
的data
字段指回请求。这些请求称为活动请求,与绑定到该连接的其他请求相对。活动请求用于处理客户端连接事件,并允许将其响应输出到客户端。通常,每个请求在某个时候都会变为活动状态,以便发送其输出。 -
ctx
— HTTP 模块上下文数组。每个类型为NGX_HTTP_MODULE
的模块都可以在请求中存储任何值(通常是指向结构体的指针)。该值存储在模块的ctx_index
位置的ctx
数组中。以下宏提供了方便的方式来获取和设置请求上下文-
ngx_http_get_module_ctx(r, module)
— 返回module
的上下文 -
ngx_http_set_ctx(r, c, module)
— 将c
设置为module
的上下文
-
-
main_conf
,srv_conf
,loc_conf
— 当前请求配置数组。配置存储在模块的ctx_index
位置。 -
read_event_handler
,write_event_handler
- 请求的读写事件处理器。通常,HTTP 连接的读写事件处理器都设置为ngx_http_request_handler()
。此函数会调用当前活动请求的read_event_handler
和write_event_handler
处理器。 -
cache
— 用于缓存 upstream 响应的请求缓存对象。 -
upstream
— 用于代理的请求 upstream 对象。 -
pool
— 请求内存池。请求对象本身在此内存池中分配,内存池在请求删除时销毁。对于需要在客户端连接生命周期内都可用的分配,请使用ngx_connection_t
的内存池。 -
header_in
— 客户端 HTTP 请求头部读取到的缓冲区。 -
headers_in
,headers_out
— 输入和输出 HTTP 头部对象。两个对象都包含类型为ngx_list_t
的headers
字段,用于存储原始头部列表。除此之外,可以通过单独的字段获取和设置特定头部,例如content_length_n
,status
等。 -
request_body
— 客户端请求体对象。 -
start_sec
,start_msec
— 请求创建时的时间点,用于跟踪请求持续时间。 -
method
,method_name
— 客户端 HTTP 请求方法的数字和文本表示。方法的数字值在src/http/ngx_http_request.h
中通过宏NGX_HTTP_GET
,NGX_HTTP_HEAD
,NGX_HTTP_POST
等定义。 -
http_protocol
— 客户端 HTTP 协议版本的原始文本形式(“HTTP/1.0”, “HTTP/1.1” 等)。 -
http_version
— 客户端 HTTP 协议版本的数字形式(NGX_HTTP_VERSION_10
,NGX_HTTP_VERSION_11
等)。 -
http_major
,http_minor
— 客户端 HTTP 协议版本的数字形式,拆分为主要和次要部分。 -
request_line
,unparsed_uri
— 原始客户端请求中的请求行和 URI。 -
uri
,args
,exten
— 当前请求的 URI、参数和文件扩展名。此处的 URI 值由于规范化可能与客户端发送的原始 URI 不同。在请求处理过程中,这些值可能会随着内部重定向的执行而改变。 -
main
— 指向主请求对象的指针。此对象是为了处理客户端 HTTP 请求而创建的,与子请求(用于在主请求中执行特定子任务)相对。 -
parent
— 指向子请求父请求的指针。 -
postponed
— 输出缓冲区和子请求的列表,按其发送和创建的顺序排列。此列表由 postpone 过滤器使用,以在部分输出由子请求创建时提供一致的请求输出。 -
post_subrequest
— 指向一个处理器及其上下文的指针,该处理器在子请求结束时被调用。对于主请求未使用。 -
posted_requests
— 要启动或恢复的请求列表,通过调用请求的write_event_handler
来完成。通常,此处理器包含请求的主函数,该函数首先运行请求阶段,然后生成输出。通常通过调用
ngx_http_post_request(r, NULL)
来提交请求。它总是提交到主请求的posted_requests
列表。函数ngx_http_run_posted_requests(c)
运行在传递的连接的活动请求的主请求中提交的所有请求。所有事件处理器都会调用ngx_http_run_posted_requests
,这可能导致新的请求被提交。通常,它在调用请求的读或写处理器之后被调用。 -
phase_handler
— 当前请求阶段的索引。 -
ncaptures
,captures
,captures_data
— 由请求最后一次正则匹配产生的捕获。在请求处理过程中,正则匹配可能发生在多个地方:map 查找、通过 SNI 或 HTTP Host 查找服务器、rewrite、proxy_redirect 等。查找产生的捕获存储在上述字段中。字段ncaptures
存储捕获的数量,captures
存储捕获的边界,captures_data
存储与正则表达式匹配的字符串,该字符串用于提取捕获。每次新的正则匹配后,请求的捕获都会被重置以存储新值。 -
count
— 请求引用计数器。此字段仅对主请求有意义。增加计数器通过简单的r->main->count++
完成。要减少计数器,请调用ngx_http_finalize_request(r, rc)
。子请求的创建和请求体读取过程的运行都会增加计数器。 -
subrequests
— 当前子请求嵌套级别。每个子请求继承其父请求的嵌套级别,并减一。如果该值达到零,则会生成错误。主请求的值由NGX_HTTP_MAX_SUBREQUESTS
常量定义。 -
uri_changes
— 请求剩余的 URI 更改次数。请求可以更改其 URI 的总次数受NGX_HTTP_MAX_URI_CHANGES
常量限制。每次更改后,该值递减,直到达到零,此时会生成错误。重写以及内部重定向到普通或命名 location 都被视为 URI 更改。 -
blocked
— 请求上的阻塞计数器。当此值非零时,请求无法终止。目前,此值由待处理的 AIO 操作(POSIX AIO 和线程操作)和活动缓存锁增加。 -
buffered
— 位掩码,显示哪些模块已缓冲请求产生的输出。许多过滤器可以缓冲输出;例如,sub_filter 可能由于部分字符串匹配而缓冲数据,copy 过滤器可能由于缺少空闲输出缓冲区而缓冲数据等。只要此值非零,请求就不会结束,直到刷新完成。 -
header_only
— 标志,指示输出不需要体。例如,HTTP HEAD 请求使用此标志。 -
keepalive
— 标志,指示是否支持客户端连接 keepalive。该值根据 HTTP 版本和“Connection”头部的值推断。 -
header_sent
— 标志,指示请求的输出头部已经发送。 -
internal
— 标志,指示当前请求是否为内部请求。要进入内部状态,请求必须通过内部重定向或是一个子请求。内部请求被允许进入内部 location。 -
allow_ranges
— 标志,指示可以向客户端发送部分响应,正如 HTTP Range 头部所请求的。 -
subrequest_ranges
— 标志,指示在处理子请求时可以发送部分响应。 -
single_range
— 标志,指示只能向客户端发送单一连续的输出数据范围。此标志通常在发送数据流(例如从代理服务器)时设置,并且整个响应不是在一个缓冲区中可用。 -
main_filter_need_in_memory
,filter_need_in_memory
— 标志,请求输出在内存缓冲区中产生而不是文件中。这是给 copy 过滤器的一个信号,即使启用了 sendfile,也要从文件缓冲区读取数据。这两个标志的区别在于设置它们的过滤器模块的位置。在过滤器链中在 postpone 过滤器之前调用的过滤器设置filter_need_in_memory
,请求只有当前请求的输出以内存缓冲区形式到来。在过滤器链中稍后调用的过滤器设置main_filter_need_in_memory
,请求主请求和所有子请求在发送输出时都将文件读取到内存中。 -
filter_need_temporary
— 标志,请求输出在临时缓冲区中产生,而不是在只读内存缓冲区或文件缓冲区中。这用于可能直接在发送的缓冲区中更改输出的过滤器。
配置
每个 HTTP 模块可以有三种类型的配置
- 主配置 — 适用于整个
http
块。作为模块的全局设置。 - 服务器配置 — 适用于单个
server
块。作为模块的特定服务器设置。 - Location 配置 — 适用于单个
location
,if
或limit_except
块。作为模块的特定 location 设置。
配置结构体在 nginx 配置阶段通过调用函数创建,这些函数分配结构体、初始化它们并合并它们。以下示例展示了如何为模块创建一个简单的 location 配置。该配置有一个设置 foo
,类型为无符号整数。
typedef struct { ngx_uint_t foo; } ngx_http_foo_loc_conf_t; static ngx_http_module_t ngx_http_foo_module_ctx = { NULL, /* preconfiguration */ NULL, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ ngx_http_foo_create_loc_conf, /* create location configuration */ ngx_http_foo_merge_loc_conf /* merge location configuration */ }; static void * ngx_http_foo_create_loc_conf(ngx_conf_t *cf) { ngx_http_foo_loc_conf_t *conf; conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_foo_loc_conf_t)); if (conf == NULL) { return NULL; } conf->foo = NGX_CONF_UNSET_UINT; return conf; } static char * ngx_http_foo_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child) { ngx_http_foo_loc_conf_t *prev = parent; ngx_http_foo_loc_conf_t *conf = child; ngx_conf_merge_uint_value(conf->foo, prev->foo, 1); }
如示例所示,ngx_http_foo_create_loc_conf()
函数创建一个新的配置结构体,而 ngx_http_foo_merge_loc_conf()
将配置与更高层级的配置合并。实际上,服务器和 location 配置不仅存在于服务器和 location 级别,还为它们以上的所有级别创建。具体来说,服务器配置也在主级别创建,而 location 配置在主、服务器和 location 级别创建。这些配置使得可以在 nginx 配置文件的任何级别指定特定于服务器和 location 的设置。最终配置会向下合并。提供了许多宏,如 NGX_CONF_UNSET
和 NGX_CONF_UNSET_UINT
,用于指示缺失的设置并在合并时忽略它。标准的 nginx 合并宏,如 ngx_conf_merge_value()
和 ngx_conf_merge_uint_value()
,提供了方便的方式来合并设置,并在没有任何配置提供显式值时设置默认值。有关不同类型宏的完整列表,请参见 src/core/ngx_conf_file.h
。
以下宏可用于在配置时访问 HTTP 模块的配置。它们都将 ngx_conf_t
引用作为第一个参数。
-
ngx_http_conf_get_module_main_conf(cf, module)
-
ngx_http_conf_get_module_srv_conf(cf, module)
-
ngx_http_conf_get_module_loc_conf(cf, module)
以下示例获取指向标准 nginx 核心模块 ngx_http_core_module 的 location 配置的指针,并替换结构体 handler
字段中保存的 location 内容处理器。
static ngx_int_t ngx_http_foo_handler(ngx_http_request_t *r); static ngx_command_t ngx_http_foo_commands[] = { { ngx_string("foo"), NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS, ngx_http_foo, 0, 0, NULL }, ngx_null_command }; static char * ngx_http_foo(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { ngx_http_core_loc_conf_t *clcf; clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); clcf->handler = ngx_http_bar_handler; return NGX_CONF_OK; }
以下宏可用于在运行时访问 HTTP 模块的配置。
-
ngx_http_get_module_main_conf(r, module)
-
ngx_http_get_module_srv_conf(r, module)
-
ngx_http_get_module_loc_conf(r, module)
这些宏接收对 HTTP 请求 ngx_http_request_t
的引用。请求的主配置永远不会改变。服务器配置在为请求选择虚拟服务器后可能会从默认值改变。为处理请求选择的 location 配置可能会因重写操作或内部重定向而多次改变。以下示例展示了如何在运行时访问模块的 HTTP 配置。
static ngx_int_t ngx_http_foo_handler(ngx_http_request_t *r) { ngx_http_foo_loc_conf_t *flcf; flcf = ngx_http_get_module_loc_conf(r, ngx_http_foo_module); ... }
阶段
每个 HTTP 请求都会经历一系列阶段。在每个阶段,都会对请求执行不同类型的处理。模块特定的处理器可以在大多数阶段注册,许多标准的 nginx 模块注册其阶段处理器,以此在请求处理的特定阶段被调用。阶段是依次处理的,阶段处理器在请求到达该阶段后被调用。以下是 nginx HTTP 阶段列表。
-
NGX_HTTP_POST_READ_PHASE
— 第一个阶段。ngx_http_realip_module 在此阶段注册其处理器,以便在调用任何其他模块之前启用客户端地址替换。 -
NGX_HTTP_SERVER_REWRITE_PHASE
— 处理在server
块中定义(但在location
块外部)的重写指令的阶段。ngx_http_rewrite_module 在此阶段安装其处理器。 -
NGX_HTTP_FIND_CONFIG_PHASE
— 特殊阶段,根据请求 URI 选择 location。在此阶段之前,相关虚拟服务器的默认 location 被分配给请求,并且请求 location 配置的任何模块都会接收默认服务器 location 的配置。此阶段将新的 location 分配给请求。在此阶段不能注册额外的处理器。 -
NGX_HTTP_REWRITE_PHASE
— 与NGX_HTTP_SERVER_REWRITE_PHASE
相同,但用于在上一个阶段选择的 location 中定义的重写规则。 -
NGX_HTTP_POST_REWRITE_PHASE
— 特殊阶段,如果请求的 URI 在重写期间更改,则将请求重定向到新的 location。这是通过请求再次经历NGX_HTTP_FIND_CONFIG_PHASE
来实现的。在此阶段不能注册额外的处理器。 -
NGX_HTTP_PREACCESS_PHASE
— 不同类型处理器的通用阶段,与访问控制无关。标准的 nginx 模块,如 ngx_http_limit_conn_module 和 ngx_http_limit_req_module 在此阶段注册其处理器。 -
NGX_HTTP_ACCESS_PHASE
— 验证客户端是否已授权进行请求的阶段。标准的 nginx 模块,如 ngx_http_access_module 和 ngx_http_auth_basic_module 在此阶段注册其处理器。默认情况下,客户端必须通过在此阶段注册的所有处理器的授权检查,请求才能继续到下一个阶段。satisfy 指令可用于允许处理继续,如果任何阶段处理器授权客户端。 -
NGX_HTTP_POST_ACCESS_PHASE
— 特殊阶段,处理 satisfy any 指令。如果某些访问阶段处理器拒绝了访问,并且没有显式允许访问,则请求会结束。在此阶段不能注册额外的处理器。 -
NGX_HTTP_PRECONTENT_PHASE
— 在生成内容之前调用的处理器的阶段。标准模块,如 ngx_http_try_files_module 和 ngx_http_mirror_module 在此阶段注册其处理器。 -
NGX_HTTP_CONTENT_PHASE
— 通常生成响应的阶段。多个 nginx 标准模块在此阶段注册其处理器,包括 ngx_http_index_module 或ngx_http_static_module
。它们会依次被调用,直到其中一个生成输出。也可以按 location 设置内容处理器。如果 ngx_http_core_module 的 location 配置设置了handler
,则将其作为内容处理器调用,而在此阶段安装的处理器会被忽略。 -
NGX_HTTP_LOG_PHASE
— 执行请求日志记录的阶段。目前,只有 ngx_http_log_module 在此阶段注册其处理器用于访问日志记录。日志阶段处理器在请求处理的最后阶段,就在释放请求之前被调用。
以下是 preaccess 阶段处理器的示例。
static ngx_http_module_t ngx_http_foo_module_ctx = { NULL, /* preconfiguration */ ngx_http_foo_init, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ NULL, /* create location configuration */ NULL /* merge location configuration */ }; static ngx_int_t ngx_http_foo_handler(ngx_http_request_t *r) { ngx_table_elt_t *ua; ua = r->headers_in.user_agent; if (ua == NULL) { return NGX_DECLINED; } /* reject requests with "User-Agent: foo" */ if (ua->value.len == 3 && ngx_strncmp(ua->value.data, "foo", 3) == 0) { return NGX_HTTP_FORBIDDEN; } return NGX_DECLINED; } static ngx_int_t ngx_http_foo_init(ngx_conf_t *cf) { ngx_http_handler_pt *h; ngx_http_core_main_conf_t *cmcf; cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module); h = ngx_array_push(&cmcf->phases[NGX_HTTP_PREACCESS_PHASE].handlers); if (h == NULL) { return NGX_ERROR; } *h = ngx_http_foo_handler; return NGX_OK; }
阶段处理器应返回特定的代码
-
NGX_OK
— 继续到下一个阶段。 -
NGX_DECLINED
— 继续到当前阶段的下一个处理器。如果当前处理器是当前阶段的最后一个,则移动到下一个阶段。 -
NGX_AGAIN
,NGX_DONE
— 暂停阶段处理,直到将来的某个事件发生,例如异步 I/O 操作或仅是一个延迟。假定稍后通过调用ngx_http_core_run_phases()
来恢复阶段处理。 - 阶段处理器返回的任何其他值都被视为请求结束码,特别是 HTTP 响应码。请求将使用提供的代码结束。
对于某些阶段,返回码的处理方式略有不同。在 content 阶段,除 NGX_DECLINED
之外的任何返回码都被视为结束码。location 内容处理器的任何返回码都被视为结束码。在 access 阶段,在 satisfy any 模式下,除 NGX_OK
, NGX_DECLINED
, NGX_AGAIN
, NGX_DONE
之外的任何返回码都被视为拒绝。如果没有后续的 access 处理器以不同的代码允许或拒绝访问,则拒绝码将成为结束码。
变量
访问现有变量
变量可以通过索引(这是最常用的方法)或名称(参见下文)引用。索引在配置阶段创建,当变量添加到配置中时。要获取变量索引,使用 ngx_http_get_variable_index()
ngx_str_t name; /* ngx_string("foo") */ ngx_int_t index; index = ngx_http_get_variable_index(cf, &name);
此处,cf
是指向 nginx 配置的指针,name
指向包含变量名称的字符串。该函数在出错时返回 NGX_ERROR
,否则返回有效的索引,通常存储在模块配置中的某个位置以供将来使用。
所有 HTTP 变量都在给定 HTTP 请求的上下文中求值,结果特定于该 HTTP 请求并缓存在其中。所有求值变量的函数都返回 ngx_http_variable_value_t
类型,表示变量值
typedef ngx_variable_value_t ngx_http_variable_value_t; typedef struct { unsigned len:28; unsigned valid:1; unsigned no_cacheable:1; unsigned not_found:1; unsigned escape:1; u_char *data; } ngx_variable_value_t;
其中
-
len
— 值的长度 -
data
— 值本身 -
valid
— 值有效 -
not_found
— 未找到变量,因此data
和len
字段无关紧要;例如,当请求中未传递相应的参数时,变量如$arg_foo
可能发生这种情况 -
no_cacheable
— 不缓存结果 -
escape
— 日志模块内部使用,标记输出时需要转义的值。
ngx_http_get_flushed_variable()
和 ngx_http_get_indexed_variable()
函数用于获取变量的值。它们具有相同的接口——接受 HTTP 请求 r
作为变量求值的上下文,以及一个标识它的 index
。典型用法示例
ngx_http_variable_value_t *v; v = ngx_http_get_flushed_variable(r, index); if (v == NULL || v->not_found) { /* we failed to get value or there is no such variable, handle it */ return NGX_ERROR; } /* some meaningful value is found */
这两个函数之间的区别在于,ngx_http_get_indexed_variable()
返回缓存的值,而 ngx_http_get_flushed_variable()
会刷新不可缓存变量的缓存。
一些模块,如 SSI 和 Perl,需要处理在配置时名称未知的变量。因此不能使用索引来访问它们,但可以使用 ngx_http_get_variable(r, name, key)
函数。它搜索具有给定 name
及其从名称派生的哈希 key
的变量。
创建变量
要创建变量,使用 ngx_http_add_variable()
函数。它接受配置(变量在此注册)、变量名称和控制函数行为的标志作为参数
NGX_HTTP_VAR_CHANGEABLE
— 允许变量的重新定义:如果另一个模块定义同名变量,则不会发生冲突。这使得 set 指令可以覆盖变量。NGX_HTTP_VAR_NOCACHEABLE
— 禁用缓存,这对于像$time_local
这样的变量很有用。NGX_HTTP_VAR_NOHASH
— 指示此变量只能通过索引访问,不能通过名称访问。当已知 SSI 或 Perl 等模块不需要此变量时,这是一种小的优化。NGX_HTTP_VAR_PREFIX
— 变量名称是一个前缀。在这种情况下,处理器必须实现额外的逻辑来获取特定变量的值。例如,所有 “arg_
” 变量都由同一个处理器处理,该处理器在请求参数中执行查找并返回特定参数的值。
该函数在出错时返回 NULL,否则返回指向 ngx_http_variable_t
的指针
struct ngx_http_variable_s { ngx_str_t name; ngx_http_set_variable_pt set_handler; ngx_http_get_variable_pt get_handler; uintptr_t data; ngx_uint_t flags; ngx_uint_t index; };
get
和 set
处理器用于获取或设置变量值,data
传递给变量处理器,index
存储分配的变量索引,用于引用变量。
通常,模块创建一个以 null 结尾的静态 ngx_http_variable_t
结构体数组,并在预配置阶段处理,将变量添加到配置中,例如
static ngx_http_variable_t ngx_http_foo_vars[] = { { ngx_string("foo_v1"), NULL, ngx_http_foo_v1_variable, 0, 0, 0 }, ngx_http_null_variable }; static ngx_int_t ngx_http_foo_add_variables(ngx_conf_t *cf) { ngx_http_variable_t *var, *v; for (v = ngx_http_foo_vars; v->name.len; v++) { var = ngx_http_add_variable(cf, &v->name, v->flags); if (var == NULL) { return NGX_ERROR; } var->get_handler = v->get_handler; var->data = v->data; } return NGX_OK; }
示例中的此函数用于初始化 HTTP 模块上下文的 preconfiguration
字段,并在解析 HTTP 配置之前被调用,以便解析器可以引用这些变量。
get
处理器负责在特定请求的上下文中求值变量,例如
static ngx_int_t ngx_http_variable_connection(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) { u_char *p; p = ngx_pnalloc(r->pool, NGX_ATOMIC_T_LEN); if (p == NULL) { return NGX_ERROR; } v->len = ngx_sprintf(p, "%uA", r->connection->number) - p; v->valid = 1; v->no_cacheable = 0; v->not_found = 0; v->data = p; return NGX_OK; }
它在发生内部错误(例如,内存分配失败)时返回 NGX_ERROR
,否则返回 NGX_OK
。要了解变量求值的状态,请检查 ngx_http_variable_value_t
中的标志(参见上文描述)。
set
处理器允许设置由变量引用的属性。例如,$limit_rate
变量的 set 处理器修改请求的 limit_rate
字段
... { ngx_string("limit_rate"), ngx_http_variable_request_set_size, ngx_http_variable_request_get_size, offsetof(ngx_http_request_t, limit_rate), NGX_HTTP_VAR_CHANGEABLE|NGX_HTTP_VAR_NOCACHEABLE, 0 }, ... static void ngx_http_variable_request_set_size(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) { ssize_t s, *sp; ngx_str_t val; val.len = v->len; val.data = v->data; s = ngx_parse_size(&val); if (s == NGX_ERROR) { ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, "invalid size \"%V\"", &val); return; } sp = (ssize_t *) ((char *) r + data); *sp = s; return; }
复杂值
复杂值,尽管其名称如此,但提供了一种简单的方式来求值包含文本、变量及其组合的表达式。
ngx_http_compile_complex_value
中的复杂值描述在配置阶段被编译为 ngx_http_complex_value_t
,该结构体在运行时用于获取表达式求值的结果。
ngx_str_t *value; ngx_http_complex_value_t cv; ngx_http_compile_complex_value_t ccv; value = cf->args->elts; /* directive arguments */ ngx_memzero(&ccv, sizeof(ngx_http_compile_complex_value_t)); ccv.cf = cf; ccv.value = &value[1]; ccv.complex_value = &cv; ccv.zero = 1; ccv.conf_prefix = 1; if (ngx_http_compile_complex_value(&ccv) != NGX_OK) { return NGX_CONF_ERROR; }
这里,ccv
包含初始化复杂值 cv
所需的所有参数
-
cf
— 配置指针 -
value
— 要解析的字符串(输入) -
complex_value
— 编译后的值(输出) -
zero
— 启用值零终止的标志 -
conf_prefix
— 以配置前缀(nginx 当前查找配置的目录)作为结果的前缀 -
root_prefix
— 以根前缀(正常的 nginx 安装前缀)作为结果的前缀
当结果需要传递给需要零终止字符串的库时,zero
标志很有用;处理文件名时,前缀很方便。
成功编译后,cv.lengths
包含关于表达式中是否存在变量的信息。NULL 值表示表达式仅包含静态文本,因此可以存储在一个简单的字符串中,而不是作为复杂值。
ngx_http_set_complex_value_slot()
是一个方便的函数,用于在指令声明本身中完全初始化复杂值。
在运行时,可以使用 ngx_http_complex_value()
函数计算复杂值
ngx_str_t res; if (ngx_http_complex_value(r, &cv, &res) != NGX_OK) { return NGX_ERROR; }
给定请求 r
和之前编译的值 cv
,该函数求值表达式并将结果写入 res
。
请求重定向
HTTP 请求总是通过 ngx_http_request_t
结构体的 loc_conf
字段连接到 location。这意味着在任何时候都可以通过调用 ngx_http_get_module_loc_conf(r, module)
从请求中检索任何模块的 location 配置。请求的 location 在其生命周期内可能会改变几次。最初,默认服务器的默认服务器 location 被分配给请求。如果请求切换到不同的服务器(通过 HTTP “Host” 头部或 SSL SNI 扩展选择),请求也会切换到该服务器的默认 location。location 的下一次更改发生在 NGX_HTTP_FIND_CONFIG_PHASE
请求阶段。在此阶段,会根据请求 URI 在为服务器配置的所有非命名 location 中选择一个 location。ngx_http_rewrite_module 可以在 NGX_HTTP_REWRITE_PHASE
请求阶段通过 rewrite
指令更改请求 URI,并将请求发送回 NGX_HTTP_FIND_CONFIG_PHASE
阶段,以便根据新的 URI 选择新的 location。
也可以在任何时候通过调用 ngx_http_internal_redirect(r, uri, args)
或 ngx_http_named_location(r, name)
之一将请求重定向到新的 location。
ngx_http_internal_redirect(r, uri, args)
函数更改请求 URI 并将请求返回到 NGX_HTTP_SERVER_REWRITE_PHASE
阶段。请求将使用服务器默认 location 继续处理。稍后在 NGX_HTTP_FIND_CONFIG_PHASE
阶段,会根据新的请求 URI 选择新的 location。
以下示例使用新的请求参数执行内部重定向。
ngx_int_t ngx_http_foo_redirect(ngx_http_request_t *r) { ngx_str_t uri, args; ngx_str_set(&uri, "/foo"); ngx_str_set(&args, "bar=1"); return ngx_http_internal_redirect(r, &uri, &args); }
函数 ngx_http_named_location(r, name)
将请求重定向到命名 location。location 的名称作为参数传递。该 location 在当前服务器的所有命名 location 中查找,之后请求切换到 NGX_HTTP_REWRITE_PHASE
阶段。
以下示例将请求重定向到命名 location @foo。
ngx_int_t ngx_http_foo_named_redirect(ngx_http_request_t *r) { ngx_str_t name; ngx_str_set(&name, "foo"); return ngx_http_named_location(r, &name); }
这两个函数—— ngx_http_internal_redirect(r, uri, args)
和 ngx_http_named_location(r, name)
都可以在 nginx 模块已经将一些上下文存储在请求的 ctx
字段中时调用。这些上下文可能与新的 location 配置变得不一致。为了防止不一致,所有请求上下文都会被这两个重定向函数擦除。
调用 ngx_http_internal_redirect(r, uri, args)
或 ngx_http_named_location(r, name)
会增加请求的 count
。为了保持一致的请求引用计数,在重定向请求后调用 ngx_http_finalize_request(r, NGX_DONE)
。这将结束当前请求的代码路径并减少计数器。
重定向和重写请求会变为内部请求,可以访问 internal
location。内部请求设置了 internal
标志。
子请求
子请求主要用于将一个请求的输出插入到另一个请求中,可能与其它数据混合。子请求看起来像一个普通请求,但与其父请求共享一些数据。特别是,所有与客户端输入相关的字段都共享,因为子请求不接收客户端的任何其他输入。子请求的请求字段 parent
包含指向其父请求的链接,对于主请求则为 NULL。字段 main
包含指向请求组中主请求的链接。
子请求从 NGX_HTTP_SERVER_REWRITE_PHASE
阶段开始。它经历与普通请求相同的后续阶段,并根据其自己的 URI 分配 location。
子请求中的输出头部总是被忽略。ngx_http_postpone_filter
将子请求的输出体放置在相对于父请求产生的其他数据的正确位置。
子请求与活动请求的概念相关。如果 c->data == r
,则请求 r
被视为活动请求,其中 c
是客户端连接对象。在任何给定时间点,请求组中只有活动请求被允许将其缓冲区输出到客户端。非活动请求仍然可以将其输出发送到过滤器链,但它不会超过 ngx_http_postpone_filter
,并保持由该过滤器缓冲,直到该请求变为活动状态。以下是请求激活的一些规则
- 最初,主请求是活动的。
- 活动请求的第一个子请求在创建后立即变为活动状态。
- 一旦在该请求之前的所有数据都已发送,
ngx_http_postpone_filter
会激活活动请求子请求列表中的下一个请求。 - 请求结束时,其父请求被激活。
通过调用函数 ngx_http_subrequest(r, uri, args, psr, ps, flags)
创建子请求,其中 r
是父请求,uri
和 args
是子请求的 URI 和参数,psr
是输出参数,接收新创建的子请求引用,ps
是一个回调对象,用于通知父请求子请求正在结束,flags
是标志的位掩码。以下标志可用
-
NGX_HTTP_SUBREQUEST_IN_MEMORY
- 输出不发送到客户端,而是存储在内存中。此标志仅影响由其中一个代理模块处理的子请求。子请求结束(finalized)后,其输出可在类型为ngx_buf_t
的r->out
中获取。 -
NGX_HTTP_SUBREQUEST_WAITED
- 即使子请求在结束时不是活动状态,也会设置其done
标志。SSI 过滤器使用此子请求标志。 -
NGX_HTTP_SUBREQUEST_CLONE
- 子请求作为其父请求的克隆创建。它从与父请求相同的 location 开始,并从相同的阶段继续处理。
以下示例创建一个 URI 为 /foo
的子请求。
ngx_int_t rc; ngx_str_t uri; ngx_http_request_t *sr; ... ngx_str_set(&uri, "/foo"); rc = ngx_http_subrequest(r, &uri, NULL, &sr, NULL, 0); if (rc == NGX_ERROR) { /* error */ }
此示例克隆当前请求并为子请求设置一个结束回调。
ngx_int_t ngx_http_foo_clone(ngx_http_request_t *r) { ngx_http_request_t *sr; ngx_http_post_subrequest_t *ps; ps = ngx_palloc(r->pool, sizeof(ngx_http_post_subrequest_t)); if (ps == NULL) { return NGX_ERROR; } ps->handler = ngx_http_foo_subrequest_done; ps->data = "foo"; return ngx_http_subrequest(r, &r->uri, &r->args, &sr, ps, NGX_HTTP_SUBREQUEST_CLONE); } ngx_int_t ngx_http_foo_subrequest_done(ngx_http_request_t *r, void *data, ngx_int_t rc) { char *msg = (char *) data; ngx_log_error(NGX_LOG_INFO, r->connection->log, 0, "done subrequest r:%p msg:%s rc:%i", r, msg, rc); return rc; }
子请求通常在 body 过滤器中创建,在这种情况下,它们的输出可以像任何显式请求的输出一样处理。这意味着最终,子请求的输出会在创建子请求之前传递的所有显式缓冲区之后,以及创建之后传递的任何缓冲区之前发送给客户端。即使对于大量嵌套的子请求,这种顺序也会保留。以下示例将子请求的输出插入到所有请求数据缓冲区之后,但在带有 last_buf
标志的最终缓冲区之前。
ngx_int_t ngx_http_foo_body_filter(ngx_http_request_t *r, ngx_chain_t *in) { ngx_int_t rc; ngx_buf_t *b; ngx_uint_t last; ngx_chain_t *cl, out; ngx_http_request_t *sr; ngx_http_foo_filter_ctx_t *ctx; ctx = ngx_http_get_module_ctx(r, ngx_http_foo_filter_module); if (ctx == NULL) { return ngx_http_next_body_filter(r, in); } last = 0; for (cl = in; cl; cl = cl->next) { if (cl->buf->last_buf) { cl->buf->last_buf = 0; cl->buf->last_in_chain = 1; cl->buf->sync = 1; last = 1; } } /* Output explicit output buffers */ rc = ngx_http_next_body_filter(r, in); if (rc == NGX_ERROR || !last) { return rc; } /* * Create the subrequest. The output of the subrequest * will automatically be sent after all preceding buffers, * but before the last_buf buffer passed later in this function. */ if (ngx_http_subrequest(r, ctx->uri, NULL, &sr, NULL, 0) != NGX_OK) { return NGX_ERROR; } ngx_http_set_ctx(r, NULL, ngx_http_foo_filter_module); /* Output the final buffer with the last_buf flag */ b = ngx_calloc_buf(r->pool); if (b == NULL) { return NGX_ERROR; } b->last_buf = 1; out.buf = b; out.next = NULL; return ngx_http_output_filter(r, &out); }
子请求(subrequest)也可以用于数据输出以外的其他目的。例如,ngx_http_auth_request_module 模块在 NGX_HTTP_ACCESS_PHASE
阶段创建一个子请求。为了在该阶段禁用输出,子请求会设置 header_only
标志。这会阻止子请求的正文发送给客户端。请注意,子请求的头部永远不会发送给客户端。子请求的结果可以在回调处理程序中进行分析。
请求终结
HTTP 请求通过调用函数 ngx_http_finalize_request(r, rc)
来结束。通常,内容处理程序在所有输出缓冲区发送到过滤链后结束请求。此时,并非所有输出都已发送到客户端,其中一部分可能仍然缓冲在过滤链的某个位置。如果是这样,ngx_http_finalize_request(r, rc)
函数会自动安装一个特殊的处理程序 ngx_http_writer(r)
来完成发送输出。请求也会在发生错误或需要向客户端返回标准 HTTP 响应码时结束。
函数 ngx_http_finalize_request(r, rc)
期望以下 rc
值
-
NGX_DONE
- 快速结束。递减请求的count
,如果达到零,则销毁请求。在当前请求销毁后,客户端连接可以用于更多请求。 -
NGX_ERROR
,NGX_HTTP_REQUEST_TIME_OUT
(408
),NGX_HTTP_CLIENT_CLOSED_REQUEST
(499
) - 错误结束。尽快终止请求并关闭客户端连接。 -
NGX_HTTP_CREATED
(201
),NGX_HTTP_NO_CONTENT
(204
), 大于或等于NGX_HTTP_SPECIAL_RESPONSE
(300
) 的代码 - 特殊响应结束。对于这些值,nginx 要么向客户端发送该代码的默认响应页面,要么如果为该代码配置了 error_page 位置,则执行内部重定向。 - 其他代码被认为是成功的结束代码,并且可能会激活请求写入器以完成发送响应正文。一旦正文完全发送,请求的
count
就会递减。如果达到零,则请求被销毁,但客户端连接仍然可以用于其他请求。如果count
为正,则请求中存在未完成的活动,这些活动将在稍后结束。
请求体
为了处理客户端请求正文,nginx 提供了 ngx_http_read_client_request_body(r, post_handler)
和 ngx_http_discard_request_body(r)
函数。第一个函数读取请求正文并使其通过 request_body
请求字段可用。第二个函数指示 nginx 丢弃(读取并忽略)请求正文。每个请求都必须调用这两个函数之一。通常,内容处理程序会进行调用。
不允许从子请求中读取或丢弃客户端请求正文。这必须始终在主请求中完成。创建子请求时,它继承父请求的 request_body
对象,如果主请求之前已经读取了请求正文,子请求可以使用该对象。
函数 ngx_http_read_client_request_body(r, post_handler)
启动读取请求正文的过程。当正文完全读取后,会调用 post_handler
回调函数来继续处理请求。如果请求正文缺失或已经读取,回调会立即调用。函数 ngx_http_read_client_request_body(r, post_handler)
分配类型为 ngx_http_request_body_t
的 request_body
请求字段。该对象的 bufs
字段以缓冲区链的形式保存结果。如果 client_body_buffer_size 指令指定的容量不足以将整个正文放入内存中,则正文可以保存在内存缓冲区或文件缓冲区中。
以下示例读取客户端请求正文并返回其大小。
ngx_int_t ngx_http_foo_content_handler(ngx_http_request_t *r) { ngx_int_t rc; rc = ngx_http_read_client_request_body(r, ngx_http_foo_init); if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { /* error */ return rc; } return NGX_DONE; } void ngx_http_foo_init(ngx_http_request_t *r) { off_t len; ngx_buf_t *b; ngx_int_t rc; ngx_chain_t *in, out; if (r->request_body == NULL) { ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); return; } len = 0; for (in = r->request_body->bufs; in; in = in->next) { len += ngx_buf_size(in->buf); } b = ngx_create_temp_buf(r->pool, NGX_OFF_T_LEN); if (b == NULL) { ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); return; } b->last = ngx_sprintf(b->pos, "%O", len); b->last_buf = (r == r->main) ? 1 : 0; b->last_in_chain = 1; r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = b->last - b->pos; rc = ngx_http_send_header(r); if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) { ngx_http_finalize_request(r, rc); return; } out.buf = b; out.next = NULL; rc = ngx_http_output_filter(r, &out); ngx_http_finalize_request(r, rc); }
以下请求字段决定了如何读取请求正文
-
request_body_in_single_buf
- 将正文读取到单个内存缓冲区中。 -
request_body_in_file_only
- 总是将正文读取到文件中,即使它适合内存缓冲区。 -
request_body_in_persistent_file
- 创建后不要立即取消链接文件。带有此标志的文件可以移动到另一个目录。 -
request_body_in_clean_file
- 请求结束时取消链接文件。当文件本来应该移动到另一个目录但由于某种原因未能移动时,这会很有用。 -
request_body_file_group_access
- 启用对文件的组访问,将默认的 0600 访问掩码替换为 0660。 -
request_body_file_log_level
- 记录文件错误的严重级别。 -
request_body_no_buffering
- 无缓冲地读取请求正文。
request_body_no_buffering
标志启用无缓冲模式读取请求正文。在此模式下,调用 ngx_http_read_client_request_body()
后,bufs
链可能只保留正文的一部分。要读取下一部分,请调用 ngx_http_read_unbuffered_request_body(r)
函数。返回值 NGX_AGAIN
和请求标志 reading_body
表示有更多数据可用。如果调用此函数后 bufs
为 NULL,则当前没有数据可读。当请求正文的下一部分可用时,将调用请求回调 read_event_handler
。
请求体过滤器
读取请求正文的一部分后,通过调用存储在 ngx_http_top_request_body_filter
变量中的第一个正文过滤器处理程序,将其传递给请求正文过滤链。假设每个正文处理程序都会调用链中的下一个处理程序,直到调用最终处理程序 ngx_http_request_body_save_filter(r, cl)
。此处理程序收集 r->request_body->bufs
中的缓冲区,并在必要时将其写入文件。最后一个请求正文缓冲区具有非零的 last_buf
标志。
如果过滤器计划延迟数据缓冲区,它应该在第一次被调用时将标志 r->request_body->filter_need_buffering
设置为 1
。
以下是一个简单的请求正文过滤器示例,它将请求正文延迟一秒。
#include <ngx_config.h> #include <ngx_core.h> #include <ngx_http.h> #define NGX_HTTP_DELAY_BODY 1000 typedef struct { ngx_event_t event; ngx_chain_t *out; } ngx_http_delay_body_ctx_t; static ngx_int_t ngx_http_delay_body_filter(ngx_http_request_t *r, ngx_chain_t *in); static void ngx_http_delay_body_cleanup(void *data); static void ngx_http_delay_body_event_handler(ngx_event_t *ev); static ngx_int_t ngx_http_delay_body_init(ngx_conf_t *cf); static ngx_http_module_t ngx_http_delay_body_module_ctx = { NULL, /* preconfiguration */ ngx_http_delay_body_init, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ NULL, /* create location configuration */ NULL /* merge location configuration */ }; ngx_module_t ngx_http_delay_body_filter_module = { NGX_MODULE_V1, &ngx_http_delay_body_module_ctx, /* module context */ NULL, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; static ngx_http_request_body_filter_pt ngx_http_next_request_body_filter; static ngx_int_t ngx_http_delay_body_filter(ngx_http_request_t *r, ngx_chain_t *in) { ngx_int_t rc; ngx_chain_t *cl, *ln; ngx_http_cleanup_t *cln; ngx_http_delay_body_ctx_t *ctx; ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "delay request body filter"); ctx = ngx_http_get_module_ctx(r, ngx_http_delay_body_filter_module); if (ctx == NULL) { ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_delay_body_ctx_t)); if (ctx == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } ngx_http_set_ctx(r, ctx, ngx_http_delay_body_filter_module); r->request_body->filter_need_buffering = 1; } if (ngx_chain_add_copy(r->pool, &ctx->out, in) != NGX_OK) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } if (!ctx->event.timedout) { if (!ctx->event.timer_set) { /* cleanup to remove the timer in case of abnormal termination */ cln = ngx_http_cleanup_add(r, 0); if (cln == NULL) { return NGX_HTTP_INTERNAL_SERVER_ERROR; } cln->handler = ngx_http_delay_body_cleanup; cln->data = ctx; /* add timer */ ctx->event.handler = ngx_http_delay_body_event_handler; ctx->event.data = r; ctx->event.log = r->connection->log; ngx_add_timer(&ctx->event, NGX_HTTP_DELAY_BODY); } return ngx_http_next_request_body_filter(r, NULL); } rc = ngx_http_next_request_body_filter(r, ctx->out); for (cl = ctx->out; cl; /* void */) { ln = cl; cl = cl->next; ngx_free_chain(r->pool, ln); } ctx->out = NULL; return rc; } static void ngx_http_delay_body_cleanup(void *data) { ngx_http_delay_body_ctx_t *ctx = data; if (ctx->event.timer_set) { ngx_del_timer(&ctx->event); } } static void ngx_http_delay_body_event_handler(ngx_event_t *ev) { ngx_connection_t *c; ngx_http_request_t *r; r = ev->data; c = r->connection; ngx_log_debug0(NGX_LOG_DEBUG_HTTP, c->log, 0, "delay request body event"); ngx_post_event(c->read, &ngx_posted_events); } static ngx_int_t ngx_http_delay_body_init(ngx_conf_t *cf) { ngx_http_next_request_body_filter = ngx_http_top_request_body_filter; ngx_http_top_request_body_filter = ngx_http_delay_body_filter; return NGX_OK; }
响应
在 nginx 中,HTTP 响应通过发送响应头部,然后是可选的响应正文来产生。头部和正文都通过一个过滤器链,最终被写入客户端套接字。一个 nginx 模块可以将其处理程序安装到头部或正文过滤链中,并处理来自前一个处理程序的输出。
响应头部
ngx_http_send_header(r)
函数发送输出头部。在 r->headers_out
包含生成 HTTP 响应头部所需的所有数据之前,不要调用此函数。r->headers_out
中的 status
字段必须始终设置。如果响应状态表明头部之后跟着响应正文,也可以设置 content_length_n
。此字段的默认值为 -1
,表示正文大小未知。在这种情况下,将使用分块传输编码。要输出任意头部,请追加到 headers
列表中。
static ngx_int_t ngx_http_foo_content_handler(ngx_http_request_t *r) { ngx_int_t rc; ngx_table_elt_t *h; /* send header */ r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = 3; /* X-Foo: foo */ h = ngx_list_push(&r->headers_out.headers); if (h == NULL) { return NGX_ERROR; } h->hash = 1; ngx_str_set(&h->key, "X-Foo"); ngx_str_set(&h->value, "foo"); rc = ngx_http_send_header(r); if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) { return rc; } /* send body */ ... }
头部过滤器
ngx_http_send_header(r)
函数通过调用存储在 ngx_http_top_header_filter
变量中的第一个头部过滤器处理程序来调用头部过滤链。假设每个头部处理程序都会调用链中的下一个处理程序,直到调用最终处理程序 ngx_http_header_filter(r)
。最终头部处理程序根据 r->headers_out
构建 HTTP 响应,并将其传递给 ngx_http_writer_filter
进行输出。
要在头部过滤链中添加处理程序,请在配置时将其地址存储在全局变量 ngx_http_top_header_filter
中。先前的处理程序地址通常存储在模块的静态变量中,并在新添加的处理程序退出之前由新处理程序调用。
以下是一个头部过滤器模块示例,它会向所有状态为 200
的响应添加 HTTP 头部“X-Foo: foo
”。
#include <ngx_config.h> #include <ngx_core.h> #include <ngx_http.h> static ngx_int_t ngx_http_foo_header_filter(ngx_http_request_t *r); static ngx_int_t ngx_http_foo_header_filter_init(ngx_conf_t *cf); static ngx_http_module_t ngx_http_foo_header_filter_module_ctx = { NULL, /* preconfiguration */ ngx_http_foo_header_filter_init, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ NULL, /* create location configuration */ NULL /* merge location configuration */ }; ngx_module_t ngx_http_foo_header_filter_module = { NGX_MODULE_V1, &ngx_http_foo_header_filter_module_ctx, /* module context */ NULL, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; static ngx_http_output_header_filter_pt ngx_http_next_header_filter; static ngx_int_t ngx_http_foo_header_filter(ngx_http_request_t *r) { ngx_table_elt_t *h; /* * The filter handler adds "X-Foo: foo" header * to every HTTP 200 response */ if (r->headers_out.status != NGX_HTTP_OK) { return ngx_http_next_header_filter(r); } h = ngx_list_push(&r->headers_out.headers); if (h == NULL) { return NGX_ERROR; } h->hash = 1; ngx_str_set(&h->key, "X-Foo"); ngx_str_set(&h->value, "foo"); return ngx_http_next_header_filter(r); } static ngx_int_t ngx_http_foo_header_filter_init(ngx_conf_t *cf) { ngx_http_next_header_filter = ngx_http_top_header_filter; ngx_http_top_header_filter = ngx_http_foo_header_filter; return NGX_OK; }
响应体
要发送响应正文,请调用 ngx_http_output_filter(r, cl)
函数。该函数可以多次调用。每次调用时,它会以缓冲区链的形式发送响应正文的一部分。在最后一个正文缓冲区中设置 last_buf
标志。
以下示例生成一个完整的 HTTP 响应,其正文为“foo”。为了使该示例也作为子请求和主请求正常工作,在输出的最后一个缓冲区中设置了 last_in_chain
标志。last_buf
标志仅为主请求设置,因为子请求的最后一个缓冲区并不结束整个输出。
static ngx_int_t ngx_http_bar_content_handler(ngx_http_request_t *r) { ngx_int_t rc; ngx_buf_t *b; ngx_chain_t out; /* send header */ r->headers_out.status = NGX_HTTP_OK; r->headers_out.content_length_n = 3; rc = ngx_http_send_header(r); if (rc == NGX_ERROR || rc > NGX_OK || r->header_only) { return rc; } /* send body */ b = ngx_calloc_buf(r->pool); if (b == NULL) { return NGX_ERROR; } b->last_buf = (r == r->main) ? 1 : 0; b->last_in_chain = 1; b->memory = 1; b->pos = (u_char *) "foo"; b->last = b->pos + 3; out.buf = b; out.next = NULL; return ngx_http_output_filter(r, &out); }
响应体过滤器
函数 ngx_http_output_filter(r, cl)
通过调用存储在 ngx_http_top_body_filter
变量中的第一个正文过滤器处理程序来调用正文过滤链。假设每个正文处理程序都会调用链中的下一个处理程序,直到调用最终处理程序 ngx_http_write_filter(r, cl)
。
正文过滤器处理程序接收一个缓冲区链。处理程序应该处理这些缓冲区,并将可能的新链传递给下一个处理程序。值得注意的是,传入链的链链接 ngx_chain_t
属于调用者,不得重复使用或更改。在处理程序完成后,调用者可以立即使用其输出链链接来跟踪它已发送的缓冲区。为了保存缓冲区链或在传递给下一个过滤器之前替换一些缓冲区,处理程序需要分配自己的链链接。
以下是一个简单的正文过滤器示例,它计算正文中的字节数。结果作为 $counter
变量可用,可以在访问日志中使用。
#include <ngx_config.h> #include <ngx_core.h> #include <ngx_http.h> typedef struct { off_t count; } ngx_http_counter_filter_ctx_t; static ngx_int_t ngx_http_counter_body_filter(ngx_http_request_t *r, ngx_chain_t *in); static ngx_int_t ngx_http_counter_variable(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data); static ngx_int_t ngx_http_counter_add_variables(ngx_conf_t *cf); static ngx_int_t ngx_http_counter_filter_init(ngx_conf_t *cf); static ngx_http_module_t ngx_http_counter_filter_module_ctx = { ngx_http_counter_add_variables, /* preconfiguration */ ngx_http_counter_filter_init, /* postconfiguration */ NULL, /* create main configuration */ NULL, /* init main configuration */ NULL, /* create server configuration */ NULL, /* merge server configuration */ NULL, /* create location configuration */ NULL /* merge location configuration */ }; ngx_module_t ngx_http_counter_filter_module = { NGX_MODULE_V1, &ngx_http_counter_filter_module_ctx, /* module context */ NULL, /* module directives */ NGX_HTTP_MODULE, /* module type */ NULL, /* init master */ NULL, /* init module */ NULL, /* init process */ NULL, /* init thread */ NULL, /* exit thread */ NULL, /* exit process */ NULL, /* exit master */ NGX_MODULE_V1_PADDING }; static ngx_http_output_body_filter_pt ngx_http_next_body_filter; static ngx_str_t ngx_http_counter_name = ngx_string("counter"); static ngx_int_t ngx_http_counter_body_filter(ngx_http_request_t *r, ngx_chain_t *in) { ngx_chain_t *cl; ngx_http_counter_filter_ctx_t *ctx; ctx = ngx_http_get_module_ctx(r, ngx_http_counter_filter_module); if (ctx == NULL) { ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_counter_filter_ctx_t)); if (ctx == NULL) { return NGX_ERROR; } ngx_http_set_ctx(r, ctx, ngx_http_counter_filter_module); } for (cl = in; cl; cl = cl->next) { ctx->count += ngx_buf_size(cl->buf); } return ngx_http_next_body_filter(r, in); } static ngx_int_t ngx_http_counter_variable(ngx_http_request_t *r, ngx_http_variable_value_t *v, uintptr_t data) { u_char *p; ngx_http_counter_filter_ctx_t *ctx; ctx = ngx_http_get_module_ctx(r, ngx_http_counter_filter_module); if (ctx == NULL) { v->not_found = 1; return NGX_OK; } p = ngx_pnalloc(r->pool, NGX_OFF_T_LEN); if (p == NULL) { return NGX_ERROR; } v->data = p; v->len = ngx_sprintf(p, "%O", ctx->count) - p; v->valid = 1; v->no_cacheable = 0; v->not_found = 0; return NGX_OK; } static ngx_int_t ngx_http_counter_add_variables(ngx_conf_t *cf) { ngx_http_variable_t *var; var = ngx_http_add_variable(cf, &ngx_http_counter_name, 0); if (var == NULL) { return NGX_ERROR; } var->get_handler = ngx_http_counter_variable; return NGX_OK; } static ngx_int_t ngx_http_counter_filter_init(ngx_conf_t *cf) { ngx_http_next_body_filter = ngx_http_top_body_filter; ngx_http_top_body_filter = ngx_http_counter_body_filter; return NGX_OK; }
构建过滤器模块
编写正文或头部过滤器时,要特别注意过滤器在过滤器顺序中的位置。nginx 标准模块注册了许多头部和正文过滤器。nginx 标准模块注册了许多头部和正文过滤器,并且相对于它们在正确位置注册新过滤器模块非常重要。通常,模块在其后配置处理程序中注册过滤器。在处理过程中调用过滤器的顺序显然与注册它们的顺序相反。
对于第三方过滤器模块,nginx 提供了一个特殊插槽 HTTP_AUX_FILTER_MODULES
。要在此插槽中注册过滤器模块,请在模块的配置中将 ngx_module_type
变量设置为 HTTP_AUX_FILTER
。
以下示例显示了一个过滤器模块配置文件,假设该模块只有一个源文件 ngx_http_foo_filter_module.c
。
ngx_module_type=HTTP_AUX_FILTER ngx_module_name=ngx_http_foo_filter_module ngx_module_srcs="$ngx_addon_dir/ngx_http_foo_filter_module.c" . auto/module
缓冲区重用
在发出或更改缓冲区流时,通常希望重用已分配的缓冲区。nginx 代码中一种标准且广泛采用的方法是为此目的保留两个缓冲区链:free
和 busy
。free
链保存所有可以重用的空闲缓冲区。busy
链保存由当前模块发送但仍被其他过滤器处理程序使用的所有缓冲区。如果缓冲区的尺寸大于零,则被认为是正在使用中。通常,当过滤器消耗缓冲区时,其 pos
(或文件缓冲区的 file_pos
)会移向 last
(文件缓冲区的 file_last
)。一旦缓冲区完全消耗,它就可以被重用了。要将新释放的缓冲区添加到 free
链,只需遍历 busy
链,并将链头中尺寸为零的缓冲区移至 free
。此操作非常常见,因此有一个特殊的函数用于此目的,即 ngx_chain_update_chains(free, busy, out, tag)
。该函数将输出链 out
追加到 busy
中,并将 busy
顶部的空闲缓冲区移至 free
。只有具有指定 tag
的缓冲区会被重用。这使得模块可以仅重用它自己分配的缓冲区。
以下示例是一个正文过滤器,它在每个传入缓冲区之前插入字符串“foo”。如果可能,该模块分配的新缓冲区会被重用。请注意,要使此示例正常工作,还需要设置一个头部过滤器并将 content_length_n
重置为 -1
,但此处未提供相关代码。
typedef struct { ngx_chain_t *free; ngx_chain_t *busy; } ngx_http_foo_filter_ctx_t; ngx_int_t ngx_http_foo_body_filter(ngx_http_request_t *r, ngx_chain_t *in) { ngx_int_t rc; ngx_buf_t *b; ngx_chain_t *cl, *tl, *out, **ll; ngx_http_foo_filter_ctx_t *ctx; ctx = ngx_http_get_module_ctx(r, ngx_http_foo_filter_module); if (ctx == NULL) { ctx = ngx_pcalloc(r->pool, sizeof(ngx_http_foo_filter_ctx_t)); if (ctx == NULL) { return NGX_ERROR; } ngx_http_set_ctx(r, ctx, ngx_http_foo_filter_module); } /* create a new chain "out" from "in" with all the changes */ ll = &out; for (cl = in; cl; cl = cl->next) { /* append "foo" in a reused buffer if possible */ tl = ngx_chain_get_free_buf(r->pool, &ctx->free); if (tl == NULL) { return NGX_ERROR; } b = tl->buf; b->tag = (ngx_buf_tag_t) &ngx_http_foo_filter_module; b->memory = 1; b->pos = (u_char *) "foo"; b->last = b->pos + 3; *ll = tl; ll = &tl->next; /* append the next incoming buffer */ tl = ngx_alloc_chain_link(r->pool); if (tl == NULL) { return NGX_ERROR; } tl->buf = cl->buf; *ll = tl; ll = &tl->next; } *ll = NULL; /* send the new chain */ rc = ngx_http_next_body_filter(r, out); /* update "busy" and "free" chains for reuse */ ngx_chain_update_chains(r->pool, &ctx->free, &ctx->busy, &out, (ngx_buf_tag_t) &ngx_http_foo_filter_module); return rc; }
负载均衡
ngx_http_upstream_module 提供了将请求传递给远程服务器所需的基本功能。实现特定协议(如 HTTP 或 FastCGI)的模块使用此功能。该模块还提供创建自定义负载均衡模块的接口,并实现默认的轮询方法。
least_conn 和 hash 模块实现了替代的负载均衡方法,但它们实际上是作为 upstream 轮询模块的扩展实现的,并与其共享大量代码,例如服务器组的表示。 keepalive 模块是一个独立的模块,扩展了 upstream 功能。
ngx_http_upstream_module 可以通过在配置文件中放置相应的 upstream 块来显式配置,也可以通过使用接受 URL(该 URL 在某个时刻会被解析为服务器列表)的指令(如 proxy_pass)来隐式配置。替代的负载均衡方法仅适用于显式 upstream 配置。upstream 模块配置有自己的指令上下文 NGX_HTTP_UPS_CONF
。该结构定义如下
struct ngx_http_upstream_srv_conf_s { ngx_http_upstream_peer_t peer; void **srv_conf; ngx_array_t *servers; /* ngx_http_upstream_server_t */ ngx_uint_t flags; ngx_str_t host; u_char *file_name; ngx_uint_t line; in_port_t port; ngx_uint_t no_port; /* unsigned no_port:1 */ #if (NGX_HTTP_UPSTREAM_ZONE) ngx_shm_zone_t *shm_zone; #endif };
-
srv_conf
— upstream 模块的配置上下文。 -
servers
—ngx_http_upstream_server_t
数组,解析upstream
块中一组 server 指令的结果。 -
flags
— 主要标记负载均衡方法支持哪些功能的标志。这些功能配置为 server 指令的参数-
NGX_HTTP_UPSTREAM_CREATE
— 区分显式定义的 upstream 和由 proxy_pass 指令及其“朋友”(FastCGI、SCGI 等)自动创建的 upstream -
NGX_HTTP_UPSTREAM_WEIGHT
— 支持“weight
”参数 -
NGX_HTTP_UPSTREAM_MAX_FAILS
— 支持“max_fails
”参数 -
NGX_HTTP_UPSTREAM_FAIL_TIMEOUT
— 支持“fail_timeout
”参数 -
NGX_HTTP_UPSTREAM_DOWN
— 支持“down
”参数 -
NGX_HTTP_UPSTREAM_BACKUP
— 支持“backup
”参数 -
NGX_HTTP_UPSTREAM_MAX_CONNS
— 支持“max_conns
”参数
-
-
host
— upstream 名称。 -
file_name, line
— 配置文件名以及upstream
块所在的行。 -
port
和no_port
— 不用于显式定义的 upstream 组。 -
shm_zone
— 如果有的话,此 upstream 组使用的共享内存区域。 -
peer
— 保存初始化 upstream 配置的通用方法的对象
实现负载均衡算法的模块必须设置这些方法并初始化私有typedef struct { ngx_http_upstream_init_pt init_upstream; ngx_http_upstream_init_peer_pt init; void *data; } ngx_http_upstream_peer_t;
data
。如果在配置解析期间未初始化init_upstream
,则ngx_http_upstream_module
会将其设置为默认的ngx_http_upstream_init_round_robin
算法。-
init_upstream(cf, us)
— 负责初始化服务器组并在成功时初始化init()
方法的配置时方法。典型的负载均衡模块使用upstream
块中的服务器列表来创建其使用的有效数据结构,并将其自己的配置保存在data
字段中。 -
init(r, us)
— 初始化用于负载均衡的每个请求的ngx_http_upstream_peer_t.peer
结构(不要与上面描述的每个 upstream 的ngx_http_upstream_srv_conf_t.peer
混淆)。它作为data
参数传递给所有处理服务器选择的回调。
-
当 nginx 必须将请求传递给另一个主机进行处理时,它会使用配置的负载均衡方法来获取要连接的地址。该方法从 ngx_peer_connection_t
类型的 ngx_http_upstream_t.peer
对象中获取
struct ngx_peer_connection_s { ... struct sockaddr *sockaddr; socklen_t socklen; ngx_str_t *name; ngx_uint_t tries; ngx_event_get_peer_pt get; ngx_event_free_peer_pt free; ngx_event_notify_peer_pt notify; void *data; #if (NGX_SSL || NGX_COMPAT) ngx_event_set_peer_session_pt set_session; ngx_event_save_peer_session_pt save_session; #endif ... };
该结构具有以下字段
-
sockaddr
,socklen
,name
— 要连接的 upstream 服务器地址;这是负载均衡方法的输出参数。 -
data
— 负载均衡方法的每个请求数据;保存选择算法的状态,通常包括指向 upstream 配置的链接。它作为参数传递给所有处理服务器选择的方法(参见下方)。 -
tries
— 允许连接到 upstream 服务器的尝试次数。 -
get
,free
,notify
,set_session
, 和save_session
- 下面描述的负载均衡模块方法。
所有方法都接受至少两个参数:对等连接对象 pc
和由 ngx_http_upstream_srv_conf_t.peer.init()
创建的 data
。请注意,由于负载均衡模块的“链式”,它可能与 pc.data
不同。
-
get(pc, data)
— 当 upstream 模块准备将请求传递给 upstream 服务器并需要知道其地址时调用的方法。该方法必须填充ngx_peer_connection_t
结构的sockaddr
、socklen
和name
字段。返回值为以下之一-
NGX_OK
— 服务器已选择。 -
NGX_ERROR
— 发生内部错误。 -
NGX_BUSY
— 当前没有可用的服务器。这可能由于多种原因发生,包括:动态服务器组为空,组中所有服务器都处于失败状态,或者组中所有服务器都已处理最大连接数。 -
NGX_DONE
— 底层连接已重用,无需创建新的连接到 upstream 服务器。此值由keepalive
模块设置。
-
-
free(pc, data, state)
— 当 upstream 模块完成与特定服务器的工作后调用的方法。state
参数是 upstream 连接的完成状态,一个位掩码,具有以下可能的值-
NGX_PEER_FAILED
— 尝试失败 -
NGX_PEER_NEXT
— upstream 服务器返回代码403
或404
的特殊情况,这些代码不被视为失败。 -
NGX_PEER_KEEPALIVE
— 当前未使用
tries
计数器。 -
-
notify(pc, data, type)
— 在 OSS 版本中当前未使用。 -
set_session(pc, data)
和save_session(pc, data)
— 特定于 SSL 的方法,用于启用会话到 upstream 服务器的缓存。实现由轮询均衡方法提供。
示例
nginx-dev-examples 仓库提供了 nginx 模块示例。
代码风格
通用规则
- 最大文本宽度为 80 个字符
- 缩进为 4 个空格
- 无制表符,无尾随空格
- 同一行的列表元素用空格分隔
- 十六进制字面量使用小写
- 文件名、函数名、类型名和全局变量使用
ngx_
或更具体的如ngx_http_
和ngx_mail_
前缀
size_t ngx_utf8_length(u_char *p, size_t n) { u_char c, *last; size_t len; last = p + n; for (len = 0; p < last; len++) { c = *p; if (c < 0x80) { p++; continue; } if (ngx_utf8_decode(&p, last - p) > 0x10ffff) { /* invalid UTF-8 */ return n; } } return len; }
文件
典型的源文件可能包含以下由两个空行分隔的部分
- 版权声明
- 包含文件
- 预处理器定义
- 类型定义
- 函数原型
- 变量定义
- 函数定义
版权声明如下所示
/* * Copyright (C) Author Name * Copyright (C) Organization, Inc. */
如果文件被显著修改,作者列表应该更新,新作者添加到顶部。
ngx_config.h
和 ngx_core.h
文件总是首先包含,接着是 ngx_http.h
、ngx_stream.h
或 ngx_mail.h
之一。然后是可选的外部头文件
#include <ngx_config.h> #include <ngx_core.h> #include <ngx_http.h> #include <libxml/parser.h> #include <libxml/tree.h> #include <libxslt/xslt.h> #if (NGX_HAVE_EXSLT) #include <libexslt/exslt.h> #endif
头文件应该包含所谓的“头文件保护”
#ifndef _NGX_PROCESS_CYCLE_H_INCLUDED_ #define _NGX_PROCESS_CYCLE_H_INCLUDED_ ... #endif /* _NGX_PROCESS_CYCLE_H_INCLUDED_ */
注释
- 不使用“
//
”注释 - 文本使用英语书写,首选美式拼写
- 多行注释的格式如下
/* * The red-black tree code is based on the algorithm described in * the "Introduction to Algorithms" by Cormen, Leiserson and Rivest. */
/* find the server configuration for the address:port */
预处理器
宏名称以 ngx_
或 NGX_
(或更具体的)前缀开头。常量宏名称使用大写。参数化宏和用于初始化的宏使用小写。宏名称和值之间用至少两个空格分隔
#define NGX_CONF_BUFFER 4096 #define ngx_buf_in_memory(b) (b->temporary || b->memory || b->mmap) #define ngx_buf_size(b) \ (ngx_buf_in_memory(b) ? (off_t) (b->last - b->pos): \ (b->file_last - b->file_pos)) #define ngx_null_string { 0, NULL }
条件在括号内,否定在括号外
#if (NGX_HAVE_KQUEUE) ... #elif ((NGX_HAVE_DEVPOLL && !(NGX_TEST_BUILD_DEVPOLL)) \ || (NGX_HAVE_EVENTPORT && !(NGX_TEST_BUILD_EVENTPORT))) ... #elif (NGX_HAVE_EPOLL && !(NGX_TEST_BUILD_EPOLL)) ... #elif (NGX_HAVE_POLL) ... #else /* select */ ... #endif /* NGX_HAVE_KQUEUE */
类型
类型名称以“_t
”后缀结尾。定义的类型名称用至少两个空格分隔
typedef ngx_uint_t ngx_rbtree_key_t;
结构类型使用 typedef
定义。在结构内部,成员类型和名称对齐
typedef struct { size_t len; u_char *data; } ngx_str_t;
在文件中的不同结构之间保持对齐一致。指向自身的结构的名称以“_s
”结尾。相邻结构定义用两个空行分隔
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;
每个结构成员都在自己的行上声明
typedef struct { ngx_uint_t hash; ngx_str_t key; ngx_str_t value; u_char *lowcase_key; } ngx_table_elt_t;
结构内部的函数指针具有以“_pt
”结尾的定义类型
typedef ssize_t (*ngx_recv_pt)(ngx_connection_t *c, u_char *buf, size_t size); typedef ssize_t (*ngx_recv_chain_pt)(ngx_connection_t *c, ngx_chain_t *in, off_t limit); typedef ssize_t (*ngx_send_pt)(ngx_connection_t *c, u_char *buf, size_t size); typedef ngx_chain_t *(*ngx_send_chain_pt)(ngx_connection_t *c, ngx_chain_t *in, off_t limit); typedef struct { ngx_recv_pt recv; ngx_recv_chain_pt recv_chain; ngx_recv_pt udp_recv; ngx_send_pt send; ngx_send_pt udp_send; ngx_send_chain_pt udp_send_chain; ngx_send_chain_pt send_chain; ngx_uint_t flags; } ngx_os_io_t;
枚举具有以“_e
”结尾的类型
typedef enum { ngx_http_fastcgi_st_version = 0, ngx_http_fastcgi_st_type, ... ngx_http_fastcgi_st_padding } ngx_http_fastcgi_state_e;
变量
变量声明按基本类型长度排序,然后按字母顺序排序。类型名称和变量名称对齐。类型和名称“列”用两个空格分隔。大数组放在声明块的末尾
u_char | | *rv, *p; ngx_conf_t | | *cf; ngx_uint_t | | i, j, k; unsigned int | | len; struct sockaddr | | *sa; const unsigned char | | *data; ngx_peer_connection_t | | *pc; ngx_http_core_srv_conf_t | |**cscfp; ngx_http_upstream_srv_conf_t| | *us, *uscf; u_char | | text[NGX_SOCKADDR_STRLEN];
静态和全局变量可以在声明时初始化
static ngx_str_t ngx_http_memcached_key = ngx_string("memcached_key");
static ngx_uint_t mday[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
static uint32_t ngx_crc32_table16[] = { 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, ... 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c };
有许多常用的类型/名称组合
u_char *rv; ngx_int_t rc; ngx_conf_t *cf; ngx_connection_t *c; ngx_http_request_t *r; ngx_peer_connection_t *pc; ngx_http_upstream_srv_conf_t *us, *uscf;
函数
所有函数(即使是静态函数)都应该有原型。原型包含参数名称。长原型在续行上使用一个缩进换行
static char *ngx_http_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); static ngx_int_t ngx_http_init_phases(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf); static char *ngx_http_merge_servers(ngx_conf_t *cf, ngx_http_core_main_conf_t *cmcf, ngx_http_module_t *module, ngx_uint_t ctx_index);
定义中的函数名称以新行开始。函数体的大括号开口和闭口位于不同的行上。函数体缩进。函数之间有两个空行
static ngx_int_t ngx_http_find_virtual_server(ngx_http_request_t *r, u_char *host, size_t len) { ... } static ngx_int_t ngx_http_add_addresses(ngx_conf_t *cf, ngx_http_core_srv_conf_t *cscf, ngx_http_conf_port_t *port, ngx_http_listen_opt_t *lsopt) { ... }
函数名称和开括号之间没有空格。长函数调用换行,以便续行从第一个函数参数的位置开始。如果不可能,将第一个续行格式化,使其在位置 79 结束
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "http header: \"%V: %V\"", &h->key, &h->value); hc->busy = ngx_palloc(r->connection->pool, cscf->large_client_header_buffers.num * sizeof(ngx_buf_t *));
应使用 ngx_inline
宏代替 inline
static ngx_inline void ngx_cpuid(uint32_t i, uint32_t *buf);
表达式
除“.
”和“−>
”之外的二元运算符应与其操作数用一个空格分隔。一元运算符和下标不与其操作数用空格分隔
width = width * 10 + (*fmt++ - '0');
ch = (u_char) ((decoded << 4) + (ch - '0'));
r->exten.data = &r->uri.data[i + 1];
类型转换与其被转换的表达式用一个空格分隔。类型转换中的星号与类型名称用空格分隔
len = ngx_sock_ntop((struct sockaddr *) sin6, p, len, 1);
如果表达式不适合单行,则换行。首选的换行点是二元运算符。续行与表达式的开始位置对齐
if (status == NGX_HTTP_MOVED_PERMANENTLY || status == NGX_HTTP_MOVED_TEMPORARILY || status == NGX_HTTP_SEE_OTHER || status == NGX_HTTP_TEMPORARY_REDIRECT || status == NGX_HTTP_PERMANENT_REDIRECT) { ... }
p->temp_file->warn = "an upstream response is buffered " "to a temporary file";
作为最后的手段,可以将表达式换行,使续行在位置 79 结束
hinit->hash = ngx_pcalloc(hinit->pool, sizeof(ngx_hash_wildcard_t) + size * sizeof(ngx_hash_elt_t *));
上述规则也适用于子表达式,每个子表达式都有自己的缩进级别
if (((u->conf->cache_use_stale & NGX_HTTP_UPSTREAM_FT_UPDATING) || c->stale_updating) && !r->background && u->conf->cache_background_update) { ... }
有时,方便在类型转换后换行。在这种情况下,续行缩进
node = (ngx_rbtree_node_t *) ((u_char *) lr - offsetof(ngx_rbtree_node_t, color));
指针显式与 NULL
进行比较(而不是 0
)
if (ptr != NULL) { ... }
条件语句和循环
“if
”关键字与其条件用一个空格分隔。开括号位于同一行,如果条件占用多行,则位于专用行上。闭括号位于专用行上,可选地后跟“else if
/ else
”。通常,在“else if
/ else
”部分之前有一个空行
if (node->left == sentinel) { temp = node->right; subst = node; } else if (node->right == sentinel) { temp = node->left; subst = node; } else { subst = ngx_rbtree_min(node->right, sentinel); if (subst->left != sentinel) { temp = subst->left; } else { temp = subst->right; } }
类似的格式规则适用于“do
”和“while
”循环
while (p < last && *p == ' ') { p++; }
do { ctx->node = rn; ctx = ctx->next; } while (ctx);
“switch
”关键字与其条件用一个空格分隔。开括号位于同一行。闭括号位于专用行上。“case
”关键字与“switch
”对齐
switch (ch) { case '!': looked = 2; state = ssi_comment0_state; break; case '<': copy_end = p; break; default: copy_end = p; looked = 0; state = ssi_start_state; break; }
大多数“for
”循环的格式如下
for (i = 0; i < ccf->env.nelts; i++) { ... }
for (q = ngx_queue_head(locations); q != ngx_queue_sentinel(locations); q = ngx_queue_next(q)) { ... }
如果“for
”语句的某些部分被省略,则通过“/* void */
”注释指示
for (i = 0; /* void */ ; i++) { ... }
空循环体也通过“/* void */
”注释指示,该注释可以放在同一行
for (cl = *busy; cl->next; cl = cl->next) { /* void */ }
无限循环如下所示
for ( ;; ) { ... }
标签
标签用空行包围,并按前一级别缩进
if (i == 0) { u->err = "host not found"; goto failed; } u->addrs = ngx_pcalloc(pool, i * sizeof(ngx_addr_t)); if (u->addrs == NULL) { goto failed; } u->naddrs = i; ... return NGX_OK; failed: freeaddrinfo(res); return NGX_ERROR;
调试内存问题
要调试内存问题,例如缓冲区溢出或 use-after-free 错误,可以使用某些现代编译器支持的 AddressSanitizer (ASan)。要在 gcc
和 clang
中启用 ASan,请使用 -fsanitize=address
编译器和链接器选项。构建 nginx 时,可以通过将该选项添加到 configure
脚本的 --with-cc-opt
和 --with-ld-opt
参数来完成。
由于 nginx 中的大多数分配都是从 nginx 内部内存池中进行的,因此启用 ASan 可能不足以调试内存问题。内部内存池从系统中分配一大块内存,然后从中切割出较小的分配。但是,通过将 NGX_DEBUG_PALLOC
宏设置为 1
可以禁用此机制。在这种情况下,分配直接传递给系统分配器,使其完全控制缓冲区边界。
以下配置行总结了上面提供的信息。建议在开发第三方模块和在不同平台上测试 nginx 时使用。
auto/configure --with-cc-opt='-fsanitize=address -DNGX_DEBUG_PALLOC=1' --with-ld-opt=-fsanitize=address
常见陷阱
编写 C 模块
最常见的陷阱是尝试编写一个功能齐全的 C 模块,而实际上是可以避免的。在大多数情况下,您的任务可以通过创建适当的配置来完成。如果编写模块不可避免,请尽量使其尽可能小而简单。例如,模块可以只导出一些变量。
在开始编写模块之前,考虑以下问题
C 字符串
nginx 中最常用的字符串类型 ngx_str_t 不是 C 风格的零结尾字符串。您不能将数据传递给标准 C 库函数,例如 strlen()
或 strstr()
。相反,应该使用接受 ngx_str_t
的 nginx 对应函数,或者传递指向数据和长度的指针。但是,在某些情况下 ngx_str_t
持有指向零结尾字符串的指针:作为配置文件解析结果的字符串是零结尾的。
全局变量
避免在您的模块中使用全局变量。拥有全局变量很可能是一个错误。任何全局数据都应该绑定到配置周期并从相应的内存池中分配。这使得 nginx 可以执行平滑的配置重载。尝试使用全局变量很可能会破坏此功能,因为将无法同时拥有两个配置并将其清除。有时需要全局变量。在这种情况下,需要特别注意正确管理重新配置。此外,检查您的代码使用的库是否具有隐式全局状态,这些状态在重载时可能会被破坏。
手动内存管理
与其处理容易出错的 malloc/free 方法,不如学习如何使用 nginx内存池。内存池是创建并绑定到一个对象(配置、周期、连接或HTTP 请求)上的。当对象被销毁时,关联的内存池也会被销毁。因此,在使用对象时,可以从相应的内存池中分配所需数量的内存,而不必担心释放内存,即使在发生错误的情况下也是如此。
线程
建议避免在 nginx 中使用线程,因为这肯定会破坏事物:大多数 nginx 函数不是线程安全的。预期线程只会执行系统调用和线程安全的库函数。如果您需要运行一些与客户端请求处理无关的代码,正确的方法是在 init_process
模块处理程序中调度一个计时器,并在计时器处理程序中执行所需的操作。在内部,nginx 使用线程来提升 IO 相关操作的性能,但这是一种特殊情况,有很多限制。
阻塞库
一个常见的错误是使用内部阻塞的库。大多数现有库本质上是同步和阻塞的。换句话说,它们一次执行一个操作,浪费时间等待来自其他对等方的响应。结果是,当使用此类库处理请求时,整个 nginx worker 都会被阻塞,从而降低性能。只使用提供异步接口且不阻塞整个进程的库。
对外部服务的 HTTP 请求
模块通常需要对某些外部服务执行 HTTP 调用。一个常见的错误是使用一些外部库(例如 libcurl)来执行 HTTP 请求。为了一项可以通过 nginx 本身完成的任务,引入大量外部(可能是阻塞的!)代码是完全没有必要的。
需要外部请求时有两种基本使用场景
- 在处理客户端请求的上下文(例如,内容处理程序中)
- 在 worker 进程的上下文(例如,计时器处理程序中)
在第一种情况下,最好的方法是使用子请求 API。与其直接访问外部服务,您可以在 nginx 配置中声明一个位置,并将您的子请求导向该位置。这个位置不仅限于代理请求,还可以包含其他 nginx 指令。此类方法的示例是 ngx_http_auth_request module 中实现的 auth_request 指令。
对于第二种情况,可以使用 nginx 中提供的基本 HTTP 客户端功能。例如,OCSP module 实现了简单的 HTTP 客户端。