Confucius Blog
Руководство Эмиллера по разработке модулей для Nginx 09.09.2008
Оригинал Emiller's Guide To Nginx Module Development, черновик от 14 июля 2008 г. (изменения). Автор: Эван Миллер, перевод Петра Леонова.
  • Брюс Уэйн: Что это?
  • Люциус Фокс: Бэтмобиль? Ох… лучше вам не знать.

Чтобы лучше разобраться в Nginx'е, web-сервере, надо сначала понять Бэтмена, персонажа комиксов.

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

Но кем бы был Бэтмен без своего Пояса.

Пояс Бэтмена Рисунок 1: Пояс Бэтмена, в обнимку с пузом Кристиана Бэйла.

В любой момент в Поясе Бэтмена может найтись набор отмычек, пара бумерангов, бэт-наручники, бэт-маячок, бэт-дротик, прибор ночного видения, термитные гранаты, дымовая завеса, фонарик, обруч из криптонита, паяльная лампа или iPhone. Если Бэтмену надо кого-то усыпить, ослепить, оглушить, выследить, пристукнуть, притормозить, довести до слез или заэсэмэсить насмерть, то он тянется к своему Поясу. Для Бэтмена он так много значит, что он скорей забыл бы одеть штаны, чем пояс. А у Бэтмена штанов и нет, вместо них ему приходится носить резиновые бронеритузы (рис. 1).

Вместо Пояса Бэтмена у Nginx имеется свой набор модулей. Когда нужно сжать запрос или передать его по частям, Nginx запускает соответсвующий модуль. Когда Nginx блокирует доступ с какого-либо IP адреса или проверяет данные HTTP-авторизации, на самом деле это делает один из модулей. Если Nginx подсоединяется к Memcache или FastCGI-серверу, то именно модуль связывает их.

Пояс Бэтмена напичкан всякими полезностями, но иногда Бэтмену нужно что-то новенькое. Например, обнаружился новый противник, которого не удержать бэт-наручниками и не сломить бумерангом. Или Бэтмену скоро пригодится новая способность, такая как способность дышать под водой. Вот тогда-то Бэтмен и зовет Люциуса Фокса, чтобы тот придумал новую бэт-штуковину.

Люциус Фокс Рисунок 2: Брюс Уэйн (он же Бэтмен) со своим инженером Люциусом Фоксом.

Целью этого руководства является подробно рассказать вам о модулях Nginx, чтобы вы смогли стать как Люциус Фокс. После прочтения руководства вы сможете проектировать и реализовывать отличные модули, которые помогут Nginx делать то, чего он раньше не умел. Система модулей Nginx содержит много ньюансов и особенностей, так что вы, наверно, будете часто возвращаться к этому руководству. Я постарался изложить концепции настолько доступно, насколько это возможно. но, без сомнений, создание модулей для Nginx все равно остается трудной задачей.

Но кто говорил, что создавать бэт-штуковины будет легко?

Содержание
  1. Для начала
  2. Устройство модулей в первом приближении
  3. Компоненты модуля Nginx
    1. Структуры конфигурации модуля
    2. Директивы модуля
    3. Контекст модуля
      1. create_loc_conf
      2. merge_loc_conf
    4. Описание модуля
    5. Установка модуля
      1. Установка обработчика
      2. Установка фильтра
  4. Обработчики, фильтры и балансировщики нагрузки
    1. Устройство обработчиков (не проксирующих)
      1. Получение конфигурации локейшна
      2. Генерация ответа
      3. Отправка заголовка ответа
      4. Отправка тела ответа
Для начала

Вы должны неплохо знать Си. Не просто его синтаксис, а то, как работать со структурами и не бояться указателей и ссылок на функции. А также иметь представление о препроцессоре и макросах. Если вам надо немного освежить знания, то ничто не сможет сравниться с K&R(англ.).

Полезно понимать основы HTTP. Мы же, вообще-то, собираемся работать с web-сервером.

Пригодятся знания структуры конфигурационного файла Nginx'а. Вот основные моменты: существуют четыре контекста (называеются они main — главный, server — сервер, upstream — апстрим, и location — локейшн) в которых могут быть директивы с одним и более параметрами. Директивы в главном контексте применяются ко всему-всему; директивы из котекста сервера применяются к конкретному хосту/порту; директивы в апстриме описывают набор бэкендов; а директивы в контексте локешна применяются к разным путям запроса (например, "/", "/images" и т.д.) Локешн наследует конфигурацию содержащему его серверному контексту, а сервер наследует главному контексту. Контекст апстрима не наследует никому, у него собственные директивы, которых больше нигде не используются. Я буду иногда упоминать эти четыре контекста, так что… не забывайте про них.

Ну что же, начнем!

1. Устройство модулей в первом приближении У модулей Nginx'a могут быть три роли, которые мы рассмотрим:
  • обработчики обрабатывают запрос и генерируют данные ответа
  • фильтры обрабатывают данные, полученные от обработчика
  • балансировщики выбирают бэкенд, которому передать запрос, если определено несколько бэкендов

Модули делают реальную работу, которую обычно делают web-серверы: когда Nginx отправляет файл или проксирует запрос к другому серверу, то это делает модуль-обработчик. Когда Nginx гзипит данные или обрабатывает SSI-директивы, он делает это с помощью модуля-фильтра. Ядро Nginx'а берет на себя работу с сетью и реализацию протоколов, а также запускает модули, которые необходимы для обработки запроса. Децентрализованная архитектура позволяет нам создавать отдельные компоненты, которые делают что-то, что нам нужно.

Замечание: в отличие от модулей Apache, модули Nginx'а не подгружаются динамически (другими словами, модули вкомпилированы прямо в бинарник Nginx'а).

Как же тогда модули задействуются? Обычно, на стадии загрузки сервера каждый обработчик получает шанс прикрепиться к каким-либо локейшнам из конфигурационного файла. Если несколько обработчиков попробуют занять один локейшн, то победит только один (в хорошем конфиге такого не произойдет). Обработчик может завершиться с тремя результатами: все хорошо, произошла ошибка, или он может отказаться от обработки локешна в пользу обработка по умолчанию (обычно, это выдача статических файлов).

Если обработчик является реверс-прокси, то ему понадобится помощь балансировщика нагрузки. Балансировщик получает запрос вместе с набором бэкендов и принимает решение, какому серверу передать запрос. Nginx поставлется с двумя модулями балансировки: round-robin, который выбирает серверы по очереди, и модуль с методом хеширования IP адреса, который гарантирует, что запрос конкретного клиента каждый раз будет передаваться одному и тому же бэкенду.

Если обработчик не вернул ошибку, управление перейдет к фильтрам. Один локешн могут фильтровать несколько модулей, так, например, ответ может быть сжат, а потом выдаваться chunk'ами. Порядок запуска фильтров определяется на этапе компиляции. Фильры используют классический паттерн «цепочка обязанностей»: запускается один фильтр, делает свою работу, потом запускается второй, и так далее, пока не выполнится последний фильтр, и Nginx завершит обработку запроса.

Самая вкусная особенность цепочки фильтров заключается в том, что один фильтр не должен ждать, пока другой завершит свою работу целиком. Можно начать обрабатывать результат работы предыдущего фильтра по мере поступления, почти как птоки (пайпы) в юниксе. Фильры оперируют буферами, размер которых, обычно, равен размеру страницы (4 Кб), но размер всегда можно задать в nginx.conf. Это означает, например, то, что что модуль может начать сжимать ответ и отправлять его клиенту еще до того, как бэкенд полностью передат данные ответа. Чудесно!

Чтобы увидеть картину в целом, рассмотрим типичный цикл обработки запроса:
  1. клиент посылает HTTP-запрос;
  2. Nginx выбирает подходящий обработчик на основе конфига;
  3. балансировщик (если необходимо) выбирет бэкенд;
  4. обработчик делает свое дело и передает каждый буфер с данными результата первому фильтру;
  5. фильтр передает результаты второму фильтру;
  6. второй — третьему, третий — четвертому, и так далее;
  7. получившийся ответ отправляется клиенту.
Я сказал «типичный» цикл потому, что обработку в Nginx'е можно настраивать как угодно. Определить когда и как должен запускать модуль может оказаться непростой задачей для разработчика (я бы сказал очень даже не простой задачей). Настройка модуля проходит в следствии вызова ряда колбеков, и их не так уж мало. Конкретно, можно определить функцию, которая будет запущена:
  • Прямо перед чтением конфигурационного файла
  • Для каждой директивы конфигурации локешна или сервера по мере их поступления
  • Когда Nginx инициализирует главную конфигурацию
  • Когда Nginx инициализирует конфигурацию сервера (хост/порт)
  • Когда Nginx мерджит конфигурацию сервера с главной конфигурацией
  • Когда Nginx инициализирует конфигурацию локешна
  • Когда Nginx мерджит конфигурацией сервера с вложенной конфигурацией локешна
  • Когда запускается главный процесс Nginx'а
  • Когда запускается новый рабочий процесс
  • Когда рабочий процесс завершается
  • Когда главный процесс завершается
  • Для обработки запроса
  • Для фильтрации заголовка ответа
  • Для фильтрации тела ответа
  • Для выбора бэкенда
  • В момент инициализации запроса к бэкенда
  • В момент переинициализации запроса к бэкенду
  • Для обработки ответа от бэкенда
  • В момент завершения работы с бэкендом

Боже мой! Это может смутить. В вашем распоряжении большая мощь, но можно начать делать что-то полезное, используя всего несколько хуков и соответсвующих функций. Время погрузиться в модули Nginx'а.

2. Компоненты модуля Nginx

Как я уже сказал, в вашем распоряжении огромный запас гибкости для разработки модуля для Nginx'а. В этом разделе те части, которые есть практически в любом модуле. Это моможет нам лучше понять устройство модуля. А так же вы сможете оценить, когда можно будет перейти к написанию собственного модуля.

2.1. Структуры конфигурации модуля Модуль можно описать с помощью трех разных конфигурационных структур, по одной для главного контекста, контекста сервера и контекста локешна. Большинству модулей достаточно конфигурации локешна. Для структур принято такое именование: ngx_http_<название_модуля>_(main|srv|loc)_conf_t. Вот пример, взятый из модуля dav:

typedef struct {
    ngx_uint_t  methods;
    ngx_flag_t  create_full_put_path;
    ngx_uint_t  access;
} ngx_http_dav_loc_conf_t;

Заметьте, что в Nginx'е используются специальные типы данных (ngx_uint_t и ngx_flag_t). Это просто алиасы для простых типов данных, которые мы все знаем и любим (см. core/ngx_config.h, если есть сомнения).

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

2.2. Директивы модуля

Директивы описыватся в статическом массиве элементов типа ngx_command_t. Вот пример того, как их определять (взят из маленького модуля, который я написал):

static ngx_command_t  ngx_http_circle_gif_commands[] = {
    { ngx_string("circle_gif"),
      NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS,
      ngx_http_circle_gif,
      NGX_HTTP_LOC_CONF_OFFSET,
      0,
      NULL },

    { ngx_string("circle_gif_min_radius"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_num_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_circle_gif_loc_conf_t, min_radius),
      NULL },
      ...
      ngx_null_command
};
А вот определение структуры ngx_command_t (той, что мы сейчас заполняем), оно взято из файла core/ngx_conf_file.h:

struct ngx_command_t {
    ngx_str_t             name;
    ngx_uint_t            type;
    char               *(*set)(ngx_conf_t *cf, ngx_command_t *cmd, void *conf);
    ngx_uint_t            conf;
    ngx_uint_t            offset;
    void                 *post;
};
На первый взгляд слишком много, но у каждого поля свое назначение. В name хранится имя директивы, обязательно без пробелов. Используется тип ngx_str_t, значение которого чаще всего создаются с помощью макроса ngx_str("proxy_pass"). Замечание: структура ngx_str_t состоит из поля data, которое содержит саму строку, и поля len, в котором хранится длина строки. В большинстве случаев Nginx использует эту структуру взамен обычных строк. Значение type задается с помощью набора флагов, которые определяют, где можно использовать эту директиву, и сколько она принимает параметров. Значение получается с помощью бинарного или:
  • NGX_HTTP_MAIN_CONF: разрешает использовать директиву в главном контексте
  • NGX_HTTP_SRV_CONF: в контексте сервера (хоста)
  • NGX_HTTP_LOC_CONF: в контексте локешна
  • NGX_HTTP_UPS_CONF: в контексте апстрима
  • NGX_CONF_NOARGS: сообщает, что директива не принимает аргументы
  • NGX_CONF_TAKE1: принимает ровно 1 аргументы
  • NGX_CONF_TAKE2: принимает ровно 2 аргумента
  • NGX_CONF_TAKE7: принимает ровно 7 аргументов
  • NGX_CONF_FLAG: тип аргумента должен быть булев??? ("on" или "off")
  • NGX_CONF_1MORE: директиве принимает 1 или более аргументов
  • NGX_CONF_2MORE: директиве принимает 2 или более аргументов
Есть еще несколько опций, спотрите core/ngx_conf_file.h. В элементе структуры set хранится указатель на функцию, вызываемую для настройки какой-то части модуля; обычно, эта функция преводит данные из аргументов в удобный формат и сохраняет их в соответстующей структуре конфигурации модуля. Функция принимает три аргумента:
  1. указатель на структуру ngx_conf_t, которая содержит переданные директиве аргументы
  2. указатель на текущую структуру ngx_command_t
  3. указатель на собственную структуру конфигурации модуля
Функция будет вызвана тогда, когда соответствующая директива будет встречена в конфигурационном файле. Nginx предосталяет набор функций для перевода разных типов параметров в структуру для последующей обработки. Среди этих функций хочу виделить следуюющие:
  • ngx_conf_set_flag_slot: переводит "on" или "off" в 1 или 0
  • ngx_conf_set_str_slot: певодит строку параметра в ngx_str_t
  • ngx_conf_set_num_slot: парсит число и возвращает его как int
  • ngx_conf_set_size_slot: парсит размер ("8k", "1m" и т.д.) и возвращает его как size_t
Есть еще несколько функций, и они тоже очень удобны (посотрите core/ngx_conf_file.h). Модули могут описывать здесь и свои функции, если по каким-то причинам не достаточно встроенных. Откуда эти функции узнают, куда сохранять данные? В этом им помогают следующие два свойстваструктуры ngx_command_t: conf и offset. conf указывае Nginx'у куда сохранить данные: в главную, серверную или конфигурационную структуру локешна (задается с помощью NGX_HTTP_MAIN_CONF_OFFSET, NGX_HTTP_SRV_CONF_OFFSET, or NGX_HTTP_LOC_CONF_OFFSET). offset указывает в какую часть структуры записать значение. И,наконец, post это еще одна штукенция, которая может пригодится модулю на этапе конфигурации. Чаще всего равна NULL. Набор директив заканчивается ngx_null_command.

2.3. Контекст модуля

Это статическая структура типа ngx_http_module_t, в которой определяются несколько указателей на функции для создание трех конфигураций и сливания их вместе. Называют ее ngx_http_<имя_модуля>_module_ctx. Вот назначение этих функций по порядку:
  • перед конфигурацией
  • после конфигурации
  • создание главной конфигурации (то есть выделение памяти и задание значений по умолчанию)
  • инициализация главной конфигурации (переопределение данных на взятые из nginx.conf)
  • создание конфигурации сервера
  • сливание ее с главной конфигурацие
  • создание конфигурации локешна
  • сливание ее с конфигурацие сервера
Функции принимают разные аргументы в зависимости от своего назначения. Вот описание этой структуры, взятое из http/ngx_http_config.h, в котором видны разные сигнатуры функций-колбеков:

typedef struct {
    ngx_int_t   (*preconfiguration)(ngx_conf_t *cf);
    ngx_int_t   (*postconfiguration)(ngx_conf_t *cf);

    void       *(*create_main_conf)(ngx_conf_t *cf);
    char       *(*init_main_conf)(ngx_conf_t *cf, void *conf);

    void       *(*create_srv_conf)(ngx_conf_t *cf);
    char       *(*merge_srv_conf)(ngx_conf_t *cf, void *prev, void *conf);

    void       *(*create_loc_conf)(ngx_conf_t *cf);
    char       *(*merge_loc_conf)(ngx_conf_t *cf, void *prev, void *conf);
} ngx_http_module_t;
Указатели на те функции, которые вам не пригодятся, можете заполнить NULL, и Nginx сделает вид, что не заметил их. Большинство обработчиков используют только две последние: чтобы выделить память для структуры конфигурации (называется ngx_http_<имя_модуля>_create_loc_conf), и слить ее с конфигурацией выше (называется ngx_http_<имя_модуля>_merge_loc_conf). Функция, сливающая вместе конфиги, также может вернуть ошибку, что остановит загрузку сервера. Вот пример пример структуры контекста модуля:

static ngx_http_module_t  ngx_http_circle_gif_module_ctx = {
    NULL,                          /* перед конфигурацией */
    NULL,                          /* после конфигурации */

    NULL,                          /* создание главной конфигурации */
    NULL,                          /* инициализация главной конфигурации */

    NULL,                          /* создание конфигурации сервера */
    NULL,                          /* сливание ее с главной конфигурацие */

    ngx_http_circle_gif_create_loc_conf,  /* создание конфигурации локешна */
    ngx_http_circle_gif_merge_loc_conf /* сливание ее с конфигурацие сервера */
};
Пришло время разобраться со всем этим подробнее. Эти конфигурационные колбеки очень похожи во всех модулях и используют одну часть Nginx API, так что их надо хорошо знать.

2.3.1. create_loc_conf

Вот так выглядит минимальная реализация функции create_loc_conf, взятая из моего модуля circle_gif (за подробностями прошу в исходник). Она получает структуру (ngx_conf_t) и возвращает вновь созданную структуру конфигурации модуля (в этом примере ngx_http_circle_gif_loc_conf_t).
static void *
ngx_http_circle_gif_create_loc_conf(ngx_conf_t *cf)
{
    ngx_http_circle_gif_loc_conf_t  *conf;

    conf = ngx_pcalloc(cf->pool, sizeof(ngx_http_circle_gif_loc_conf_t));
    if (conf == NULL) {
        return NGX_CONF_ERROR;
    }
    conf->min_radius = NGX_CONF_UNSET_UINT;
    conf->max_radius = NGX_CONF_UNSET_UINT;
    return conf;
}
Прошу заметить важную особенность управления памятью в Nginx'е: он сам позаботится о вызове free только тогда, когда для выделения памяти вы используете ngx_palloc (умную обертку для malloc) или ngx_pcalloc (умную обертку для calloc). Возможными вариантами задания UNSET являются: NGX_CONF_UNSET_UINT, NGX_CONF_UNSET_PTR, NGX_CONF_UNSET_SIZE, NGX_CONF_UNSET_MSEC, и для всех случаев NGX_CONF_UNSET. UNSET показывает функции сливания конфигов, что это значение надо переопределить.

2.3.2. merge_loc_conf

Вот так выглядит функция сливания конфигов в модуле circle_gif:
static char *
ngx_http_circle_gif_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_circle_gif_loc_conf_t *prev = parent;
    ngx_http_circle_gif_loc_conf_t *conf = child;

    ngx_conf_merge_uint_value(conf->min_radius, prev->min_radius, 10);
    ngx_conf_merge_uint_value(conf->max_radius, prev->max_radius, 20);

    if (conf->min_radius < 1) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
            "min_radius должен быть больше или равен 1");
        return NGX_CONF_ERROR;
    }
    if (conf->max_radius < conf->min_radius) {
        ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
            "max_radius должен быть больше или равен min_radius");
        return NGX_CONF_ERROR;
    }

    return NGX_CONF_OK;
}
Приятной особенностью Nginx'а является набор функций для сливания разных типов данных (ngx_conf_merge_<тип_данных>_value); их аргументами являются
  1. значение текущего локешна
  2. значение, задаваемое, если #1 не установлено
  3. задаваемое по умолчанию, если не установлено ни #1, ни #2
Результат будет записан в первый аргумент. Примерами таких функций являются ngx_conf_merge_size_value, ngx_conf_merge_msec_value и другие. Обратитесь к core/ngx_conf_file.h за полным списком.
А вот вопрос: как же этим функциям удается записать данные в первый аргумент, если он передается по значению? Ответ прост: это макросы (они разворачиваются в несколько конструкций if).
Интересно так же то, как эти функции обрабатывают ошибки. В случае неудачи в лог выводится сообщение, и функция возвращает NGX_CONF_ERROR. В этом случает запуск сервера прекращается. Так как сообщение выводится с уровнем NGX_LOG_EMERG, оно будет продублировано в поток ошибок. Кстати, core/ngx_log.h содержит полный список уровнй вывода.)

2.4. Описание модуля

Теперь добавим еще один уровель абстракции, структуру ngx_module_t. Переменную назовем ngx_http_<имя_модуля>_module. В ней описываются указатели на контекст и директивы модуля вместе с остальными колбеками(завершение треда, завершение процесса и т.д.). Определение модуля иногда используется, чтобы найти какие-либо данные, связанные с этим модулем. Определение модуля часто выглядит так:

ngx_module_t  ngx_http_<имя_модуля>_module = {
    NGX_MODULE_V1,
    &ngx_http_<имя_модуля>_module_ctx, /* контекст модуля */
    ngx_http_<module name>_commands,   /* директивы модуля */
    NGX_HTTP_MODULE,               /* тип модуля */
    NULL,                          /* инициализация мастера */
    NULL,                          /* инициализация модуля */
    NULL,                          /* инициализация процесса */
    NULL,                          /* инициализация треда */
    NULL,                          /* завершение треда */
    NULL,                          /* завершение процесса */
    NULL,                          /* завершение мастера */
    NGX_MODULE_V1_PADDING
};
…замените <имя_модуля> на что-нибудь полезное. Модули погут определять колбеки для моментов создания и уничтожения процессов и тредов, но большинство модулей стараются не усложнять себе жизнь. Чтобы посмотреть список аргументов, обратитесь к core/ngx_conf_file.h.)

2.5. Установка модуля

Модули устанавливаются двумя способами: обработчики чаще всего устанавливаются колбеком директивы, а фильтры устанавливаются в постконфигурационном колбеке в структуре контекста модуля. Наконец, мы собираемся указать Nginx'у, где искать наш код. Балансировщики в этом вопросе особенные, их мы рассмотрим позже в разделе Устройство балансировщиков нагрузки.

2.5.1. Установка обработчика

Обработчики устанавливаются с помощью кода внутри колбеков, вызываемых директивами, которые относятся к модулю. Например, моя структура ngx_command_t из модуля circle_gif выглядит примерно так:

    { ngx_string("circle_gif"),
      NGX_HTTP_LOC_CONF|NGX_CONF_NOARGS,
      ngx_http_circle_gif,
      0,
      0,
      NULL }
Третьим аргументом как раз является колбек, в примере это ngx_http_circle_gif. Вспомним, что аргументами этого колбека являются: структура директивы (ngx_conf_t, в кторой сохранены параметры из конфигурационного файла), соответствующая структура ngx_command_t и указатель на структуру конфигурации модуля. В моем модуле circle_gif эта функция выглядит так:

static char *
ngx_http_circle_gif(ngx_conf_t *cf, ngx_command_t *cmd, void *conf)
{
    ngx_http_core_loc_conf_t  *clcf;

    clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module);
    clcf->handler = ngx_http_circle_gif_handler;

    return NGX_CONF_OK;
}
Она выполняет работу в два этапа. Во-первых, получает внутреннюю структуру описывающую этот локешн. Во-вторых, устанавливает в ней обработчик (тоже колбек). Просто, не правда ли?

2.5.2. Установка фильтра

Фльтры устанавливаются на этапе постконфигурации. Бывает два типа фильтров: фильтры заголовков которые обрабатывают HTTP-заголовки, и фильтры тела ответа, которые обрабатывают собственно данные. Мы устанавливаем оба за один раз. В качестве простого примера посмотрим на chunked-фильтр, его контекст выглядит так:

static ngx_http_module_t  ngx_http_chunked_filter_module_ctx = {
    NULL,                                  /* preconfiguration */
    ngx_http_chunked_filter_init,          /* postconfiguration */
  ...
};
Вот что происходит в ngx_http_chunked_filter_init:
static ngx_int_t
ngx_http_chunked_filter_init(ngx_conf_t *cf)
{
    ngx_http_next_header_filter = ngx_http_top_header_filter;
    ngx_http_top_header_filter = ngx_http_chunked_header_filter;

    ngx_http_next_body_filter = ngx_http_top_body_filter;
    ngx_http_top_body_filter = ngx_http_chunked_body_filter;

    return NGX_OK;
}
Что это значит? Если вы помните, фильтры работают по принципу цепочки обязанностей. Когда обработчик сгенерирует ответ, он вызывает две функции: ngx_http_output_filter, которая вызывает глобальную ngx_http_top_body_filter; и ngx_http_send_header, вызывает другую глобальную ngx_top_header_filter. Функции ngx_http_top_body_filter и ngx_http_top_header_filter являются гловными в цепочках фильтров соответственно заголовка и тела ответа. Каждое звено в цепи содержит ссылку на следующее звено (ссылки называются ngx_http_next_body_filter и ngx_http_next_header_filter). Когда фильтр закончит выполнение, он просто вызывает следующий, пока не будет вызван специальный фильтр, который уже заворачивает данные в HTTP-ответ. Все что делает функция filter_init, это добавляет свой модуль в обе эти цепи. Эта функция сохраняет ссылку на бывший первым фильтр в своей собственной переменной и определяет свои колбеки, как первые в цепочках. Цепочка действует по принципу LIFO (Last In Frist Out): последним добавлен — первым обработан.
Заметка на полях: а как точно это работает? Каждый фильтр возвращает либо ошибку, либо результат специального выражения: return ngx_http_next_body_filter(); Таким обазом, если очередь в цепочке фильтров дошла до последнего («специального») фильтра, просто возвращается "OK", но если проихошла ошибка, оставшаяся цепочка пропускается и Nginx выводит соответствующее сообщение об ошибке. Это простой однонаправленный список с бастрой обработкой ошибок, выполненный в виде указателей на функции. Превосходно.

3. Обработчики, фильтры и балансировщики нагрузки

Теперь рассмотрим пару простейших модулей под микроскопом и разберемся как они работает

3.1. Устройство обработчиков (не проксирующих)

Обработчики, обычно, выполняют четыре шага: получают конфигурацию локешна, генерируют соответствующий ответ, отправляют заголовки и отправляют тело ответа. Хентдлер получает один аргумент: структуру запроса. В структуре запроса хранится много молезной информации о запросе клиента. Такой как, метод запроса, URI и загловки. Мы рассмотрим эти четыре шага один за другим.

3.1.1. Получение конфигурации локейшна

Это простая часть. Все, что надо сделать, это вызвать ngx_http_get_module_loc_conf и передать параметрами текущий запрос и описание модуля. Вот соответствующая часть хендлера из моего модуля circle_gif:

static ngx_int_t
ngx_http_circle_gif_handler(ngx_http_request_t *r)
{
    ngx_http_circle_gif_loc_conf_t  *circle_gif_config;
    circle_gif_config = ngx_http_get_module_loc_conf(r, ngx_http_circle_gif_module);
    ...
Вот так я получил доступ ко всем переменным, сохраненным на этапе сливания конфигов.

3.1.2. Генерация ответа

Это та самая интересная часть, где модули делают полезную работу. Нам поможет стуктура запроса, а именно эти ее свойства:

typedef struct {
...
/* пул памяти, используемый в функциях типа ngx_palloc */
    ngx_pool_t                       *pool;
    ngx_str_t                         uri;
    ngx_str_t                         args;
    ngx_http_headers_in_t             headers_in;

...
} ngx_http_request_t;
uri это путь запроса, например "/query.cgi". args содержит нераспарсенные параметры запроса (идущая за знаком вопроса часть), например "name=john". headers_in хранит много полезной информации, такой как куки и информация о браузере, но большинству модулей эти данные не пригождаюися. Посмотрите http/ngx_http_request.h, если интересно. Этого вполне достаточно, чтобы смочь составить какой-нибудь полезный ответ. Полное описание структуры ngx_http_request_t можно найти в http/ngx_http_request.h.

3.1.3. Отправка заголовков

Заголовки ответа живут в структуре называемой headers_out. Она в свою очередь хранится в структуре запроса. Обработчик хапроса выставляет те заголовки, которые ему надо и вызывает ngx_http_send_header(r). Вот некоторые из самых полезный элементов headers_out:
typedef stuct {
...
    ngx_uint_t                        status;
    size_t                            content_type_len;
    ngx_str_t                         content_type;
    ngx_table_elt_t                  *content_encoding;
    off_t                             content_length_n;
    time_t                            date_time;
    time_t                            last_modified_time;
..
} ngx_http_headers_out_t;
Остальное можно найти в http/ngx_http_request.h. Так, например, если модуль должен выставить Content-Type в "image/gif", Content-Length в 100 и вернуть код ответа 200 OK, то следующий код поможет ему сделать это:

    r->headers_out.status = NGX_HTTP_OK;
    r->headers_out.content_length_n = 100;
    r->headers_out.content_type.len = sizeof("image/gif") - 1;
    r->headers_out.content_type.data = (u_char *) "image/gif";
    ngx_http_send_header(r);
Большинство стандартных загловоков HTTP доступны (где-либо) для изменения вами. Однако, некоторые хаголовки задать немного сложнее чем те, которые вы видели выше. На пример, content_encoding имеет тип (ngx_table_elt_t*), поэтому модуль должен сам выделить память для этого заголовка. Это можно сделать с помощью функции ngx_list_push, которая принимает ngx_list_t (похож на массив) и возвращает указатель на вновь созданный элемен в этом списке (типа ngx_table_elt_t). Код ниже устанавливает заголовок Content-Encoding в значение "deflate" и отправляет заголовки:

    r->headers_out.content_encoding = ngx_list_push(&r->headers_out.headers);
    if (r->headers_out.content_encoding == NULL) {
        return NGX_ERROR;
    }
    r->headers_out.content_encoding->hash = 1;
    r->headers_out.content_encoding->key.len = sizeof("Content-Encoding") - 1;
    r->headers_out.content_encoding->key.data = (u_char *) "Content-Encoding";
    r->headers_out.content_encoding->value.len = sizeof("deflate") - 1;
    r->headers_out.content_encoding->value.data = (u_char *) "deflate";
    ngx_http_send_header(r);
Этот механизм, обычно, используется тогда, когда заголовок может иметь более одного значения одновременно. Этот прием (теоретически) позволяет фильтрам легче добавлять или удалять соответствующие значения, не изменяя другие, так как им не приходится заниматься работой со строками.

3.1.4. Отправка тела ответа

Теперь, когда модуль сгенерировал ответ и записал его в память, ему необходимо присвоить овет специальному буферу и затем, передать буфер в специальное звено чепочки, а потом вызвать «отправку ответа» на этом звене. Зачем нужна звенья и цепочка? Nginx позволяет обработчикам генерировать (а фильтрам обрабатывать) ответ по одному буферу за раз. Каждое звено цепи хранит ссылку на следующие звен или NULL если оно последнее. Чтобы не усложнять пример, предстваим, что у нас есть только один буфер (и одно звено цепи). Сначала модуль должен объявить буфер и звено цепи.

    ngx_buf_t    *b;
    ngx_chain_t   out;
Следующим шагом надо выделить память для буфера и добавить его в данные ответа:

    b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t));
    if (b == NULL) {
        ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
            "Не удалось выделить буфер ответа.");
        return NGX_HTTP_INTERNAL_SERVER_ERROR;
    }

    b->pos = some_bytes; /* позиция первого байта в блоке данных */
    b->last = some_bytes + some_bytes_length; /* последняя позиция */

    b->memory = 1; /* данные храняться в памяти только для чтения */
    /* (то есть фильтры должны скопировать эти данные перед обработкой, вместо того, чтобы изменять их) */

    b->last_buf = 1; /* буферов в запросе больше не будет */
А здесь модуль присваивает буфер звену цепи:

    out.buf = b;
    out.next = NULL;
И наконец, мы отправляем ответ и возвращаем статут вызова отправки за один раз:

    return ngx_http_output_filter(r, &out);
Цепочки буферов — это критически важная часть модели ввода/вывода в Nginx'е, так что вы должны ими хорошо овладеть.
Очевидный вопрос: зачем буферу флаг last_buf, если мы можем определить, что он последний проверив "next" на NULL? Ответ: цепь может быть незавершенной, то есть состоять из множества буферов, не не все буферы уже подготовлены в запросе или ответе. Таким образом, некоторые буферы будут в конце цепи, но не в конце запроса. И это приводит нас к…

3.2. Устройство апстримов (они же прокси)

Продолжение следует.
Теги:
  • общее
Очень жду ваших комментариев на почту или на гитхаб.