Спецификация интерфейса FastCGI
- Введение
- Начальное состояние процесса
- Основы протокола
- Типы служебных записей
- Типы прикладных записей
- Ошибки
Введение
FastCGI — открытый унифицированный стандарт, расширяющий интерфейс CGI и позволяющий создавать высокопроизводительные web-приложения без использования специфичных API web-сервера.
Цель данной спецификации — с точки зрения FastCGI-приложения описать интерфейс взаимодействия между ним и web-сервером (также реализующим интерфейс FastCGI). Описание различных возможностей web-сервера, например, по управлению FastCGI-приложениями, выходит за рамки данной спецификации.
Спецификация написана для случая использования платформы Unix, хотя описанный здесь протокол не зависит от порядка следования байт и в общем случае может использоваться на других платформах.
Интерфейс FastCGI является расширением (улучшением) CGI, поэтому будет описываться в параллельном сравнии с традиционной реализацией интерфейса CGI 1.1 в Unix. FastCGI проектировался для поддержки «долгоживужих» приложений — служб (серсисов). Это основное отличие от традиционной реализации CGI 1.1, где для обработки каждого запроса создается процесс, однократно используется, после чего завершается.
Стартовые условия у FastCGI-процесса более жесткие, чем у CGI, который при запуске имеет доступ к переменным оркужения и может использовать стандартные потоки ввода-вывода STDIN
, STDOUT
и STDERR
. Связь же между web-сервером и FastCGI-процессом осуществляется через один сокет, который процесс должен слушать на предмет входящих подключений от web-сервера.
После приема соединения от web-сервера FastCGI-процесс обменивается данными с использованием простого протокола, решающего две задачи: организация двунаправленного обмена в рамках одного соединения (для эмуляции STDIN
, STDOUT
, STDERR
) и организация нескольких независимых FastCGI-сессий в рамках одного соединения.
FastCGI приложение выполняет одну из трех определенных протоколом ролей. Наиболее привычная (и основная) — Responder
, в которой приложение получает данные HTTP-запроса и формирует HTTP-ответ (аналогично CGI 1.1). Вторая роль — Authorizer
: приложение получает данные HTTP-запроса и генерирует решение авторизован / не авторизован. Третья — Filter
, в которой приложение также получает данные HTTP-запроса и дополнительно поток данных, хранящихся на сервере, после чего выполняет фильтрацию (преобразование) данных и формирует HTTP-ответ. Может использоваться для изменения «на лету» статического контекта (HTML-документов, изображений), а также для их кеширования.
В дальнейшем изложении во избежание неоднозначностей термин «приложение» следует понимать как «FastCGI приложение», либо «FastCGI-процесс». Термин «web-сервер» или просто «сервер» как какое-либо ПО, запускающее приложение, таким ПО может быть как сам web-сервер или его дополнительный модуль, так и сторонний сервер приложений.
Начальное состояние процесса
Список аргументов
Web-сервер или сервер приложений может передавать запускаемому приложению аргументы командной строки. По умолчанию передается единственный аргумент — имя (последний компонент пути к приложению). Запускаемым приложением может быть также интерпретируемый скрипт (начинающийся с #!
), тогда интерпретатору передаются все аргументы, указанные в начале скрипта, например #!/usr/bin/perl -w
.
Файловые дескрипторы
При запуске приложения web-сервер оставляет единственный файловый дескриптор FCGI_LISTENSOCK_FILENO
, который ссылается на сокет, созданный сервером. Приложение слушает этот дескриптор на предмет входищих соединений от сервера.
FCGI_LISTENSOCK_FILENO
равен STDIN_FILENO
(имеет тот же номер в таблице дескрипторов — 0), остальные стандартные дескрипторы — STDOUT_FILENO
и STDERR_FILENO
— закрываются перед запуском приложения. Проверенный метод определить в каком режиме запущено текущее приложение — выполнить в нем вызов getpeername
для дескриптора FCGI_LISTENSOCK_FILENO
(вызов для локального сокета возвращает адрес / хост противоположного конца сокета). Для FastCGI приложения вызов вернет -1 и выставит переменную errno
в ENOTCONN
(сокет не подключен).
Web-сервер сам выбирает необходимый тип сокета: Unix-сокет для локального взаимодействия (файловый сокет, AF_UNIX
), либо TCP/IP-сокет для удаленного (AF_INET
). Оба сокета имеют одинаковый интерфейс, поэтому детали транспорта скрыты от разработчика FastCGI-приложения.
Переменные окружения
Сервер может передавать дополнительные параметры приложению через переменные окружения (переменные среды). Текущая спецификация определяет только одну такую переменную — FCGI_WEB_SERVER_ADDRS
(содержит список доверенных хостов, от которых могут приходить FastCGI-запросы), однако сервер может предоставлять возможность задания любого набора переменных. Например, иногда имеет смысл задавать переменные PATH
, PERL5LIB
и т.д., чтобы указать специфичные настройки поиска библиотек для конкретного приложения, либо группы приложений.
Другие начальные параметры
Также web-сервер может предоставлять возможности по установке любых других начальных параметров, таких как корневая директория, пользователь и группа, рабочая директория процесса, приоритет, ограничения на память, число открытых дескрипторов.
Основы протокола
Нотация
Для описания формата сообщений будем использовать C-подобный синтаксис. Все элементы структур определяются в терминах unsigned char
и передаются компилятором ISO C в обычном порядке: первый байт идет первым, второй вторым и т.д.
Для упрощения и сокращения определений примем два соглашения. Во-первых, если два следующие друг за другом элемента структуры имеют одинаковое имя и разные суффиксы B0
и B1
, то в этих элементах хранится одно число с указанным именем, восстанавливаемое как B1 << 8 + B1
. Это соглашение очевидным образом распространяется и на случай более чем двух байт. Во-вторых, определение вида:
- struct {
- unsigned char mumbleLengthB1;
- unsigned char mumbleLengthB0;
- ... /* другие элементы */
- unsigned char mumbleData[mumbleLength];
- };
означает структуру переменной длины, где длина элемента mumbleData
определяется значениями ранее определенных элементов.
Прием соединения от сервера
FastCGI-приложение принимает соединение от web-сервера, выполняя вызов accept
для дескриптора FCGI_LISTENSOCK_FILENO
. Вызов является блокирующим, то есть в случае отсуствия входящих соединений от сервера приложение будет находиться ожидании. Если соединение было успешно принято и установлена переменная окружения FCGI_WEB_SERVER_ADDRS
, приложение проверяет, что IP-адрес поключившейся к сокету машины есть в списке доверенных хостов FCGI_WEB_SERVER_ADDRS
(список IP, раделенных запятыми, например: 213.180.204.11,87.250.251.11
), в противном случае закрывает соединение. Приложение может одновременно открывать несколько транспортных соединений, но это не является обязательным поведением.
Записи
Приложения выполняет запросы сервера, используя простой протокол. Детали протокола зависят от конкретной роли приложения, но в общем случае взаимодействие выглядит так: сперва web-сервер отправляет параметры и другие данные приложению, приложение формирует и отправляет ответ, после чего отправляет признак конца сессии.
Все передаваемые данные оборачиваются в FastCGI-записи — единицу данных протокола. FastCGI-записи служат для организации двунаправленного обмена и мультиплексирования нескольких сессий в рамках одного соединения.
- typedef struct {
- /* Заголовок */
- unsigned char version;
- unsigned char type;
- unsigned char requestIdB1;
- unsigned char requestIdB0;
- unsigned char contentLengthB1;
- unsigned char contentLengthB0;
- unsigned char paddingLength;
- unsigned char reserved;
- /* Тело записи */
- unsigned char contentData[contentLength];
- unsigned char paddingData[paddingLength];
- };
FastCGI-запись состоит из заголовка фиксированной длины и следующегего за ним содержимого и выравнивающих данных переменной длины. Запись содержит 7 элементов:
version
— верся протокола FastCGI, данная спецификация описывает версиюFCGI_VERSION_1
type
— тип записи, он определяет выполняемые функции и формат содержимого записи; типы будут рассмотрены нижеrequestId
— идентификатор FastCGI-запроса (сессии)contentLength
— длина содержимого записиpaddingLength
— длина выравнивающих данныхcontentData
— данные записи (до 65535 байт), интерпретируемые в соответсивии с типомpaddingData
— данные для выравнивания размера FastCGI-записи (до 255 байт), полезной информации не несут, должны игнорироваться
Далее для описания константных записей будем использовать синтаксис инициализации структур языка C, при этом будем исключать элементы version
и paddingData
и представлять requestId
в виде простого числа. Так {FCGI_END_REQUEST, 1, {FCGI_REQUEST_COMPLETE, 0}}
— запись типа FCGI_END_REQUEST
с requestId
равным 1 и содержимым {FCGI_REQUEST_COMPLETE, 0}
.
Выравнивание. Протокол разрешает отправителю выполнять выравнимание записей по длине и обязует получателя интерпретировать paddingLength
и игнорировать данные paddingData
. Такой механизм позволяет отправителю держать данные в выровненном виде, что способствует более эффективной обработке. Опыт работы с протоколами X window показывает выигрыш в производительности при использовании такого подхода. Рекомендуется хранить запись как последовательность секций по 8 байт (длина порции фиксированной длины FastCGI-записи — заголовка — составляет 8 байт).
Управление идентификаторами запросов. Приложение запоминает состояние обработки для каждого активного requestId
и рамках транспортного соединения. Идентификатор R
становится активным, когда приложение получает запрос {FCGI_BEGIN_REQUEST, R, ...}
и становится неактивным, когда отправляет серверу ответ {FCGI_END_REQUEST, R, ...}
. Web-сервер переиспользует идентификаторы запросов, приоритетным для него является использование меньших ID, что позволяет использовать в приложении более простые структуры данных для ассоциации состояний запросов с ID (простой статический массив, вместо очередей или Hash-массивов / ассоциативных массивов). Для неактивных ID все типы запросов игнорируются, пока не придет FCGI_BEGIN_REQUEST
. Приложение вправе не поддерживать одновременную обработку нескольких запросов, тогда задача управления идентификаторами существенно упрощается.
Разновидности типов записей. Классифицировать типы FastCGI-записей можно по двум признакам. По назначению — на служебные (management) и прикладные (application). Служебные не ассоциированы с конкретным запросом, это данные уровня конфигурации всего приложения, они позволяют настраивать свойство протокола и должны иметь нулевой requestId
. Второй вариант классификации — по характеру транспортируемых данных — делятся на дискретные (discrete) и поточные (stream). Дискретные содержат цельные единицы данных, поточные — часть байтовой последовательности. То есть поток данных упаковывается как последовательность поточных записей ненулевой длины, с завершающей записью нулевой длины (признак конца потока).
Эти две классификации независимы, хотя в текущей версии спецификации все служебныме записи являются дискретными, а основные прикладные записи — поточными.
Пары имя-значение
Во всех своих ролях FastCGI-приложению необходимо принимать (иногда и отправлять) различное число переменных. Для целей кодирования таких переменных и применяется формат имя-значение. FastCGI-приложение представляет пару как структуру из следующих элементов: длина имени, длина значения, имя, значение. Длина менее 127 байт (7 младших бит, старший бит выполняет роль флага) кодируется одним байтом, иначе — четырьмя.
- typedef struct {
- unsigned char nameLengthB0; /* nameLengthB0 >> 7 == 0 */
- unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
- unsigned char nameData[nameLength];
- unsigned char valueData[valueLength];
- } FCGI_NameValuePair11;
- typedef struct {
- unsigned char nameLengthB0; /* nameLengthB0 >> 7 == 0 */
- unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
- unsigned char valueLengthB2;
- unsigned char valueLengthB1;
- unsigned char valueLengthB0;
- unsigned char nameData[nameLength];
- /* valueLength: ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0 */
- unsigned char valueData[valueLength];
- } FCGI_NameValuePair14;
- typedef struct {
- unsigned char nameLengthB3; /* nameLengthB3 >> 7 == 1 */
- unsigned char nameLengthB2;
- unsigned char nameLengthB1;
- unsigned char nameLengthB0;
- unsigned char valueLengthB0; /* valueLengthB0 >> 7 == 0 */
- /* nameLength: ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0 */
- unsigned char nameData[nameLength];
- unsigned char valueData[valueLength];
- } FCGI_NameValuePair41;
- typedef struct {
- unsigned char nameLengthB3; /* nameLengthB3 >> 7 == 1 */
- unsigned char nameLengthB2;
- unsigned char nameLengthB1;
- unsigned char nameLengthB0;
- unsigned char valueLengthB3; /* valueLengthB3 >> 7 == 1 */
- unsigned char valueLengthB2;
- unsigned char valueLengthB1;
- unsigned char valueLengthB0;
- /* nameLength: ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0 */
- unsigned char nameData[nameLength];
- /* valueLength: ((B3 & 0x7f) << 24) + (B2 << 16) + (B1 << 8) + B0 */
- unsigned char valueData[valueLength];
- } FCGI_NameValuePair44;
Такой формат позволяет передавать пары имя-значение в сыром бинарном виде без дополнительной перекодировки и экранирования сециальных символов, а также упрощает поиск и обработку из-за заранее известных длин имени и значения.
Закрытие транспортного соединения
Web-сервер контролирует жизненный цикл транспортного соединения и может его закрыть, если для него нет активных FastCGI-сессий. Также право закрытия соединения может быть делегировано сервером FastCGI-приложению (необходимые флаги выставляются в запросе FCGI_BEGIN_REQUEST
), в таком случае приложение самостоятельно закрывает соединение по окончании обработки запроса. Такая гибкость позволяет приспосабливаться под разные виды приложений: простые приложения могут каждый раз открывать новое соединение, выполнять в его рамках не более одного запроса и закрывать соединение. Более сложные приложения могут поддерживать соединение в течение длительного времени и обрабатывать запросы параллельно.
Когда приложение закрывает соединение или обнаруживает, что соединение закрыто сервером, оно пытается принять новое соединение (используя блокирующий вызов accept
для стандартного дескриптора FCGI_LISTENSOCK_FILENO
).
Типы служебных записей
Передача параметров серверу
Для получения каких-либо параметров FastCGI-приложения web-сервер может выполнять служебный запрос {FCGI_GET_VALUES, 0, ...}
(обычно это делается при старте приложения). В теле запроса (contentData
) передается последовательность пар имя-значение с именами параметров, которые требуются серверу, и пустыми значениями. В ответ приложение отправляет запись {FCGI_GET_VALUES_RESULT, 0, ...}
, в теле которой передает пары имя-значение с уже заполненными значениями. Если запрошенный сервером параметр не поддерживается приложение исключает его из ответа.
Запрос FCGI_GET_VALUES
спроектирован как унифицированный способ получения любых конфигурационных параметров приложения. Текущей спецификацией определен следующий начальный набор параметров, помогающий серверу управлять запросами и соединениями:
FCGI_MAX_CONNS
— максимальное число транспортных соединений, которые может одновременно поддерживать приложениеFCGI_MAX_REQS
— максимальное число запросов, одновременно обслуживаемых приложениемFCGI_MPXS_CONNS
— флаг, поддерживается ли мультиплексирование запросов:1
— поддерживается,0
— в рамках соединеия может обрабатываться только один запрос
Следует обратить внимание на то, что значения этих переменных передаются в текстовом, а не в бинарном виде. То есть для передачи числа 5
(0x05
) нужно передать строку "5"
(0x35
).
Приложение может получить запрос FCGI_GET_VALUES
в любое время, при этом он должен быть обработан FastCGI-библиотекой, реализующей протокол, на низком уровне без вовлечения в процесс высокоуровневого кода приложения.
Неизвестный тип записи
Число служебных записей может увеличиться в следующих версиях протокола. Для корректной поддержки такой ситуации введен тип записи FCGI_UNKNOWN_TYPE
. В случае, если приложение получило служебную запись неизвестного типа T
, оно должно ответить записью {FCGI_UNKNOWN_TYPE, 0, {T}}
. Тело записи (contentData
) имеет следующий вид:
- typedef struct {
- unsigned char type; /* неизвестный тип записи */
- unsigned char reserved[7];
- } FCGI_UnknownTypeBody;
Типы прикладных записей
Запрос начала сессии
Для начала обработки запроса сервер отправляет запись типа FCGI_BEGIN_REQUEST
со следующим содержимым (contentData
):
- typedef struct {
- unsigned char roleB1;
- unsigned char roleB0;
- unsigned char flags;
- unsigned char reserved[5];
- } FCGI_BeginRequestBody;
Элемент role
определяет роль, выполнение которой web-сервер о ожидает от приложения. В настоящей спецификации определены следующие константы:
FCGI_RESPONDER
— обработчик запросов (самая распространенная роль)FCGI_AUTHORIZER
— сервер авторизацииFCGI_FILTER
— фильтр статического (как правило) контента
Более детально роли будут рассмотрены в соответвующем разделе. Элемент flags
содержит бит управления закрытием соединения. Если выражение flags & FCGI_KEEP_CONN
истино, приложение будет удерживать открытое соединение (управление соединением осуществляет web-сервер), иначе после обработки текущего запроса соединенеие должно быть закрыто приложением.
Параметры запроса
Для передачи параметров запроса — HTTP-заголовков и прочих переменных — сервер отправляет приложению запись типа FCGI_PARAMS
. FCGI_PARAMS
— это неупорядоченный набор пар имя-значение.
Бинарные потоки
Для эмуляции стандартных потоков ВВ используются потоковые FastCGI записи типов FCGI_STDIN
, FCGI_STDOUT
и FCGI_STDERR
. Для передачи дополнительных данных от сервера приложению введен еще один тип потоковой записи — FCGI_DATA
. Записи FCGI_DATA
используются для разделения дополнительных данных и стандартного ввода, в частности в роли фильтр.
Прерывание обработки запроса
Web-сервер может информировать приложение о прерывании пользователем процесса обработки запроса с помощью отправки записи типа FCGI_ABORT_REQUEST
. При получении записи {FCGI_ABORT_REQUEST, R}
приложение должно при первой возможности завершить обработку запроса и ответить записью {FCGI_END_REQUEST, R, {FCGI_REQUEST_COMPLETE, appStatus}}
. В отличие от служебных записей обработка этого запроса не может быть выполнена в библиотеке реализации FastCGI-интерфейса. Такая обработка — это высокоуровневая логика приложения.
В подавляющем большинстве случаев время выполнения запроса приложением очень мало, поэтому в обработке запроса FCGI_ABORT_REQUEST
нет необходимости. Исключение составляют приложения, передающие клиенту большие объемы данных (файлы, видео), либо выполняющие другую ресурсоемкую обработку (построение отчетов, сетевое взаимодействие).
В случае, если web-сервер не выполняет мультиплектирование запросов в рамках одного соединения, прерывание обработки запроса может быть выполнено с помощью закрытия соединения (в случае мультиплексирования приведет к прерыванию обработки всех запросов).
Завершение сессии
Приложение отвечает записью FCGI_END_REQUEST
при завершении обработки запроса, либо при отказе от обработки запроса. Содержимое записи (contentData
) имеет следующий вид:
- typedef struct {
- unsigned char appStatusB3;
- unsigned char appStatusB2;
- unsigned char appStatusB1;
- unsigned char appStatusB0;
- unsigned char protocolStatus;
- unsigned char reserved[3];
- } FCGI_EndRequestBody;
Элемент appStatus
содержит код статуса уровня логики приложения. Элемент protocolStatus
— код статуса уровня протокола, определены следующие возможные значения:
FCGI_REQUEST_COMPLETE
— нормальное завершение обработки запросаFCGI_CANT_MPX_CONN
— отказ от обработки запроса; возникает в случае, если web-сервер отправляет второй запрос в рамках одного соединения приложению, которое не поддерживает такую функциональностьFCGI_OVERLOADED
— отказ от обработки запроса; возникает в случае, если для обработки запроса приложению не хватает некоторых ограниченных ресурсов (например, соединения с БД)FCGI_UNKNOWN_ROLE
— отказ от обработки запроса; возникает в случае, если сервер в записи на начало обработки запроса указал неподдерживаемую приложением роль
- Это вольный, частичный, дополненный перевод документа FastCGI Specification 1.0 от 29 April 1996
- Роли
Authorizer
иFilter
— это частные случаи ролиResponder
, сейчас являются рудиментами и представляют только исторический интерес. Функциональность по авторизованному доступу к статическим страницам уже не актуальна — эта логика перенесена в фреймворки и различные CMS. Большинство функций фильтрации и кеширования эффективно реализованы на стороне web-сервера. - Закрытие дескрипторов — это только поведение сервера по умолчанию, на которое следует закладываться при написании FastCGI-приложения (ограничение по функциональности «снизу»). Ограничения «сверху» нет: например, для удобства отладки сервер может читать данные с
STDERR_FILENO
. - Подавляющее большинство FastCGI-приложений не поддерживает одновременную обработку запросов. Возможность использования разных ID запросов в рамках одного соединения поддерживается далеко не всеми web-серверами.
- Текущая версия — 1.0 — первая и единственная с 1996 года, появление новых версий уже маловероятно.