Emiller’s Advanced Topics In Nginx Module Development

原文链接 https://www.evanmiller.org/nginx-modules-guide-advanced.html

相较于Emiller’s Guide To Nginx Module Development 描述了编写 Nginx 简单处理程序、过滤器或负载均衡器的基础问题,这篇文档涵盖了三个高级主题:共享内存、子请求和解析,适合雄心勃勃的 Nginx 开发者。因为这些主题处于 Nginx 宇宙的边界上,所以这里的代码可能很少。示例可能已经过时。但是希望你不仅能够顺利完成,而且能够掌握一些额外的工具。

共享内存Shared Memory

Nginx 在非线程化的情况下允许工作进程在它们之间共享内存。然而,这与标准池分配器有很大不同,因为共享段具有固定大小,并且在不重新启动 nginx 或以其他方式释放其内容的情况下无法调整大小。

提前声明

首先,警告黑客。本指南是在亲身体验 nginx 中的共享内存几个月后编写的,虽然我尽力做到准确(并花了一些时间刷新我的记忆),但不能保证它。你已被警告。

此外,这些知识 100% 来自阅读源代码和对核心概念进行逆向工程,因此可能有更好的方法来完成所描述的大部分内容。

哦,本指南基于 0.6.31,尽管据我所知 0.5.x 是 100% 兼容 ,而 0.7.x 也没有带来我所知道的破坏兼容性的变化。 有关 nginx 中共享内存的实际使用情况,请参阅我的  upstream_fair module

这可能根本不适用于 Windows。过去他的出现更容易导致的coredump.

 

生成使用共享内存

要在 nginx 中创建共享内存段,您需要:

  • 提供构造函数来初始化
  • 调用 ngx_shared_memory_add

这两点包含了主要陷阱(我遇到过),

1  您的构造函数将被多次调用,您可以自行判断是否是第一次调用(并且应该设置一些东西),或者不是(并且可能应该不理会所有内容)。共享内存构造函数的原型如下所示:

static ngx_int_t init(ngx_shm_zone_t *shm_zone, void *data);

数据变量将包含 oshm_zone->data 的内容,其中 oshm_zone 是“旧的”shm 区域描述符(稍后会详细介绍)。这个变量是唯一可以在重新加载后仍然存在的值,所以如果你不想丢失共享内存的内容,就必须使用它。

您的构造函数可能看起来与 upstream_fair 中的构造函数大致相似,即:

static ngx_int_t
init(ngx_shm_zone_t *shm_zone, void *data)
{
        if (data) { /* we're being reloaded, propagate the data "cookie" */
                shm_zone->data = data;
                return NGX_OK;
        }

        /* set up whatever structures you wish to keep in the shm */

        /* initialise shm_zone->data so that we know we have
        been called; if nothing interesting comes to your mind, try
        shm_zone->shm.addr or, if you're desperate, (void*) 1, just set
        the value to something non-NULL for future invocations
        */
        shm_zone->data = something_interesting;

        return NGX_OK;
}

2 访问 shm 段时必须小心。 添加共享内存段的界面如下所示:

ngx_shm_zone_t *
ngx_shared_memory_add(ngx_conf_t *cf, ngx_str_t *name, size_t size,
        void *tag);

cf 是对配置文件的引用(您可能会创建segment以响应配置选项),name 是段的名称(作为 ngx_str_t,即计数字符串),size 是以字节为单位的大小( 通常会四舍五入到最接近页面大小的倍数,例如在许多流行的体系结构中为 4KB)并且标签是用于检测命名冲突的标签。

  • 如果您使用相同的名称、标签和大小多次调用 ngx_shared_memory_add,您将只会获得一个段;
  • 如果您指定不同的名称,您将获得几个不同的段;
  • 如果您指定相同的名称但大小或标签不同,您将收到错误消息。

标签值的一个不错的选择可能是例如 指向你的模块描述符的指针。

可以看下相关代码, 判断了, 名称, tag, 和长度, 只有长度为0,会将长度进行更新, 如果名字, tag,长度都要求一致

ngx_shm_zone_t *
ngx_shared_memory_add(ngx_conf_t *cf, ngx_str_t *name, size_t size, void *tag)
{
    ngx_uint_t        i;
    ngx_shm_zone_t   *shm_zone;
    ngx_list_part_t  *part;

    part = &cf->cycle->shared_memory.part;
    shm_zone = part->elts;

    for (i = 0; /* void */ ; i++) {

        if (i >= part->nelts) {
            if (part->next == NULL) {
                break;
            }
            part = part->next;
            shm_zone = part->elts;
            i = 0;
        }

        if (name->len != shm_zone[i].shm.name.len) {
            continue;
        }

        if (ngx_strncmp(name->data, shm_zone[i].shm.name.data, name->len)
            != 0)
        {
            continue;
        }

        if (tag != shm_zone[i].tag) {
            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                            "the shared memory zone \"%V\" is "
                            "already declared for a different use",
                            &shm_zone[i].shm.name);
            return NULL;
        }

        if (shm_zone[i].shm.size == 0) {
            shm_zone[i].shm.size = size;
        }

        if (size && size != shm_zone[i].shm.size) {
            ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
                            "the size %uz of shared memory zone \"%V\" "
                            "conflicts with already declared size %uz",
                            size, &shm_zone[i].shm.name, shm_zone[i].shm.size);
            return NULL;
        }

        return &shm_zone[i];
    }

    shm_zone = ngx_list_push(&cf->cycle->shared_memory);

    if (shm_zone == NULL) {
        return NULL;
    }

    shm_zone->data = NULL;
    shm_zone->shm.log = cf->cycle->log;
    shm_zone->shm.addr = NULL;
    shm_zone->shm.size = size;
    shm_zone->shm.name = *name;
    shm_zone->shm.exists = 0;
    shm_zone->init = NULL;
    shm_zone->tag = tag;
    shm_zone->noreuse = 0;

    return shm_zone;
}

 

在调用 ngx_shared_memory_add 并收到新的 shm_zone 描述符后,必须在shm_zone->init 中设置构造函数。 等等……添加段之后? 是的,这是一个主要问题。 这意味着在调用 ngx_shared_memory_add  时不会创建段(因为您稍后才指定构造函数)。 真正发生的事情看起来像这样(大大简化):

  • 解析整个配置文件,记录所请求的shm段落,
  • 然后一次性创建/销毁所有段落。在此处调用构造函数。
    • 请注意,每次调用构造函数时,都会使用另一个shm_zone的值。原因是描述符的生命周期与周期(在Apache术语中称为生成)一样长,而段落的生命周期则与主进程和所有工作进程一样长。为了让一些数据在重新加载时保留下来,您可以访问旧描述符的->data字段(如上所述)。
  • 重新启动开始处理请求的工作进程
  • 收到SIGHUP信号后,返回步骤1

另外,你必须设置构造函数,否则nginx将认为你的段未使用,并且根本不会创建它。

现在你已经知道了,很明显你不能指望在解析配置时访问共享内存。你可以通过shm_zone->shm.addr访问整个段(在段真正创建之前它将为NULL)。任何第一次解析运行之后的访问(例如在请求处理程序内或在后续重新加载中)都应该是可以的。

 

使用  slab allocator

现在您有了新的闪亮的 shm 段,如何使用它?最简单的方法是使用 nginx 提供的另一种内存工具,即 slab 分配器。 Nginx 足以在每个新的 shm 段中为您初始化 slab,因此您可以使用它,或者忽略 slab 结构并用您自己的数据覆盖它们。

包含下面两个接口函数:

  • void *ngx_slab_alloc(ngx_slab_pool_t *pool, size_t size);
  • void ngx_slab_free(ngx_slab_pool_t *pool, void *p);

第一个参数申请内存头指针 (ngx_slab_pool_t *)shm_zone->shm.addr 另外一个参数是申请内存大小,或者要释放块的指针,( ngx_slab_free 未被调用)

在模块初始化时使用ngx_slab_alloc申请内存, 后续增加使用ngx_slab_alloc_locked

 

锁机制 Spinlocks, atomic memory access

请记住,共享内存本质上是危险的,因为您可以让多个进程同时访问它。 slab 分配器有一个 per-segment 锁(shpool->mutex),用于保护段免受并发修改。

你也可以自己获取和释放锁,如果你想在段上实现一些更复杂的操作,比如搜索或遍历树,这很有用。下面的两个片段本质上是等价的:

/*
void *new_block;
ngx_slab_pool_t *shpool = (ngx_slab_pool_t *)shm_zone->shm.addr;
*/

new_block = ngx_slab_alloc(shpool, ngx_pagesize);
ngx_shmtx_lock(&shpool->mutex);
new_block = ngx_slab_alloc_locked(shpool, ngx_pagesize);
ngx_shmtx_unlock(&shpool->mutex);

事实上,ngx_slab_alloc 看起来和上面几乎一模一样。

如果您执行任何不依赖于新分配(或者更确切地说,释放)的操作,请使用 slab 互斥锁保护它们。但是,请记住,nginx 互斥量是作为自旋锁(非休眠)实现的,因此虽然它们在无竞争的情况下非常快,但在等待时很容易占用 100% 的 CPU。因此,不要在持有互斥量的同时执行任何长时间运行的操作(尤其是 I/O,但您应该完全避免任何系统调用)。

您还可以通过 ngx_mutex_init()ngx_mutex_lock()ngx_mutex_unlock() 函数使用您自己的互斥体进行更细粒度的锁定

作为锁的替代方案,您可以使用保证以不间断的方式读取或写入的原子变量(任何工作进程都不会看到正在被另一个进程写入的值的一半)。

原子变量定义为 ngx_atomic_t or ngx_atomic_uint_t  类型(取决于符号)。它们应该至少有 32 位。要简单地读取或无条件地设置一个原子变量,你不需要任何特殊的结构:

ngx_atomic_t i = an_atomic_var;
an_atomic_var = i + 5;

请注意,同时可能发生任何事情;上下文切换、在其他 CPU 上执行代码等。

所以要以原子方式读取和修改变量,您有两个函数(依赖于平台),它们的接口在 src/os/unix/ngx_atomic.h 中声明:

  • ngx_atomic_cmp_set(lock, old, new) 以原子方式检索 *lock 的旧值并将新值存储在同一地址下。如果 *lock 在覆盖之前等于旧的,则返回 1。
  • ngx_atomic_fetch_add(value, add)以原子方式将 add 添加到 *value 并返回旧的 *value。

平台在定义操作类型是都增加 typedef volatile ngx_atomic_uint_t ngx_atomic_t;  volatile 防止编译器优化

nginx自己实现了一个ngx_spinlock(&ngx_thread_pool_done_lock, 1, 2048);, 在thread pool中可以看到使用。第一个参数对应lock, 第二个是要设置的值, 第三个是针对多核的一个设置一个等待的时间。(通过for循环)

Using rbtrees

好吧,你的数据已经被整齐地分配,用合适的锁保护起来,但你也想以某种方式组织它。同样,nginx 有一个非常好的结构就是为了这个目的——红黑树。

亮点(API 方面):

  • 需要一个插入回调,它将元素插入树中(可能根据一些预定义的顺序),然​​后调用 ngx_rbt_red(the_newly_added_node) 来重新平衡树
  • 需要将所有叶子设置为预定义的哨兵对象(不是 NULL)

本章是关于共享内存的,不是 rbtrees 所以阅读 upstream_fair 的源代码,了解如何创建和遍历 rbtree。

struct ngx_rbtree_s {
ngx_rbtree_node_t *root;
ngx_rbtree_node_t *sentinel;
ngx_rbtree_insert_pt insert;  // 开发给开发这节点处理
};

void ngx_rbtree_insert(ngx_rbtree_t *tree, ngx_rbtree_node_t *node);
void ngx_rbtree_delete(ngx_rbtree_t *tree, ngx_rbtree_node_t *node);
void ngx_rbtree_insert_value(ngx_rbtree_node_t *root, ngx_rbtree_node_t *node,
ngx_rbtree_node_t *sentinel);
void ngx_rbtree_insert_timer_value(ngx_rbtree_node_t *root,
ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel);
ngx_rbtree_node_t *ngx_rbtree_next(ngx_rbtree_t *tree,
ngx_rbtree_node_t *node);

 

 

Subrequests

子请求是 Nginx 最强大的方面之一。使用子请求,您可以返回与客户端最初请求的不同 URL 的结果。一些 Web 框架将此称为“内部重定向”。但 Nginx 更进一步:模块不仅可以执行多个子请求并将输出组合成一个响应,子请求可以执行它们自己的子子请求,子子请求可以发起子子子请求,并且……你明白了。子请求可以映射到硬盘上的文件、其他处理程序或上游服务器;从 Nginx 的角度来看,这无关紧要。据我所知,只有过滤器可以发出子请求请求
Internal redirects

如果您只想返回一个不同于客户端最初请求的 URL,您将需要使用ngx_http_internal_redirect 函数。它的原型是

ngx_int_t
ngx_http_internal_redirect(ngx_http_request_t *r, ngx_str_t *uri, ngx_str_t *args)

其中 r 是请求结构,uriargs是新的 URI。请注意,URI 必须是已在 nginx.conf 中定义的位置;例如,您不能重定向到任意域。处理程序应该返回ngx_http_internal_redirect的返回值,即重定向处理程序通常会像这样结束:

return ngx_http_internal_redirect(r, &uri, &args);

内部重定向用于ngx_http_index_module 模块(它将以 / 结尾的 URL 映射到 index.html)以及 Nginx 的 X-Accel-Redirect 功能。

 

A single subrequest

子请求最适用于根据原始响应中的数据插入附加内容。例如,SSI(服务器端包含)模块使用过滤器扫描返回文档的内容,然后用指定 URL 的内容替换“include”指令。 我们将从一个更简单的示例开始。

我们将制作一个过滤器,将文档的全部内容视为要检索的 URL,然后将新文档附加到 URL 本身。请记住,URL 必须是 nginx.conf 中的一个location。

static ngx_int_t
ngx_http_append_uri_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
    int                 rc; 
    ngx_str_t           uri;
    ngx_http_request_t *sr;

    /* First copy the document buffer into the URI string */
    uri.len = in->buf->last - in->buf->pos;
    uri.data = ngx_palloc(r->pool, uri.len);
    if (uri.data == NULL)
        return NGX_ERROR;
    ngx_memcpy(uri.data, in->-buf->pos, uri.len);

    /* Now return the original document (i.e. the URI) to the client */
    rc = ngx_http_next_body_filter(r, in);

    if (rc == NGX_ERROR)
        return rc;

    /* Finally issue the subrequest */
    return ngx_http_subrequest(r, &uri, NULL /* args */, 
        NULL /* callback */, 0 /* flags */);
}

ngx_http_subrequest 定义:

ngx_int_t ngx_http_subrequest(ngx_http_request_t *r,
    ngx_str_t *uri, ngx_str_t *args, ngx_http_request_t **psr, 
        ngx_http_post_subrequest_t *ps, ngx_uint_t flags)
  • *r 原始请求
  • *uri ,*args  sub-request的uri和参数
  • **psr 指针指向一个新的子请求结构。
  • *ps 子请求完成后的回调方法详,见 http/ngx_http_request.h
  • flags 支持多项设置:
    • NGX_HTTP_SUBREQUEST_IN_MEMORY: 将子请求的结果存储在连续的内存块中(通常不需要)
    • NGX_HTTP_SUBREQUEST_BACKGROUND:创建后台子请求。此类子请求不参与主请求的响应 构造,也就不会占用主请求的响应时间,但它依然会保持对主请求的引用。
    • NGX_HTTP_SUBREQUEST_WAITED – 即使子请求在最终确定时未处于活动状态,也会设置子请求的完成标志。该子请求标志由 SSI 过滤器使用。
    • NGX_HTTP_SUBREQUEST_CLONE – 子请求是作为其父的克隆而创建的。它是在同一位置开始的,并从与父级请求相同的阶段开始。
      • NGX_HTTP_ZERO_IN_URI: URI 包含 ASCII 代码为 0 的字符(也称为“\0”),或包含“%00”(最新版本已经不支持)

子请求的结果将插入到您期望的位置。如果你想修改子请求的结果,你可以使用另一个过滤器(或同一个过滤器!)。您可以通过此测试判断过滤器是在主请求上运行还是在子请求上运行

if (r == r->main) { 
    /* primary request */
} else {
    /* subrequest */
}

发出单个子请求的模块的最简单示例是 ngx_http_addition_filter_module模块。

可以通过类似

location / {
    add_before_body /before_action;
    add_after_body  /after_action;
}

方式配置在原有location 处理前后增加一下处理动作。

 

Sequential subrequests

注意,2009 年 8 月 13 日:由于 Nginx 0.7.25 中引入的 Nginx 子请求处理的变化,本节可能已过时。你要承担风险。 -EM

新版本中机制改为通过 ngx_http_post_request 方式发送请求, 而不是再次调用ngx_http_handler

您可能认为发出多个子请求很简单:很简单
int rc1, rc2, rc3;
rc1 = ngx_http_subrequest(r, uri1, ...);
rc2 = ngx_http_subrequest(r, uri2, ...);
rc3 = ngx_http_subrequest(r, uri3, ...);

你错了!请记住 Nginx 是单线程的。子请求可能需要访问网络,如果是这样,Nginx 需要在等待响应期间返回到其他工作。所以我们需要查看ngx_http_subrequest,的返回值,可以是以下之一:

  • NGX_OK: 子请求已发送到post队列等待发送
  • NGX_ERROR: 出现某种服务器错误
NGX_AGAIN, NGX_DONE:原文中提到的这两个返回码目前一不支持

如果你的子请求返回 NGX_AGAIN,你的过滤器也应该立即返回 NGX_AGAIN。当该子请求完成并且结果已发送到客户端时,Nginx 可以再次调用您的过滤器,您可以从中发出下一个子请求(或在子请求之间做一些工作)。当然,它有助于在上下文结构中跟踪您计划的子请求。您还应该注意立即返回错误。

如果你的子请求返回NGX_OK,说明子请求已经将消息通过 ngx_http_post_request 发送出去, 可以进行下一步操作。

让我们做一个简单的例子。假设我们的上下文结构包含一个 URI 数组,以及下一个子请求的索引:

typedef struct {
    ngx_array_t  uris;
    int          i;
} my_ctx_t;

然后一个简单地将这些 URI 的内容连接在一起的过滤器可能看起来像:

static ngx_int_t
ngx_http_multiple_uris_body_filter(ngx_http_request_t *r, ngx_chain_t *in)
{
    my_ctx_t  *ctx;
    int rc = NGX_OK;
    ngx_http_request_t *sr;

    if (r != r->main) { /* subrequest */
        return ngx_http_next_body_filter(r, in);
    }

    ctx = ngx_http_get_module_ctx(r, my_module);
    if (ctx == NULL) {
        /* populate ctx and ctx->uris here */
    }
    while (rc == NGX_OK && ctx->i < ctx->uris.nelts) {
        rc = ngx_http_subrequest(r, &((ngx_str_t *)ctx->uris.elts)[ctx->i++],
            NULL /* args */, &sr, NULL /* cb */, 0 /* flags */);
    }

    return rc; /* NGX_OK/NGX_ERROR/NGX_DONE/NGX_AGAIN */
}

原文这部分已经过时, 适合0.7.29 之前的版本

Let’s think this code through. There might be more going on than you expect.

First, the filter is called on the original response. Based on this response we populate ctx and ctx->uris. Then we enter the while loop and call ngx_http_subrequest for the first time.

If ngx_http_subrequest returns NGX_OK then we move onto the next subrequest immediately. If it returns with NGX_AGAIN, we break out of the while loop and return NGX_AGAIN.

Suppose we’ve returned an NGX_AGAIN. The subrequest is pending some network activity, and Nginx has moved on to other things. But when that subrequest is finished, Nginx will call our filter at least two more times:

  1. once with r set to the subrequest, and in set to buffers from the subrequest’s response
  2. once with r set to the original request, and in set to NULL

To distinguish these two cases, we must test whether r == r->main. In this example we call the next filter if we’re filtering the subrequest. But if we’re in the main request, we’ll just pick up the while loop where we last left off. in will be set to NULL because there aren’t actually any new buffers to process.

When the last subrequest finishes and all is well, we return NGX_OK.

This example is of course greatly simplified. You’ll have to figure out how to populate ctx->uris on your own. But the example shows how simple it is to re-enter the subrequesting loop, and break out as soon as we get an error or NGX_AGAIN.

 

类似的  ngx_http_mirror_module模块, 通过post消息处理机制处理。

static ngx_int_t
ngx_http_mirror_handler_internal(ngx_http_request_t *r)
{
    ngx_str_t                   *name;
    ngx_uint_t                   i;
    ngx_http_request_t          *sr;
    ngx_http_mirror_loc_conf_t  *mlcf;

    mlcf = ngx_http_get_module_loc_conf(r, ngx_http_mirror_module);

    name = mlcf->mirror->elts;

    for (i = 0; i < mlcf->mirror->nelts; i++) {
        if (ngx_http_subrequest(r, &name[i], &r->args, &sr, NULL,
                                NGX_HTTP_SUBREQUEST_BACKGROUND)
            != NGX_OK)
        {
            return NGX_HTTP_INTERNAL_SERVER_ERROR;
        }

        sr->header_only = 1;
        sr->method = r->method;
        sr->method_name = r->method_name;
    }

    return NGX_DECLINED;
}

 

Parallel subrequests

也可以一次发出多个子请求,而无需等待先前的子请求完成。事实上,即使对于 Emiller 在 Nginx 模块开发中的高级主题,这种技术也太高级了。有关示例,请参见 SSI 模块

 

使用 Ragel 解析

如果您的模块正在处理任何类型的输入,无论是传入的 HTTP 标头还是成熟的模板语言,您都需要编写一个解析器。解析是其中一件看似简单的事情——将字符串转换为结构体有多难?——但肯定有正确的解析方式和错误的解析方式。不幸的是,Nginx 通过选择(我的感觉)错误的方式树立了一个坏榜样。

Nginx的解析代码有什么问题?

/* Random snippet from Nginx parsing code */

for (p = ctx->pos; p < last; p++) {
    ch = *p;

    switch (state) {
        case ssi_tag_state:
            switch (ch) {
                case '!':
                    /* do stuff */
            ...

Nginx 使用状态机进行所有解析,无论是 SSI 包含、HTTP 标头还是 Nginx 配置文件。状态机,您可能还记得大学计算理论课上的内容,读取一盘字符,根据读取的内容从一个状态移动到另一个状态,并可能根据读取的字符和所处的状态执行某些操作。因此,例如,如果我想用状态机解析正小数点数,我可能有一个“读取句点左侧的内容”状态、一个“只读取一个句点”状态和一个“读取句点右侧的内容” “状态,并在我读入每个数字时在它们之间移动。

不幸的是,状态机解析器通常冗长、复杂、难以理解且难以修改。从软件开发的角度来看,更好的方法是使用解析器生成器。解析器生成器将高级、高度可读的解析规则转换为低级状态机。解析器生成器的编译代码实际上与手写状态机的代码相同,但您的代码更易于使用。

类似编译原理中的词法分析

有许多可用的解析器生成器,每个都有自己的特殊语法,但我将特别关注一个解析器生成器:Ragel。 Ragel 是一个不错的选择,因为它设计用于处理缓冲输入。鉴于 Nginx 的缓冲链架构,无论您是否真的愿意,您很有可能会解析缓冲输入。

这里是关于词法,语法分析器 wiki https://en.wikipedia.org/wiki/Comparison_of_parser_generators

Ragel除了支持C/C++外, 还可以支持go 和 汇编。

安装

使用系统自带包或http://www.colm.net/open-source/ragel/ 下载

 

Ragel使用

将解析器函数与模块的其余部分放在一个单独的文件中是个好主意。然后您将需要:

  • 为解析器创建头文件 (.h)
  • 包含模块中的标头
  • 创建一个 Ragel (.rl)
  • 文件 从 Ragel 文件生成 C (.c) 文件
  • 在模块配置中包含 C 文件

头文件应该只有解析器函数的原型,您可以通过通常的#include “my_module_parser.h” 指令将其包含在您的模块中。真正的工作是编写 Ragel 文件。我们将通过一个简单的例子来工作。官方 Ragel 用户指南(http://www.colm.net/files/ragel/ragel-guide-6.10.pdf)整整 56 页,为程序员提供了强大的功能,但我们将只介绍您真正需要的简单解析器的 Ragel 部分。

Ragel 会将特殊 Ragel 命令转化为C语言的文件。 Ragel 命令位于由 %%{ 和 }%% 包围的代码块中。您需要在解析器中使用的前两个 Ragel 命令是:

%%{
    machine my_parser;
    write data;
}%%

这两个命令应该出现在任何预处理器指令之后但在您的解析器函数之前。 machine 命令为 Ragel 即将为您构建的状态机命名。写入命令将创建状态机将使用的状态定义。不要太担心这些命令。

接下来,您可以像常规 C 一样开始编写解析器函数。它可以接受您想要的任何参数,并且应该在成功时返回带有 NGX_OK 的 ngx_int_t,在失败时返回 NGX_ERROR。您应该传入参数,如果不是指向要解析的输入的指针,那么至少应该传入某种包含输入数据的上下文结构。

Ragel 会隐式地为你创建一些变量。为了使用 Ragel,您需要自己定义其他变量。在函数的顶部,您需要声明:

  • u_char *p – 输入参数开始指针。
  • u_char *pe – 输入参数结束指针。
  • int cs – 状态机的状态。

Ragel 将从 p 指向的任何地方开始解析,并在到达 pe 时结束。因此 p 和 pe 都应该是连续内存块上的指针。请注意,当 Ragel 在特定输入上完成运行时,您可以保存 cs 的值(机器状态)并在您停止的地方恢复对其他输入缓冲区的解析。通过这种方式,Ragel 可以跨多个输入缓冲区工作,并完美地融入 Nginx 的事件驱动架构。

写一个语法

接下来我们要为我们的解析器编写 Ragel 语法。语法只是一组规则,用于指定允许输入的类型; Ragel 文法很特别,因为它允许我们在扫描每个字符时执行操作。要利用 Ragel,您必须学习 Ragel 语法语法;这并不困难,但也不是微不足道的。

Ragel 语法由规则集定义。规则在等号左侧有一个任意名称,在右侧有一个规范,后跟一个分号。规则规范是正则表达式和操作的混合体。我们将在一分钟内开始行动。

最重要的规则称为“main”。所有语法都必须有一个 main 规则。 main 的规则是特殊的,因为 1) 名称不是任意的,并且 2) 它使用 := 而不是 = 来将名称与规范分开。

我们的字节范围解析器的“主要”规则非常简单:

main := "bytes=" byte_range_set;

该规则只是说“输入应包含字符串 bytes=,后跟遵循称为 byte_range_set 的规则的输入。”所以我们需要定义规则byte_range_set:

byte_range_set = byte_range_specs ( "," byte_range_specs )*;

该规则只是说“byte_range_set 由一个 byte_range_specs 后跟零个或多个逗号组成,每个逗号后跟一个 byte_range_specs。”换句话说,byte_range_set 是 byte_range_specs 的逗号分隔列表。您可能会将 * 识别为 Kleene 星号或来自正则表达式。

byte_range_set = byte_range_specs ( "," byte_range_specs )*;

该规则只是说“byte_range_set 由一个 byte_range_specs 后跟零个或多个逗号组成,每个逗号后跟一个 byte_range_specs。”换句话说,byte_range_set 是 byte_range_specs 的逗号分隔列表。您可能会将 * 识别为 Kleene 星号或来自正则表达式。

接下来我们需要定义 byte_range_specs 规则:

byte_range_specs = byte_range_spec >new_range;

字符很特殊。它说new_range不是另一个规则的名字,而是一个动作的名字,动作应该在这条规则的开头,即byte_range_specs的开头。最重要的特殊字符是:

  • > – 应该在这条规则的开头采取行动
  • $ – 在处理每个字符时应采取行动
  • % – 应在此规则末尾采取行动

还有其他的,您可以在 Ragel 用户指南中阅读。这些足以让您入门而不会太混乱。 在我们开始行动之前,让我们完成定义我们的规则。

在 byte_range_specs(复数)的规则中,我们引用了一个名为 byte_range_spec(单数)的规则。它被定义为:

byte_range_spec = [0-9]+ $start_incr
                  "-"
                  [0-9]+ $end_incr;

此规则指出“读取一个或多个数字,为​​每个数字执行动作 start_incr,然后读取破折号,然后读取一个或多个数字,为​​每个数字执行动作 end_incr。”请注意,在 byte_range_spec 的开头或结尾没有执行任何操作。

当你实际编写语法时,你应该按照我这里的相反顺序编写规则。规则应仅引用先前定义的其他规则。所以“main”应该永远是语法中的最后一条规则,而不是第一条。

我们的字节范围语法现在完成了;是时候指定操作了。

指定操作

动作是可以访问一些特殊变量的 C 代码块。最重要的特殊变量是:

  • fc – 当前正在读取的字符
  • fpc – 指向正在读取的当前字符的指针

fc 对 $ 操作最有用,即对字符串或正则表达式的每个字符执行的操作。 fpc 对于 > 和 % 动作更有用,即在规则开始或结束时采取的动作。 回到我们的字节范围示例,

这里是 new_range 操作。它不使用任何特殊变量。

action new_range {
    if ((range = ngx_array_push(&ctx->ranges)) == NULL) {
        return NGX_ERROR;
    }
    range->start = 0; range->end = 0;
}

new_range 出奇的沉闷。它只是在存储在上下文结构中的“范围”数组上分配了一个新的“范围”结构。请注意,只要我们包含正确的头文件,Ragel 操作就可以完全访问 Nginx API。

接下来我们定义剩下的两个动作,start_incr 和 end_incr。这些操作将正整数解析为适当的变量。当我们读取数字的每一位时,我们想要将存储的数字乘以 10 并添加该数字。这里我们利用了上面描述的特殊变量 fc:

action start_incr { range->start = range->start * 10 + (fc - '0'); }

action end_incr { range->end = range->end * 10 + (fc - '0'); }

请注意减去“0”以将字符转换为整数的旧解析技巧。

这就是动作。我们几乎完成了我们的解析器。

 

合并到一起

动作和语法应该放在解析器函数内的 Ragel 块中,但在 p、pe 和 cs 的声明之后。即,类似于:

ngx_int_t my_parser(/* some context, the request struct, etc. */) 
{
    int cs;
    u_char *p = ctx->beginning_of_data;
    u_char *pe = ctx->end_of_data;

    %%{
        /* Actions */
        action some_action { /* C code goes here */ }
        action other_action { /* etc. */ }

        /* Grammar */
        my_rule = [0-9]+ "-" >some_action;
        main := ( my_rule )* >other_action;

        write init;
        write exec;
    }%%

    if (cs < my_parser_first_final) {
        return NGX_ERROR;
    }

    return NGX_OK;
}

我们在这里添加了一些额外的部分。第一个是 write init 和 write exec。这些是 Ragel 的命令,用于在此处插入生成的解析器(用 C 编写)。

另一个额外的位是 cs 与 my_parser_first_final 的比较。回想一下,cs 存储解析器的状态。此检查可确保解析器在完成输入处理后处于有效状态。如果我们跨多个输入缓冲区进行解析,那么我们将把 cs 存储在某个地方并在我们想继续解析时检索它,而不是进行此检查。
最后,我们准备生成实际的解析器。到目前为止,我们编写的代码应该在 Ragel (.rl) 文件中;当我们准备好编译时,我们只需运行命令:
ragel my_parser.rl

此命令将生成一个名为“my_parser.c”的文件。为了确保它是由 Nginx 编译的,您需要在模块的“配置”文件中添加一行,如下所示

NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/my_parser.c"

一旦掌握了使用 Ragel 进行解析的窍门,您就会想知道没有它您是怎么做的。你实际上想要在你的 Nginx 模块中编写解析器。 Ragel 为富有想象力的开发人员打开了一组全新的可能模块.

没有涵盖的主题

  • 并行子请求
  • 内置数据结构(红黑树、数组、哈希表……)
  • 访问控制模块

此篇文章子请求部分已经过时, 使用词法语法分析工具有nginx结合比较新颖, 但是也无法支持到动态解析,还是对于固定规则进行处理

 

图片from王銘欽

Comments are closed.