Разработка модулей для nginx

Конференция HighLoad++ 2008

Валерий Холодков

Введение

Материал, который Вы читаете в данный момент, основан на моих собственных исследованиях исходного кода и материалах списка рассылки nginx. Поскольку я не являюсь автором nginx и участвовал в обсуждении далеко не всех деталей реализации, приведенная информация может быть не верна на 100%.

Вы предупреждены!

Материал организован следующим образом: в первой главе описываются общие принципы работы асинхронных серверов и объясняется почему разработка модулей для nginx требует особого подхода, во второй главе описывается API, который предоставляет nginx модулям, в третьей главе описываются особенности взаимодействия nginx и модулей, в четвертой и последней главе описываются подходы к реализации модулей.

1. Асинхронные серверы

Главной особенностью реализации nginx, является то, что все сетевые операции ввода-вывода выполняются асинхронно относительно выполнения рабочих процессов. Это дает следующие преимущества:

Однако обладает следующими недостатками: Рабочие процессы nginx выполняют цикл в начале которого с помощью специального системного вызова все сокеты опрашиваются на предмет появления событий. Событием может быть: В завершении цикла выполняется обработка событий. Для всех запросов на установку соединения выполняется прием нового соединения. Для всех сокетов, имеющих данные во входном буфере выполняется прием и обработка этих данных. Для всех сокетов, имеющих свободное пространство в выходном буфере выполняется отправка новых данных. В каждом из этих случаев используются неблокирующие операции.

Когда обрабатывается один сокет одним из рабочих процессов, все остальные сокеты этого рабочего процесса продолжают ожидать обработки. Если обработка одного сокета затягивается, то остальные сокеты начинают испытывать "голод": приходящие данные скапливаются во входных буферах сокетов, а готовые к записи сокеты не получают новых данных. На клиентской стороне подобная ситуация выглядит как "зависание". Для предотвращения голодания сокетов сервер и компоненты сервера должны быть реализованы с использованием следующих принципов:

В UNIX дисковые операции ввода-вывода всегда блокирующие (за исключением POSIX realtime API). Казалось бы, они должны противоречить последнему принципу. Однако, за счет кэширования и упреждающего чтения блокирование удается свести к минимуму.

Из-за описанных выше ограничений полномасштабные веб-приложения сложно реализовать исключительно в модулях nginx.

2. API nginx

2.1. Управление памятью

Управление памятью в nginx осуществляется с помощью пулов. Пул -- это последовательность предварительно выделенных блоков динамической памяти. Пул привязан к объекту, который определяет время жизни всех объектов, выделенных в пуле. Таким объектом может быть, например, запрос или цикл обработки событий. Пулы используются исходя из следующих соображений:

Для выделения памяти используются следующие функции:

void *ngx_palloc(ngx_pool_t *pool, size_t size);
void *ngx_pcalloc(ngx_pool_t *pool, size_t size);

pool -- пул, из которого будет выделена память;
size -- размер выделяемой памяти в байтах;
результат -- указатель на выделенную память, либо NULL, если не удалось выделить.

Функция ngx_pcalloc в дополнение заполняет выделенную память нулями.

Для маловероятного случая освобождения памяти используется функция ngx_pfree:

ngx_int_t ngx_pfree(ngx_pool_t *pool, void *p);
pool -- пул, в который будет возвращена память;
size -- размер выделяемой структуры;
результат -- NGX_OK, если удалось освободить, NGX_DECLINED, если невозможно освободить.

Для регистрации деструктора (например для закрытия файловых дескрипторов или удаления файлов) используется следующие структура и функции:

typedef struct ngx_pool_cleanup_s  ngx_pool_cleanup_t;

struct ngx_pool_cleanup_s {
    ngx_pool_cleanup_pt   handler;
    void                 *data;
    ngx_pool_cleanup_t   *next;
};

ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *p, size_t size);

p -- пул, в котором будет зарегистрирован деструктор;
size -- размер выделяемой структуры-контекста, которая будет передана деструктору;
результат -- указатель на деструктор, если удалось выделить, NULL, если не удалось выделить.

После выполнения ngx_pool_cleanup_add поле data указывает на контекст длиной size, который переходит в распоряжение пользователя.

Поля в структуре ngx_pool_cleanup_t имеют следующее значение:
handler -- хэндлер, который будет вызван при удалении пула;
data -- указатель на структуру-контекст, которая будет передана деструктору;
next -- указатель на следующий деструктор в пуле.

Пример использования:

static void ngx_sample_cleanup_handler(void *data);

static ngx_int_t ngx_http_sample_module_handler(ngx_http_request_t *r)
{
    ngx_pool_cleanup_t    *cln;
    ngx_sample_cleanup_t  *scln;

    cln = ngx_pool_cleanup_add(r->pool, sizeof(ngx_sample_cleanup_t));

    if(cln == NULL)
        return NGX_ERROR;

    cln->handler = ngx_sample_cleanup_handler;

    scln = cln->data;

    [... инициализация scln ...]
}

static void ngx_sample_cleanup_handler(void *data)
{
    ngx_sample_cleanup_t        *scln = data;

    [... использование scln ...]
}

2.2. Векторы

Векторы в nginx описываются следующей структурой:

struct ngx_array_s {
    void        *elts;
    ngx_uint_t   nelts;
    size_t       size;
    ngx_uint_t   nalloc;
    ngx_pool_t  *pool;
};

typedef struct ngx_array_s ngx_array_t;

pool -- пул, в котором будет распределена память под элементы;
elts -- указатель на элементы;
nelts -- число элементов в векторе в данный момент;
size -- размер элемента вектора в байтах;
nalloc -- число элементов, для которых распределена память в данный момент.

Для создания вектора используется функция ngx_array_create:

ngx_array_t *ngx_array_create(ngx_pool_t *p, ngx_uint_t n, size_t size);

p -- пул, в котором будет распределена память под вектор и его элементы;
n -- число элементов, под которые будет зарезервирована память;
size -- размер элемента вектора в байтах;
результат -- указатель на вектор, если удалось выделить, NULL, если не удалось выделить.

Пример создания вектора:

typedef struct {
    [...]
} ngx_sample_struct_t;

{
    ngx_array_t *v;

    v = ngx_array_create(pool, 10, sizeof(ngx_sample_struct_t));

    if (v == NULL) {
        return NGX_ERROR;
    }
}

Если память под структуру ngx_array_t предварительно выделена, то для её инициализации используется функция ngx_array_init:

ngx_int_t ngx_array_init(ngx_array_t *array, ngx_pool_t *pool, ngx_uint_t n, size_t size);

array -- указатель на структуру ngx_array_t;
p -- пул, в котором будет распределена память под элементы вектора;
n -- число элементов, под которые будет зарезервирована память;
size -- размер элемента вектора в байтах;
результат -- NGX_OK, если удалось выделить, NGX_ERROR, если не удалось выделить.

Пример инициализации вектора:

typedef struct {
    ngx_array_t v;
    [...]
} ngx_sample_struct_t;

{
    ngx_sample_struct_t t;

    if(ngx_array_init(&t.v, pool, 10, sizeof(ngx_sample_struct_t)) != NGX_OK) {
        return NGX_ERROR;
    }
}

Для добавления элементов в конец вектора используются функции ngx_array_push и ngx_array_push_n:

void *ngx_array_push(ngx_array_t *a);
void *ngx_array_push_n(ngx_array_t *a, ngx_uint_t n);

a -- вектор, к которому добавляется элемент(ы);
n -- число элементов, которые будут выделены;
результат -- указатель на элемент, если удалось выделить, NULL, если не удалось выделить.

Функция ngx_array_push_n добавляет n элементов к вектору.

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

Пример:

typedef struct {
    [...]
} ngx_sample_struct_t;

{
    ngx_array_t *v;
    
    [...]

    h = ngx_array_push(v);
    if (h == NULL) {
        return NGX_ERROR;
    }
    
    [... использование h...]
}

Для удаления вектора используется функция ngx_array_destroy. Удаление вектора выполняется только в том случае, если память, распределенная под его элементы, располагалась в конце пула.

void ngx_array_destroy(ngx_array_t *a);

a -- вектор, который будет удален;

2.3. Списки

См. оригинал главы

Общие сведения

В nginx (для 0.6.29 вполне актуально) есть связанные списки. Каждый список представляет собой структуру:

typedef struct {
    ngx_list_part_t  *last;
    ngx_list_part_t   part;
    size_t            size;
    ngx_uint_t        nalloc;
    ngx_pool_t       *pool;
} ngx_list_t;

Здесь:

Список состоит из частей (часть — выделенный блок памяти):

typedef struct ngx_list_part_s  ngx_list_part_t;

struct ngx_list_part_s {
    void             *elts;
    ngx_uint_t        nelts;
    ngx_list_part_t  *next;
};

Здесь:

Т.е. список — это несколько частей (минимум 1), в которых располагаются элементы. Каждая часть имеет свою длину и указатель на следующую часть или NULL, если в ней еще есть место. Размер всех частей равен.

Работа со списком

Создание

Для создания списка используется функция:

ngx_list_t *ngx_list_create(ngx_pool_t *pool, ngx_uint_t n, size_t size);

size — размер одного элемента, n — количество элементов в части, pool — пул памяти, в котором выделяется память.

Функция создает новый список и инициализирует его (создает первую часть и определяет размер части). Возвращаемое значение — указатель на список или NULL в случае ошибки.

Инициализация

Список можно не создавать, а просто инициализировать (если у нас он определен на стеке), используя функцию:

static ngx_inline ngx_int_t ngx_list_init(ngx_list_t *list, ngx_pool_t *pool, ngx_uint_t n, size_t size)

Параметры ее аналогичны ngx_list_create, за исключением первого — это указатель на список, который будет инициализироваться (или переинициализироваться). Функция возвращает либо NGX_OK в случае успеха, либо NGX_ERROR в случае проблем. Функция небезопасна, т.е. можно переинициализировать любой существующий список и все старые данные потеряются ☺

Добавление элемента

Для добавления элемента в список используется функция:

void *ngx_list_push(ngx_list_t *list);

Она принимает указатель на список и возвращает указатель на место в части, в которое можно записывать значения. Если часть закончилась, создается новая и возвращается указатель на ее первый элемент.

Навигация по элементам списка

Для навигации по списку можно использовать такой код:

part = &list.part;
data = part->elts;

for (i = 0 ;;i++) {
  if (i >= part->nelts) {
    if (part->next == NULL) {
      break;
    }

    part = part->next;
    data = part->elts;
    i = 0;
  }

  /* use element of list as data[i] */

}

Вместо комментария вставляется ваш код, в котором будет происходить обращение к элементам списка.

Пример использования списка

Пример функции, в которой создается список из двух частей и осуществляется навигация по нему:

void ngx_list_sample_using(ngx_http_request_t *r)
{
  ngx_list_t       list;
  ngx_uint_t       i;
  ngx_list_part_t *part;
  ngx_uint_t      *list_element;
  ngx_uint_t      *sum = 0;

  if (ngx_list_init(&list, r->pool, 5, sizeof(ngx_uint_t)) == NGX_ERROR) {
    return;
  }

  for (i = 0; i < 10; i++) {
    list_element = ngx_list_push(&list);
    if (list_element == NULL) {
      return;
    }
    *list_element = i; 
  }

  part = &list.part;
  data = part->elts;

  for (i = 0 ;;i++) {
    if (i >= part->nelts) {
      if (part->next == NULL) {
        break;
      }

      part = part->next;
      data = part->elts;
      i = 0;
    }

  sum += data[i];

  }
/* here sum is 45 */
}

2.4. Управление буферами и очередями

2.4.1 Управление буферами

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

typedef struct ngx_buf_s ngx_buf_t;

struct ngx_buf_s {
    [...]

    u_char          *pos; /* начало окна */
    u_char          *last; /* конец окна */
    u_char          *start; /* начало буфера */
    u_char          *end; /* конец буфера */

    unsigned         temporary:1; /* = 1 */

    [...]
};

Для неизменяемой памяти:

struct ngx_buf_s {
    [...]

    u_char          *pos; /* начало окна */
    u_char          *last; /* конец окна */
    u_char          *start; /* начало буфера */
    u_char          *end; /* конец буфера */

    unsigned         memory:1; /* = 1 */

    [...]
};

Для файла:

struct ngx_buf_s {
    [...]

    off_t            file_pos; /* начало окна */
    off_t            file_last /* конец окна */
    ngx_file_t      *file; /* указатель на файл */

    unsigned         in_file:1; /* = 1 */

    [...]
};

Окно определяет часть буфера, которую осталось отправить, осталось обработать, либо заполнена полученными данными. В процессе заполнения указатель last перемещается в направлении end, в процессе обработки или отправления указатель pos перемещается в направлении last (или file_pos в направлении file_last). В случае, если все данные буфера отправлены или обработаны, то pos == last или file_pos == file_last. В случае, если буфер не заполнен, то pos == last == start.

Кроме того, буфер содержит флаги, описывающие как необходимо обрабатывать данные, содержащиеся в буфере:

struct ngx_buf_s {
    [...]

    unsigned         recycled:1; /* буфер повторно использован после освобождения */
    unsigned         flush:1; /* все буферизированные данные должны быть обработаны и переданы на следующий уровень после обработки этого буфера */
    unsigned         last_buf:1; /* указывает на то, что буфер является последним в потоке данных */
    unsigned         last_in_chain:1; /* указывает на то, что буфер является последним в данной цепи (очереди) */
    unsigned         temp_file:1; /* указывает на то, что буфер располагается во временном файле */

    [...]
};

В случае, если буфер константный, в оперативной памяти может находиться произвольное число структур ngx_buf_t, указывающих на идентичные данные, располагающиеся, например, в сегменте константных данных или в конфигурации модуля (см. 2.9). Окно определяет прогресс отправления этих данных.

Для выделения памяти под структуру ngx_buf_t используются макросы:

ngx_alloc_buf(pool);
ngx_calloc_buf(pool);

pool -- пул, из которого будет выделен буфер;
rvalue -- указатель на структуру
ngx_buf_t, если удалось выделить, NULL, если не удалось выделить. После выделения все поля структуры необходимо инициализировать. Макрос ngx_calloc_buf преобразуется в функцию, которая в дополнение зполняет выделенную память нулями.

Для выделения памяти под временный буфер используется следующая функция:

ngx_buf_t *ngx_create_temp_buf(ngx_pool_t *pool, size_t size);

pool -- пул, из которого будет выделен буфер;
size -- размер буфера (расстояние между start и end);
результат -- указатель на структуру
ngx_buf_t, если удалось выделить, NULL, если не удалось выделить. После выделения pos и last будут равны start и флаг temporary будет установлен в 1.

2.4.2 Управление очередями

Очереди (или цепи) связывают несколько буферов в последовательность, которая определяет порядок приема, отправления или обработки данных.

struct ngx_chain_s {
    ngx_buf_t    *buf; /* буфер, связанный с текущим звеном */
    ngx_chain_t  *next; /* следующее звено */
};

typedef struct ngx_chain_s ngx_chain_t;

Для выделения памяти под цепи используются следующие функции:

typedef struct {
    ngx_int_t    num;
    size_t       size;
} ngx_bufs_t;

ngx_chain_t *ngx_alloc_chain_link(ngx_pool_t *pool);
ngx_chain_t *ngx_create_chain_of_bufs(ngx_pool_t *pool, ngx_bufs_t *bufs);
ngx_chain_t *ngx_chain_get_free_buf(ngx_pool_t *p, ngx_chain_t **free);

функция ngx_alloc_chain_link выделяет память под одно звено из пула;

функция ngx_create_chain_of_bufs выделяет память под цепь звеньев и буферы;

функция ngx_chain_get_free_buf выделяет звено из цепи cвободных буферов или из пула, если цепь пуста.

pool -- пул, из которого будет(будут) выделен(ы) звенья/буферы при необходимости;
bufs -- структура, описывающая размер и число буферов;
free -- указатель на цепочку свободных буферов;
результат -- указатель на структуру ngx_chain_t, если удалось выделить, NULL, если не удалось выделить.

Для освобождения звеньев используется следующий макрос:

ngx_free_chain(pool, cl)

pool -- пул, в который возвращается звено, cl -- возвращаемое звено.

Управление очередями:

ngx_int_t ngx_chain_add_copy(ngx_pool_t *pool, ngx_chain_t **chain,
    ngx_chain_t *in);
void ngx_chain_update_chains(ngx_chain_t **free, ngx_chain_t **busy,
    ngx_chain_t **out, ngx_buf_tag_t tag);
функция ngx_chain_add_copy добавляет буферы из цепи in к цепи chain, выделяя новые звенья. Возвращает NGX_OK, если удалось успешно добавить, NGX_ERROR, если не удалось выделить память;

функция ngx_chain_update_chains добавляет все обработанные или отправленные буферы с тегом tag из цепи out и busy к цепи free, оставшиеся добавляет к цепи busy.

pool -- пул, из которого будут выделены звенья при необходимости,

2.5. Строки

В nginx строки хранятся в Pascal-like форме с целью избежать накладных расходов при вычислении длины, а так же копирования в некоторых ситуациях.

typedef struct {
    size_t      len;
    u_char     *data;
} ngx_str_t;
len -- длина строки в байтах, data -- указатель на память, содержащую строку.

2.6. Переменные

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

typedef void (*ngx_http_set_variable_pt) (ngx_http_request_t *r,
    ngx_http_variable_value_t *v, uintptr_t data);
typedef ngx_int_t (*ngx_http_get_variable_pt) (ngx_http_request_t *r,
    ngx_http_variable_value_t *v, uintptr_t data);
ngx_http_set_variable_pt -- тип функций, вызываемых для установки значения переменной; ngx_http_get_variable_pt -- тип функций, вызываемых для получения значения переменной.

Аргументы функций:
r -- запрос, в контексте которого устанавливает или запрашивается значение переменной,
v -- устанавливаемое или получаемое значение переменной,
data -- контекст переменной.

Сама переменная описывается следующей структурой:
struct ngx_http_variable_s {
    ngx_str_t                     name;
    ngx_http_set_variable_pt      set_handler;
    ngx_http_get_variable_pt      get_handler;
    uintptr_t                     data;
    ngx_uint_t                    flags;

    [...]
};
name -- имя переменной,
set_handler -- функция установки значения,
get_handler -- функция получения значения,
data -- контекст переменной,
flags -- флаги:

Добавление переменной

Для добавления новой переменной используется следующая функция:

ngx_http_variable_t *ngx_http_add_variable(ngx_conf_t *cf, ngx_str_t *name,
    ngx_uint_t flags);
cf -- конфигурация, в которой создается переменная,
name -- имя переменной,
flags -- флаги,
результат -- указатель на структуру ngx_http_variable_t.

Пример вызова:
static ngx_str_t  ngx_http_var_name = ngx_string("var");

[...]

{
    ngx_http_variable_t  *var;

    var = ngx_http_add_variable(cf, &ngx_http_var_name, NGX_HTTP_VAR_NOCACHEABLE);
}

Получения индекса переменной

ngx_int_t ngx_http_get_variable_index(ngx_conf_t *cf, ngx_str_t *name);
cf -- конфигурация, в которой определена переменная,
name -- имя переменной,
результат -- индекс переменной.

Получения строкового значения переменной

typedef struct {
    unsigned    len:28;

    unsigned    valid:1;
    unsigned    no_cacheable:1;
    unsigned    not_found:1;

    [...]

    u_char     *data;
} ngx_variable_value_t;

typedef ngx_variable_value_t  ngx_http_variable_value_t;

ngx_http_variable_value_t *ngx_http_get_indexed_variable(ngx_http_request_t *r,
    ngx_uint_t index);
r -- запрос, в контексте которого запрашивается переменная,
index -- индекс переменной,
результат -- значение переменной в виде указателя на структуру ngx_http_variable_value_t (синоним ngx_variable_value_t).
Поля в структуре ngx_variable_value_t означают следующее:

2.7. Скрипты

Скрипты в nginx -- байт-код для генерации строк. Скрипт можно создать или скомпилировать из шаблона, затем выполнить произвольное число раз. Шаблон -- это строка со ссылками на переменные, которые имеют вид $varible_name или ${variable_name}. Переменные могут быть символьными, либо позиционными, которые имеют индексы от 0 до 9: $0, ... $9. Позиционные переменные заполняются модулем rewrite. При выполнении скрипта используются значения переменных на этот момент выполнения, либо на момент попадания значения переменной в кэш, если значение кэшируемо. Для компиляции шаблона необходимо заполнить следующую структуру:

typedef struct {
    ngx_conf_t                 *cf; /* указатель на конфигурацию ngx_conf_t */
    ngx_str_t                  *source /* компилируемый шаблон */;

    ngx_array_t               **lengths; /* код для определения длины результата */
    ngx_array_t               **values; /* код для генерации результата */
    
    ngx_uint_t                  variables; /* предполагаемое число переменных */

    unsigned                    complete_lengths:1; /* генерировать код для определения длины */
    unsigned                    complete_values:1; /* генерировать код для генерации значения */
} ngx_http_script_compile_t;

Заполненную структуру необходимо передать в функцию ngx_http_script_compile. Пример:

static ngx_str_t ngx_http_script_source = ngx_string("Your IP-address is $remote_addr");

{
    ngx_http_script_compile_t   sc;
    ngx_array_t                 *lengths = NULL;
    ngx_array_t                 *values = NULL;

    ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));

    sc.cf = cf;
    sc.source = &ngx_http_script_source;
    sc.lengths = &lengths;
    sc.values = &values;
    sc.variables = 1;
    sc.complete_lengths = 1;
    sc.complete_values = 1;

    if (ngx_http_script_compile(&sc) != NGX_OK) {
        return NGX_CONF_ERROR;
    }

    return NGX_CONF_OK;
}

Для выполнения скрипта используется функция ngx_http_script_run:

u_char *ngx_http_script_run(ngx_http_request_t *r, ngx_str_t *value,
    void *code_lengths, size_t reserved, void *code_values);
r -- запрос, в контексте которого выполняется скрипт,
value -- указатель на строку, в которую будет помещен результат,
code_lengths -- указатель на код для получения длины результата,
reserved -- зарезервированный аргумент,
code_values -- указатель на код для получения результата,
результат -- указатель на байт памяти, следующий за последним байтом результата, либо NULL, если при выполнении скрипта произошла ошибка.

Пример:
[...]
{
    ngx_str_t value;

    if (ngx_http_script_run(r, &value, lengths->elts, 0,
        values->elts) == NULL)
    {
        return NGX_ERROR;
    }

    [...]
}

Если число переменных в шаблоне неизвестно, то можно использовать функцию ngx_http_script_variables_count для их подсчета:

ngx_uint_t ngx_http_script_variables_count(ngx_str_t *value);

value -- указатель на строку,
результат -- число переменных в строке.

Пример:

static ngx_str_t ngx_http_script_source = ngx_string("Your IP-address is $remote_addr");

{
    ngx_int_t                   n;
    ngx_http_script_compile_t   sc;
    ngx_array_t                 *lengths = NULL;
    ngx_array_t                 *values = NULL;

    n = ngx_http_script_variables_count(&ngx_http_script_source);

    if(n > 0) {
        ngx_memzero(&sc, sizeof(ngx_http_script_compile_t));

        sc.cf = cf;
        sc.source = &ngx_http_script_source;
        sc.lengths = &lengths;
        sc.values = &values;
        sc.variables = n;
        sc.complete_lengths = 1;
        sc.complete_values = 1;

        if (ngx_http_script_compile(&sc) != NGX_OK) {
            return NGX_CONF_ERROR;
        }
    }

    return NGX_CONF_OK;
}

Если шаблон не содержит переменных, то можно соптимизировать вызов ngx_http_script_run, проверив, что любой из векторов, сожержищих байт-код, не инициализирован:

[...]
{
    ngx_str_t value;

    if (lengths == NULL) {
        value.data = ngx_http_script_source.data;
        value.len = ngx_http_script_source.len;
    }else{
        if (ngx_http_script_run(r, &value, lengths->elts, 0,
            values->elts) == NULL)
        {
            return NGX_ERROR;
        }
    }

    [...]
}

2.8. Регулярные выражения

Регулярные выражения в nginx реализованы на основе библиотеки PCRE и доступны только при её наличии в системе. В исходном коде доступность регулярных выражений индицируется макросом NGX_PCRE. Регулярные выражения используюутся по принципу аналогичному скриптам: компилируются один раз, затем многократно выполняются. Для компиляции регулярного выражения используется функция ngx_regex_compile:

#if (NGX_PCRE)
typedef pcre  ngx_regex_t;

ngx_regex_t *ngx_regex_compile(ngx_str_t *pattern, ngx_int_t options,
    ngx_pool_t *pool, ngx_str_t *err);
#endif

pattern -- указатель на строку, содержащую регулярное выражение;
options -- флаги, задающие некоторые параметры;
pool -- пул, в котором будет выделена память под регулярное выражение;
err -- строка, содержащая текстовое описание ошибки, которая произошла при компиляции регулярного выражения;
результат -- указатель на структуру, содержащую скомпилированное регулярное выражение.

Параметр options может содержать флаг NGX_REGEX_CASELESS, означающий, что регулярное выражение не чувствительно к регистру символов.

Пример компиляции регулярного выражения:


Для подсчета числа ссылок в регулярном выражении используется функция ngx_regex_capture_count

#if (NGX_PCRE)
ngx_int_t ngx_regex_capture_count(ngx_regex_t *re);
#endif

re -- указатель на скомпилированное регулярное выражение;
результат -- число ссылок.

Для выполнения регулярного выражения используется функция ngx_regex_exec

#if (NGX_PCRE)
ngx_int_t ngx_regex_exec(ngx_regex_t *re, ngx_str_t *s, int *captures,
    ngx_int_t size);
#endif

re -- указатель на скомпилированное регулярное выражение;
s -- указатель строку, для которой будет выполнено регулярное выражение;
captures -- указатель вектор, в который будут помещены позиции подстрок, соответствующие ссылкам;
size -- число элементов вектора captures;
результат -- 0, если регулярное выражение совпало, NGX_REGEX_NO_MATCHED, если регулярное выражение не совпало, значение меньше NGX_REGEX_NO_MATCHED если произошла ошибка.

Число элементов вектора captures должно быть ровно в три раза больше числа ссылок в регулярном выражении. В первые две трети вектора помещаются позиции подстрок, соответствующие ссылкам в регулярном выражении, оставшаяся часть используется библиотекой PCRE для внутренних нужд. Каждый первый элемент первых двух третей вектора содержит позицию первого символа подстроки, каждый второй -- позицию, следющую за позицией последнего символа подстроки.

2.9. Конфигурация модуля

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

ngx_http_conf_get_module_main_conf(cf, module)
ngx_http_conf_get_module_srv_conf(cf, module)
ngx_http_conf_get_module_loc_conf(cf, module)
cf -- указатель на структуру ngx_conf_t (конфигурация),
module -- структура
ngx_module_t (описание модуля),
rvalue -- указатель на конфигурацию модуля соответствующего уровня.

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

ngx_http_get_module_main_conf(r, module)
ngx_http_get_module_srv_conf(r, module)
ngx_http_get_module_loc_conf(r, module)
r -- указатель на структуру ngx_http_request_t (запрос),
module -- структура
ngx_module_t (описание модуля),
rvalue -- указатель на конфигурацию модуля соответствующего уровня.

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

Контекст модуля во время обработки запроса храниться в бинарном виде в структурах определяемых разработчиком. Для установки контекста модуля используется следующий макрос:

ngx_http_set_ctx(r, c, module)

r -- указатель на структуру ngx_http_request_t (запрос), c -- указатель на контекст модуля (структура определяемая разработчиком), module -- структура ngx_module_t (описание модуля).

Для доступа к контексту модуля используется следующий макрос:

ngx_http_get_module_ctx(r, module)

r -- указатель на структуру ngx_http_request_t (запрос), module -- структура ngx_module_t (описание модуля). rvalue -- указатель на контекст модуля.

3. Nginx и модули

3.1. Фазы обработки запросов в nginx

Nginx обрабатывает запросы с использованием нескольких фаз. На каждой фазе вызываются 0 или более хэндлеров.

  1. NGX_HTTP_SERVER_REWRITE_PHASE -- фаза преобразования URI запроса на уровне виртуального сервера;
  2. NGX_HTTP_FIND_CONFIG_PHASE -- фаза поиска контекста запроса (location'a);
  3. NGX_HTTP_REWRITE_PHASE -- фаза преобразования URI запроса на уровне location'a;
  4. NGX_HTTP_POST_REWRITE_PHASE -- фаза обработки результатов преобразования URI запроса;
  5. NGX_HTTP_PREACCESS_PHASE -- фаза подготовки данных для проверки ограничений доступа к ресурсу;
  6. NGX_HTTP_ACCESS_PHASE -- фаза проверки ограничений доступа к ресурсу;
  7. NGX_HTTP_POST_ACCESS_PHASE -- фаза обработки результатов проверки ограничений доступа к ресурсу;
  8. NGX_HTTP_CONTENT_PHASE -- фаза генерации ответа;
  9. NGX_HTTP_LOG_PHASE -- фаза записи в лог.
На всех фазах могут быть зарегистрированы пользовательские хэндлеры, за исключением следующих фаз:
Хэндлером могут быть возвращены следующие константы:

Для регистрации хэндлера необходимо обратиться к основной конфигурации модуля ngx_http_core_module и добавить хэндлер к одному из элементов вектора phases. Пример регистрации хэндлера на фазе NGX_HTTP_CONTENT_PHASE:

static ngx_int_t
ngx_http_sample_module_init(ngx_conf_t *cf)
{
    ngx_http_handler_pt        *h;
    ngx_http_core_main_conf_t  *cmcf;

    cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_core_module);

    h = ngx_array_push(&cmcf->phases[NGX_HTTP_CONTENT_PHASE].handlers);
    if (h == NULL) {
        return NGX_ERROR;
    }

    *h = ngx_http_sample_module_handler;

    return NGX_OK;
}

Хэндлеры фаз вызываются вне зависимости от конфигурации. В связи с этим, хэндлер должен уметь определять когда он не применим и возвращать NGX_DECLINED, и делать это как можно быстрее, чтобы избежать потери производительности.

Фаза NGX_HTTP_ACCESS_PHASE используется для вызова хэндлеров, ограничивающих доступ к ресурсам. На этой фазе порядок перехода к следующим хэндлерам или фазам определяеться директивой satisfy. Значения, возвращаемые хэндлером, преобретают дополнительный смысл:

В случае satisfy all для перехода к следующей фазе необходимо, чтобы все хэндлеры вернули NGX_OK. В случае satisfy any нужно чтобы хотя бы один хэндлер вернул NGX_OK.

Фаза NGX_HTTP_CONTENT_PHASE используется для генерации ответа. Если в конфигурации уровня location'а модуля ngx_http_core_module переопределен параметр handler, то все запросы направляются этому хэндлеру, в противном случае используются хэндлеры фазы NGX_HTTP_CONTENT_PHASE из основной конфигурации. Хэндлер location'а не может быть возобновлен: возврат NGX_DONE не приводит к повторному вызову хэндлера. Пример переопределния хэндлера location'а:

static char *
ngx_http_sample_module_command(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_sample_handler;

    return NGX_CONF_OK;
}

3.2. Встраивание модуля в nginx

Для встраивания в nginx модуль должен содержать метаинформацию, которая описывает как инициализировать и конфигурировать модуль. Метаинформация представляется структурой ngx_module_t:

struct ngx_module_s {

    [...]

    ngx_uint_t            version;

    void                 *ctx;
    ngx_command_t        *commands;
    ngx_uint_t            type;

    [...]

    ngx_int_t           (*init_module)(ngx_cycle_t *cycle);

    ngx_int_t           (*init_process)(ngx_cycle_t *cycle);

    [...]

    void                (*exit_process)(ngx_cycle_t *cycle);

    void                (*exit_master)(ngx_cycle_t *cycle);

    [...]

};

typedef struct ngx_module_s      ngx_module_t;
Назначение полей:
version -- содержит версию модуля (на данный момент 1),
ctx -- указатель на глобальный контекст модуля,
commands -- указатель на вектор описателей директив модуля,
type -- тип модуля: NGX_HTTP_MODULE, NGX_EVENT_MODULE, NGX_MAIL_MODULE и другие,
init_module -- хэндлер, вызываемый при инициализации модуля в основном процессе,
init_process -- хэндлер, вызываемый при инициализации рабочего процесса,
exit_process -- хэндлер, вызываемый при завершении рабочего процесса,
exit_master -- хэндлер, вызываемый при завершении основного процесса.

Пример определения:

#include <ngx_config.h>
#include <ngx_core.h>

[...]

ngx_module_t  ngx_http_some_module = {
    NGX_MODULE_V1,
    &ngx_http_some_module_ctx,             /* module context */
    ngx_http_some_commands,                /* module directives */
    NGX_HTTP_MODULE,                       /* module type */
    NULL,                                  /* init master */
    NULL,                                  /* init module */
    NULL,                                  /* init process */
    NULL,                                  /* init thread */
    NULL,                                  /* exit thread */
    NULL,                                  /* exit process */
    NULL,                                  /* exit master */
    NGX_MODULE_V1_PADDING
};

Примечание: экземпляр типа ngx_module_t должен быть объявлен с квалификатором extern. Поскольку все определения имеют квалификатор extern, то в примере он опущен.

Описание HTTP-модуля

HTTP-модули в поле ctx структуры ngx_module_t содержат указатель на структуру ngx_http_module_t.

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;

Назначение полей структуры:
preconfiguration -- хэндлер, вызываемый перед обработкой файла конфигурации,
postconfiguration -- хэндлер, вызываемый после обработки файла конфигурации,
create_main_conf -- хэндлер, вызываемый для создания основной конфигурации,
init_main_conf -- хэндлер, вызываемый для инициализации основной конфигурации,
create_srv_conf -- хэндлер, вызываемый для создания конфигурации виртуального сервера,
merge_srv_conf -- хэндлер, вызываемый для слияния конфигураций виртуального сервера,
create_loc_conf -- хэндлер, вызываемый создания конфигурации location'а,
merge_loc_conf -- хэндлер, вызываемый для слияния конфигураций location'а.

Любое из полей может содержать значение NULL, означающее, что вызывать хэндлер не нужно.

Пример определения:

ngx_http_module_t  ngx_http_some_module_ctx = {
    ngx_http_some_module_add_variables,    /* preconfiguration */
    NULL,                                  /* postconfiguration */

    NULL,                                  /* create main configuration */
    NULL,                                  /* init main configuration */

    NULL,                                  /* create server configuration */
    NULL,                                  /* merge server configuration */

    ngx_http_some_module_create_loc_conf,  /* create location configuration */
    NULL                                   /* merge location configuration */
};

3.2.1. Описание и обработка директив конфигурации модуля

Директивы конфигурации описываются структурой ngx_command_t:

struct ngx_command_s {
    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;
};

#define ngx_null_command  { ngx_null_string, 0, NULL, 0, 0, NULL }

typedef struct ngx_command_s     ngx_command_t;

Назначение полей структуры:
name -- название директивы,
type -- тип директивы, число аргументов и блоки, в которых может присутствовать директива:
ФлагЗначение
NGX_CONF_NOARGSДиректива не принимает аргументов
NGX_CONF_TAKE1 ... NGX_CONF_TAKE7Директива принимает указанное количество аргументов
NGX_CONF_TAKE12Директива принимает 1 или 2 аргумента
NGX_CONF_TAKE13Директива принимает 1 или 3 аргумента
NGX_CONF_TAKE123Директива принимает 1, 2 или 3 аргумента
NGX_CONF_TAKE1234Директива принимает 1, 2, 3 или 4 аргумента
NGX_CONF_BLOCKДополнительный аргумент директивы является блоком
NGX_CONF_FLAGДиректива является флагом
NGX_CONF_ANYДиректива принимает 0 или более аргументов
NGX_CONF_1MOREДиректива принимает 1 или более аргументов
NGX_CONF_2MOREДиректива принимает 2 или более аргументов
NGX_DIRECT_CONFДиректива может присутствовать в основном файле конфигурации
NGX_MAIN_CONFДиректива может присутствовать на корневом уровне конфигурации
NGX_ANY_CONFДиректива может присутствовать на любом уровне конфигурации
NGX_HTTP_MAIN_CONFДиректива может присутствовать на уровне физического HTTP-сервера
NGX_HTTP_SRV_CONFДиректива может присутствовать на уровне виртуального HTTP-сервера
NGX_HTTP_LOC_CONFДиректива может присутствовать на уровне location'а
NGX_HTTP_LMT_CONFДиректива может присутствовать в блоке limit_except
NGX_HTTP_LIF_CONFДиректива может присутствовать в блоке if() на уровне локейшена
set -- хэндлер, вызываемый в момент обнаружения директивы. Для удобства реализовано множество стандартных хэндлеров:
Название хэндлераТип данныхТип поля в конфигурации модуля, на который указывает offset
ngx_conf_set_flag_slotФлагngx_flag_t
ngx_conf_set_str_slotСтрокаngx_str_t
ngx_conf_set_str_array_slotВектор строкУказатель на ngx_array_t -> ngx_str_t
ngx_conf_set_keyval_slotВектор пар ключ-значениеУказатель на ngx_array_t -> ngx_keyval_t
ngx_conf_set_num_slotЦелое число со знакомngx_int_t
ngx_conf_set_size_slotДлинаsize_t
ngx_conf_set_off_slotСмещениеoff_t
ngx_conf_set_msec_slotМиллисекундыngx_msec_t
ngx_conf_set_sec_slotСекундыtime_t
ngx_conf_set_bufs_slotЧисло и размер буферовngx_bufs_t
ngx_conf_set_enum_slotПеречисляемое значениеngx_uint_t
ngx_conf_set_bitmask_slotБитовая картаngx_uint_t
ngx_conf_set_path_slotПуть в файловой системе и число символов в хэшированных каталогахngx_path_t
ngx_conf_set_access_slotПрава доступаngx_uint_t
conf -- уровень конфигурации модуля, на которую ссылается директива, либо 0, если директива имеет специализированный обработчик,
offset -- смещение поля в конфигурации модуля, которое задается этой директивой,
post -- указатель на дескриптор постпроцессинга,

Список директив модуля описываться вектором, который заканчивается значением ngx_null_command. Пример:

typedef struct {
    ngx_str_t   foobar;
} ngx_http_some_module_loc_conf_t;

static ngx_command_t  ngx_http_some_module_commands[] = {

    { ngx_string("foobar"),
      NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
      ngx_conf_set_str_slot,
      NGX_HTTP_LOC_CONF_OFFSET,
      offsetof(ngx_http_some_module_loc_conf_t, foobar),
      NULL },

    ngx_null_command
};

Описана директива foobar, принимающая 1 аргумент. Директива может присутствовать на основном уровне конфигурации, конфигурации виртуального HTTP-сервера и локейшена. Аргумент директивы конвертируется в строку и записывается в поле foobar конфигурации модуля .

Создание конфигураций

Перед обработкой файла конфигурации структуры, содержащие конфигурацию, должны быть выделены и инициализированы. Для этого используются хэндлеры create_main_conf, create_srv_conf и create_loc_conf.

Слияние конфигураций

Для упрощения конфигурирования, на каждом из уровней конфигурации создается шаблон конфигурации каждого из последующих уровней. Рассмотрим пример:

http {
    server {

        zip_buffers 10 4k;

        location /foobar {
            # gzip_buffers 10 4k наследована с предыдущего уровня
            gzip on;
        }

        location /foobaz {
            # gzip_buffers 10 4k наследована с предыдущего уровня
        }
    }
}

При обработке блока server будет создана шаблонная конфигурация для модуля ngx_http_gzip_filter_module и к ней будет применена директива gzip_buffers. При переходе к обработке блока location /foobar {} будет создана ещё одна конфигурация для модуля ngx_http_gzip_filter_module и к ней будет применена директива gzip. После обработки блока http {} шаблонную конфигурацию необходимо слить с конфигурацией блоков /foobar и /foobaz. В процессе слияния все неустановленные параметры конфигурации заполняются значениями из соответствующих параметров шаблонной конфигурации, либо значением по-умолчанию. Для упрощения слияния используются следующие макросы:

НазваниеТип данныхТип поля в конфигурации модуля
ngx_conf_merge_ptr_valueУказательpointer
ngx_conf_merge_uint_valueЦелое числоngx_uint_t
ngx_conf_merge_msec_valueВремя в миллисекундахngx_msec_t
ngx_conf_merge_sec_valueВремя в секундахtime_t
ngx_conf_merge_size_valueДлинаsize_t
ngx_conf_merge_bufs_valueЧисло и размер буферовngx_bufs_t
ngx_conf_merge_bitmask_valueБитовая картаngx_uint_t
ngx_conf_merge_path_valueПуть в файловой системе и число символов в хэшированных каталогахngx_path_t

Для слияния конфигурации виртуального сервера используется хэндлер merge_srv_conf, для конфигурации location'a используется хэндлер merge_loc_conf. Пример:

typedef struct {
    ngx_str_t str_param;
    ngx_uint_t int_param;
} ngx_http_sample_module_loc_conf_t;

static char *
ngx_http_sample_merge_loc_conf(ngx_conf_t *cf, void *parent, void *child)
{
    ngx_http_sample_module_loc_conf_t  *prev = parent;
    ngx_http_sample_module_loc_conf_t  *conf = child;

    ngx_conf_merge_str_value(conf->str_param, prev->str_param, "default value");

    ngx_conf_merge_uint_value(conf->int_param,
                              prev->int_param, 1);

    return NGX_CONF_OK;
}

ngx_http_module_t  ngx_http_some_module_ctx = {
    NULL,                                  /* preconfiguration */
    NULL,                                  /* postconfiguration */

    NULL,                                  /* create main configuration */
    NULL,                                  /* init main configuration */

    NULL,                                  /* create server configuration */
    NULL,                                  /* merge server configuration */

    ngx_http_some_module_create_loc_conf,  /* create location configuration */
    ngx_http_some_module_merge_loc_conf    /* merge location configuration */
};

3.2.2. Описание и вычисление переменных модуля

Поддерживаемые модулем переменные должны быть созданы перед разбором блоков файла конфигурации, в которых эти переменные могут встретиться. Чтобы создать переменные до разбора файлов конфигурации нужно использовать хэндлер preconfiguration в структуре ngx_http_module_t. Пример:

static ngx_int_t
ngx_http_some_module_add_variables(ngx_conf_t *cf);

ngx_http_module_t  ngx_http_some_module_ctx = {
    ngx_http_some_module_add_variables,    /* preconfiguration */

    [...]
};

static ngx_http_variable_t ngx_http_some_module_variables[] = {

    { ngx_string("var"), NULL, ngx_http_some_module_variable,
      0,
      NGX_HTTP_VAR_NOCACHEABLE, 0 },

    { ngx_null_string, NULL, NULL, 0, 0, 0 }
};

static ngx_int_t
ngx_http_some_module_add_variables(ngx_conf_t *cf)
{
    ngx_http_variable_t  *var, *v;

    for (v = ngx_http_some_module_variables; v->name.len; v++) {
        var = ngx_http_add_variable(cf, &v->name, v->flags);
        if (var == NULL) {
            return NGX_ERROR;
        }

        var->get_handler = v->get_handler;
        var->data = v->data;
    }

    return NGX_OK;
}
Для генерации значения переменной необходимо реализовать функцию, которая заполняет структуру ngx_http_variable_value_t, используя данные из запроса, из контекстов, конфигураций модулей или из других источников. Пример:
static ngx_int_t
ngx_http_some_module_variable(ngx_http_request_t *r,
    ngx_http_variable_value_t *v,  uintptr_t data)
{
    v->valid = 1;
    v->no_cacheable = 1;
    v->not_found = 0;

    v->data = (u_char*)"42";
    v->len = 2;

    return NGX_OK;
}

3.3. Компиляция модуля и сборка с nginx

Для сборки модуля с nginx необходимо указать путь к каталогу с исходным кодом модуля скрипту ./configure в параметре --add-module= командной строки. В указанном каталоге должен находиться файл config. Файл config -- это скрипт, который система сборки nginx включает и выполняет на стадии конфигурации. Задача скрипта -- установить набор переменных, управляющих сборкой. Список наиболее важных переменных:

Имя перменнойНазначение
ngx_addon_nameИмя текущего дополнительного модуля
NGX_ADDON_SRCSСписок всех исходных файлов всех дополнительных модулей, которые нужно скомпилировать
NGX_ADDON_DEPSСписок всех зависимых файлов всех дополнительных модулей (как правило заголовочные файлы).
HTTP_MODULESСписок всех HTTP модулей
HTTP_AUX_FILTER_MODULESСписок всех вспомогательных фильтров
USE_MD5Использовать ли поддержку MD5 (YES/NO)
USE_SHA1Использовать ли поддержку SHA-1 (YES/NO)
USE_ZLIBИспользовать ли библиотеку zlib (YES/NO)

Для ссылок на каталог, в котором расположены файлы модуля, используется переменная ngx_addon_dir. Пример файла config:

ngx_addon_name=ngx_http_sample_module
HTTP_MODULES="$HTTP_MODULES ngx_http_sample_module"
NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/ngx_http_sample_module.c"

Предположим, файлы модуля расположены в каталоге /home/valery/work/sample_module. Для включения модуля nginx нужно конфигурировать следующим образом:

path/to/nginx$ ./configure --add-module=/home/valery/work/sample_module

Далее:

path/to/nginx$ make
path/to/nginx$ make install

Инсталлированному экземпляру nginx станут доступны директивы и переменные подключенного модуля.

4. Модули

Это -- недописанная глава. Она должна содержать важный и интересный материал, но у автора пока нет хорошей идеи, относительно того, как его преподнести.

Связаться с автором

Valery Kholodkov valery+nginx@grid.net.ru
Пожалуйста, используйте расширение адреса при составлении письма мне.

Ссылки

Nginx: www.sysoev.ru/nginx/ -- это веб-сервер разработанный Игорем Сысоевым.

Благодарности


Copyright (C) 2008 Valery Kholodkov