Источник: Nuances of Programming
Сокеты и TCP
С помощью сокетов процессы на компьютере взаимодействуют друг с другом через файловую систему. Сокеты представляют собой особый тип файлов, предоставляющий процессам информацию для чтения и возможность записи с использованием обычного API файловой системы. TCP — дополнительный стандарт для использования сокетов по сети для обеспечения связи между несколькими машинами. Этот стандарт обеспечивает работу HTTP, а вместе с ним и интернета.
Сокеты работают по системе клиент-сервер. Первое, что происходит при запуске сервера, это создание прослушивающего сокета. Он настроен с IP и портом и представляет собой специальный сокет, который используется для связи между ОС и сервером, а не между сервером и клиентами. После создания прослушивающего сокета сервер отправляет ему команду accept. ОС отвечает попыткой подключиться к IP-адресу и возвращается на сервер.
Затем создается клиентский сокет для входящего соединения, который используется для передачи данных между сервером и клиентом. После того, как сервер завершает соединение с клиентом, он снова отправляет команду accept, и следующее соединение возвращается на сервер*.
*Ниже мы рассмотрим многопоточные серверы, которые работают с несколькими подключениями одновременно.
В этот момент вступает в силу концепция backlog — управляемая операционной системой очередь со всеми клиентами, которые предстоит обработать. При создании слушающего сокета необходимо определить допустимое количество клиентов в этой очереди. Это число является компромиссом между экономией ресурсов на создание очереди клиентов и отказом при максимальной загрузке.
Сокеты, Node и сеть
В стандартной библиотеке Node сокеты используются с помощью пакета net, который предоставляет возможность соединения с сокетами, чтение из и запись в них, а также запуска сервера. Мы начнем с создания серверного сокета и прослушивания IP и порта. listen будет отправлять сокету команду accept и прочитывать все полученные данные.
Следующий шаг — выполнение действий при подключении клиента к сокету. Библиотека net во многом зависит от шаблона наблюдатель, известному всем разработчикам JavaScript. Таким образом, прослушивание входящих подключений выполняется путем прослушивания события connection:
При каждом подключении клиента происходит обратный вызов с клиентским сокетом этого соединения в качестве параметра. С помощью этого сокета можно прочитывать данные, отправленные клиентом, и отправлять обратный ответ. Для этого используется событие data в сокете соединения, которому предоставляется обратный вызов, принимающий Buffer с отправленными клиентом данными. Мы также можем вызвать write в сокет для обратной отправки данных.
Последняя задача — закрытие (end) соединения после окончания работы. В противном случае может возникнуть переполнение открытых соединений. Эта логика не касается многокомпонентных тел и заголовка Keep-Alive. Они не являются необходимыми для написания простого и понятного сервера и могут быть добавлены позже.
Это все, что нужно для работы с 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, которая используется для совместимости со старыми клиентами. Но сейчас мы не будем рассматривать этот момент.
Со второй строки появляются заголовки — пары ключ-значение, соединенные с запросом. Каждый из них находится на отдельной строке, а ключ отделяется от значения с помощью :. Последний заголовок сопровождается пустой строкой, после которой начинается тело запроса. Визуально это выглядит следующим образом:
Парсинг строки запроса, представленной выше, можно выполнить с помощью магической строки, показанной ниже. С помощью этой функции можно вызывать контроллеры по системе, схожей с написанием конечных точек в бэкенд-фреймворке.
HTTP-ответ следует тем же принципам, что и HTTP-запрос. Основное отличие заключается в первой строке, где показан результат запроса вместо запрашиваемого ресурса. После этого находятся знакомые заголовки и тело.
Из этой информации можно создать интерфейс Response и написать функцию, которая превращает его в строку перед отправкой обратно через сокет:
При добавлении этого кода обратно в сервер мы получаем следующий фрагмент, который позволяет просматривать веб-сайт в браузере, обслуживаемый нашим собственным сервером! В HTTP есть еще множество элементов, например, куки. Они извлекаются из заголовка Cookie, что возможно осуществить в данной реализации. Настройка куки выполняется через заголовок Set-Cookie, который может встречаться в ответе несколько раз. Его нужно добавить в интерфейс Response, поскольку ATM не позволяет использовать несколько заголовков с одним и тем же именем.
Многопоточные серверы
На данный момент наш сервер работает только на одном процессе. Это означает, что при обработке одного запроса все входящие соединения накапливаются в резерве. Увидеть этот процесс в действии можно, обновив код для вычисления последовательности Фибоначчи, равной 100, и отправки обратного ответа. Если вы откроете несколько вкладок в браузере и просмотрите вывод консоли, то увидите, что подключается только первая вкладка, а остальные остаются в очереди, пока сервер не завершит вычисление первого результата.
Это может стать серьезной проблемой при работе с приложением, в котором есть медленные конечные точки (например, функция экспорта). Они блокируют весь сервер и вызывают время простоя до завершения экспорта. Есть множество способов избежать этого с помощью модели параллелизма, которая позволяет серверу принимать входящие соединения во время работы конечной точки экспорта. Многие серверы производственного уровня помещают клиентские сокеты в очередь и позволяют другим потокам обрабатывать их.
В этой статье мы остановимся на использовании workerpool для выполнения тяжелых вычислений и сохранения всех подобных соединений в основном процессе. Workerpool позволяет сохранить пул фоновых воркеров в Node, которому можно передавать задачи для выполнения в фоновом режиме. Пул находит и присваивает воркер для выполнения работы. Функция пула exec возвращает промис, который разрешается при завершении задачи. Таким образом, обработка входящих соединений не блокируется во время вычисления последовательности Фибоначчи из 100 чисел:
Теперь при открытии сайта в нескольких вкладках все соединения принимаются в терминале. Все вычисления выполняются различными воркерами без блокировки запросов из-за тяжелых задач.
Читайте также:
Читайте нас в телеграмме и vk
Перевод статьи Wim Jongeneel: A Web Server From Scratch in TypeScript and Node