开发指南
简介
代码布局
-
auto
— 构建脚本 -
src
-
core
— 基本类型和函数 — 字符串、数组、日志、池等。 -
event
— 事件核心-
modules
— 事件通知模块:epoll
、kqueue
、select
等。
-
-
http
— 核心 HTTP 模块和通用代码-
modules
— 其他 HTTP 模块 -
v2
— HTTP/2
-
-
mail
— 邮件模块 -
os
— 平台特定代码-
unix
-
win32
-
-
stream
— 流模块
-
包含文件
以下两个 #include
语句必须出现在每个 Nginx 文件的开头
#include <ngx_config.h> #include <ngx_core.h>
此外,HTTP 代码应该包含
#include <ngx_http.h>
邮件代码应该包含
#include <ngx_mail.h>
流代码应该包含
#include <ngx_stream.h>
整数
出于通用目的,Nginx 代码使用两种整数类型,ngx_int_t
和 ngx_uint_t
,它们分别是 intptr_t
和 uintptr_t
的类型定义。
常见返回码
大多数 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 结尾。在大多数情况下,它不以 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
数组的大小必须是 3 的倍数,这是 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
传递,这使得可以在一个 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 启动时会创建一个名为“初始化周期”的占位符周期,然后由根据配置构建的实际周期替换。
周期的成员包括
-
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 缓存加载器”调用。
- 路径管理器 — 定期执行。通常,管理器会从目录中删除旧文件,并更新 nginx 内存以反映更改。处理程序由专用的“nginx 缓存管理器”进程调用。
-
open_files
— 打开的文件对象列表,类型为ngx_open_file_t
,通过调用函数ngx_conf_open_file()
创建。目前,Nginx 使用这种类型的打开文件进行日志记录。读取配置后,Nginx 会打开open_files
列表中的所有文件,并将每个文件描述符存储在对象的fd
字段中。文件以追加模式打开,如果文件不存在则创建。当 Nginx 工作进程收到重新打开信号(通常是USR1
)时,列表中的文件会被重新打开。在这种情况下,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 请求或流会话。 -
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 工作进程的连接数。所有连接结构在工作进程启动时预先创建,并存储在周期对象的connections
字段中。要检索连接结构,请使用ngx_get_connection(s, log)
函数。它以套接字描述符作为其s
参数,该描述符需要包装在连接结构中。
由于每个工作进程的连接数是有限制的,因此 Nginx 提供了一种获取当前正在使用的连接的方法。要启用或禁用连接的重用,请调用ngx_reusable_connection(c, reusable)
函数。调用ngx_reusable_connection(c, 1)
会在连接结构中设置reuse
标志,并将连接插入到周期的reusable_connections_queue
中。每当ngx_get_connection()
发现周期的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
— 指示 EOF 正在套接字上挂起,即使在 EOF 之前可能有一些数据可用。该标志通过EPOLLRDHUP
epoll
事件或EV_EOF
kqueue
标志传递。 -
error
— 指示在读取(对于读取事件)或写入(对于写入事件)期间发生错误的标志。 -
cancelable
— 计时器事件标志,指示在关闭工作进程时应忽略该事件。优雅的工作进程关闭会延迟,直到没有计划的不可取消的计时器事件。 -
posted
— 指示事件已发布到队列中的标志。 -
queue
— 用于将事件发布到队列的队列节点。
I/O 事件
通过调用ngx_get_connection()
函数获得的每个连接都有两个附加事件,c->read
和c->write
,它们用于接收套接字准备好读取或写入的通知。所有此类事件都以边沿触发模式工作,这意味着它们仅在套接字状态发生变化时才触发通知。例如,对套接字执行部分读取不会使 Nginx 传递重复的读取通知,直到套接字上有更多数据到达。即使底层 I/O 通知机制本质上是电平触发(poll
、select
等),Nginx 也会将通知转换为边沿触发。为了使 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 事件和超时事件到期的时长。
发布事件
可以发布事件,这意味着它的处理程序将在当前事件循环迭代中的某个时间点之后被调用。发布事件是简化代码和避免堆栈溢出的良好实践。发布的事件保存在发布队列中。ngx_post_event(ev, q)
宏将事件ev
发布到发布队列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 主进程外,所有 Nginx 进程都执行 I/O,因此都有一个事件循环。(Nginx 主进程则大部分时间都在sigsuspend()
调用中等待信号到达。)Nginx 事件循环在ngx_process_events_and_timers()
函数中实现,该函数会重复调用,直到进程退出。
事件循环包含以下阶段
- 通过调用
ngx_event_find_timer()
查找最接近过期的超时。此函数查找计时器树中最左边的节点,并返回该节点过期之前的毫秒数。 - 通过调用特定于事件通知机制(由 Nginx 配置选择)的处理程序来处理 I/O 事件。此处理程序等待至少一个 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
— 主进程,读取 Nginx 配置,创建周期并启动和控制子进程。它不执行任何 I/O 操作,并且仅响应信号。其周期函数为 `ngx_master_process_cycle()`。 -
NGX_PROCESS_WORKER
— 工作进程,处理客户端连接。它由主进程启动,并响应其信号和通道命令。其周期函数为 `ngx_worker_process_cycle()`。可以有多个工作进程,由 `worker_processes` 指令配置。 -
NGX_PROCESS_SINGLE
— 单进程,仅在 `master_process off` 模式下存在,并且是该模式下运行的唯一进程。它创建周期(就像主进程一样)并处理客户端连接(就像工作进程一样)。其周期函数为 `ngx_single_process_cycle()`。 -
NGX_PROCESS_HELPER
— 辅助进程,目前有两种类型:缓存管理器和缓存加载器。两种进程的周期函数均为 `ngx_cache_manager_process_cycle()`。
Nginx 进程处理以下信号:
-
NGX_SHUTDOWN_SIGNAL
(大多数系统上的 `SIGQUIT`) — 优雅关闭。接收到此信号后,主进程会向所有子进程发送关闭信号。当没有子进程剩余时,主进程销毁周期池并退出。当工作进程接收到此信号时,它关闭所有监听套接字并等待直到没有计划中的不可取消事件,然后销毁周期池并退出。当缓存管理器或缓存加载器进程接收到此信号时,它立即退出。当进程接收到此信号时,`ngx_quit` 变量设置为 `1`,并在处理后立即重置。当工作进程处于关闭状态时,`ngx_exiting` 变量设置为 `1`。 -
NGX_TERMINATE_SIGNAL
(大多数系统上的 `SIGTERM`) — 终止。接收到此信号后,主进程会向所有子进程发送终止信号。如果子进程在 1 秒内未退出,则主进程发送 `SIGKILL` 信号将其杀死。当没有子进程剩余时,主进程销毁周期池并退出。当工作进程、缓存管理器进程或缓存加载器进程接收到此信号时,它销毁周期池并退出。接收到此信号时,变量 `ngx_terminate` 设置为 `1`。 -
NGX_NOACCEPT_SIGNAL
(大多数系统上的 `SIGWINCH`) — 关闭所有工作进程和辅助进程。接收到此信号后,主进程关闭其子进程。如果先前启动的新 Nginx 二进制文件退出,则旧主进程的子进程将重新启动。当工作进程接收到此信号时,它将在 `debug_points` 指令设置的调试模式下关闭。 -
NGX_RECONFIGURE_SIGNAL
(大多数系统上的 `SIGHUP`) — 重新配置。接收到此信号后,主进程重新读取配置并基于它创建一个新的周期。如果成功创建了新的周期,则删除旧的周期并启动新的子进程。同时,旧的子进程接收 `NGX_SHUTDOWN_SIGNAL` 信号。在单进程模式下,Nginx 创建一个新的周期,但保留旧的周期,直到不再有客户端与其关联的活动连接。工作进程和辅助进程忽略此信号。 -
NGX_REOPEN_SIGNAL
(大多数系统上的 `SIGUSR1`) — 重新打开文件。主进程将此信号发送给工作进程,工作进程重新打开与周期相关的所有 `open_files`。 -
NGX_CHANGEBIN_SIGNAL
(大多数系统上的 `SIGUSR2`) — 更改 Nginx 二进制文件。主进程启动一个新的 Nginx 二进制文件并传入所有监听套接字的列表。以文本格式传递的列表,位于 `“NGINX”` 环境变量中,由分号分隔的描述符号组成。新的 Nginx 二进制文件读取 `“NGINX”` 变量并将套接字添加到其初始化周期中。其他进程忽略此信号。
虽然所有 Nginx 工作进程都能够接收并正确处理 POSIX 信号,但主进程不使用标准的 `kill()` 系统调用来向工作进程和辅助进程传递信号。相反,Nginx 使用进程间套接字对,允许在所有 Nginx 进程之间发送消息。但是,目前消息仅从主进程发送到其子进程。消息携带标准信号。
线程
可以将原本会阻塞 Nginx 工作进程的任务卸载到单独的线程中。例如,可以将 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 并没有为每个任务创建一个新线程,而是实现了 线程池 策略。可以为不同的目的配置多个线程池(例如,在不同的磁盘集上执行 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
— 模块在配置脚本控制台输出中的名称。 -
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` 之前,以便过滤器处理程序在复制过滤器处理程序之后执行。对于其他模块类型,默认为空字符串。
要将模块静态编译到 Nginx 中,请使用 `--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 生命周期中的某些阶段被调用。模块生命周期包含以下事件:
- 配置指令处理程序在主进程上下文中按照其在配置文件中出现的顺序调用。
- 配置解析成功后,在主进程上下文中调用 `init_module` 处理程序。每次加载配置时,都会在主进程中调用 `init_module` 处理程序。
- 主进程创建一个或多个工作进程,并在每个工作进程中调用 `init_process` 处理程序。
- 当工作进程从主进程接收关闭或终止命令时,它调用 `exit_process` 处理程序。
- 主进程在退出之前调用 `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_t
值。 -
ngx_conf_set_off_slot
— 将偏移量转换为以字节为单位的off_t
值。 -
ngx_conf_set_msec_slot
— 将时间转换为以毫秒为单位的ngx_msec_t
值。 -
ngx_conf_set_sec_slot
— 将时间转换为以秒为单位的time_t
值。 -
ngx_conf_set_bufs_slot
— 将提供的两个参数转换为ngx_bufs_t
对象,该对象保存缓冲区的数量和大小。 -
ngx_conf_set_enum_slot
— 将提供的参数转换为ngx_uint_t
值。post
字段中传递的ngx_conf_enum_t
的以空字符结尾的数组定义了可接受的字符串和相应的整数值。 -
ngx_conf_set_bitmask_slot
— 将提供的参数转换为ngx_uint_t
值。每个参数的掩码值进行按位或运算以产生结果。post
字段中传递的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()
在完整的响应已发送到客户端并且可以销毁请求时被调用。如果启用了客户端连接保持活动功能,则会调用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
— 用于代理的请求上游对象。 -
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
— 输出缓冲区和子请求的列表,按它们发送和创建的顺序排列。该列表由延迟过滤器使用,以便在请求的一部分由子请求创建时提供一致的请求输出。 -
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
— 由请求的最后一次正则表达式匹配生成的正则表达式捕获。在请求处理期间的许多地方都可能发生正则表达式匹配:映射查找、通过 SNI 或 HTTP Host 进行服务器查找、重写、proxy_redirect 等。查找生成的捕获存储在上述字段中。字段ncaptures
保存捕获的数量,captures
保存捕获边界,captures_data
保存与之匹配的字符串,并用于提取捕获。在每次新的正则表达式匹配后,请求捕获都会重置为保存新值。 -
count
— 请求引用计数器。该字段仅对主请求有意义。增加计数器是通过简单的r->main->count++
完成的。要减少计数器,请调用ngx_http_finalize_request(r, rc)
。创建子请求和运行请求体读取过程都会增加计数器。 -
subrequests
— 当前子请求嵌套级别。每个子请求都继承其父级的嵌套级别,减少 1。如果值达到 0,则会生成错误。主请求的值由NGX_HTTP_MAX_SUBREQUESTS
常量定义。 -
uri_changes
— 请求剩余的 URI 更改次数。请求可以更改其 URI 的总次数受NGX_HTTP_MAX_URI_CHANGES
常量限制。每次更改都会递减该值,直到它达到 0,此时会生成错误。重写和到普通或命名位置的内部重定向被视为 URI 更改。 -
blocked
— 请求上持有的块的计数器。当此值不为零时,请求无法终止。目前,此值通过挂起的 AIO 操作(POSIX AIO 和线程操作)和活动缓存锁来增加。 -
buffered
— 显示哪些模块缓冲了请求生成的输出的位掩码。许多过滤器可以缓冲输出;例如,sub_filter 由于部分字符串匹配可以缓冲数据,copy 过滤器由于缺少空闲输出缓冲区而可以缓冲数据等。只要此值不为零,请求就不会完成,等待刷新。 -
header_only
— 指示输出不需要正文的标志。例如,此标志由 HTTP HEAD 请求使用。 -
keepalive
— 指示是否支持客户端连接保持活动状态的标志。该值从 HTTP 版本和“Connection”头的值推断得出。 -
header_sent
— 指示输出头是否已由请求发送的标志。 -
internal
— 指示当前请求是内部请求的标志。要进入内部状态,请求必须通过内部重定向或为子请求。内部请求允许进入内部位置。 -
allow_ranges
— 指示可以根据 HTTP Range 头的请求向客户端发送部分响应的标志。 -
subrequest_ranges
— 指示在处理子请求时可以发送部分响应的标志。 -
single_range
— 指示只能向客户端发送单个连续的输出数据范围的标志。此标志通常在发送数据流时设置,例如来自代理服务器,并且整个响应不在一个缓冲区中可用。 -
main_filter_need_in_memory
,filter_need_in_memory
— 请求在内存缓冲区而不是文件中生成输出的标志。这是向 copy 过滤器发出的信号,即使启用了 sendfile,也要从文件缓冲区读取数据。这两个标志之间的区别在于设置它们的过滤器模块的位置。在过滤器链中延迟过滤器之前调用的过滤器设置filter_need_in_memory
,请求仅当前请求输出在内存缓冲区中。在过滤器链中稍后调用的过滤器设置main_filter_need_in_memory
,请求主请求和所有子请求在发送输出时都将文件读入内存。 -
filter_need_temporary
— 请求在临时缓冲区中生成请求输出,但不在只读内存缓冲区或文件缓冲区中生成请求输出的标志。这由可能直接更改发送缓冲区的输出的过滤器使用。
配置
每个 HTTP 模块可以具有三种类型的配置
- 主配置 — 应用于整个
http
块。充当模块的全局设置。 - 服务器配置 — 应用于单个
server
块。充当模块的服务器特定设置。 - 位置配置 — 应用于单个
location
、if
或limit_except
块。充当模块的位置特定设置。
配置结构在 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 级别,而且也为它们上方的所有级别创建。具体来说,服务器配置也在主级别创建,位置配置在主、服务器和位置级别创建。这些配置使得能够在 nginx 配置文件的任何级别指定服务器和位置特定的设置。最终配置向下合并。许多宏(如 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
字段中保存的位置内容处理器。
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 响应代码。请求将使用提供的代码结束。
对于某些阶段,返回代码的处理方式略有不同。在内容阶段,除 NGX_DECLINED
之外的任何返回代码都被视为结束代码。location 内容处理程序返回的任何返回代码都被视为结束代码。在访问阶段,在 satisfy any 模式下,除 NGX_OK
、NGX_DECLINED
、NGX_AGAIN
、NGX_DONE
之外的任何返回代码都被视为拒绝。如果没有后续的访问处理程序使用不同的代码允许或拒绝访问,则拒绝代码将成为结束代码。
变量
访问现有变量
可以通过索引(这是最常见的方法)或名称引用变量(请参阅 下方)。索引是在配置阶段创建的,当时变量被添加到配置中。要获取变量索引,请使用 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` 保存分配的变量索引,用于引用变量。
通常,模块会创建 `ngx_http_variable_t` 结构的以 null 结尾的静态数组,并在预配置阶段处理该数组以将变量添加到配置中,例如
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
请求阶段更改请求 URI,这是 rewrite 指令的结果,并将请求发送回 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
- 输出不会发送到客户端,而是存储在内存中。该标志仅影响由代理模块之一处理的子请求。子请求完成之后,其输出在类型为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; }
子请求通常在主体过滤器中创建,在这种情况下,它们的输出可以像来自任何显式请求的输出一样对待。这意味着,最终子请求的输出会被发送到客户端,在所有在创建子请求之前传递的显式缓冲区和在创建之后传递的任何缓冲区之后。即使对于大型的子请求层次结构,也会保留此顺序。以下示例在所有请求数据缓冲区之后插入来自子请求的输出,但在带有 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); }
子请求也可以用于数据输出以外的其他目的。例如, ngx_http_auth_request_module 模块在 NGX_HTTP_ACCESS_PHASE
阶段创建子请求。为了在此处禁用输出,会在子请求上设置 header_only
标志。这可以防止子请求主体发送到客户端。请注意,子请求的标头永远不会发送到客户端。可以在回调处理程序中分析子请求的结果。
请求结束
通过调用函数 ngx_http_finalize_request(r, rc)
完成 HTTP 请求。通常由内容处理程序在所有输出缓冲区发送到过滤器链后完成。此时,所有输出可能尚未发送到客户端,其中一些可能仍缓存在过滤器链中的某个位置。如果是这种情况,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 location 的内部重定向。 - 其他代码被视为成功的完成代码,并且可能会激活请求写入器以完成发送响应主体。主体完全发送后,请求
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
中。先前的处理程序地址通常存储在模块中的静态变量中,并在新添加的处理程序退出之前由其调用。
以下头过滤器模块示例将 HTTP 头“X-Foo: foo
”添加到状态为 200
的每个响应中。
#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 模块实现了替代的负载均衡方法,但实际上是作为上游轮循模块的扩展来实现的,并与其共享大量代码,例如服务器组的表示。 keepalive 模块是一个独立的模块,扩展了上游功能。
ngx_http_upstream_module 可以通过将相应的 upstream 块放置到配置文件中来显式配置,或者通过使用诸如 proxy_pass 之类的指令来隐式配置,这些指令接受在某个时间点被评估为服务器列表的 URL。替代负载均衡方法仅在显式上游配置中可用。上游模块配置有其自己的指令上下文 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
— 上游模块的配置上下文。 -
servers
—ngx_http_upstream_server_t
数组,它是解析upstream
块中的一组 server 指令的结果。 -
flags
— 标志,主要标记负载均衡方法支持哪些功能。这些功能配置为 server 指令的参数-
NGX_HTTP_UPSTREAM_CREATE
— 区分由 proxy_pass 指令和“朋友”(FastCGI、SCGI 等)显式定义的上游和自动创建的上游。 -
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
— 上游的名称。 -
file_name, line
— 配置文件的名称以及upstream
块所在的行。 -
port
和no_port
— 不用于显式定义的上游组。 -
shm_zone
— 此上游组使用的共享内存区域(如果有)。 -
peer
— 用于保存初始化上游配置的通用方法的对象
实现负载均衡算法的模块必须设置这些方法并初始化私有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
结构(不要与上面描述的每个上游ngx_http_upstream_srv_conf_t.peer
混淆)。它作为data
参数传递给所有处理服务器选择回调。
-
当 Nginx 需要将请求传递给另一个主机进行处理时,它会使用配置的负载均衡方法获取要连接的地址。该方法是从 ngx_http_upstream_t.peer
对象(类型为 ngx_peer_connection_t
)中获取的。
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
— 要连接的上游服务器的地址;这是负载均衡方法的输出参数。 -
data
— 负载均衡方法的每个请求数据;保持选择算法的状态,通常包括上游配置的链接。它作为参数传递给所有处理服务器选择的方法(参见 下方)。 -
tries
— 允许尝试连接到上游服务器的 次数。 -
get
、free
、notify
、set_session
和save_session
- 负载均衡模块的方法,如下所述。
所有方法至少接受两个参数:一个对等连接对象 pc
和由 ngx_http_upstream_srv_conf_t.peer.init()
创建的 data
。请注意,由于负载均衡模块的“链式”关系,它可能与 pc.data
不同。
-
get(pc, data)
— 当上游模块准备好将请求传递给上游服务器并需要知道其地址时调用的方法。该方法必须填充ngx_peer_connection_t
结构体的sockaddr
、socklen
和name
字段。返回值之一为:-
NGX_OK
— 已选择服务器。 -
NGX_ERROR
— 发生内部错误。 -
NGX_BUSY
— 当前没有可用的服务器。这可能由多种原因导致,包括:动态服务器组为空,组中所有服务器都处于失败状态,或者组中所有服务器都已处理最大连接数。 -
NGX_DONE
— 底层连接已重用,无需创建到上游服务器的新连接。此值由keepalive
模块设置。
-
-
free(pc, data, state)
— 当上游模块完成与特定服务器的工作时调用的方法。state
参数是上游连接的完成状态,一个具有以下可能值的位掩码:-
NGX_PEER_FAILED
— 尝试 失败 -
NGX_PEER_NEXT
— 上游服务器返回代码403
或404
的特殊情况,这不被视为 失败。 -
NGX_PEER_KEEPALIVE
— 当前未使用
tries
计数器。 -
-
notify(pc, data, type)
— 在 OSS 版本中当前未使用。 -
set_session(pc, data)
和save_session(pc, data)
— 支持 SSL 的方法,用于启用将会话缓存到上游服务器。实现由轮询负载均衡方法提供。
示例
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;
调试内存问题
要调试内存问题(例如缓冲区溢出或释放后使用错误),您可以使用某些现代编译器支持的 地址消毒器 (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 工作进程会被阻塞,从而降低性能。仅使用提供异步接口且不会阻塞整个进程的库。
HTTP 请求到外部服务
模块通常需要对某些外部服务执行 HTTP 调用。一个常见的错误是使用一些外部库,例如 libcurl,来执行 HTTP 请求。完全没有必要为了完成 nginx 本身可以完成的任务而引入大量外部(可能 阻塞!)代码。
当需要外部请求时,有两种基本的使用场景。
- 在处理客户端请求的上下文中(例如,在内容处理程序中)。
- 在工作进程的上下文中(例如,定时器处理程序)。
在第一种情况下,最好使用 子请求 API。与其直接访问外部服务,不如在 nginx 配置中声明一个 location 并将您的子请求定向到此 location。此 location 不限于 代理 请求,还可以包含其他 nginx 指令。此类方法的一个示例是 auth_request 指令,它在 ngx_http_auth_request 模块 中实现。
对于第二种情况,可以使用 nginx 中提供的基本 HTTP 客户端功能。例如,OCSP 模块 实现了一个简单的 HTTP 客户端。