Найти в Дзене
Nuances of programming

Веб-сервер с нуля в TypeScript и Node

С помощью сокетов процессы на компьютере взаимодействуют друг с другом через файловую систему. Сокеты представляют собой особый тип файлов, предоставляющий процессам информацию для чтения и возможность записи с использованием обычного API файловой системы.
Оглавление

Источник: Nuances of Programming

Сокеты и TCP

С помощью сокетов процессы на компьютере взаимодействуют друг с другом через файловую систему. Сокеты представляют собой особый тип файлов, предоставляющий процессам информацию для чтения и возможность записи с использованием обычного API файловой системы. TCP — дополнительный стандарт для использования сокетов по сети для обеспечения связи между несколькими машинами. Этот стандарт обеспечивает работу HTTP, а вместе с ним и интернета.

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

Затем создается клиентский сокет для входящего соединения, который используется для передачи данных между сервером и клиентом. После того, как сервер завершает соединение с клиентом, он снова отправляет команду accept, и следующее соединение возвращается на сервер*.

*Ниже мы рассмотрим многопоточные серверы, которые работают с несколькими подключениями одновременно.

В этот момент вступает в силу концепция backlog — управляемая операционной системой очередь со всеми клиентами, которые предстоит обработать. При создании слушающего сокета необходимо определить допустимое количество клиентов в этой очереди. Это число является компромиссом между экономией ресурсов на создание очереди клиентов и отказом при максимальной загрузке.

Сокеты, Node и сеть

В стандартной библиотеке Node сокеты используются с помощью пакета net, который предоставляет возможность соединения с сокетами, чтение из и запись в них, а также запуска сервера. Мы начнем с создания серверного сокета и прослушивания IP и порта. listen будет отправлять сокету команду accept и прочитывать все полученные данные.

-2

Следующий шаг — выполнение действий при подключении клиента к сокету. Библиотека net во многом зависит от шаблона наблюдатель, известному всем разработчикам JavaScript. Таким образом, прослушивание входящих подключений выполняется путем прослушивания события connection:

-3

При каждом подключении клиента происходит обратный вызов с клиентским сокетом этого соединения в качестве параметра. С помощью этого сокета можно прочитывать данные, отправленные клиентом, и отправлять обратный ответ. Для этого используется событие data в сокете соединения, которому предоставляется обратный вызов, принимающий Buffer с отправленными клиентом данными. Мы также можем вызвать write в сокет для обратной отправки данных.

Последняя задача — закрытие (end) соединения после окончания работы. В противном случае может возникнуть переполнение открытых соединений. Эта логика не касается многокомпонентных тел и заголовка Keep-Alive. Они не являются необходимыми для написания простого и понятного сервера и могут быть добавлены позже.

-4

Это все, что нужно для работы с TCP-соединениями в Node. При вызове по адресу curl появится ответ. Однако открытие localhost:3000 в браузере не будет доступно, поскольку для этого нужно реализовать стандарт HTTP.

curl 127.0.0.1:3000
hello world%

HTTP

HTTP — стандарт для связи через TCP-сокеты. Он описывает способ форматирования сообщений и то, как сервер должен управлять соединениями. HTTP-сообщение от клиента к серверу (запрос) выглядит следующим образом:

GET / HTTP/1.1
Host: localhost:3000

Здесь мы видим больше знакомых концепций, используемых при вызовах API, например, с fetch. Сообщение начинается с глагола (метода): GET, затем находится URL, в данном случае домашняя страница /. При написании фреймворка за сервером он используется для маршрутизации, поиска верного контроллера и выполнения действий с ним. Последняя запись в первой строке — версия HTTP, которая используется для совместимости со старыми клиентами. Но сейчас мы не будем рассматривать этот момент.

Со второй строки появляются заголовки — пары ключ-значение, соединенные с запросом. Каждый из них находится на отдельной строке, а ключ отделяется от значения с помощью :. Последний заголовок сопровождается пустой строкой, после которой начинается тело запроса. Визуально это выглядит следующим образом:

-5

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

-6

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

-7

Из этой информации можно создать интерфейс Response и написать функцию, которая превращает его в строку перед отправкой обратно через сокет:

-8

При добавлении этого кода обратно в сервер мы получаем следующий фрагмент, который позволяет просматривать веб-сайт в браузере, обслуживаемый нашим собственным сервером! В HTTP есть еще множество элементов, например, куки. Они извлекаются из заголовка Cookie, что возможно осуществить в данной реализации. Настройка куки выполняется через заголовок Set-Cookie, который может встречаться в ответе несколько раз. Его нужно добавить в интерфейс Response, поскольку ATM не позволяет использовать несколько заголовков с одним и тем же именем.

-9

Теперь это настоящий сервер!
Теперь это настоящий сервер!

Многопоточные серверы

На данный момент наш сервер работает только на одном процессе. Это означает, что при обработке одного запроса все входящие соединения накапливаются в резерве. Увидеть этот процесс в действии можно, обновив код для вычисления последовательности Фибоначчи, равной 100, и отправки обратного ответа. Если вы откроете несколько вкладок в браузере и просмотрите вывод консоли, то увидите, что подключается только первая вкладка, а остальные остаются в очереди, пока сервер не завершит вычисление первого результата.

-11

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

В этой статье мы остановимся на использовании workerpool для выполнения тяжелых вычислений и сохранения всех подобных соединений в основном процессе. Workerpool позволяет сохранить пул фоновых воркеров в Node, которому можно передавать задачи для выполнения в фоновом режиме. Пул находит и присваивает воркер для выполнения работы. Функция пула exec возвращает промис, который разрешается при завершении задачи. Таким образом, обработка входящих соединений не блокируется во время вычисления последовательности Фибоначчи из 100 чисел:

-12

Теперь при открытии сайта в нескольких вкладках все соединения принимаются в терминале. Все вычисления выполняются различными воркерами без блокировки запросов из-за тяжелых задач.

Читайте также:

Читайте нас в телеграмме и vk

Перевод статьи Wim Jongeneel: A Web Server From Scratch in TypeScript and Node