Web-сервер на платформе TeqFW может работать в одном из трёх режимов: HTTP/1, HTTP/2 и HTTPS (over HTTP/2). Первый режим (HTTP/1) применяется для разработки — запуск приложения локально и доступ к нему из браузера напрямую, второй — для работы в связке с другими серверами (например, доступ через nginx-прокси), третий — в качестве отдельного сервера в live-режиме.
Основы HTTP
HTTP всегда работает по схеме “запрос — ответ”. Вначале отправляется стартовая строка с адресом ресурса на сервере, за ней следуют HTTP-заголовки, потом — тело запроса (опционально). При ответе сервер сначала посылает HTTP-заголовки, а затем — тело ответа. Тело ответа может быть растянуто во времени (как в Server Sent Events — заголовки, событие, событие, …).
В nodejs есть две библиотеки для запуска web-сервера: http и http2. Сервера в обеих библиотеках построены по общему принципу — на каждый HTTP-запрос генерируется ряд событий, на которые можно повесить собственные функции-обработчики (listeners, “прослушки”):
server.on('connect', onConnect);
server.on('error', onError);
server.on('request', onRequest);
server.on('close', onClose);
… HTTP-сервера на каждый запрос формируют структуры для работы с
входными и выходными данными и предоставляют разработчику возможность
прослушивать событие request для доступа к ним:
server.on('request', (req, res) => {});
Структуры req и res являются также потоками, за чтение/запись данных в которые отвечает прослушка (listener), т.е. — разработчик приложения.
Важным является то, что сообщения в HTTP состоят из двух частей: заголовков и тела. Все заголовки всегда отправляются перед телом (после начала отправки тела запроса/ответа заголовки отправлять более нельзя).
Основы обработки запроса в TeqFW
Прослушка события request выполняет предварительный анализ
поступившего запроса, оставляя только те запросы, которые соответствуют
допустимым HTTP-методам (для TeqFW — HEAD, GET & POST). На все другие
запросы прослушка возвращает код 405 (Method Not
Allowed).
Для POST-запросов, у которых Content-Type начинается с text/plain
или с application/json, прослушка считывает тело запроса и преобразует
его в текст или в JSON-объект, который затем прикрепляется к структуре
req. Во всех остальных случаях прослушка входной поток не трогает.
Разработчики teq-плагинов могут создавать собственные обработчики
web-запросов, которые request-прослушка использует для обработки
поступившего запроса и формирования ответа. Обработчики по очереди
анализируют запрос и, если запрос находится в их зоне ответственности,
выполняют соответствующие действия. Если в результате этих действий
формируются HTTP-заголовки для их отправки клиенту, то они добавляются к
структуре res (через response.setHeader(name, value)). Если
обработчик считает, что ответом должно быть содержимое некоего файла, то
он указывает имя файла в res[‘teqFile’], если ответом должен быть
текст, то он указывается в res[‘teqBody’].
Последним request-прослушка запускает обработчик
TeqFw_Web_Back_Handler_Final, который анализирует res-структуру и
формирует ответ (сначала все заголовки, затем отдаёт файл или текст тела
сообщения). Если ни один из обработчиков запрос не обработал, то
финальный обработчик возвращает код 404 (Not
Found).
Если какой-то обработчик считает, что он должен сам полностью ответить на сообщение, то он может это сделать. Другие обработчики, включая финальный, должны учитывать такую возможность.
Диспетчер запросов
В TeqFW прослушка для request-события создаётся в модуле
TeqFw_Web_Back_Server_Dispatcher (метод getListener()). Перед
подключением прослушки Диспетчер сканирует все плагины в поисках
обработчиков web-запросов и инициализирует их:
await dispatcher.createHandlers();
const onRequest = dispatcher.getListener();
server.on('request', onRequest);
Обработчики запросов
Обработчик web-запросов — это объект, имплементирующий интерфейс
TeqFw_Web_Back_Api_Request_IHandler:
class TeqFw_Web_Back_Api_Request_IHandler {
getProcessor() {}
async init() {}
requestIsMine({method, address, headers} = {}) {}
}
Метод init() предназначен для асинхронной инициализации обработчика Диспетчером, метод requestIsMine() сообщает Диспетчеру, что обработчик признаёт запрос “своим”, метод getProcessor() возвращает Диспетчеру функцию — процессор запроса:
/**
* @param {http.IncomingMessage|http2.Http2ServerRequest} req
* @param {http.ServerResponse|http2.Http2ServerResponse} res
*/
function process(req, res) {}
Подключение обработчиков происходит в дескрипторе teq-плагина (teqfw.json):
{
"@teqfw/web": {
"handlers": {
"TeqFw_Web_Back_Handler_Upload": {
"after": ["TeqFw_Web_Back_Handler_WAPI"],
"before": ["TeqFw_Web_Back_Handler_Static"],
"space": ["upload"]
}
}
}
}
Порядок подключения обработчиков определяется на основании данных в
узлах after и before. Узел space позволяет обработчику
зарезервировать адресное пространство для “своих” запросов (см.
“Адресация
ресурсов”).
Обработка запроса Диспетчером
При обработке HTTP-запроса Диспетчер проверяет метод запроса (HEAD, GET,
POST) и считывает тело запроса (если тип содержимого text/plain или
application/json). После чего проверяет очередь обработчиков и
выбирает те, кто готов обработать запрос основываясь на данных
заголовков и адресе запроса. Отобранные обработчики запускаются
по-очереди в асинхронном режиме:
/** @type {TeqFw_Web_Back_Model_Address} */
const mAddress = spec['TeqFw_Web_Back_Model_Address$'];
/** @type {TeqFw_Web_Back_Server_Respond.respond405|function} */
const respond405 = spec['TeqFw_Web_Back_Server_Respond.respond405'];
/** @type {TeqFw_Web_Back_Api_Request_IHandler[]} */
const handlers = [];
async function onRequest(req, res) {
function isMethodAllowed(method) {}
async function parseBody(method, headers, req) {}
const {headers, method, url} = req;
if (isMethodAllowed(method)) {
await parseBody(method, headers, req);
const address = mAddress.parsePath(url);
// collect processors
const active = [];
for (const one of handlers) {
if (one.requestIsMine({method, address, headers})) {
active.push(one.getProcessor());
}
}
// run processors one by one
for (const one of active) {
await one(req, res);
}
} else {
respond405(res);
}
}
В общем случае обработчик не должен сам отправлять ответ — ему неизвестно, какие ещё обработчики есть в очереди после него. Так как результатом обработки запроса является сообщение, состоящее из заголовков и тела запроса, то каждый обработчик может добавить свои заголовки в res (через response.setHeader(name, value)), а тело ответа или имя файла для отправки клиенту сохранить в res[‘teqBody’] или в res[‘teqFile’] соответственно. Если обработчик считает, что его данных достаточно для формирования ответа, он добавляет код ответа в res[‘teqStatus’].
Таким образом, каждый обработчик из очереди может поучаствовать в
обработке запроса, добавить какие-либо данные в req-структуру (будут
доступны для следующих обработчиков) или посмотреть в res-структуре,
был ли запрос обработан предыдущими обработчиками и с каким результатом.
Финальный обработчик
В общем случае за формирование ответа на запрос отвечает финальный
обработчик — TeqFw_Web_Back_Handler_Final. Он проверяет, что запрос не
был обработан полностью каким-либо обработчиком (res.headersSent),
смотрит, что нужно отправить в качестве ответа (файл или текст),
формирует и отправляет ответ.
function process(req, res) {
if (!res.headersSent) {
const headers = res.getHeaders();
const statusCode = res[DEF.RES_STATUS] ?? HTTP_STATUS_OK;
const file = res[DEF.RES_FILE];
const body = res[DEF.RES_BODY];
let stat;
if (file) {
if (existsSync(file) && (stat = statSync(file)) && stat.isFile()) {
// …
const readStream = createReadStream(file);
res.writeHead(statusCode, headers);
pipeline(readStream, res);
}
} else if (body) {
res.writeHead(statusCode, headers);
res.end(body);
} else {
respond404(res);
}
}
}
Если запрос вообще не был обработан ни одним обработчиком, то финальный обработчик отправляет клиенту ответ со статусом 404 (Not Found).
Доступные обработчики
На момент написания публикации в плагине @teqfw/web определены
следующие обработчики:
TeqFw_Web_Back_Handler_SSE: для использования Server Sent Events;TeqFw_Web_Back_Handler_WAPI: Web API, позволяющий клиенту получать JSON-данные через GET-запросы или обмениваться с сервером JSON-данными через POST-запросы;TeqFw_Web_Back_Handler_Upload: загрузка файлов на сервер;TeqFw_Web_Back_Handler_Static: раздача статики;TeqFw_Web_Back_Handler_Final: финальный обработчик;
Обработчики задают пространства
адресов,
очерчивая свою “зону ответственности” (sse, api, upload, src и
web):
{
"@teqfw/web": {
"handlers": {
"TeqFw_Web_Back_Handler_Static": {
"spaces": ["src", "web"]
}
}
}
}
Модель адреса в платформе (TeqFw_Web_Back_Model_Address) способна
определять, к какому пространству относится запрашиваемый ресурс:
/** @type {TeqFw_Web_Back_Model_Address} */
const mAddress = await container.get('TeqFw_Web_Back_Model_Address$');
// …
const address = mAddress.parsePath(url);
if (address?.space === 'api') { /* … */ }
Раздача статики
За раздачу статики отвечает обработчик TeqFw_Web_Back_Handler_Static.
Вся статика делится на две группы:
- пространство
src: исходные кодыnpm-пакетов из./node_modules/; - пространство
web: произвольные файлы из каталогов./web/teq-плагинов;
Пространство src
Каждый teq-плагин прописывает в дескрипторе ./teqfw.json настройки
для автозагрузки кода контейнером
объектов.
Например, для teq-плагина с именем @vnd/pkg указана такая настройка
автозагрузки:
{
"@teqfw/di": {
"autoload": {
"ns": "Vnd_Pkg",
"path": "./sources"
}
}
}
Обработчик статики использует эту настройку, чтобы в пространстве
./src/ предоставлять доступ к исходникам. Например, адрес:
https://host.com/src/@vnd/pkg/Path/To/Module.mjs
соответствует такому пути к файлу внутри проекта:
./node_modules/@vnd/pkg/sources/Path/To/Module.mjs
Если npm-пакет не является teq-плагином (нет дескриптора
./teqfw.json в корне пакета), то для таких пакетов маппинг исходников
можно указать вручную в дескрипторе того плагина, который использует
этот npm-пакет. Вот, например, маппинг статики для плагина
@teqfw/vue :
{
"@teqfw/web": {
"statics": {
"/vue-router/": "/vue-router/dist/",
"/vue/": "/vue/dist/"
}
}
}
Таким образом, подобный маппинг позволяет перенаправить все адреса:
https://host.com/src/vue/… на файловую систему:
./node_modules/vue/dist/…
Пространство web
Каждый teq-плагин может иметь в корне каталог ./web/, всё содержимое
которого может быть доступным через web-сервер. Обработчик статики при
инициализации сканирует все teq-плагины и составляет карту для доступа
к таким ресурсам. Так, адрес:
https://host.com/web/@vnd/pkg/css/style.css
соответствует такому пути к файлу внутри проекта:
./node_modules/@vnd/pkg/web/css/style.css
Web API (сервисы)
Обработчик TeqFw_Web_Back_Handler_WAPI отвечает за пространство
./api/ и реагирует на GET&POST методы. Если POST-запрос содержит
какие-то данные, то ожидается, что это будет валидный JSON.
Сервисы внутри api-пространства адресуются маршрутами (routes,
endpoints) без учёта метода запроса (один и тот же сервис и для GET,
и для POST). Каждый teq-плагин определяет собственное
подпространство в соответствии со своим npm-именем и уже внутри
собственного подпространства определяет имя маршрута, по которому
располагается соответствующий wapi-сервис:
http://localhost/api/@teqfw/web/load/namespaces
Результатом работы сервиса также является JSON.
WAPI-сервисы teq-плагина регистрируются в его дескрипторе:
{
"@teqfw/web": {
"wapi": [
"TeqFw_Web_Back_WAPI_Load_Config",
"TeqFw_Web_Back_WAPI_Load_FilesToCache",
"TeqFw_Web_Back_WAPI_Load_Namespaces"
]
}
}
Подробное описание данного обработчика выходит за рамки этой статьи и требует отдельной публикации.
Server Sent Events и Files Upload
Эти обработчики принципиально реализованы, но на данной стадии создавать
описание было бы преждевременным. В SSE непонятен механизм группового
использования канала связи с клиентом (когда различные teq-плагины
пишут в канал свои события) — сейчас возможен только один сервис на
канал. А в upload-обработчике пока непонятно, каким образом, без
аутентификации и авторизации, обеспечить ограниченный доступ к загрузке
файлов на сервер.
Поэтому для отключения обработчиков, которые не предполагается
использовать в приложении, в дескрипторе ./teqfw.json приложения можно
прописать:
{
"@teqfw/web": {
"excludes": {
"handlers": ["TeqFw_Web_Back_Handler_Upload"]
}
}
}
Резюме
Web-сервер платформы TeqFW может запускаться в одном из трёх режимов:
- HTTP/1
- HTTP/2
- HTTPS
В нём реализованы обработка:
- статики;
- запросы к сервисам Web API;
- загрузка файлов на сервер;
- генерация событий сервером (Server Sent Events);
При необходимости возможно добавление дополнительных обработчиков (аутентификация, логирование и т.п.).