Введение в контейнеры и бригады


Архитектура фильтров сервера Apache 2.0 стала главной особенностью, которая отличает этот сервер от других веб-серверов, в том числе и от Apache 1.x. Эта архитектура сделала сервер уникальной по своей мощи и гибкости платформой для приложений. Но эта мощь имеет свою цену - сперва необходимо научиться работать с ней. При изучении архитектуры наибольшее затруднение вызывает работа с контейнерами (Buckets) и бригадами (Brigades) - строительными блоками фильтров.
В этой статье мы расскажем о контейнерах и бригадах в объеме достаточном для того, чтобы Вы смогли начать работать с ними. Также мы разработаем простой, но функциональный модуль-фильтр, который будет работать напрямую с контейнерами и бригадами.

Работа с контейнерами и бригадами осуществляется напрямую посредством низкоуровнего API, который довольно сложен при использовании. Но так как это самый низкий уровень, то он полностью показывает весь механизм взаимодействия. В следующих статьях мы осветим другие вопросы, связанные с этой темой, а именно: отладку и альтернативные пути работы с данными. Один из таких путей работы уже продемонстрирован в статье “Основы управления ресурсами в Apache: APR Пулы (pools)”.

Базовые концепции

Базовыми концепциями архитектуры фильтров являются контейнеры и бригады. Давайте сперва рассмотрим их самих, перед тем как начать изучать работу с ними.

Контейнер (Bucket)

Контейнер - это место для хранения данных любого типа. Хотя в большинстве случаев это просто блок памяти, контейнер может хранить файл или даже поток данных, например, от исполняемой программы. В зависимости от типа данных и методов их обработки используются различные типы контейнеров. В терминах ООП структура apr_bucket является абстрактным базовым классом, от которого наследуются контейнеры определенного типа.
Есть несколько различных типов, как контейнеров данных, так и контейнеров метаданных. Ниже мы обсудим все эти типы.

Бригада (Brigade)

При обычной работе такой сущности, как отдельный контейнер, не существует: они объединены в бригады контейнеров. Бригада может содержать любое количество контейнеров. Контейнеры в бригаде организованы в виде кольцевого списка. Бригада делает работу с данными более гибкой и эффективной. Ваш фильтр получает и передает данные именно с помощью бригад.

А зачем нам они?

А зачем нам нужны контейнеры и бригады? Неужели мы не можем хранить и передавать просто блоки данных? Может быть просто использовать указатель void* с длиной буфера или строки С++?
На первую часть этого вопроса мы уже ответили: контейнеры - это не только данные, они - некоторые абстракции, которые изначально определяют также и типы используемых данных. А как тогда они оправдывают возросшую сложность перед простыми буферами данных?
Вторая причина, побуждающая использовать контейнеры и бригады, это то, что они позволяют более эффективно работать с памятью. Мы продемонстрируем простой, но весьма полезный пример такого использования: фильтр, который выводит текст простых текстовых документов в виде HTML страниц с добавлением некоторой информации сверху и снизу в каждом документе.
HTML страница конечно же может содержать блоки обычного теста, заключенного в тег <pre> для сохранения пробелов и форматирования. Поэтому главная задача данного фильтра - это просто передача текста и вставка дополнительной информации. Но есть одна проблема - некоторые специальные символы необходимо исключить из передаваемых данных. Для соответствия спецификациям HTML и браузеров мы исключим четыре символа: <, >, & и ", и заменим их на: &lt; &gt; и т.п.
При такой замене мы не можем просто поменять символы, так как новая последовательность больше на 3-5 байтов, чем первоначальная. Если бы мы использовали простой буфер, то нам необходимо было бы его либо расширять функцией realloc(), либо копировать обрабатываемые блоки по частям. А при повторении таких операций использование памяти станет неэффективным. Наилучшее решение этой задачи - это дважды просканировать данные: один раз для подсчета длины нового буфера, а второй раз для копирования данных с подстановками.
Но и такой подход не самый эффективный. Используя контейнеры и бригады вместо простого буфера, мы можем заменять символы “на лету” без всякого выделения или копирования больших блоков памяти. Это гораздо эффективней, особенно когда количество символов подлежащих замене гораздо меньше размера всего документа, как в нашем случае.
Это делается в следующем порядке:
  1. В контейнере находим подлежащий замене символ.
  2. Разбиваем контейнер на три части: до и после найденного символа, и сам символ. Теперь у нас три контейнера.
  3. Удаляем символ, оставляем только два контейнера: “до символа” и “после символа”.
  4. Создаем новый контейнер с подстановкой, вставляя его на место удаленного символа.
При этом вместо перемещения больших блоков памяти мы просто манипулировали указателями. А все данные, которые менялись, - это только сам удаленный символ и несколько байтов, заменившие его.

Пример реализации: модуль mod_txt

mod_txt - это простой модуль-фильтр, который отображает обычные текстовые файлы в виде HTML (или XHTML) файла с некоторыми заголовками. Когда запрашивается текстовый файл, модуль преобразует текст в HTML и вставляет его между двумя заголовками.
Все это делается с помощью контейнеров, используя низкоуровневый API. А также демонстрирует вставку данных в файл и замену символов без какого-либо выделения больших блоков памяти.

Контейнерные функции

Вначале мы расскажем о двух функциях, предназначенных для вставки данных: одна для простых замен, другая для вставки данных в файл.
При создании файлового контейнера требуется указатель на открытый файл и диапазон данных файла. Если мы передаем файл полностью, то в качестве диапазона нужно указать его размер. Для лучшей производительности мы откроем его с использованием блокировки и флага sendfile.
static apr_bucket* txt_file_bucket(request_rec* r, const char* fname) {
apr_file_t* file = NULL;
apr_finfo_t finfo;
if (apr_stat(&finfo, fname, APR_FINFO_SIZE, r->pool) != APR_SUCCESS) {
return NULL;
}
if (apr_file_open(&file, fname, APR_READ|APR_SHARELOCK|APR_SENDFILE_ENABLED,
APR_OS_DEFAULT, r->pool ) != APR_SUCCESS ) {
return NULL ;
}
if (!file) {
return NULL ;
}
return apr_bucket_file_create(file, 0, finfo.size, r->pool,
r->connection->bucket_alloc) ;
}
Для проведения текстовых подстановок мы можем просто создать контейнер со вставляемой строкой. Самый подходящий для этого тип контейнера - временный (transient):
static apr_bucket* txt_esc(char c, apr_bucket_alloc_t* alloc ) {
switch (c) {
case ‘<’: return apr_bucket_transient_create(“&lt;”, 4, alloc) ;
case ‘>’: return apr_bucket_transient_create(“&gt;”, 4, alloc) ;
case ‘&’: return apr_bucket_transient_create(“&amp;”, 5, alloc) ;
case ‘"’: return apr_bucket_transient_create(“&quot;”, 6, alloc) ;
default: return NULL ;
}
}

Фильтр

Сам по себе фильтр довольно простой, но существует несколько тонких моментов, которые надо рассмотреть поподробнее. Комментировать эти моменты будем по ходу дела. Запомните также, что контейнеры файлов обоих заголовков (вставляемые заголовки хранятся в файлах) создаются в функции filter_init (пропущено для краткости)
static int txt_filter(ap_filter_t* f, apr_bucket_brigade *bb) {
apr_bucket* b;
txt_ctxt* ctxt = (txt_ctxt*)f->ctx;
if ( ctxt == NULL ){
txt_filter_init(f) ;
ctxt = f->ctx ;
}
Главный цикл: Такая конструкция типична при итерационной обработке входных данных
for ( b = APR_BRIGADE_FIRST(bb);
b != APR_BRIGADE_SENTINEL(bb);
b = APR_BUCKET_NEXT(b) ) {
const char* buf;
size_t bytes;
Как и в любом фильтре, мы должны проверять на EOS (флаг конца потока). Когда мы дойдем до него надо вставить завершающий заголовок.
if ( APR_BUCKET_IS_EOS(b) ) {
/* конец файла - вставляем заголовок */
if ( ctxt->foot && ! (ctxt->state & TXT_FOOT ) ) {
ctxt->state |= TXT_FOOT;
APR_BUCKET_INSERT_BEFORE(b, ctxt->foot);
}
Обычно контейнер содержит данные, и мы можем просто прочитать их в буфер:
} else if ( apr_bucket_read(b, &buf, &bytes, APR_BLOCK_READ) == APR_SUCCESS ) {
/* У нас есть контейнер с текстом. Теперь проведем в нем подстановки*/
size_t count = 0;
const char* p = buf;
Теперь ищем подлежащие замене символы и заменяем их:
while ( count < bytes ) {
size_t sz = strcspn(p, “<>&\"”);
count += sz;
Мы подошли к основному: замена символа
if ( count < bytes ) {
apr_bucket_split(b, sz); //Отделяем часть буфера до символа
b = APR_BUCKET_NEXT(b); //Пропускаем ее
APR_BUCKET_INSERT_BEFORE(b, txt_esc(p[sz],
f->r->connection->bucket_alloc)); //Делаем подстановку
apr_bucket_split(b, 1); //Отделяем от буфера заменяемый символ
APR_BUCKET_REMOVE(b); // и удаляем его
b = APR_BUCKET_NEXT(b); // Сдвигаем курсор на конец обработанной части
count += 1 ;
p += sz + 1 ;
}
}}}
Далее вставляем первый заголовок, если он уже не вставлен. Запомните следующие:
  • Это надо сделать после основного цикла во избежание обработки самого заголовка.
  • Это работает, потому что мы можем вставить контейнер в любое место в бригаде, а в этом случае вставляем его в начало бригады.
  • И как в случае с завершающим заголовком, позаботимся об избежании повторной вставки.
if ( ctxt->head && ! (ctxt->state & TXT_HEAD ) ) {
ctxt->state |= TXT_HEAD;
APR_BRIGADE_INSERT_HEAD(bb, ctxt->head);
}
Теперь мы закончили все манипуляции с данными и передаем их вниз по цепочке фильтров.
return ap_pass_brigade(f->next, bb);
}
Запомните, что мы создаем новый контейнер каждый раз, когда заменяем символ. А не можем ли мы подготовить четыре контейнера заранее - по одному для каждого из символов, которые будут заменяться, а затем повторно использовать их при каждой замене?
Проблема здесь в том, что каждый контейнер связан с соседними контейнерами. Поэтому, если мы будем использовать повторно тот же самый контейнер, мы потеряем эти ссылки, и бригада пропустит данные, пришедшие между двумя заменами. Следовательно, нам необходимо каждый раз использовать новый контейнер. А это означает, что такая техника станет неэффективной, если потребуется изменить большие объемы получаемых данных.

Типы контейнеров

Выше мы использовали два типа контейнеров данных: файловый и временный, и один тип контейнера метаданных - EOS. Существуют еще несколько типов контейнеров, подходящих для различных типов данных и метаданных.

Простые контейнеры данных

Выше, когда мы создавали временный контейнер, мы вставляли блок памяти в выходной поток. Но использование данного контейнера не самый эффективный путь для удаления символа. Причина в том, что использование временной памяти может привести к порче данных. Чтобы гарантированно избежать этого, нам надо использовать вместо
case ‘<’: return apr_bucket_transient_create(“&lt;”, 4, alloc);
следующий код
static const char* lt = “&lt;”;

case ‘<’: return apr_bucket_immortal_create(lt, 4, alloc) ;
Когда же мы создаем постоянный контейнер, мы гарантируем, что память всегда будет доступна в течение существования контейнера.
Третий вариант - это использование контейнера пула. Он использует память, выделенную из пула. Этот вариант может привести к порче данных только тогда, когда пул уничтожится раньше контейнера, который выделял из него память.

Контейнер кучи

Контейнер кучи - это еще один вид контейнеров данных. Но его использование отличается от всех описанных выше. Очень редко необходимо явно создавать контейнер кучи: обычно они создаются неявно при использовании stdio-подобного API для передачи данных следующему фильтру.

Внешние контейнеры данных

Файловый контейнер, с которым мы уже встречались, требовался нам для записи файла (или его части) в поток. Хотя нам и надо найти его размер, но все же не надо полностью его читать. Если флаг sendfile установлен, то операционная система (через APR) оптимизирует передачу файла.
Тип контейнера mmap похож на предыдущий, и предназначен для mmap-файлов. APR может преобразовывать файловые контейнеры в контейнеры mmap-файлов.
Есть еще два типа контейнеров - контейнер потока (pipe) и контейнер сокета (soket), которые понадобятся нам при вставке данных, полученных от внешнего источника через IPC.

Контейнеры метаданных

Помимо контейнеров данных, есть еще два типа контейнеров метаданных. Первый тип контейнера - EOS очень важен: он обязательно должен быть послан в конце потока данных, и он служит сигналом того, что поток данных завершился. Другой тип контейнера метаданных - это редко используемый контейнер FLUSH.

Завершение

Вот и все, что мы хотели рассказать Вам о контейнерах и бригадах Apache. В статье мы постарались привести полезный пример фильтра (использовать данный фильтр можно, например, в области бесплатного хостинга). Надеюсь, что Вам понравился данный материал. Если у Вас остались вопросы, то отправляйте их на info@apachedev.ru. Я обязательно отвечу на них.
Автор: Ник Кью (Nick Kew)
Перевод: Сипягин Максим
Оригинал документа (en)

These icons link to social bookmarking sites where readers can share and discover new web pages.
  • News2.ru
  • NewsLand.ru
  • del.icio.us
  • BobrDobr.ru
  • Ma.gnolia
  • Digg
  • Reddit
  • Technorati
  • Slashdot
  • Netscape
  • DZone
  • ThisNext
  • Furl
  • YahooMyWeb
Опубликовано в: Архитектура Февраль 12, 2006

Комментариев нет »

Комментариев нет.

Оставить комментарий

You must be logged in to post a comment.

Работает на WordPress