开发#

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

源代码#

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

编码风格#

您的更改应与 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 字节之后 可能以 null 结尾,也可能不以 null 结尾。 在大多数情况下不是。 但是,在代码的某些部分(例如,解析配置时), ngx_str_t 对象已知以 null 结尾,这 简化了字符串比较,并使将字符串传递给 系统调用变得更容易。

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 * (以 null 结尾)

  • %*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 个小数位。 例如,:samp: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 可以设置为 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

成功编译后,:samp: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 — 用于 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 的指针)。 当 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;

    /* 此处为自定义的每棵树数据 */
} my_tree_t;

typedef struct {
    ngx_rbtree_node_t  rbnode;

    /* 自定义的每个节点数据 */
    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) {
    /* 未找到键 */
}

通配符匹配#

要创建使用通配符的哈希,请使用 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;

    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 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 指令创建, 并在周期、配置、客户端连接和其他对象的几乎每个阶段都可用。

Angie 提供以下日志记录宏:

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

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

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 worker 启动时被继承。 每次重新加载 Angie 配置时,都会从新的 Angie 配置创建一个新周期; 旧周期通常在新周期成功创建后被删除。

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

周期的成员包括:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Buffer#

对于输入/输出操作,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;
};

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

使用缓冲区和链的示例:

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 protocol 客户端地址和端口,如果为连接启用了 PROXY protocol。

  • 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() 函数获得的每个连接都附加了两个事件,:samp: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) 为事件设置超时,:samp: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 — master 进程,读取 NGINX 配置,创建 cycle,并启动和控制子进程。 它不执行任何 I/O,仅响应信号。 其 cycle 函数是 ngx_master_process_cycle()

  • NGX_PROCESS_WORKER — worker 进程,处理客户端 连接。 它由 master 进程启动,并响应其信号和 channel 命令。 其 cycle 函数是 ngx_worker_process_cycle()。 可以有多个 worker 进程,由 worker_processes 指令配置。

  • NGX_PROCESS_SINGLE — single 进程,仅存在于 master_process off 模式下,是该模式下运行的唯一进程。 它创建 cycle(像 master 进程一样)并处理客户端连接 (像 worker 进程一样)。 其 cycle 函数是 ngx_single_process_cycle()

  • NGX_PROCESS_HELPER — helper 进程,目前 有两种类型:cache manager 和 cache loader。 两者的 cycle 函数都是 ngx_cache_manager_process_cycle()

Angie 进程处理以下信号:

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

  • NGX_TERMINATE_SIGNAL`(在大多数系统上是 :samp:`SIGTERM)— 终止。 收到此信号后,master 进程向所有 子进程发送终止信号。 如果子进程在 1 秒内未退出,master 进程发送 SIGKILL 信号来杀死它。 当没有子进程剩余时,master 进程销毁 cycle 池并 退出。 当 worker 进程、cache manager 进程或 cache loader 进程 收到此信号时,它销毁 cycle 池并退出。 收到此信号时,变量 ngx_terminate 被设置为 1

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

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

  • NGX_REOPEN_SIGNAL`(在大多数系统上是 :samp:`SIGUSR1)— 重新打开文件。 master 进程将此信号发送给 worker,worker 重新打开所有 与 cycle 相关的 open_files

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

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

线程#

可以将原本会阻塞 Angie worker 进程的任务卸载到单独的线程中。 例如,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 实现了 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 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 — 模块的名称,显示在 configure 脚本的控制台输出中。

  • 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`(用于动态模块)或 :samp:`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 参数传递给 configure 脚本。 要将模块编译为稍后动态加载到 Angie 中,使用 --add-dynamic-module=/path/to/module 参数。

核心模块#

模块是 Angie 的构建块,其大部分功能都是作为模块实现的。 模块源文件必须包含一个 ngx_module_t 类型的全局变量,其定义如下:

struct ngx_module_s {

    /* 省略私有部分 */

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

    /* 省略未来扩展的存根 */
};

省略的私有部分包括模块版本和签名,使用预定义的宏 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 模块和 event 模块也是核心模块。 核心模块的上下文定义为:

typedef struct {
    ngx_str_t             name;
    void               *(*create_conf)(ngx_cycle_t *cycle);
    char               *(*init_conf)(ngx_cycle_t *cycle, void *conf);
} ngx_core_module_t;

其中 name 是模块名称字符串, create_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 .. NGX_CONF_TAKE7 — 指令恰好接受指定数量的参数。

  • NGX_CONF_TAKE12NGX_CONF_TAKE13NGX_CONF_TAKE23NGX_CONF_TAKE123NGX_CONF_TAKE1234 — 指令可以接受不同数量的参数。 选项限于指定的数字。 例如,:samp:NGX_CONF_TAKE12 表示它接受一个或两个参数。

指令类型的标志:

  • NGX_CONF_BLOCK — 指令是一个块,即它可以在其开闭大括号内包含其他指令,甚至实现自己的解析器来处理内部内容。

  • NGX_CONF_FLAG — 指令接受布尔值, onoff

指令上下文定义它可以出现在配置中的位置:

  • 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 转换为值为 1 或 0 的 ngx_flag_t 值。

  • ngx_conf_set_str_slot — 将字符串存储为 ngx_str_t 类型的值。

  • ngx_conf_set_str_array_slot — 将值追加到字符串 ngx_str_t 的数组 ngx_array_t 中。 如果数组不存在则创建它。

  • ngx_conf_set_keyval_slot — 将键值对追加到键值对 ngx_keyval_t 的数组 ngx_array_t 中。 第一个字符串成为键,第二个成为值。 如果数组不存在则创建它。

  • ngx_conf_set_num_slot — 将指令的参数转换为 ngx_int_t 值。

  • ngx_conf_set_size_slot — 将 大小 转换为以字节表示的 size_t 值。

  • ngx_conf_set_off_slot — 将 偏移量 转换为以字节表示的 off_t 值。

  • ngx_conf_set_msec_slot — 将 时间 转换为以毫秒表示的 ngx_msec_t 值。

  • ngx_conf_set_sec_slot — 将 时间 转换为以秒表示的 time_t 值。

  • ngx_conf_set_bufs_slot — 将提供的两个参数转换为包含缓冲区数量和 大小ngx_bufs_t 对象。

  • ngx_conf_set_enum_slot — 将提供的参数转换为 ngx_uint_t 值。 在 post 字段中传递的以 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_OFFSEThttp 块内 location 块的配置。

  • 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 对象本身,:samp: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 数组中模块的 ctx_index 位置。 以下宏提供了获取和设置请求上下文的便捷方式:

    • 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 — 请求最后一次正则表达式匹配产生的正则捕获。 正则匹配可能在请求处理期间的多个地方发生: map 查找、通过 SNI 或 HTTP Host 进行服务器查找、rewrite、proxy_redirect 等。 查找产生的捕获存储在上述字段中。 字段 ncaptures 保存捕获的数量, captures 保存捕获的边界, captures_data 保存与正则表达式匹配的字符串, 用于提取捕获。 每次新的正则匹配后,请求捕获都会重置以保存新值。

  • count — 请求引用计数器。 该字段仅对主请求有意义。 增加计数器通过简单的 r->main->count++ 完成。 要减少计数器,调用 ngx_http_finalize_request(r, rc)。 创建子请求和运行请求体读取过程都会增加计数器。

  • subrequests — 当前子请求嵌套级别。 每个子请求继承其父级的嵌套级别,减一。 如果该值达到零,则会生成错误。 主请求的值由 NGX_HTTP_MAX_SUBREQUESTS 常量定义。

  • uri_changes — 请求剩余的 URI 更改次数。 请求可以更改其 URI 的总次数受 NGX_HTTP_MAX_URI_CHANGES 常量限制。 每次更改时,该值都会递减,直到达到零,此时会生成错误。 重写和内部重定向到普通或命名位置都被视为 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 模块可以有三种类型的配置:

  • 主配置 — 应用于整个 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 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() 将配置与更高级别的配置合并。 实际上,服务器和位置配置不仅存在于服务器和位置级别, 还会在它们之上的所有级别创建。 具体来说,服务器配置也会在主级别创建, 位置配置会在主级别、服务器级别和位置级别创建。 这些配置使得可以在 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)

以下示例获取标准 核心 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 选择位置的特殊阶段。 在此阶段之前,相关虚拟服务器的默认位置被分配给请求, 任何请求位置配置的模块都会接收默认服务器位置的配置。 此阶段为请求分配新位置。 此阶段不能注册额外的处理器。

  • NGX_HTTP_REWRITE_PHASE — 与 NGX_HTTP_SERVER_REWRITE_PHASE 相同, 但用于在前一阶段选择的位置中定义的重写规则。

  • NGX_HTTP_POST_REWRITE_PHASE — 如果请求的 URI 在重写期间发生更改, 则将请求重定向到新位置的特殊阶段。 这是通过请求再次经过 NGX_HTTP_FIND_CONFIG_PHASE 来实现的。 此阶段不能注册额外的处理器。

  • NGX_HTTP_PREACCESS_PHASE — 用于不同类型处理器的通用阶段, 与访问控制无关。 标准 Angie 模块 Limit ConnLimit Req 在此阶段注册其处理器。

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

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

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

  • NGX_HTTP_CONTENT_PHASE — 通常生成响应的阶段。 多个标准 Angie 模块在此阶段注册其处理器, 包括 Index。 它们按顺序调用,直到其中一个产生输出。 也可以按位置设置内容处理器。 如果 HTTP 模块 模块的位置配置设置了 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 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_AGAINNGX_DONE — 暂停阶段处理, 直到某个未来事件,例如异步 I/O 操作或只是延迟。 假定稍后通过调用 ngx_http_core_run_phases() 恢复阶段处理。

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

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

示例#

nginx-dev-examples 仓库提供了适用于 Angie 的 nginx 模块示例。

代码风格#

通用规则#

  • 最大文本宽度为 80 个字符

  • 缩进为 4 个空格

  • 不使用制表符,不使用尾随空格

  • 同一行上的列表元素用空格分隔

  • 十六进制字面量为小写

  • 文件名、函数和类型名称以及全局变量具有 ngx_ 前缀或更具体的前缀, 如 ngx_http_ngx_mail_

size_t
ngx_utf8_length(u_char *p, size_t n)
{
    u_char  c, *last;
    size_t  len;

    last = p + n;

    for (len = 0; p < last; len++) {

        c = *p;

        if (c < 0x80) {
            p++;
            continue;
        }

        if (ngx_utf8_decode(&p, last - p) > 0x10ffff) {
            /* invalid UTF-8 */
            return n;
        }
    }

    return len;
}

文件#

典型的源文件可能包含以下部分,用两个空行分隔:

  • 版权声明

  • 包含文件

  • 预处理器定义

  • 类型定义

  • 函数原型

  • 变量定义

  • 函数定义

版权声明如下所示:

/*
 * Copyright (C) Author Name
 * Copyright (C) Organization, Inc.
 */

如果文件被大幅修改,应更新作者列表,新作者添加到顶部。

始终首先包含 ngx_config.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 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;
    }
}

类似的格式规则适用于 dowhile 循环:

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),某些现代编译器支持该工具。 要在 gccclang 中启用 ASan, 请使用 -fsanitize=address 编译器和链接器选项。 在构建 Angie 时,可以通过将该选项添加到 configure 脚本的 --with-cc-opt--with-ld-opt 参数来完成。

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

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

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

常见陷阱#

编写 C 模块#

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

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

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

  • 是否可以使用内置的脚本语言解决问题, 例如 PerlNJS

C 字符串#

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

全局变量#

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

手动内存管理#

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

线程#

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

阻塞库#

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

向外部服务发送 HTTP 请求#

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

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

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

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

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

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