开发#

Angie 是一个开源项目, 欢迎所有贡献者。

源代码#

您可以从我们的公共仓库克隆 Angie 源代码: MercurialGit

编码风格#

您的更改应与 Angie 其余代码保持一致; 编码约定 是一个很好的起点。

小技巧

如有疑问,请检查附近的代码以遵循其风格, 或者简单地在代码库中搜索以获得灵感。

提交消息#

从历史上看,提交日志使用英语维护。

以所做工作的单行摘要开始。 它可能有一个前缀,提交日志将其用于受影响的代码部分。 摘要最多可以有 67 个字符长, 后面可以跟一个空行和更多详细信息。

一个好的消息说明了什么导致了更改、对此做了什么, 以及现在的情况如何:

API: bad things removed, good things added.

As explained elsewhere[1], the original API was bad because stuff;
this change was introduced to improve that aspect locally.

Levels of goodness have been implemented to mitigate the badness;
this is now the preferred way to work.  Also, the badness is gone.

[1] https://example.com

可能被忽视的细节:

  • 摘要以句号结尾,以大写字母开头。

  • 如果使用前缀,后面跟小写字母。

  • 双空格分隔单行内的句子。

最终检查#

  • 尽力验证更改在 所有 目标平台上都能工作。

  • 对于每个平台,运行测试套件以确保没有回归:

    $ cd tests
    $ prove .
    

    详细信息请参见 tests/README 文件。

  • 确保您对 法律条款 感到满意。

提交贡献#

要发送补丁,请在我们的 GitHub 镜像 上创建拉取请求。

如有问题和建议,请通过 GitHub Issues 联系开发者。

编码约定#

源代码遵循以下结构和约定。

代码布局#

  • auto — 构建脚本

  • src

    • core — 基本类型和函数 — 字符串、数组、日志、 内存池等。

    • event — 事件核心

      • modules — 事件通知模块: epollkqueueselect 等。

    • http — 核心 HTTP 模块和通用代码

      • modules — 其他 HTTP 模块

      • v2 — HTTP/2

    • mail — 邮件模块

    • os — 平台特定代码

      • unix

      • win32

    • stream — 流模块

包含文件#

以下两个 #include 语句必须出现在每个 Angie 文件的 开头:

#include <ngx_config.h>
#include <ngx_core.h>

除此之外,HTTP 代码应该包含

#include <ngx_http.h>

邮件代码应该包含

#include <ngx_mail.h>

流代码应该包含

#include <ngx_stream.h>

整数#

对于一般用途,Angie 代码使用两种整数类型, ngx_int_tngx_uint_t,它们分别是 intptr_tuintptr_t 的类型定义。

常见返回码#

Angie 中的大多数函数返回以下代码:

  • 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_errnongx_socket_errno 的值可能会导致 性能问题。 如果错误值可能被多次使用,请将其存储在类型为 ngx_err_t 的局部变量中。 要设置错误,请使用 ngx_set_errno(errno)ngx_set_socket_errno(errno) 宏。

ngx_errnongx_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 字符串,Angie 使用无符号字符类型指针 u_char *

Angie 字符串类型 ngx_str_t 定义如下:

typedef struct {
    size_t      len;
    u_char     *data;
} ngx_str_t;

len 字段保存字符串长度,data 保存字符串数据。 保存在 ngx_str_t 中的字符串在 len 字节之后可能以空字符结尾,也可能不以空字符结尾。 在大多数情况下不以空字符结尾。 但是,在代码的某些部分(例如,解析配置时),ngx_str_t 对象已知以空字符结尾,这简化了字符串比较并使将字符串传递给系统调用变得更容易。

Angie 中的字符串操作在 src/core/ngx_string.h 中声明。 其中一些是标准 C 函数的包装器:

  • ngx_strcmp()

  • ngx_strncmp()

  • ngx_strstr()

  • ngx_strlen()

  • ngx_strchr()

  • ngx_memcmp()

  • ngx_memset()

  • ngx_memcpy()

  • ngx_memmove()

其他字符串函数是 Angie 特有的

  • 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 字符串字面量 textngx_str_t 类型的静态初始化器

  • ngx_null_stringngx_str_t 类型的静态空字符串初始化器

  • ngx_str_set(str, text) — 用 C 字符串字面量 text 初始化 ngx_str_t * 类型的字符串 str

  • ngx_str_null(str) — 用空字符串初始化 ngx_str_t * 类型的字符串 str

格式化#

以下格式化函数支持 Angie 特定类型:

  • 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 中。其中一些是:

  • %Ooff_t

  • %Ttime_t

  • %zssize_t

  • %ingx_int_t

  • %pvoid *

  • %Vngx_str_t *

  • %su_char * (以空字符结尾)

  • %*ssize_t + u_char *

您可以在大多数类型前加上 u 使其成为无符号类型。 要将输出转换为十六进制,请使用 Xx

数值转换#

Angie 中实现了几个数值转换函数。 前四个函数各自将给定长度的字符串转换为指定类型的正整数。 出错时返回 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

正则表达式#

Angie 中的正则表达式接口是 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 结构中的 capturesnamed_captures 字段分别包含在正则表达式中找到的所有捕获和命名捕获的计数。

然后可以使用编译后的正则表达式来匹配字符串:

#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 结构中的 capturesnamed_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 的可选整数数组,以及数组的 sizecaptures 数组的大小必须是三的倍数,这是 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 — 用于系统日志条目: "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 的指针)。 当 Angie 从系统调用返回到事件循环时,时间总是会被更新。 要立即更新时间,调用 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")。

容器#

数组#

Angie 数组类型 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;

/* 创建一个字符串数组,为 10 个元素预分配内存 */
a = ngx_array_create(pool, 10, sizeof(ngx_str_t));

/* 为 10 个元素初始化字符串数组 */
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);

链表#

在 Angie 中,链表是数组的序列,针对插入可能大量的项目进行了优化。 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 */ }

/* 向链表添加项目 */

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");

/* 遍历链表 */

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 字段设置为零。 以这种方式标记的项目在遍历头部时会被显式跳过。

队列#

在 Angie 中,队列是一个侵入式双向链表,每个节点定义如下:

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);

/* 在此处插入更多节点 */

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 中声明。 支持精确匹配和通配符匹配。 后者需要额外的设置,在下面的单独章节中描述。

在初始化哈希之前,你需要知道它将要 容纳的元素数量,以便 Angie 能够最优地构建它。 需要配置的两个参数是 max_sizebucket_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_SMALLNGX_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_sizebucket_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_headdns_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"; - 将匹配 ".example.org" */
/* key = "foo.example.com"; - 将匹配 "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() 的包装器

内存池#

大多数 Angie 分配都在内存池中完成。 在 Angie 内存池中分配的内存在内存池被销毁时会自动释放。 这提供了良好的分配性能并使内存控制变得容易。

内存池在内部将对象分配在连续的内存块中。 一旦一个块满了,就会分配一个新块并将其添加到内存池内存块列表中。 当请求的分配太大而无法放入块中时,请求会转发给系统分配器, 返回的指针存储在内存池中以便进一步释放。

Angie 内存池的类型是 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)在 Angie 中被积极使用, 因此 Angie 内存池实现提供了一种重用它们的方法。 ngx_pool_tchain 字段保存 一个准备重用的先前分配链接的列表。 要在内存池中高效分配链式链接,请使用 ngx_alloc_chain_link(pool) 函数。 此函数在内存池列表中查找空闲链式链接,如果内存池列表为空则分配新的链式链接。 要释放链接,请调用 ngx_free_chain(pool, cl) 函数。

清理处理程序可以在内存池中注册。 清理处理程序是一个带有参数的回调函数,在内存池被销毁时调用。 内存池通常与特定的 Angie 对象(如 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);
}

共享内存#

Angie 使用共享内存在进程之间共享通用数据。 ngx_shared_memory_add(cf, name, size, tag) 函数向循环中添加 一个新的共享内存条目 ngx_shm_zone_t。 该函数接收区域的 namesize。 每个共享区域必须有一个唯一的名称。 如果具有提供的 nametag 的共享区域条目已经存在, 则重用现有的区域条目。 如果具有相同名称的现有条目具有不同的标签,则函数失败并返回错误。 通常,模块结构的地址作为 tag 传递, 这使得在一个 Angie 模块内可以按名称重用共享区域。

共享内存条目结构 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() 对。

为了在共享内存中分配,Angie 提供了 slab 池 ngx_slab_pool_t 类型。 用于分配内存的 slab 池会在每个 Angie 共享区域中自动创建。 该池位于共享区域的开头,可以通过表达式 (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_tmutex 字段中可用的互斥锁。 互斥锁最常用于 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;

    ctx = 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 Angie 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;
}

日志记录#

Angie 使用 ngx_log_t 对象进行日志记录。 Angie 日志记录器支持多种输出类型:

  • 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

通常,日志记录器由现有的 Angie 代码从 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(存储在 :samp:`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);

上面的示例会产生如下的日志条目:

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);
}
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

周期#

周期对象存储从特定配置创建的 Angie 运行时上下文。 其类型为 ngx_cycle_t。 当前周期由全局变量 ngx_cycle 引用,并在 Angie 工作进程启动时被继承。 每次重新加载 Angie 配置时,都会从新的 Angie 配置创建一个新周期;通常在新周期成功创建后删除旧周期。

周期由 ngx_init_cycle() 函数创建,该函数将前一个周期作为其参数。 该函数定位前一个周期的配置文件,并从前一个周期继承尽可能多的资源。 在 Angie 启动时创建一个称为"初始周期"的占位符周期,然后被从配置构建的实际周期替换。

周期的成员包括:

  • pool — 周期池。 为每个新周期创建。

  • log — 周期日志。 最初从旧周期继承,在读取配置后设置为指向 new_log

  • new_log — 由配置创建的周期日志。 它受根作用域 error_log 指令影响。

  • connectionsconnection_n — 类型为 ngx_connection_t 的连接数组,由事件模块在初始化每个 Angie 工作进程时创建。 Angie 配置中的 worker_connections 指令设置连接数 connection_n

  • free_connectionsfree_connection_n — 当前可用连接的列表和数量。 如果没有可用连接,Angie 工作进程将拒绝接受新客户端或连接到上游服务器。

  • filesfiles_n — 用于将文件描述符映射到 Angie 连接的数组。 此映射由具有 NGX_USE_FD_EVENT 标志的事件模块使用(目前是 polldevpoll)。

  • conf_ctx — 核心模块配置数组。 这些配置在读取 Angie 配置文件期间创建和填充。

  • modulesmodules_n — 类型为 ngx_module_t 的模块数组,包括当前配置加载的静态和动态模块。

  • listening — 类型为 ngx_listening_t 的监听对象数组。 监听对象通常由不同模块的 listen 指令添加,这些模块调用 ngx_create_listening() 函数。 监听套接字基于监听对象创建。

  • paths — 类型为 ngx_path_t 的路径数组。 路径由将要操作特定目录的模块调用 ngx_add_path() 函数添加。 如果缺少这些目录,Angie 在读取配置后会创建它们。 此外,可以为每个路径添加两个处理程序:

    • 路径加载器 — 在启动或重新加载 Angie 后 60 秒内仅执行一次。 通常,加载器读取目录并将数据存储在 Angie 共享内存中。 该处理程序从专用的 Angie 进程"cache loader"调用。

    • 路径管理器 — 定期执行。 通常,管理器从目录中删除旧文件并更新 Angie 内存以反映更改。 该处理程序从专用的"cache manager"进程调用。

  • open_files — 类型为 ngx_open_file_t 的打开文件对象列表,通过调用 ngx_conf_open_file() 函数创建。 目前,Angie 使用这种打开文件进行日志记录。 读取配置后,Angie 打开 open_files 列表中的所有文件,并将每个文件描述符存储在对象的 fd 字段中。 文件以追加模式打开,如果缺少则创建。 列表中的文件在 Angie 工作进程收到重新打开信号(通常是 USR1)时重新打开。 在这种情况下,fd 字段中的描述符更改为新值。

  • shared_memory — 共享内存区域列表,每个通过调用 ngx_shared_memory_add() 函数添加。 共享区域映射到所有 Angie 进程中的相同地址范围,用于共享公共数据,例如 HTTP 缓存内存树。

缓冲区#

对于输入/输出操作,Angie 提供缓冲区类型 ngx_buf_t。 通常,它用于保存要写入目标或从源读取的数据。 缓冲区可以引用内存或文件中的数据,从技术上讲,缓冲区可以同时引用两者。 缓冲区的内存单独分配,与缓冲区结构 ngx_buf_t 无关。

ngx_buf_t 结构具有以下字段:

  • startend — 为缓冲区分配的内存块的边界。

  • poslast — 内存缓冲区的边界;通常是 start .. end 的子范围。

  • file_posfile_last — 文件缓冲区的边界,表示为从文件开头的偏移量。

  • tag — 用于区分缓冲区的唯一值;由不同的 Angie 模块创建,通常用于缓冲区重用。

  • file — 文件对象。

  • temporary — 指示缓冲区引用可写内存的标志。

  • memory — 指示缓冲区引用只读内存的标志。

  • in_file — 指示缓冲区引用文件中数据的标志。

  • flush — 指示需要刷新缓冲区之前的所有数据的标志。

  • recycled — 指示缓冲区可以重用并需要尽快消费的标志。

  • sync — 指示缓冲区不携带数据或特殊信号(如 flushlast_buf)的标志。 默认情况下,Angie 将此类缓冲区视为错误条件,但此标志告诉 Angie 跳过错误检查。

  • 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;
};

每个链节点保持对其缓冲区的引用和对下一个链节点的引用。

使用缓冲区和链的示例:

typedef struct ngx_chain_s  ngx_chain_t;

struct ngx_chain_s {
    ngx_buf_t    *buf;
    ngx_chain_t  *next;
};

每个链节点保持对其缓冲区的引用和对下一个链节点的引用。

使用缓冲区和链的示例:

ngx_chain_t *
ngx_get_my_chain(ngx_pool_t *pool)
{
    ngx_buf_t    *b;
    ngx_chain_t  *out, *cl, **ll;

    /* first buf */
    cl = ngx_alloc_chain_link(pool);
    if (cl == NULL) { /* error */ }

    b = ngx_calloc_buf(pool);
    if (b == NULL) { /* error */ }

    b->start = (u_char *) "foo";
    b->pos = b->start;
    b->end = b->start + 3;
    b->last = b->end;
    b->memory = 1; /* read-only memory */

    cl->buf = b;
    out = cl;
    ll = &cl->next;

    /* second buf */
    cl = ngx_alloc_chain_link(pool);
    if (cl == NULL) { /* error */ }

    b = ngx_create_temp_buf(pool, 3);
    if (b == NULL) { /* error */ }

    b->last = ngx_cpymem(b->last, "foo", 3);

    cl->buf = b;
    cl->next = NULL;
    *ll = cl;

    return out;
}

网络#

连接#

连接类型 ngx_connection_t 是套接字描述符的包装器。 它包含以下字段:

  • fd — 套接字描述符

  • data — 任意连接上下文。 通常,它是指向构建在连接之上的更高级对象的指针, 例如 HTTP 请求或 Stream 会话。

  • readwrite — 连接的读写事件。

  • recvsendrecv_chainsend_chain — 连接的 I/O 操作。

  • pool — 连接池。

  • log — 连接日志。

  • sockaddrsocklenaddr_text — 远程套接字地址的二进制和文本形式。

  • local_sockaddrlocal_socklen — 本地 套接字地址的二进制形式。 最初,这些字段为空。 使用 ngx_connection_local_sockaddr() 函数获取 本地套接字地址。

  • proxy_protocol_addrproxy_protocol_port - PROXY 协议客户端地址和端口,如果为连接启用了 PROXY 协议。

  • ssl — 连接的 SSL 上下文。

  • reusable — 标志,指示连接处于可重用状态。

  • close — 标志,指示连接正在被重用并需要关闭。

Angie 连接可以透明地封装 SSL 层。 在这种情况下,连接的 ssl 字段持有指向 ngx_ssl_connection_t 结构的指针,保存连接的所有 SSL 相关数据, 包括 SSL_CTXSSLrecvsendrecv_chainsend_chain 处理程序也被设置为 支持 SSL 的函数。

Angie 配置中的 worker_connections 指令 限制每个 Angie worker 的连接数。 所有连接结构在 worker 启动时预先创建并存储在 cycle 对象的 connections 字段中。 要检索连接结构,使用 ngx_get_connection(s, log) 函数。 它将套接字描述符作为 s 参数,该描述符需要 包装在连接结构中。

由于每个 worker 的连接数有限,Angie 提供了一种 获取当前正在使用的连接的方法。 要启用或禁用连接的重用,调用 ngx_reusable_connection(c, reusable) 函数。 调用 ngx_reusable_connection(c, 1) 在连接结构中设置 reuse 标志,并将连接插入到 cycle 的 reusable_connections_queue 中。 每当 ngx_get_connection() 发现 cycle 的 free_connections 列表中没有可用连接时, 它调用 ngx_drain_connections() 释放 特定数量的可重用连接。 对于每个这样的连接,设置 close 标志并调用其读处理程序, 该处理程序应该通过调用 ngx_close_connection(c) 释放连接 并使其可供重用。 要退出连接可重用状态,调用 ngx_reusable_connection(c, 0)。 HTTP 客户端连接是 Angie 中可重用连接的一个例子;它们 被标记为可重用,直到从客户端接收到第一个请求字节。

事件#

事件#

Angie 中的事件对象 ngx_event_t 提供了一种 通知特定事件已发生的机制。

ngx_event_t 中的字段包括:

  • data — 事件处理程序中使用的任意事件上下文, 通常作为指向与事件相关的连接的指针。

  • handler — 事件发生时要调用的回调函数。

  • write — 指示写事件的标志。 缺少此标志表示读事件。

  • active — 指示事件已注册接收 I/O 通知的标志, 通常来自通知机制如 epollkqueuepoll

  • ready — 指示事件已接收到 I/O 通知的标志。

  • delayed — 指示由于速率限制而延迟 I/O 的标志。

  • timer — 用于将事件插入定时器树的红黑树节点。

  • timer_set — 指示事件定时器已设置且尚未过期的标志。

  • timedout — 指示事件定时器已过期的标志。

  • eof — 指示读取数据时发生 EOF 的标志。

  • pending_eof — 指示套接字上有待处理的 EOF 的标志, 即使在此之前可能有一些可用数据。 该标志通过 EPOLLRDHUP epoll 事件或 EV_EOF kqueue 标志传递。

  • error — 指示在读取(对于读事件)或写入(对于写事件) 期间发生错误的标志。

  • cancelable — 定时器事件标志,指示在关闭 worker 时 应忽略该事件。 优雅的 worker 关闭会延迟,直到没有计划的不可取消定时器事件。

  • posted — 指示事件已发布到队列的标志。

  • queue — 用于将事件发布到队列的队列节点。

I/O 事件#

通过调用 ngx_get_connection() 函数获得的每个连接都有两个附加事件, c->readc->write,用于接收套接字准备好读取或写入的通知。 所有此类事件都在边缘触发模式下运行,这意味着它们只在套接字状态发生变化时才触发通知。 例如,在套接字上进行部分读取不会使 Angie 发送重复的读取通知,直到更多数据到达套接字。 即使底层 I/O 通知机制本质上是水平触发的(pollselect 等), Angie 也会将通知转换为边缘触发。 为了使 Angie 事件通知在不同平台上的所有通知系统中保持一致, 必须在处理 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 类型,是事件发生的时间。 树结构支持快速插入和删除操作,以及访问最近的超时, Angie 使用它来确定等待 I/O 事件和超时事件到期的时间。

发布事件#

事件可以被发布,这意味着其处理程序将在当前事件循环迭代中的某个时刻被调用。 发布事件是简化代码和避免堆栈溢出的良好实践。 发布的事件保存在发布队列中。 ngx_post_event(ev, q) 宏将事件 ev 发布到发布队列 qngx_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 */ }
}

事件循环#

除了 Angie 主进程外,所有 Angie 进程都执行 I/O 操作,因此都有一个事件循环。 (Angie 主进程则将大部分时间花费在 sigsuspend() 调用上,等待信号到达。) Angie 事件循环在 ngx_process_events_and_timers() 函数中实现,该函数被重复调用直到进程退出。

事件循环包含以下阶段:

  • 通过调用 ngx_event_find_timer() 找到最接近过期的超时。 该函数在定时器树中找到最左边的节点,并返回该节点过期前的毫秒数。

  • 通过调用处理程序来处理 I/O 事件,该处理程序特定于由 Angie 配置选择的事件通知机制。 该处理程序等待至少一个 I/O 事件发生,但只等到下一个超时过期。 当读或写事件发生时,ready 标志被设置,事件的处理程序被调用。 对于 Linux,通常使用 ngx_epoll_process_events() 处理程序,它调用 epoll_wait() 来等待 I/O 事件。

  • 通过调用 ngx_event_expire_timers() 使定时器过期。 从最左边的元素开始向右遍历定时器树,直到找到未过期的超时。 对于每个过期的节点,timedout 事件标志被设置,timer_set 标志被重置,事件处理程序被调用。

  • 通过调用 ngx_event_process_posted() 处理已发布的事件。 该函数重复从已发布事件队列中移除第一个元素并调用该元素的处理程序,直到队列为空。

所有 Angie 进程也处理信号。 信号处理程序只设置全局变量,这些变量在 ngx_process_events_and_timers() 调用后被检查。

进程#

Angie 中有几种类型的进程。 进程的类型保存在 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()

Angie 进程处理以下信号:

  • NGX_SHUTDOWN_SIGNAL`(在大多数系统上是 :samp:`SIGQUIT)— 优雅关闭。 收到此信号后,主进程向所有子进程发送关闭信号。 当没有子进程剩余时,主进程销毁循环池并退出。 当工作进程收到此信号时,它关闭所有监听套接字并等待直到没有不可取消的事件被调度,然后销毁循环池并退出。 当缓存管理器或缓存加载器进程收到此信号时,它立即退出。 当进程收到此信号时,ngx_quit 变量被设置为 1,并在处理后立即重置。 当工作进程处于关闭状态时,ngx_exiting 变量被设置为 1

  • NGX_TERMINATE_SIGNAL`(在大多数系统上是 :samp:`SIGTERM)— 终止。 收到此信号后,主进程向所有子进程发送终止信号。 如果子进程在 1 秒内没有退出,主进程发送 SIGKILL 信号来杀死它。 当没有子进程剩余时,主进程销毁循环池并退出。 当工作进程、缓存管理器进程或缓存加载器进程收到此信号时,它销毁循环池并退出。 收到此信号时,变量 ngx_terminate 被设置为 1

  • NGX_NOACCEPT_SIGNAL`(在大多数系统上是 :samp:`SIGWINCH)— 关闭所有工作进程和辅助进程。 收到此信号后,主进程关闭其子进程。 如果先前启动的新 Angie 二进制文件退出,旧主进程的子进程会重新启动。 当工作进程收到此信号时,它在由 debug_points 指令设置的调试模式下关闭。

  • NGX_RECONFIGURE_SIGNAL`(在大多数系统上是 :samp:`SIGHUP)— 重新配置。 收到此信号后,主进程重新读取配置并基于它创建新的循环。 如果新循环创建成功,旧循环被删除,新的子进程被启动。 同时,旧的子进程收到 NGX_SHUTDOWN_SIGNAL 信号。 在单进程模式下,Angie 创建新的循环,但保留旧的循环直到不再有客户端与其绑定活动连接。 工作进程和辅助进程忽略此信号。

  • NGX_REOPEN_SIGNAL`(在大多数系统上是 :samp:`SIGUSR1)— 重新打开文件。 主进程向工作进程发送此信号,工作进程重新打开与循环相关的所有 open_files

  • NGX_CHANGEBIN_SIGNAL`(在大多数系统上是 :samp:`SIGUSR2)— 更换 Angie 二进制文件。 主进程启动一个新的 Angie 二进制文件并传入所有监听套接字的列表。 通过 "NGINX" 环境变量传递的文本格式列表,由用分号分隔的描述符编号组成。 新的 Angie 二进制文件读取 "NGINX" 变量并将套接字添加到其初始化循环中。 其他进程忽略此信号。

虽然所有 Angie 工作进程都能够接收并正确处理 POSIX 信号,但主进程不使用标准的 kill() 系统调用来向工作进程和辅助进程传递信号。 相反,Angie 使用进程间套接字对,允许在所有 Angie 进程之间发送消息。 但是,目前消息只从主进程发送到其子进程。 这些消息携带标准信号。

线程#

可以将原本会阻塞 Angie 工作进程的任务卸载到单独的线程中。 例如,Angie 可以配置为使用线程来执行 文件 I/O。 另一个用例是没有异步接口的库,因此无法正常与 Angie 一起使用。 请记住,线程接口是现有异步处理客户端连接方法的辅助工具,绝不是替代品。

为了处理同步,提供了以下 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);

Angie 没有为每个任务创建新线程,而是实现了 线程池 策略。 可以为不同目的配置多个线程池(例如,在不同的磁盘集合上执行 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 Angie 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;
}

模块#

添加新模块#

每个独立的 Angie 模块都位于一个单独的目录中,该目录至少包含两个文件: config 和一个包含模块源代码的文件。 config 文件包含 Angie 集成模块所需的所有信息,例如:

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 — 要构建的模块类型。 可能的值有 COREHTTPHTTP_FILTERHTTP_INIT_FILTERHTTP_AUX_FILTERMAILSTREAMMISC

  • 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 库。 可以使用以下宏来链接与 Angie 相同的库: LIBXSLTLIBGDGEOIPPCREOPENSSLMD5SHA1ZLIBPERL

  • ngx_module_link — 由构建系统设置的变量, 对于动态模块设置为 DYNAMIC,对于静态模块设置为 ADDON, 用于根据链接类型确定要执行的不同操作。

  • ngx_module_order — 模块的加载顺序; 对于 HTTP_FILTERHTTP_AUX_FILTER 模块类型很有用。 此选项的格式是以空格分隔的模块列表。 列表中当前模块名称之后的所有模块在全局模块列表中都位于它之后,这设置了模块初始化的顺序。 对于过滤器模块,较晚的初始化意味着较早的执行。

    以下模块通常用作参考。 ngx_http_copy_filter_module 为其他过滤器模块读取数据, 并放置在列表底部附近,因此它是最先执行的模块之一。 ngx_http_write_filter_module 将数据写入客户端套接字, 并放置在列表顶部附近,是最后执行的。

    默认情况下,过滤器模块放置在模块列表中 ngx_http_copy_filter 之前,以便过滤器处理程序在复制过滤器处理程序之后执行。 对于其他模块类型,默认值是空字符串。

要将模块静态编译到 Angie 中,请在配置脚本中使用 --add-module=/path/to/module 参数。 要编译模块以便稍后动态加载到 Angie 中,请使用 --add-dynamic-module=/path/to/module 参数。

核心模块#

模块是 Angie 的构建块,其大部分功能都是作为模块实现的。 模块源文件必须包含一个类型为 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 数组中指定的配置指令,并可以在 Angie 生命周期的特定阶段被调用。 模块生命周期包含以下事件:

  • 配置指令处理程序在主进程上下文中按照它们在配置文件中出现的顺序被调用。

  • 配置解析成功后,init_module 处理程序在主进程上下文中被调用。 每次加载配置时,init_module 处理程序都会在主进程中被调用。

  • 主进程创建一个或多个工作进程,init_process 处理程序在每个工作进程中被调用。

  • 当工作进程从主进程接收到关闭或终止命令时,它会调用 exit_process 处理程序。

  • 主进程在退出前调用 exit_master 处理程序。

由于线程在 Angie 中仅用作具有自己 API 的辅助 I/O 设施,init_threadexit_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_modulengx_errlog_modulengx_regex_modulengx_thread_pool_modulengx_openssl_module 模块。 HTTP 模块、stream 模块、mail 模块和事件模块也是核心模块。 核心模块的上下文定义为:

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_confinit_conf 是分别用于创建和初始化模块配置的函数指针。 对于核心模块,Angie 在解析新配置之前调用 create_conf,在所有配置解析成功后调用 init_conf。 典型的 create_conf 函数为配置分配内存并设置默认值。

例如,一个名为 ngx_foo_module 的简单模块可能如下所示:

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_confinit_conf 是分别指向创建和初始化模块配置的函数的指针。 对于核心模块,Angie 在解析新配置之前调用 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..:samp:NGX_CONF_TAKE7 — 指令接受确切指定数量的参数。

  • NGX_CONF_TAKE12NGX_CONF_TAKE13NGX_CONF_TAKE23NGX_CONF_TAKE123NGX_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 — 在 events 块中。

  • NGX_DIRECT_CONF — 由不创建上下文层次结构且只有一个全局配置的模块使用。此配置作为 conf 参数传递给处理程序。

配置解析器使用这些标志在指令位置错误时抛出错误,并调用提供了适当配置指针的指令处理程序,以便不同位置的相同指令可以将其值存储在不同的地方。

set 字段定义了一个处理指令并将解析值存储到相应配置中的处理程序。 有许多执行常见转换的函数:

  • ngx_conf_set_flag_slot — 将字面字符串 onoff 转换为 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_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 字段中传递的以 null 结尾的 ngx_conf_enum_t 数组定义了可接受的字符串和相应的整数值。

  • ngx_conf_set_bitmask_slot — 将提供的参数转换为 ngx_uint_t 值。每个参数的掩码值通过 OR 运算产生结果。在 post 字段中传递的以 null 结尾的 ngx_conf_bitmask_t 数组定义了可接受的字符串和相应的掩码值。

  • ngx_conf_set_path_slot — 将提供的参数转换为 ngx_path_t 值并执行所有必需的初始化。详细信息请参阅 proxy_temp_path 指令的文档。

  • ngx_conf_set_access_slot — 将提供的参数转换为文件权限掩码。详细信息请参阅 proxy_store_access 指令的文档。

conf 字段定义了传递给指令处理程序的配置结构。 核心模块只有全局配置,并设置 NGX_DIRECT_CONF 标志来访问它。 像 HTTP、Stream 或 Mail 这样的模块创建配置层次结构。 例如,模块的配置是为 serverlocationif 作用域创建的。

  • NGX_HTTP_MAIN_CONF_OFFSEThttp 块的配置。

  • NGX_HTTP_SRV_CONF_OFFSEThttp 块内 server 块的配置。

  • NGX_HTTP_LOC_CONF_OFFSEThttplocation 块的配置。

  • NGX_STREAM_MAIN_CONF_OFFSETstream 块的配置。

  • NGX_STREAM_SRV_CONF_OFFSETstream 块内 server 块的配置。

  • NGX_MAIL_MAIN_CONF_OFFSETmail 块的配置。

  • NGX_MAIL_SRV_CONF_OFFSETmail 块内 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 对象来包装新接受的客户端套接字。 每个 Angie 监听器都提供一个处理器来传递新的连接对象。 对于 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_PHASENGX_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_tdata 字段指向请求。 这样的请求被称为活动请求,与连接绑定的其他请求相对。 活动请求用于处理客户端连接事件,并被允许向客户端输出其响应。 通常,每个请求在某个时刻都会变为活动状态,以便它可以发送其输出。

  • 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_confsrv_confloc_conf — 当前请求配置数组。 配置存储在模块的 ctx_index 位置。

  • read_event_handlerwrite_event_handler - 请求的读和写事件处理器。 通常,HTTP 连接的读和写事件处理器都设置为 ngx_http_request_handler()。 此函数调用当前活动请求的 read_event_handlerwrite_event_handler 处理器。

  • cache — 用于缓存上游响应的请求缓存对象。

  • upstream — 用于代理的请求上游对象。

  • pool — 请求池。 请求对象本身在此池中分配,当请求被删除时该池被销毁。 对于需要在整个客户端连接生命周期内可用的分配,请使用 ngx_connection_t 的池。

  • header_in — 读取客户端 HTTP 请求头部的缓冲区。

  • headers_inheaders_out — 输入和输出 HTTP 头部对象。 两个对象都包含类型为 ngx_list_theaders 字段,用于保存原始头部列表。 除此之外,特定头部可作为单独字段进行获取和设置,例如 content_length_nstatus 等。

  • request_body — 客户端请求体对象。

  • start_secstart_msec — 请求创建时的时间点,用于跟踪请求持续时间。

  • methodmethod_name — 客户端 HTTP 请求方法的数字和文本表示。 方法的数字值在 src/http/ngx_http_request.h 中用宏 NGX_HTTP_GETNGX_HTTP_HEADNGX_HTTP_POST 等定义。

  • http_protocol — 客户端 HTTP 协议版本的原始文本形式("HTTP/1.0"、"HTTP/1.1" 等)。

  • http_version — 客户端 HTTP 协议版本的数字形式(NGX_HTTP_VERSION_10NGX_HTTP_VERSION_11 等)。

  • http_majorhttp_minor — 客户端 HTTP 协议版本的数字形式,分为主版本号和次版本号部分。

  • request_lineunparsed_uri — 原始客户端请求中的请求行和 URI。

  • uriargsexten — 当前请求的 URI、参数和文件扩展名。 这里的 URI 值可能由于规范化而与客户端发送的原始 URI 不同。 在整个请求处理过程中,这些值可能会因为执行内部重定向而发生变化。

  • main — 指向主请求对象的指针。 此对象是为处理客户端 HTTP 请求而创建的,与子请求相对,子请求是为在主请求内执行特定子任务而创建的。

  • parent — 指向子请求的父请求的指针。

  • postponed — 输出缓冲区和子请求的列表,按照它们被发送和创建的顺序排列。 该列表被 postpone 过滤器用来在部分内容由子请求创建时提供一致的请求输出。

  • post_subrequest — 指向带有上下文的处理器的指针, 当子请求完成时会调用该处理器。 主请求不使用此字段。

  • posted_requests — 要启动或恢复的请求列表, 通过调用请求的 write_event_handler 来完成。 通常,此处理器保存请求主函数,该函数首先运行请求阶段然后产生输出。

    请求通常通过 ngx_http_post_request(r, NULL) 调用来发布。 它总是被发布到主请求的 posted_requests 列表中。 函数 ngx_http_run_posted_requests(c) 运行所有在传入连接的活动请求的主请求中发布的请求。 所有事件处理器都会调用 ngx_http_run_posted_requests, 这可能导致新的发布请求。 通常,它在调用请求的读或写处理器之后被调用。

  • phase_handler — 当前请求阶段的索引。

  • ncapturescapturescaptures_data — 由请求的最后一次正则表达式匹配产生的正则捕获。 正则表达式匹配可能在请求处理期间的多个地方发生: 映射查找、通过 SNI 或 HTTP Host 的服务器查找、重写、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 — 指示输出头已被请求发送的标志。

  • 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_memoryfilter_need_in_memory — 请求在内存缓冲区而不是文件中产生输出的标志。 这是对 copy 过滤器的信号,即使启用了 sendfile 也要从文件缓冲区读取数据。 两个标志的区别在于设置它们的过滤器模块的位置。 在过滤器链中 postpone 过滤器之前调用的过滤器设置 filter_need_in_memory, 请求只有当前请求输出使用内存缓冲区。 在过滤器链中稍后调用的过滤器设置 main_filter_need_in_memory, 请求主请求和所有子请求在发送输出时都在内存中读取文件。

  • filter_need_temporary — 请求在临时缓冲区中产生请求输出的标志, 但不在只读内存缓冲区或文件缓冲区中。 这被可能直接在发送缓冲区中更改输出的过滤器使用。

配置#

每个 HTTP 模块可以有三种类型的配置:

  • 主配置 — 应用于整个 http 块。 作为模块的全局设置。

  • 服务器配置 — 应用于单个 server 块。 作为模块的服务器特定设置。

  • 位置配置 — 应用于单个 locationiflimit_except 块。 作为模块的位置特定设置。

配置结构在 Angie 配置阶段通过调用函数创建, 这些函数分配结构、初始化它们并合并它们。 以下示例显示如何为模块创建简单的位置配置。 该配置有一个设置 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 :samp:`location` configuration */
    ngx_http_foo_merge_loc_conf            /* merge :samp:`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() 将配置与更高级别的配置合并。 实际上,服务器和位置配置不仅存在于服务器和位置级别,还会为它们之上的所有级别创建。 具体来说,服务器配置也会在主级别创建,位置配置会在主、服务器和位置级别创建。 这些配置使得可以在 Angie 配置文件的任何级别指定特定于服务器和位置的设置。 最终配置会向下合并。 提供了许多宏,如 NGX_CONF_UNSETNGX_CONF_UNSET_UINT,用于指示缺失的设置并在合并时忽略它。 标准的 Angie 合并宏,如 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)

以下示例获取标准 Angie 核心模块 HTTP模块 的位置配置指针,并替换保存在结构的 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 请求都会经过一系列阶段。 在每个阶段中,都会对请求执行不同类型的处理。 模块特定的处理程序可以在大多数阶段中注册, 许多标准 Angie 模块通过注册其阶段处理程序来在请求处理的特定阶段被调用。 阶段按顺序处理,当请求到达某个阶段时,该阶段的处理程序就会被调用。 以下是 Angie HTTP 阶段的列表。

  • NGX_HTTP_POST_READ_PHASE — 第一个阶段。 RealIP 在此阶段注册其处理程序,以便在调用任何其他模块之前 替换客户端地址。

  • NGX_HTTP_SERVER_REWRITE_PHASE — 处理在 server 块中定义(但在 location 块外)的 重写指令的阶段。 Rewrite 在此阶段安装其处理程序。

  • 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 — 用于不同类型处理程序的通用阶段, 与访问控制无关。 标准 Angie 模块 Limit ConnLimit Req 在此阶段注册其处理程序。

  • NGX_HTTP_ACCESS_PHASE — 验证客户端是否有权限 发起请求的阶段。 标准 Angie 模块如 访问Auth Basic 在此阶段注册其处理程序。 默认情况下,客户端必须通过此阶段注册的所有处理程序的授权检查, 请求才能继续到下一阶段。 satisfy 指令 可用于在任何阶段处理程序授权客户端时允许处理继续。

  • NGX_HTTP_POST_ACCESS_PHASE — 处理 satisfy 指令 的特殊阶段。 如果某些访问阶段处理程序拒绝了访问且没有明确允许, 请求将被终止。 此阶段不能注册额外的处理程序。

  • NGX_HTTP_PRECONTENT_PHASE — 在生成内容之前 调用处理程序的阶段。 标准模块如 try_filesMirror 在此阶段注册其处理程序。

  • NGX_HTTP_CONTENT_PHASE — 通常生成响应的阶段。 多个 Angie 标准模块在此阶段注册其处理程序, 包括 Index。 它们按顺序调用,直到其中一个产生输出。 也可以基于每个 location 设置内容处理程序。 如果 HTTP模块location 配置设置了 handler,它将 作为内容处理程序被调用,此阶段安装的处理程序将被忽略。

  • NGX_HTTP_LOG_PHASE — 执行请求日志记录的阶段。 目前,只有 Log 在此阶段注册其处理程序用于访问日志记录。 日志阶段处理程序在请求处理的最后阶段被调用, 就在释放请求之前。

以下是预访问阶段处理程序的示例。

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 :samp:`location` configuration */
    NULL                                   /* merge :samp:`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_AGAINNGX_DONE — 暂停 阶段处理,直到某个未来事件发生,该事件可以是 异步 I/O 操作或仅仅是延迟,例如。 假设阶段处理将在稍后通过调用 ngx_http_core_run_phases() 来恢复。

  • 阶段处理器返回的任何其他值都被视为请求 终结代码,特别是 HTTP 响应代码。 请求将使用提供的代码终结。

对于某些阶段,返回代码的处理方式略有不同。 在内容阶段,除了 NGX_DECLINED 之外的任何返回代码都被视为终结代码。 来自位置内容处理器的任何返回代码都被视为 终结代码。 在访问阶段,在 satisfy any 模式下, 除了 NGX_OKNGX_DECLINEDNGX_AGAINNGX_DONE 之外的任何返回代码都被视为拒绝。 如果没有后续的访问处理器允许或使用不同的 代码拒绝访问,拒绝代码将成为终结代码。

示例#

nginx-dev-examples 仓库提供了 Angie 模块示例。

代码风格#

通用规则#

  • 最大文本宽度为 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.hngx_core.h 文件 总是首先包含,然后是 ngx_http.hngx_stream.hngx_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 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`(而不是 :samp:`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 <https://en.wikipedia.org/wiki/AddressSanitizer>`_(ASan)。 要在 :samp:`gccclang 中启用 ASan,请使用 -fsanitize=address 编译器和链接器选项。 构建 Angie 时,可以通过将该选项添加到 configure 脚本的 --with-cc-opt--with-ld-opt 参数来完成。

由于 Angie 中的大多数分配都是从 Angie 内部 pool 进行的,启用 ASan 可能并不总是足以调试内存问题。 内部池从系统分配一大块内存,然后从中切出较小的分配。 但是,可以通过将 NGX_DEBUG_PALLOC 宏设置为 1 来禁用此机制。 在这种情况下,分配直接传递给系统分配器,使其完全控制缓冲区边界。

以下配置行总结了上述提供的信息。 建议在开发第三方模块和在不同平台上测试 Angie 时使用。

auto/configure --with-cc-opt='-fsanitize=address -DNGX_DEBUG_PALLOC=1'
               --with-ld-opt=-fsanitize=address

常见陷阱#

编写 C 模块#

最常见的陷阱是在可以避免的情况下尝试编写一个完整的 C 模块。 在大多数情况下,您的任务可以通过创建适当的配置来完成。 如果编写模块是不可避免的,请尽量使其 尽可能小而简单。 例如,一个模块可以只导出一些 变量

在开始编写模块之前,请考虑以下问题:

  • 是否可以使用已有的 可用模块 来实现所需的功能?

  • 是否可以使用内置的脚本语言来解决问题, 例如 Perl第三方模块

C 字符串#

Angie 中最常用的字符串类型 ngx_str_t 不是 C 风格的 以零结尾的字符串。 您不能将数据传递给标准 C 库函数, 例如 strlen()strstr()。 相反,应该使用接受 ngx_str_t 或 指向数据的指针和长度的 Angie 对应函数。 但是,有一种情况下 ngx_str_t 持有 指向以零结尾字符串的指针:作为配置文件解析结果的字符串是以零结尾的。

全局变量#

避免在您的模块中使用全局变量。 使用全局变量很可能是一个错误。 任何全局数据都应该绑定到 配置周期 并从相应的 内存池 中分配。 这允许 Angie 执行优雅的配置重载。 尝试使用全局变量可能会破坏此功能, 因为将无法同时拥有两个配置并 摆脱它们。 有时需要全局变量。 在这种情况下,需要特别注意正确管理重新配置。 另外,检查您的代码使用的库是否具有隐式 全局状态,这些状态可能在重载时被破坏。

手动内存管理#

不要使用容易出错的 malloc/free 方法, 而是学习如何使用 Angie 。 池被创建并绑定到一个对象 - 配置周期连接HTTP 请求。 当对象被销毁时,关联的池也会被销毁。 因此,在处理对象时,可以从相应的池中分配所需的数量, 即使在出现错误的情况下也不用担心释放内存。

线程#

建议避免在 Angie 中使用线程,因为这肯定会破坏功能: 大多数 Angie 函数不是线程安全的。 预期线程只会执行系统调用和 线程安全的库函数。 如果您需要运行一些与客户端请求处理无关的代码, 正确的方法是在 init_process 模块处理程序中调度一个定时器,并在定时器处理程序中执行所需的操作。 在内部,Angie 使用 线程 来 提升 IO 相关操作,但这是一个有很多 限制的特殊情况。

阻塞库#

一个常见的错误是使用内部阻塞的库。 大多数现有的库本质上是同步和阻塞的。 换句话说,它们一次执行一个操作,并浪费 时间等待其他对等方的响应。 结果,当使用这样的库处理请求时,整个 Angie worker 被阻塞,从而破坏性能。 只使用提供异步接口且不会 阻塞整个进程的库。

对外部服务的 HTTP 请求#

模块经常需要对某些外部服务执行 HTTP 调用。 一个常见的错误是使用一些外部库,例如 libcurl, 来执行 HTTP 请求。 为了可以由 Angie 本身完成的任务而引入大量外部 (可能是 阻塞的!)代码 是完全不必要的。

当需要外部请求时,有两种基本使用场景:

  • 在处理客户端请求的上下文中(例如,在内容处理程序中)

  • 在工作进程的上下文中(例如,定时器处理程序)

在第一种情况下,最好使用 子请求 API。 不直接访问外部服务,而是在 Angie 配置中声明一个位置 并将您的子请求定向到此位置。 此位置不限于 代理 请求,还可以包含其他 Angie 指令。 这种方法的一个例子是在 Auth Request 模块 中实现的 auth_request 指令。

对于第二种情况,可以使用 Angie 中可用的基本 HTTP 客户端功能。 例如, OCSP 模块 实现了简单的 HTTP 客户端。