Экспериментальная поддержка HTTP/3 уже встроена в основные браузеры и начинает потихоньку пробираться на сервера. А это значит, что уже можно полностью отказаться от использования в своих nodejs
-приложениях от http-библиотеки и переключиться на http2. Насколько же отличается реализация http2
-сервера от обычного http
-сервера?
Под катом пример простого web-приложения, выполняющего типовые задачи (получение статики GET'ом, upload файлов, POST-запросы, server sent events) на серверах HTTP/1 и HTTP/2. HTTP/2 Server Push в данном примере не затрагивался. Приложение не использует внешних зависимостей (npm
-пакетов), всё сделано при помощи собственного функционала nodejs
.
Описание web-приложения
Клиентская часть - это ./index.html и ./favicon.ico (куда ж без него!). Раздача статики серверами - обычный GET-запрос. Функционал клиента позволяет загрузить файл на сервер (upload), получить список файлов (SSE - сервер передаёт клиенту имя одного файла раз в 200 мсек.), удалить выбранные файлы с сервера (типовой POST-запрос).
Запросы клиента обслуживают HTTP/1 и HTTP/2 серверы на портах 4010 и 4020. Каждый сервер реализован в собственном файле (./http1.mjs и ./http2.mjs), общий код вынесен в библиотеку ./lib.mjs. Оба сервера используют один и тот же каталог для хранения загруженных файлов.
Чтобы попробовать приложение, нужно склонировать на свой компьютер github-репозиторий flancer64/habr_node_http_servers и запустить сервера:
$ node http1.mjs &
$ node http2.mjs &
Точки входа в приложение:
Так как для доступа к HTTP/2-серверу через браузер нужно использовать шифрование (https), а сертификат в тестовом приложении самоподписной, то при первом входе на HTTP/2-сервер высвечивается предупреждение браузера, которое можно игнорить.
Резюме
Внезапно оказалось, что в nodejs
для этих 4-функций (GET, POST, upload, SSE) различий на уровне кода между HTTP/1 и HTTP/2 нет. Вернее, единственное различие - это способ создания сервера. Вот код для HTTP/1:
import {createServer} from 'http';
import {onRequest} from './lib.mjs';
const server = createServer();
server.listen(4010);
server.on('error', (err) => console.dir(err));
server.on('request', onRequest);
А это код для HTTP/2:
import {createSecureServer} from 'http2';
import {readFileSync} from 'fs';
import {onRequest} from './lib.mjs';
const server = createSecureServer({
key: readFileSync('key.pem'),
cert: readFileSync('cert.pem')
});
server.listen(4020);
server.on('error', (err) => console.dir(err));
server.on('request', onRequest);
Всё, далее оба сервера используют один и тот же библиотечный код (функция onRequest
), реализующий backend-логику web-приложения.
Как бы, это довольно очевидно: HTTP/2 является развитием HTTP/1 и базируется на принципе "запрос - ответ":
В случае с SSE получается, что ответ состоит из фрагментов и растянут во времени:
Этот базовый принцип отличает протокол HTTP (полудуплекс - сначала передача, потом приём) от WebSocket, который является полнодуплексным (приём и передача информации осуществляется независимо):
Но для меня стало неожиданным, что между http
и http2
библиотеками в nodejs
есть такая качественная обратная совместимость - просто выноси бизнес-логику отдельно и используй её на http или http2-сервере по своему желанию.
"That's all I have to say about that" (c)
Послесловие
Эта публикация довольно быстро, за два-три дня, нахватала 8 минусов, после чего ушла из фокуса аудитории. Всё эту неделю я возился с web-серверами собственной платформы - @teqfw/web и @teqfw/http2. Ведь какой основной вывод из этой публикации? А вывод такой:
между http и http2 библиотеками в nodejs есть ... качественная обратная совместимость - просто выноси бизнес-логику отдельно и используй её на http или http2-сервере по своему желанию
В nodejs, какой бы npm
-пакет не использовало ваше приложение (express, koa, @hapi/hapi, @teqfw/web, @teqfw/http2 или любой другой), в основе всегда будет одна или несколько node-библиотек http, https, http2. А в силу архитектуры HTTP-протокола, вне зависимости от версии протокола и наличия шифрования, обработка HTTP-запросов в приложении сводится к вот этому:
/** @type {module:http.Server|module:https.Server|module:http2.Http2Server|module:http2.Http2SecureServer} */
let server;
/**
* @param {module:http.IncomingMessage|module:http2.Http2ServerRequest}req
* @param {module:http.ServerResponse|module:http2.Http2ServerResponse} res
*/
function onRequest(req, res) {}
server.on('request', onRequest);
Да, для любого ООП'шника указание типа объекта в виде:
/** @type {TypeA|TypeB|...} */
let obj;
выглядит бредово, но... это JavaScript, здесь так можно.
Как следствие, можно ожидать, что выбор типа запускаемого сервера (http, https, http2) для любого из npm
-пакетов (express, koa, ...) должен задаваться через конфигурационные опции. Что-то типа:
const Server = require('excellent-server');
const app = new Server({useHttp1: true, port: 3000});
или при запуске из командной строки:
$ ./bin/tequila.mjs help web-server-start
Usage: tequila web-server-start [options]
Start web server (HTTP/1 or HTTP/2).
Options:
-1, --http1 use HTTP/1 server (default: HTTP/2)
-p, --port <port> port to use (default: 3000)
-k, --key <path> private key in PEM format to secure HTTP/2 server.
-c, --cert <path> certificates chain in PEM format to secure HTTP/2 server.
Но... express вообще не работает с HTTP/2, а Koa и Hapi встраиваются в node'овские сервера, а не инкапсулируют их:
const http2 = require('http2');
const Koa = require('koa');
const app = new Koa();
http2.createServer(options, app.callback());
Т.е., разработчику нужно обернуть своим кодом эти сервера, чтобы их можно было запускать в HTTP/1 и HTTP/2 режимах по выбору.
В общем, по итогам этой публикации я пришёл к выводу, что на моей платформе нет необходимости в двух пакетах (@teqfw/web, @teqfw/http2). Весь функционал можно уложить в один пакет. Вся разница - это в том, какая из node-библиотек применяется для создания http
-сервера. А вот дальнейшая обработка запросов в большинстве случаев от способа создания сервера не зависит (ну, разве что Server Push не будет работать для HTTP/1).
А за что эта публикация получила минуса? В основном, за "Низкий технический уровень материала":
У меня нет претензий к человеку, который заминусил публикацию из-за личной неприязни. Это мировоззренческие расхождения, это нормально. Но "низкий технический уровень"?!
Express.js у которого в NPM под 20 млн. закачек за неделю не работает с HTTP/2, а в этой статье ссылка на код, который обрабатывает по HTPP/1, HTTP/2 (HTTPS) 4 вида запросов:
GET: раздача статики
POST: отправка данных на сервер для их обработки и получение результата;
upload: загрузка файлов на сервер (через потоки, а не через буферизацию в памяти!);
Server Sent Events;
Причём без применения каких-либо дополнительных npm
-пакетов. Только лишь средствами nodejs
. Этих 4 видов запросов вполне достаточно для того, чтобы на этом базисе строить собственные web
-приложения. Всё это уместилось в 240 строках backend-кода (JS) и 209 строках frontend-кода (HTML/CSS/JS). Сейчас в моде микросервисы, так вот, для них не нужны express/koa/hapi, которые тянут за собой 50/42/30 пакетов соответственно. Вполне хватает библиотек nodejs.
У меня вопрос к тем семерым, что заминусили публикацию за низкий технический уровень: в ваших nodejs-приложениях какие web-сервера используются и какие версии HTTP поддерживаются?