<-
Apache HTTP 服务器 2.4 > 2.x中输出过滤器的准则

编写输出过滤器的指南

编写输出滤波器时会遇到许多常见的陷阱;此页面旨在记录针对新过滤器或现有过滤器作者的最佳做法。

本文档适用于Apache HTTP Server的2.0版和2.2版。尽管某些建议对于所有类型的过滤器都是通用的,但它专门针对 RESOURCE-level或CONTENT_SET-level过滤器。

支持Apache!

也可以看看

最佳

过滤器和铲斗旅

每次调用过滤器时,都会通过一个存储桶旅,其中包含一系列表示数据内容和元数据的存储桶。每个桶有 桶型 ; httpd核心模块(和apr-util提供存储桶旅接口的 库)定义和使用了许多存储桶类型,但是模块可以自由定义自己的类型。

必须准备输出过滤器以处理非标准类型的铲斗;除少数例外,过滤器无需关心要过滤的桶的类型。

过滤器可以使用APR_BUCKET_IS_METADATA宏判断存储桶是表示数据还是元数据。通常,所有元数据段都应由输出过滤器沿过滤器链向下传递。过滤器可以适当地转换,删除和插入数据桶。

所有过滤器都必须注意两种元数据存储桶类型:EOS存储桶类型和 FLUSH存储桶类型。一个EOS桶表示该响应的结尾已经到达并没有进一步桶需要被处理。甲FLUSH桶表明过滤器应立即刷新任何缓冲桶(如果适用)向下过滤器链。

FLUSH当内容生成器(或上游过滤器)知道可能有延迟才能发送更多内容时,将发送存储桶。通过FLUSH立即将存储桶向下传递 到过滤器链,过滤器可确保客户端等待未决数据的时间不会超过必要时间。

过滤器可以创建FLUSH存储桶,并根据需要将其沿过滤器链向下传递。FLUSH 不必要或太频繁地生成存储桶会损害网络利用率,因为它可能会强制发送大量的小数据包,而不是少量的大数据包。在部分非阻塞斗读取 覆盖了鼓励过滤器来生成一个事例 FLUSH桶。

铲斗旅示例

HEAP FLUSH FILE EOS

这显示了可以通过过滤器的铲斗旅。它包含两个元数据存储区(FLUSHEOS)和两个数据存储区(HEAPFILE)。

最佳

过滤器调用

对于任何给定的请求,一个输出过滤器可能仅被调用一次,并被赋予代表整个响应的单个旅。也有可能针对单个响应调用过滤器的次数与要过滤的内容的大小成比例,每次使过滤器通过一个包含单个存储桶的旅。在任何一种情况下,过滤器都必须正确运行。

每次调用时分配一个长寿命内存的输出过滤器可能会消耗与响应大小成比例的内存。需要分配内存的输出过滤器应在每个响应中分配一次;请参阅下面的维护状态

输出过滤器可以通过EOS在行中存在存储桶来区分给定响应的最终调用。EOS之后,旅中的任何水桶都应忽略。

输出过滤器绝不能使空的旅通过过滤器链。为防御起见,过滤器应准备好接受一个空的旅,并且应该返回成功而不必将此旅传递到过滤器链的下方。空旅的处理不应有任何副作用(例如更改过滤器专用的任何状态)。

如何处理空旅

apr_status_t dummy_filter(ap_filter_t *f, apr_bucket_brigade *bb)
{
    if (APR_BRIGADE_EMPTY(bb)) {
        return APR_SUCCESS;
    }
    ...
最佳

旅团结构

桶队是一个双向链接的桶列表。该列表由一个哨兵终止(在两端),该哨兵可以通过将其与所返回的指针进行比较来区别于正常存储桶APR_BRIGADE_SENTINEL。哨兵清单实际上不是有效的存储区结构;apr_bucket_read在前哨上调用常规存储区函数(例如)的任何尝试 都将具有未定义的行为(即会使进程崩溃)。

遍历和操纵铲斗旅有多种功能和宏。请参阅apr_buckets.h 标头以了解完整的覆盖范围。常用的宏包括:

APR_BRIGADE_FIRST(bb)
返回第bb旅的第一个水桶
APR_BRIGADE_LAST(bb)
返回bb旅的最后一个水桶
APR_BUCKET_NEXT(e)
在存储区e之后给出下一个存储区
APR_BUCKET_PREV(e)
在桶e之前给桶

apr_bucket_brigade结构本身被分配了一个水池,所以如果过滤器创建一个新的队,就必须确保内存使用正确界定。例如,一个过滤器会r->pool在每次调用时从请求池()中分配一个新的旅,这将违反上面有关内存使用的警告。此类过滤器应改为在每个请求的第一个调用上创建一个旅,并将该旅存储在其状态结构中

通常不建议使用 apr_brigade_destroy“摧毁”一个旅团,除非您确定该旅团将不再使用,即使那样,也应该很少使用。调用此函数不会释放旅结构使用的内存(因为它来自池),但是关联的池清理未注册。使用 apr_brigade_destroy实际上可能导致内存泄漏;如果一个“被毁”的旅在其收容池被破坏时也有水桶,那么这些水桶将不会立即被销毁。

通常,过滤器应apr_brigade_cleanup 优先于使用apr_brigade_destroy

最佳

处理桶

在处理非元数据存储桶时,重要的是要了解“ apr_bucket *”对象是数据的抽象表示

  1. 存储桶代表的数据量可以具有或可以不具有确定的长度;对于代表不确定长度数据的存储桶,该->length字段设置为值(apr_size_t)-1。例如,铲斗PIPE类型的铲斗的长度不确定;它们代表管道的输出。
  2. 存储桶代表的数据可能会映射也可能不会映射到内存中。所述FILE桶类型,例如,表示存储在磁盘上的文件的数据。

过滤器使用apr_bucket_read函数从存储桶读取数据 。调用此功能时,存储桶可能会变形为其他存储桶类型,也可能会将新存储桶插入存储桶大队。对于表示未映射到内存的数据的存储桶,必须发生这种情况。

举个例子;考虑一个包含一个FILE代表整个文件的存储桶的存储桶旅,大小为24 KB:

FILE(0K-24K)

读取此存储桶时,它将从文件中读取一个数据块,变形为一个HEAP存储桶以表示该数据,然后将数据返回给调用方。它还会插入一个FILE代表文件其余部分的新 存储桶;后apr_bucket_read来电,该旅是这样的:

HEAP(8K) FILE(8K-24K)

最佳

过滤旅

任何输出过滤器的基本功能将是遍历传入的旅并以某种方式转换(或简单地检查)内容。迭代循环的实现对于生成行为良好的输出滤波器至关重要。

以遍历整个旅的示例为例:

输出滤波器错误-不要模仿!

apr_bucket *e = APR_BRIGADE_FIRST(bb);
const char *data;
apr_size_t length;

while (e != APR_BRIGADE_SENTINEL(bb)) {
    apr_bucket_read(e, &data, &length, APR_BLOCK_READ);
    e = APR_BUCKET_NEXT(e);
}

return ap_pass_brigade(bb);

上面的实现将消耗与内容大小成比例的内存。FILE例如,如果传递了存储桶,则每个文件将存储 桶apr_bucket_read变成存储桶时,整个文件内容将被读取到内存中 。FILEHEAP

相反,下面的实现将消耗固定数量的内存来过滤任何旅。需要一个临时旅,每个响应只能分配一次,请参阅“ 维护状态”部分。

更好的输出滤波器

apr_bucket *e;
const char *data;
apr_size_t length;

while ((e = APR_BRIGADE_FIRST(bb)) != APR_BRIGADE_SENTINEL(bb)) {
    rv = apr_bucket_read(e, &data, &length, APR_BLOCK_READ);
    if (rv) ...;
    /* Remove bucket e from bb. */
    APR_BUCKET_REMOVE(e);
    /* Insert it into  temporary brigade. */
    APR_BRIGADE_INSERT_HEAD(tmpbb, e);
    /* Pass brigade downstream. */
    rv = ap_pass_brigade(f->next, tmpbb);
    if (rv) ...;
    apr_brigade_cleanup(tmpbb);
}
最佳

维持状态

需要在每个响应的多个调用上维持状态的过滤器可以使用->ctxap_filter_t结构的字段。通常在这种结构中存储一个临时旅,以避免每次调用都必须分配一个新旅,如旅结构一节所述。

维护过滤器状态的示例代码

struct dummy_state {
    apr_bucket_brigade *tmpbb;
    int filter_state;
    ...
};

apr_status_t dummy_filter(ap_filter_t *f, apr_bucket_brigade *bb)
{
    struct dummy_state *state;

    state = f->ctx;
    if (state == NULL) {

        /* First invocation for this response: initialise state structure.
         */
        f->ctx = state = apr_palloc(f->r->pool, sizeof *state);

        state->tmpbb = apr_brigade_create(f->r->pool, f->c->bucket_alloc);
        state->filter_state = ...;
    }
    ...
最佳

缓冲桶

如果过滤器决定在单个过滤器函数调用的持续时间之外存储存储桶(例如,将其存储在其 ->ctx状态结构中),则必须将这些存储桶放在一边。这是必要的,因为某些存储桶类型会提供代表临时资源(例如堆栈内存)的存储桶,这些临时资源在过滤器链完成对旅的处理后将立即超出范围。

要放置存储桶,apr_bucket_setaside 可以调用该函数。并非所有存储桶类型都可以保留,但是如果成功,存储桶将进行变形以确保其生存期至少与作为apr_bucket_setaside函数参数给出的池一样长 。

或者,ap_save_brigade可以使用该函数,该函数会将所有存储桶移动到一个单独的旅中,该旅包含的存储桶的寿命与给定的pool参数一样长。在考虑以下几点时,必须谨慎使用此功能:

  1. 返回时,ap_save_brigade保证返回的旅中的所有存储桶都将代表映射到内存中的数据。如果给定一个包含例如PIPE存储桶的输入旅,ap_save_brigade它将消耗任意数量的内存来存储管道的整个输出。
  2. ap_save_brigade从无法保留的存储桶中进行读取时,它将始终执行阻塞读取,从而消除了使用非阻塞存储桶读取的机会。
  3. 如果ap_save_brigade在不传递非NULL“ saveto(目的地)”旅参数的情况下使用该函数,则该函数将创建一个新的旅,这可能导致内存使用与内容大小成比例,如“ 旅结构”部分所述。
过滤器必须确保在给定响应(一个包含EOS桶的旅)的最后一次调用期间,处理所有缓冲的数据并将其向下传递到过滤器链中。否则,此类数据将丢失。
最佳

非阻塞存储桶读取

apr_bucket_read函数采用一个 apr_read_type_e,确定是否一个参数 阻塞非阻塞读取将来自数据源来执行。一个好的过滤器将首先尝试使用非阻塞读取从每个数据桶中读取数据。如果操作失败APR_EAGAIN,则FLUSH 在过滤器链下发送存储桶,然后使用阻塞读取重试。

这种操作模式可确保,如果使用缓慢的内容源,则位于过滤器链下游的所有过滤器都将刷新所有缓冲的存储桶。

CGI脚本是一个慢速内容源的示例,它被实现为存储桶类型。mod_cgi将发送 PIPE代表CGI脚本输出的存储桶;在等待CGI脚本产生更多输出时,从此类存储桶读取数据将会阻塞。

使用非阻塞存储桶读取的示例代码

apr_bucket *e;
apr_read_type_e mode = APR_NONBLOCK_READ;

while ((e = APR_BRIGADE_FIRST(bb)) != APR_BRIGADE_SENTINEL(bb)) {
    apr_status_t rv;

    rv = apr_bucket_read(e, &data, &length, mode);
    if (rv == APR_EAGAIN && mode == APR_NONBLOCK_READ) {

        /* Pass down a brigade containing a flush bucket: */
        APR_BRIGADE_INSERT_TAIL(tmpbb, apr_bucket_flush_create(...));
        rv = ap_pass_brigade(f->next, tmpbb);
        apr_brigade_cleanup(tmpbb);
        if (rv != APR_SUCCESS) return rv;

        /* Retry, using a blocking read. */
        mode = APR_BLOCK_READ;
        continue;
    }
    else if (rv != APR_SUCCESS) {
        /* handle errors */
    }

    /* Next time, try a non-blocking read first. */
    mode = APR_NONBLOCK_READ;
    ...
}
最佳

输出过滤器的十个规则

总而言之,这是所有输出过滤器都应遵循的一组规则:

  1. 输出过滤器不应使空的旅通过过滤链,而应容忍通过空的旅。
  2. 输出过滤器必须将所有元数据存储桶传递到过滤器链的下游;FLUSH应通过将所有未决或缓冲的存储桶沿过滤链向下传递来尊重存储桶。
  3. 输出过滤器应忽略存储区之后的所有 EOS存储区。
  4. 输出过滤器必须一次处理固定数量的数据,以确保内存消耗与要过滤的内容的大小不成比例。
  5. 输出过滤器应与存储桶类型无关,并且必须能够处理不熟悉类型的存储桶。
  6. 在调用ap_pass_brigade使一个旅通过过滤器链之后,输出筛选器应调用 apr_brigade_cleanup以确保该旅是空的,然后再使用该旅结构。输出滤波器绝不能apr_brigade_destroy用来“消灭”旅。
  7. 输出过滤器必须搁置超出过滤器功能持续时间保留的所有存储区。
  8. 输出过滤器不得忽略的返回值 ap_pass_brigade,并且必须在过滤器链上返回适当的错误。
  9. 输出过滤器必须为每个响应仅创建固定数量的存储桶旅,而不是为每个调用创建一个固定数量的存储桶。
  10. 输出过滤器应首先尝试从每个数据桶中进行非阻塞读取,FLUSH如果读取阻塞,则沿过滤器链向下发送一个桶,然后再尝试阻塞读取。
最佳

用例:在mod_ratelimit中缓冲

r1833875变化是一个很好的例子,显示在输出滤波器的情况下什么缓冲和保持状态的手段。在这种用例中,用户在用户的邮件列表上问了一个有趣的问题,即为什么mod_ratelimit似乎不使用代理内容来兑现其设置(要么以不同的速度进行速率限制,要么根本不这样做)。在深入研究解决方案之前,最好从高层次上解释其mod_ratelimit工作原理。诀窍很简单:进行速率限制设置并计算数据块大小,每200ms刷新一次到客户端。例如,假设要rate-limit 60在配置中进行设置,这些是查找块大小的高级步骤:

/* milliseconds to wait between each flush of data */
RATE_INTERVAL_MS = 200;
/* rate limit speed in b/s */
speed = 60 * 1024;
/* final chunk size is 12228 bytes */
chunk_size = (speed / (1000 / RATE_INTERVAL_MS));

如果我们将此计算应用于载有38400字节的水桶大队,则意味着过滤器将尝试执行以下操作:

  1. 将38400字节分成最多12228字节的块。
  2. 刷新前12228个字节的块并休眠200ms。
  3. 刷新第二个12228字节字节并休眠200ms。
  4. 刷新第三个12228字节块并休眠200ms。
  5. 刷新剩余的1716字节。

如果输出过滤器对每个响应仅处理一个旅,则上述伪代码可以很好地工作,但是可能会发生这样的情况,即需要以不同的旅大小来多次调用它。例如,前一个用例是当httpd直接提供某些内容(例如静态文件)时:桶队抽象负责处理整个内容,并且速率限制效果很好。但是,如果通过mod_proxy_http提供相同的静态内容(例如,后端在提供它而不是httpd服务),那么内容生成器(在这种情况下,mod_proxy_http)可以使用最大缓冲区大小,然后将数据作为存储桶旅发送给输出过滤器链定期触发多个呼叫mod_ratelimit。如果阅读器假设多次调用输出过滤器来尝试执行伪代码,每个调用都需要处理38400字节的存储桶旅,那么很容易发现一些异常:

  1. 在一个旅的最后一次冲洗与第二次冲洗之间,没有睡眠。
  2. 即使在最后一次刷新后强制睡眠,该块大小也不是理想大小(1716字节而不是12228字节),并且最终客户端的速度将很快不同于httpd的配置中设置的速度。

在这种情况下,两件事可能会有所帮助:

  1. 使用ctx内部数据结构(mod_ratelimit 针对每个响应处理周期进行初始化),以“记住”跨多个调用执行的最后一次睡眠的时间,并采取相应的措施。
  2. 如果不能将一个桶大队划分为有限数量的chunk_size块,请将剩余字节(位于桶大队的尾部)存储在临时存放区(即另一个桶大队)中,然后用于 ap_save_brigade将它们放在一边。这些字节将被前置到下一个存储桶旅,该存储桶旅将在后续调用中处理。
  3. 如果当前正在处理的存储桶旅包含流存储桶(EOS)的末尾,请避免使用先前的逻辑。如果到达流的末尾,则无需休眠或缓冲数据。

在本节开始处链接的提交还包含一些代码重构,因此在第一遍过程中阅读它并不容易,但是总体思路基本上是到目前为止所写的内容。本部分的目的不是使尝试阅读C代码的读者感到头疼,而是让他/她有一种有效使用httpd筛选器链工具集提供的工具所需的正确思维方式。

可用语言: zh 

最佳

注释

注意:
这不是“问答”部分。此处放置的评论应指向有关改进文档或服务器的建议,如果实施或被认为无效/偏离主题,我们的主持人可以将其删除。有关如何管理Apache HTTP Server的问题,应直接指向我们的IRC频道#httpd(位于Freenode上),或发送至我们的邮件列表
目前,此页面已禁用评论。