Пункт 222. Рекомендации по выходным фильтрам в версии 2.x
При написании выходных фильтров встречается ряд распространенных ошибок; эта страница предназначена для документирования лучших практик для авторов новых или существующих фильтров.
Этот документ применим как к версии 2.0, так и к версии 2.2 HTTP-сервера Apache; он специально нацелен на
фильтры 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)
{
если (APR_BRIGADE_EMPTY(bb)) {
вернуть APR_SUCCESS;
}
...
Структура бригады
Группа ведер — это двусвязный список ведер. Список завершается (с обоих концов) дозорным , который можно отличить от обычного сегмента, сравнив его с указателем, возвращаемым функцией APR_BRIGADE_SENTINEL
. Страж списка на самом деле не является допустимой структурой сегмента; любая попытка вызвать нормальные функции корзины (такие как
apr_bucket_read
) на часовом будет иметь неопределенное поведение (т.е. приведет к сбою процесса).
Имеется множество функций и макросов для перемещения и манипулирования ковшовыми бригадами; полный охват см. в заголовке apr_buckets.h. К часто используемым макросам относятся:
-
APR_BRIGADE_FIRST(bb)
- возвращает первое ведро в бригаде бб
-
APR_BRIGADE_LAST(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 *
объект " " является абстрактным представлением данных:
- Количество данных, представленных ведром, может иметь или не иметь определенную длину; для сегмента, который представляет данные неопределенной длины, в
->length
поле устанавливается значение (apr_size_t)-1
. Например, ковши ковшового PIPE
типа имеют неопределенную длину; они представляют вывод из канала.
- Данные, представленные сегментом, могут отображаться или не отображаться в памяти. Тип
FILE
сегмента, например, представляет данные, хранящиеся в файле на диске.
Фильтры считывают данные из корзины с помощью
apr_bucket_read
функции. При вызове этой функции ведро может трансформироваться в ведро другого типа, а также может вставить новое ведро в группу ведер. Это должно происходить для сегментов, которые представляют данные, не отображаемые в память.
Чтобы привести пример; рассмотрим группу сегментов, содержащую один FILE
сегмент, представляющий целый файл размером 24 килобайта:
FILE(0K-24K)
Когда это ведро будет прочитано, оно прочитает блок данных из файла, превратится в ведро HEAP
для представления этих данных и вернет данные вызывающей стороне. Он также вставляет новую
FILE
корзину, представляющую оставшуюся часть файла; после apr_bucket_read
вызова бригада выглядит так:
HEAP(8K) FILE(8K-24K)
Фильтровальные бригады
Основная функция любого выходного фильтра будет заключаться в повторении переданной бригады и преобразовании (или просто проверке) содержимого каким-либо образом. Реализация цикла итерации имеет решающее значение для создания хорошо работающего выходного фильтра.
Возьмем пример, который проходит через всю бригаду следующим образом:
Плохой выходной фильтр - не имитируйте!
apr_bucket *e = APR_BRIGADE_FIRST(bb);
константный символ *данные;
длина apr_size_t;
в то время как (e != APR_BRIGADE_SENTINEL (bb)) {
apr_bucket_read(e, &data, &length, APR_BLOCK_READ);
e = APR_BUCKET_NEXT (e);
}
вернуть ap_pass_brigade(bb);
Приведенная выше реализация будет потреблять память, пропорциональную размеру содержимого. Например, при передаче FILE
ведра все содержимое файла будет считано в память, поскольку каждый
apr_bucket_read
вызов превращает FILE
ведро в HEAP
ведро.
Напротив, приведенная ниже реализация будет потреблять фиксированный объем памяти для фильтрации любой бригады; требуется временная бригада, и она должна выделяться только один раз для каждого ответа, см. раздел «Поддержание состояния».
Лучший выходной фильтр
апрель_ведро *e;
константный символ *данные;
длина apr_size_t;
в то время как ((e = APR_BRIGADE_FIRST(bb)) != APR_BRIGADE_SENTINEL(bb)) {
rv = apr_bucket_read(e, &data, &length, APR_BLOCK_READ);
если (рв)...;
/* Удалить ведро e из bb. */
APR_BUCKET_REMOVE(д);
/* Вставляем во временную бригаду. */
APR_BRIGADE_INSERT_HEAD(tmpbb, e);
/* Передать бригаду вниз по течению. */
rv = ap_pass_brigade(f->следующий, tmpbb);
если (рв)...;
апрель_brigade_cleanup(tmpbb);
}
Поддержание состояния
Фильтр, который должен поддерживать состояние при нескольких вызовах на ответ, может использовать ->ctx
поле своей ap_filter_t
структуры. Обычно в такой структуре хранится временная бригада, чтобы не выделять новую бригаду для каждого вызова, как описано в разделе «Структура бригады».
Пример кода для сохранения состояния фильтра
структура фиктивное_состояние {
апрель_бакет_бригада *tmpbb;
интервал_состояние_фильтра;
...
};
apr_status_t dummy_filter (ap_filter_t *f, apr_bucket_brigade *bb)
{
структура фиктивное_состояние *состояние;
состояние = f->ctx;
если (состояние == NULL) {
/* Первый вызов для этого ответа: инициализировать структуру состояния.
*/
f->ctx = state = apr_palloc(f->r->pool, sizeof *state);
состояние->tmpbb = apr_brigade_create(f->r->pool, f->c->bucket_alloc);
состояние->filter_state = ...;
}
...
Буферные ведра
Если фильтр решает хранить сегменты после продолжительности одного вызова функции фильтра (например, сохраняет их в своей
->ctx
структуре состояния), эти сегменты должны быть отложены . Это необходимо, потому что некоторые типы сегментов предоставляют сегменты, представляющие временные ресурсы (например, память стека), которые выпадают из области действия, как только цепочка фильтров завершает обработку бригады.
Чтобы отложить ведро, apr_bucket_setaside
можно вызвать функцию. Не все типы корзин могут быть отложены, но в случае успеха корзина будет преобразована, чтобы гарантировать, что ее время жизни не меньше, чем пул, указанный в качестве аргумента функции
apr_bucket_setaside
.
В качестве альтернативы ap_save_brigade
можно использовать функцию, которая переместит все корзины в отдельную бригаду, содержащую корзины со временем жизни, равным заданному аргументу пула. Эту функцию следует использовать с осторожностью, принимая во внимание следующие моменты:
- По возвращении
ap_save_brigade
гарантирует, что все сегменты в возвращаемой бригаде будут представлять данные, отображаемые в память. Если задана входная группа, содержащая, например, PIPE
ведро, ap_save_brigade
она будет потреблять произвольный объем памяти для хранения всего вывода канала.
- При
ap_save_brigade
чтении из сегментов, которые нельзя отложить, он всегда будет выполнять чтение с блокировкой, исключая возможность использования неблокирующих операций чтения с сегментами.
- Если
ap_save_brigade
используется без передачи saveto
параметра бригады, отличного от NULL " " (назначение), функция создаст новую бригаду, что может привести к тому, что использование памяти будет пропорционально размеру содержимого, как описано в разделе "Структура бригады".
Фильтры должны гарантировать, что любые буферизованные данные обрабатываются и передаются по цепочке фильтров во время последнего вызова для данного ответа (бригада, содержащая корзину EOS). В противном случае такие данные будут потеряны.
Неблокирующие чтения сегментов
Функция apr_bucket_read
принимает
apr_read_type_e
аргумент, который определяет, будет ли
выполняться блокирующее или неблокирующее чтение из источника данных. Хороший фильтр сначала попытается прочитать из каждого блока данных, используя неблокирующее чтение; если это не удается с APR_EAGAIN
, то отправьте FLUSH
корзину вниз по цепочке фильтров и повторите попытку, используя чтение с блокировкой.
Этот режим работы гарантирует, что любые фильтры, расположенные дальше по цепочке фильтров, будут очищать все буферизованные корзины, если используется медленный источник контента.
CGI-скрипт является примером медленного источника контента, реализованного в виде ведра. mod_cgi
отправит
PIPE
сегменты, представляющие выходные данные CGI-скрипта; чтение из такой корзины будет заблокировано, пока сценарий CGI выдаст больше вывода.
Пример кода с использованием неблокирующего чтения сегмента
апрель_ведро *e;
режим apr_read_type_e = APR_NONBLOCK_READ;
в то время как ((e = APR_BRIGADE_FIRST(bb)) != APR_BRIGADE_SENTINEL(bb)) {
апр_статус_т рв;
rv = apr_bucket_read(e, &data, &length, mode);
если (rv == APR_EAGAIN && режим == APR_NONBLOCK_READ) {
/* Передать бригаду, содержащую флеш-базу: */
APR_BRIGADE_INSERT_TAIL(tmpbb, apr_bucket_flush_create(...));
rv = ap_pass_brigade(f->следующий, tmpbb);
апрель_brigade_cleanup(tmpbb);
если (rv != APR_SUCCESS) вернуть rv;
/* Повторите попытку, используя блокирующее чтение. */
режим = APR_BLOCK_READ;
продолжать;
}
иначе если (rv != APR_SUCCESS) {
/* обработка ошибок */
}
/* В следующий раз попробуйте сначала неблокирующее чтение. */
режим = APR_NONBLOCK_READ;
...
}
Десять правил для выходных фильтров
Таким образом, вот набор правил, которым должны следовать все выходные фильтры:
- Выходные фильтры не должны пропускать пустые бригады по цепочке фильтров, но должны быть терпимы к передаче пустых бригад.
- Выходные фильтры должны пропускать все сегменты метаданных по цепочке фильтров;
FLUSH
сегменты следует учитывать, передавая любые ожидающие или буферизованные сегменты по цепочке фильтров.
- Выходные фильтры должны игнорировать любые сегменты, следующие за
EOS
сегментом.
- Выходные фильтры должны обрабатывать фиксированный объем данных за раз, чтобы потребление памяти не было пропорционально размеру фильтруемого содержимого.
- Выходные фильтры не должны зависеть от типов корзин и должны иметь возможность обрабатывать корзины незнакомого типа.
- После вызова
ap_pass_brigade
для прохождения бригады по цепочке фильтров выходные фильтры должны вызывать
apr_brigade_cleanup
, чтобы гарантировать, что бригада пуста, прежде чем повторно использовать эту структуру бригады; выходные фильтры никогда не должны использоваться apr_brigade_destroy
для «уничтожения» бригад.
- Выходные фильтры должны отбрасывать любые сегменты, которые сохраняются после окончания действия функции фильтра.
- Выходные фильтры не должны игнорировать возвращаемое значение
ap_pass_brigade
и должны возвращать соответствующие ошибки обратно по цепочке фильтров.
- Выходные фильтры должны создавать только фиксированное количество групп сегментов для каждого ответа, а не по одной на каждый вызов.
- Выходные фильтры должны сначала попытаться выполнить неблокирующее чтение из каждого блока данных и отправить блок
FLUSH
вниз по цепочке фильтров, если чтение блокируется, прежде чем повторить попытку блокирующего чтения.