开发指南
介绍代码布局
包含文件
整数
常见返回码
错误处理
字符串
概述
格式化
数字转换
正则表达式
时间
容器
数组
列表
队列
红黑树
哈希
内存管理
堆
池
共享内存
日志
循环
缓冲
网络
连接
事件
事件
I/O 事件
定时器事件
发布的事件
事件循环
进程
线程
模块
添加新模块
核心模块
配置指令
HTTP
连接
请求
配置
阶段
变量
复杂值
请求重定向
子请求
请求完成
请求体
请求体过滤器
响应
响应体
响应体过滤器
构建过滤模块
缓冲重用
负载均衡
示例
代码风格
一般规则
文件
注释
预处理器
类型
变量
函数
表达式
条件和循环
标签
调试内存问题
常见陷阱
编写 C 模块
C 字符串
全局变量
手动内存管理
线程
阻塞库
HTTP 请求到外部服务
介绍
代码布局
auto
— 构建脚本src
core
— 基本类型和函数 — 字符串、数组、日志、池等。event
— 事件核心modules
— 事件通知模块:epoll
、kqueue
、select
等。
http
— 核心 HTTP 模块和常见代码modules
— 其他 HTTP 模块v2
— HTTP/2
mail
— 邮件模块os
— 平台特定代码unix
win32
stream
— 流模块
包含文件
每个 nginx 文件的开头必须包含以下两个 #include
语句:
#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
的 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
字节后可能是空终止的,也可能不是。在大多数情况下,它们不是。但是,在代码的某些部分(例如,解析配置时),已知ngx_str_t
对象是空终止的,这简化了字符串比较并使得更容易将字符串传递给系统调用。
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)
—ngx_str_t
类型的静态初始化器,从 C 字符串文字text
开始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 *
(以空字符结尾)%*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
十进制位置。预期该数字的字符串表示形式不应具有超过point
个小数位。例如,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
,从而使得在一个nginx模块内可以通过名称重用共享区域。
共享内存条目结构ngx_shm_zone_t
具有以下字段:
init
— 初始化回调,在共享区域映射到实际内存后调用data
— 数据上下文,用于将任意数据传递给init
回调noreuse
— 禁用从旧循环中重用共享区域的标志tag
— 共享区域标记shm
— 特定于平台的ngx_shm_t
类型对象,至少具有以下字段:addr
— 映射的共享内存地址,最初为NULLsize
— 共享内存大小name
— 共享内存名称log
— 共享内存日志exists
— 指示共享内存是否从主进程继承(特定于Windows)的标志
在解析配置后,共享区域条目在ngx_init_cycle()
中映射到实际内存。在POSIX系统上,使用mmap()
系统调用创建共享的匿名映射。在Windows上,使用CreateFileMapping()
/MapViewOfFileEx()
对创建共享的匿名映射。
为了在共享内存中分配,nginx提供了用于分配内存的板块池ngx_slab_pool_t
类型。在每个nginx共享区域中,自动创建一个用于分配内存的板块池。该池位于共享区域的开头,并且可以通过表达式(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()
函数创建的,该函数以前一个循环作为其参数。该函数定位前一个循环的配置文件,并尽可能多地从前一个循环继承资源。一个名为"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
字段中。文件以追加模式打开,并在缺少时创建。在接收到重新打开信号(通常为USR1
)后,nginx工作进程重新打开列表中的文件。在这种情况下,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
— Socket描述符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)
。nginx中的HTTP客户端连接是可重用连接的一个示例;它们被标记为可重用,直到从客户端收到第一个请求字节为止。
事件
事件
nginx中的事件对象ngx_event_t
提供了一种机制,用于通知特定事件已发生。
ngx_event_t
中的字段包括:
数据
— 通常作为指向与事件相关的连接的指针,在事件处理程序中使用的任意事件上下文。处理程序
— 当事件发生时要调用的回调函数。写
— 表示写事件的标志。缺少该标志表示读事件。活动
— 表示事件已注册接收I/O通知的标志,通常来自像epoll
、kqueue
、poll
这样的通知机制。准备就绪
— 表示事件已收到I/O通知的标志。延迟
— 表示由于速率限制而延迟I/O的标志。定时器
— 用于将事件插入定时器树的红黑树节点。定时器已设置
— 表示事件定时器已设置且尚未过期的标志。超时
— 表示事件定时器已过期的标志。EOF
— 表示在读取数据时发生了EOF的标志。待处理EOF
— 表示套接字上有EOF挂起,即使在其之前可能有一些数据可用。该标志通过EPOLLRDHUP
epoll
事件或EV_EOF
kqueue
标志传递。错误
— 表示在读取(对于读事件)或写入(对于写事件)时发生了错误的标志。可取消
— 定时器事件标志,表示在关闭工作进程时应忽略该事件。优雅的工作进程关闭被延迟,直到没有计划的非取消定时器事件。已发布
— 表示事件已发布到队列的标志。队列
— 将事件发布到队列的队列节点。
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 不是为每个任务创建一个新线程,而是实现了 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
— 模块的名称,如在配置脚本的控制台输出中所示。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模块、流模块、邮件模块和事件模块也是核心模块。核心模块的上下文定义为:
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
转换为ngx_flag_t
值,分别为1或0。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
— 将两个提供的参数转换为包含缓冲区数量和size的ngx_bufs_t
对象。ngx_conf_set_enum_slot
— 将提供的参数转换为ngx_uint_t
值。传递给post
字段的ngx_conf_enum_t
的以空结束的数组定义了可接受的字符串及相应的整数值。ngx_conf_set_bitmask_slot
— 将提供的参数转换为ngx_uint_t
值。每个参数的掩码值进行OR操作,产生结果。传递给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
数组中的ctx_index
位置。以下宏提供了一种方便的方式来获取和设置请求上下文: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主机的服务器查找、重写、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
常量限制。每次更改该值递减,直到达到零,此时会生成错误。重写和内部重定向到普通或命名位置都被视为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配置阶段通过调用函数创建,这些函数分配结构,初始化它们并合并它们。以下示例显示了如何为模块创建一个简单的位置配置。配置有一个名为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()
将一个配置与更高级别的配置合并。实际上,服务器和位置配置不仅存在于服务器和位置级别,还会为它们上面的所有级别创建。具体来说,服务器配置也会在主级别创建,位置配置会在主、服务器和位置级别创建。这些配置使得可以在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的位置配置的指针,并替换结构的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
的引用。请求的主配置永远不会改变。服务器配置可以在选择请求的虚拟服务器后从默认值更改。为处理请求选择的位置配置可以多次更改,因为重写操作或内部重定向的结果。以下示例显示了如何在运行时访问模块的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 选择位置的特殊阶段。在此阶段之前,将请求分配给相关虚拟服务器的默认位置,并且任何请求位置配置的模块都会收到默认服务器位置的配置。此阶段为请求分配新位置。在此阶段无法注册其他处理程序。NGX_HTTP_REWRITE_PHASE
— 与NGX_HTTP_SERVER_REWRITE_PHASE
相同,但用于前一阶段选择的位置中定义的重写规则。NGX_HTTP_POST_REWRITE_PHASE
— 请求在重写过程中其 URI 改变时重定向到新位置的特殊阶段。这是通过请求再次经过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
。它们按顺序调用,直到其中一个产生输出。也可以根据每个位置设置内容处理程序。如果ngx_http_core_module的位置配置具有设置handler
,则将其作为内容处理程序调用,并忽略在此阶段安装的处理程序。NGX_HTTP_LOG_PHASE
— 请求记录执行的阶段。目前,只有ngx_http_log_module在此阶段注册其处理程序以进行访问日志记录。日志阶段处理程序在请求处理的最后阶段调用,就在释放请求之前。
以下是预访问阶段处理程序的示例。
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
被视为最终化代码。从位置内容处理程序返回的任何代码都被视为最终化代码。在访问阶段,在满足任何模式下,除了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
保存分配的变量索引,用于引用变量。
通常,模块会创建一个以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
中的标志(请参阅上面的描述above)。
set
处理程序允许设置由变量引用的属性。例如,$limit_rate
变量的设置处理程序修改请求的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
字段连接到位置。这意味着在任何时候都可以通过调用ngx_http_get_module_loc_conf(r, module)
从请求中检索任何模块的位置配置。请求位置在请求的生命周期中可以多次更改。最初,将请求分配给默认服务器位置的默认服务器。如果请求切换到另一个服务器(由HTTP“Host”标头或SSL SNI扩展选择),则请求也切换到该服务器的默认位置。位置的下一个更改发生在NGX_HTTP_FIND_CONFIG_PHASE
请求阶段。在此阶段,根据请求URI从为服务器配置的所有非命名位置中选择一个位置。 ngx_http_rewrite_module可以在NGX_HTTP_REWRITE_PHASE
请求阶段更改请求URI,结果是rewrite指令将请求发送回NGX_HTTP_FIND_CONFIG_PHASE
阶段,以根据新URI选择新位置。
还可以在任何时候通过调用ngx_http_internal_redirect(r, uri, args)
或ngx_http_named_location(r, name)
将请求重定向到新位置。
ngx_http_internal_redirect(r, uri, args)
函数更改请求URI并将请求返回到NGX_HTTP_SERVER_REWRITE_PHASE
阶段。请求将继续以服务器默认位置进行。稍后在NGX_HTTP_FIND_CONFIG_PHASE
中,将根据新的请求URI选择新位置。
以下示例使用新的请求参数执行内部重定向。
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)
函数将请求重定向到命名位置。位置的名称作为参数传递。在当前服务器的所有命名位置中查找位置,之后请求切换到NGX_HTTP_REWRITE_PHASE
阶段。
以下示例执行到命名位置@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
字段中存储了一些上下文时被调用。这些上下文可能与新的位置配置不一致。为了防止不一致,这两个重定向函数都会擦除所有请求上下文。
调用 ngx_http_internal_redirect(r, uri, args)
或 ngx_http_named_location(r, name)
会增加请求的 count
。为了保持一致的请求引用计数,在重定向请求后调用 ngx_http_finalize_request(r, NGX_DONE)
。这将完成当前请求的代码路径并减少计数器。
重定向和重写的请求变为内部请求,并且可以访问 内部 位置。内部请求具有 internal
标志。
子请求
子请求主要用于将一个请求的输出插入到另一个请求中,可能与其他数据混合。子请求看起来像一个普通请求,但与其父请求共享一些数据。特别是,与客户端输入相关的所有字段都是共享的,因为子请求不会从客户端接收任何其他输入。子请求的请求字段 parent
包含一个指向其父请求的链接,对于主请求而言为空。字段 main
包含一个指向请求组中主请求的链接。
子请求从 NGX_HTTP_SERVER_REWRITE_PHASE
阶段开始。它通过与普通请求相同的后续阶段,并根据其自身的 URI 被分配位置。
子请求中的输出头总是被忽略。 ngx_http_postpone_filter
将子请求的输出主体放置在与父请求产生的其他数据相对应的正确位置。
子请求与活动请求的概念相关联。如果 c->data == r
,其中 c
是客户端连接对象,则请求 r
被视为活动请求。在任何给定时间点,请求组中只有活动请求被允许将其缓冲区输出到客户端。非活动请求仍然可以将其输出发送到过滤器链,但它不会通过 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
- 子请求创建为其父请求的克隆。它从与父请求相同的位置开始并从相同的阶段继续。
以下示例创建了一个 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 位置。- 其他代码被视为成功的完成代码,并可能激活请求写入程序以完成发送响应正文。一旦正文完全发送,请求
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
标志。
以下示例生成了一个以"foo"作为其主体的完整HTTP响应。为了使示例作为子请求和主请求工作,必须在输出的最后一个缓冲区中设置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模块是一个独立的模块,扩展了上游功能。
可以通过将相应的upstream块放入配置文件中来明确配置ngx_http_upstream_module,或者通过使用诸如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_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
— 要连接的上游服务器的地址;这是负载均衡方法的输出参数。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特定方法,用于启用将会话缓存到上游服务器。轮询均衡方法提供了实现。
示例
The nginx-dev-examples repository provides nginx module examples.
代码风格
一般规则
- 最大文本宽度为 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_ */
注释
- 不使用“
//
”注释 - 文本使用英文书写,美式拼写优先
- 多行注释格式如下:
/* * 红黑树代码基于Cormen、Leiserson和Rivest的《算法导论》中描述的算法。 */
/* 查找地址:端口的服务器配置 */
预处理器
宏名以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;
调试内存问题
要调试诸如缓冲区溢出或使用后释放错误之类的内存问题,您可以使用一些现代编译器支持的 AddressSanitizer
(ASan)。要使用gcc
和clang
启用ASan,请使用-fsanitize=address
编译器和链接器选项。在构建nginx时,可以通过将该选项添加到configure
脚本的--with-cc-opt
和--with-ld-opt
参数来完成。
由于nginx中的大多数分配都来自nginx内部的pool,启用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配置中声明一个位置,并将您的子请求定向到该位置。这个位置不仅限于代理请求,还可以包含其他nginx指令。这种方法的一个例子是在ngx_http_auth_request模块中实现的auth_request指令。
对于第二种情况,可以使用nginx中可用的基本HTTP客户端功能。例如,OCSP模块实现了简单的HTTP客户端。