编写输出滤波器时会遇到许多常见的陷阱;此页面旨在记录针对新过滤器或现有过滤器作者的最佳做法。
本文档适用于Apache HTTP Server的2.0版和2.2版。尽管某些建议对于所有类型的过滤器都是通用的,但它专门针对
RESOURCE
-level或CONTENT_SET
-level过滤器。
每次调用过滤器时,都会通过一个存储桶旅,其中包含一系列表示数据内容和元数据的存储桶。每个桶有
桶型 ; httpd
核心模块(和apr-util
提供存储桶旅接口的
库)定义和使用了许多存储桶类型,但是模块可以自由定义自己的类型。
过滤器可以使用APR_BUCKET_IS_METADATA
宏判断存储桶是表示数据还是元数据。通常,所有元数据段都应由输出过滤器沿过滤器链向下传递。过滤器可以适当地转换,删除和插入数据桶。
所有过滤器都必须注意两种元数据存储桶类型:EOS
存储桶类型和
FLUSH
存储桶类型。一个EOS
桶表示该响应的结尾已经到达并没有进一步桶需要被处理。甲FLUSH
桶表明过滤器应立即刷新任何缓冲桶(如果适用)向下过滤器链。
FLUSH
当内容生成器(或上游过滤器)知道可能有延迟才能发送更多内容时,将发送存储桶。通过FLUSH
立即将存储桶向下传递
到过滤器链,过滤器可确保客户端等待未决数据的时间不会超过必要时间。过滤器可以创建FLUSH
存储桶,并根据需要将其沿过滤器链向下传递。FLUSH
不必要或太频繁地生成存储桶会损害网络利用率,因为它可能会强制发送大量的小数据包,而不是少量的大数据包。在部分非阻塞斗读取
覆盖了鼓励过滤器来生成一个事例
FLUSH
桶。
HEAP FLUSH FILE EOS
这显示了可以通过过滤器的铲斗旅。它包含两个元数据存储区(FLUSH
和
EOS
)和两个数据存储区(HEAP
和
FILE
)。
对于任何给定的请求,一个输出过滤器可能仅被调用一次,并被赋予代表整个响应的单个旅。也有可能针对单个响应调用过滤器的次数与要过滤的内容的大小成比例,每次使过滤器通过一个包含单个存储桶的旅。在任何一种情况下,过滤器都必须正确运行。
输出过滤器可以通过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)
APR_BRIGADE_LAST(bb)
APR_BUCKET_NEXT(e)
APR_BUCKET_PREV(e)
该apr_bucket_brigade
结构本身被分配了一个水池,所以如果过滤器创建一个新的队,就必须确保内存使用正确界定。例如,一个过滤器会r->pool
在每次调用时从请求池()中分配一个新的旅,这将违反上面有关内存使用的警告。此类过滤器应改为在每个请求的第一个调用上创建一个旅,并将该旅存储在其状态结构中。
通常不建议使用
apr_brigade_destroy
“摧毁”一个旅团,除非您确定该旅团将不再使用,即使那样,也应该很少使用。调用此函数不会释放旅结构使用的内存(因为它来自池),但是关联的池清理未注册。使用
apr_brigade_destroy
实际上可能导致内存泄漏;如果一个“被毁”的旅在其收容池被破坏时也有水桶,那么这些水桶将不会立即被销毁。
通常,过滤器应apr_brigade_cleanup
优先于使用apr_brigade_destroy
。
在处理非元数据存储桶时,重要的是要了解“ apr_bucket *
”对象是数据的抽象表示:
->length
字段设置为值(apr_size_t)-1
。例如,铲斗PIPE
类型的铲斗的长度不确定;它们代表管道的输出。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
变成存储桶时,整个文件内容将被读取到内存中
。FILE
HEAP
相反,下面的实现将消耗固定数量的内存来过滤任何旅。需要一个临时旅,每个响应只能分配一次,请参阅“ 维护状态”部分。
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); }
需要在每个响应的多个调用上维持状态的过滤器可以使用->ctx
其ap_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参数一样长。在考虑以下几点时,必须谨慎使用此功能:
ap_save_brigade
保证返回的旅中的所有存储桶都将代表映射到内存中的数据。如果给定一个包含例如PIPE
存储桶的输入旅,ap_save_brigade
它将消耗任意数量的内存来存储管道的整个输出。ap_save_brigade
从无法保留的存储桶中进行读取时,它将始终执行阻塞读取,从而消除了使用非阻塞存储桶读取的机会。ap_save_brigade
在不传递非NULL“ saveto
(目的地)”旅参数的情况下使用该函数,则该函数将创建一个新的旅,这可能导致内存使用与内容大小成比例,如“ 旅结构”部分所述。该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; ... }
总而言之,这是所有输出过滤器都应遵循的一组规则:
FLUSH
应通过将所有未决或缓冲的存储桶沿过滤链向下传递来尊重存储桶。EOS
存储区。ap_pass_brigade
使一个旅通过过滤器链之后,输出筛选器应调用
apr_brigade_cleanup
以确保该旅是空的,然后再使用该旅结构。输出滤波器绝不能apr_brigade_destroy
用来“消灭”旅。ap_pass_brigade
,并且必须在过滤器链上返回适当的错误。FLUSH
如果读取阻塞,则沿过滤器链向下发送一个桶,然后再尝试阻塞读取。该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字节的水桶大队,则意味着过滤器将尝试执行以下操作:
如果输出过滤器对每个响应仅处理一个旅,则上述伪代码可以很好地工作,但是可能会发生这样的情况,即需要以不同的旅大小来多次调用它。例如,前一个用例是当httpd直接提供某些内容(例如静态文件)时:桶队抽象负责处理整个内容,并且速率限制效果很好。但是,如果通过mod_proxy_http提供相同的静态内容(例如,后端在提供它而不是httpd服务),那么内容生成器(在这种情况下,mod_proxy_http)可以使用最大缓冲区大小,然后将数据作为存储桶旅发送给输出过滤器链定期触发多个呼叫mod_ratelimit
。如果阅读器假设多次调用输出过滤器来尝试执行伪代码,每个调用都需要处理38400字节的存储桶旅,那么很容易发现一些异常:
在这种情况下,两件事可能会有所帮助:
mod_ratelimit
针对每个响应处理周期进行初始化),以“记住”跨多个调用执行的最后一次睡眠的时间,并采取相应的措施。ap_save_brigade
将它们放在一边。这些字节将被前置到下一个存储桶旅,该存储桶旅将在后续调用中处理。在本节开始处链接的提交还包含一些代码重构,因此在第一遍过程中阅读它并不容易,但是总体思路基本上是到目前为止所写的内容。本部分的目的不是使尝试阅读C代码的读者感到头疼,而是让他/她有一种有效使用httpd筛选器链工具集提供的工具所需的正确思维方式。
可用语言: zh