TeqFW: Web server
Дата публикации: 2021-12-15Web-сервер на платформе 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);
При необходимости возможно добавление дополнительных обработчиков (аутентификация, логирование и т.п.).