Всем привет!

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

Последние несколько лет я использую исключительно облачную инфраструктуру для запуска проектов, это не только позволяет существенно экономить финансовые ресурсы на этапе разработки, но и дает возможность в дальнейшем легко масштабировать мощности по мере роста нагрузки на сервера в процессе эксплуатации. По этой причине я решил, что можно пропустить описание базовых настроек сервера, по сути в моем случае они сводятся к закрытию всех портов за исключением HTTP/HTTPS (80/443) и SSH (22), доступ к которому в свою очередь разрешен только с определенных IP адресов, так как ходим в облака мы исключительно используя собственный VPN.

В сети достаточно статей о том, как настроить домен, сгенерировать и установить сертификат безопасности, а также настроить проксирование трафика на бекенд, по этому вдаваться в подробное описание этого процесса в рамках этой статья я тоже не вижу смысла. К тому же почти у всех облачных провайдеров уже давно существуют системы позволяющие автоматизировать все эти процессы и управлять целыми кластерами серверов через Managed Kubernetes и подобные инструменты.

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

Ниже представлена схема, показывающая основные части архитектуры.

схема по которой строилась серверная часть проекта
схема по которой строилась серверная часть проекта

Для этого проекта я решил использовать проверенную временем конфигурацию. На входе у нас будет прокси-сервер NGINX, который позволит мне использовать HTTP2 c TLS и он же будет проксировать все запросы на Node.js приложение.

Красным цветом на схеме отмечены пути трафика который закрыт TLS и позволяет безопасно обмениваться данными через интернет с клиентом, объектным хранилищем и базами данных. Зеленым цветом отмечен внутренний трафик который не использует шифрования так как не выходит за периметры облачного инстанса или контейнера.

Node.js приложение запускается в режиме кластера с использованием замечательного менеджера процессов PM2, который позволяет снять огромное количество вопросов связанных с эксплуатацией, начиная от запуска процессов в различных режимах и средах со своими значениями переменных окружения до развертывания и мониторинга целых кластеров состоящих из большого количества серверов. У него есть отличная панель управления с веб интерфейсом, которая позволяет наблюдать в режиме реального времени за всем что происходит с вашим приложением. Помимо этого вы можете настроить различные действия которые будут запущены на конкретном сервере или процессе, например можно очищать кеш или включать и выключать режим отладки в любое удобное время, изучать лог в режиме реального времени, добавлять любые кастомные метрики и много всего другого. Конечно же самое интересное требует оплаты, но поверьте, оно того стоит )

В завершении этой темы я хочу отметить, что данный подход отлично масштабируется в рамках одного сервера за счет увеличения используемых CPU/vCPU и запуска дополнительных процессов Node.js (он работает в однопоточном режиме) через PM2, а также можно собирать целые кластеры таких серверов вынося NGINX на отдельную машину или используя балансировщики нагрузки, которые предоставляют практически все облачные провайдеры. Таким образом можно наращивать огром��ую производительность системы. Конечно же по мере роста, будут возникать дополнительные сложности, которые сопровождают обслуживание любой системы с высокой нагрузкой, но в рамках этого проекта я так далеко не стал заглядывать.

Со схемой закончили, почти все блоки описаны, осталось собственно само Node.js приложение, оно же пресловутый "сервер".

Сервер я строил на базе замечательной библиотеки uWebSockets.js, которая "из коробки" поддерживает HTTP и WebSocket транспорт, что позволит мне использовать в разных частях приложения оптимальные варианты общения между клиентом и сервером.

Частый вопрос в личных сообщениях был о том почему я не использую только WebSocket, тем более, что у меня уже были проекты, которые построены исключительно только на этом транспорте и я обещал ответить на этот вопрос в статье. Так вот основное предназначение HTTP в этом проекте это безопасная передача refreshToken посредством HTTP-only cookie и загрузка необходимых ресурсов на этапе инициализации клиента. Первый снимает головную боль о способе хранения ключей на клиенте для сохранения авторизации при последующих сеансах работы приложения, а второй позволяет загружать необходимые данные без лишней нагрузки на сервер с возможностью переноса в последствии этой задачи на CDN, ведь я надеюсь, что игра найдет своих пользователей.

Теперь я хочу рассказать как устроено само приложение. Для наглядности я подготовил еще одну схему, которая отображает его архитектуру.

архитектура приложения
архитектура приложения

Как показано на схеме во главе всего у меня стоит uWebSockets.js, ее интерфейс предоставляет возможность обрабатывать HTTP запросы содержащие данные отправленные пользователем - заголовки (headers) и само тело сообщения (body). В функциях обрабатывающих эти сообщения, производится разбор заголовков, которые могут содержать ключи доступа (JWT), куки (cookies) и другую полезную информацию, которая необходима для корректной обработки запроса. На этом же уровне проводятся все базовые проверки поступающих данных - ключи доступа проверяются на соответствие подписи, тело сообщения преобразуется в JSON и так далее. Все запросы не прошедшие базовую проверку попросту игнорируются.

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

Функции всех модулей имеют один интерфейс для взаимодействия. По сути это параметры самой функции и их всего два - отправитель (sender) и данные (data), оба параметры - объекты, таким образом каждая функция получает всю необходимую информацию для обработки сообщения. Объект отправитель (sender) содержит данные о пользователе - его идентификатор, роль в системе, набор кук, разложенный в виде объекта ключ-значение и другую необходимую информацию. Данные (data) переданные в функцию в свою очередь проходят проверку при помощи библиотеки UTP.js, о которой я упоминал в предыдущей статье. Она позволяет проверить типизацию, определить наличие обязательных полей, убрать все лишнее и получить объект строго соответствующий описанию схемы данных для данной конкретной функции. Здесь думаю имеет смысл рассказать немного подробнее, в чем плюс. На стороне сервера, мы один раз описываем в рамках протокола все схемы данных, которыми обмениваются сервер и клиент, а затем используем эти схемы для проверки соответствия данных этим описаниям и для кодирования/декодирования бинарных пакетов которыми обмениваются в последствии клиент и сервер.

Все это в целом описывает процесс обработки сообщений и для HTTP транспорта и для WebSocket, за исключением одного момента. WebSocket это не HTTP, для создания соединения браузер отправляет специальный запрос который сообщает серверу, что клиент желает установить WebSocket соединение и в этом случае uWebSockets.js предоставляет нам возможность обработать это сообщение также как обычный HTTP запрос и провести процесс подключения клиента к сокету. Кстати для тех из вас, кто не до конца понимает что такое сокеты и как вообще работает сеть, я рекомендую прочитать наверное одну из лучших технических книг, которые мне доводилось читать, ее автор Pieter Hintjens, CEO компании iMatix. Так вот в процессе обработки этого пакета мы имеем возможность авторизовать пользователя также, как это происходит при обычном HTTP запросе. И здесь у меня будет вопрос к аудитории. Дело в том, что WebSocket соединение не может быть установлено из браузера с передачей дополнительных заголовков, таких как Authorization и каким тогда, на ваш взгляд, наилучшим образом можно реализовать передачу токена? Поделитесь своим мнением в комментариях, я думаю это будет интересно.

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

На этом описание архитектуры сервера я заканчиваю, а в следующей статье хочу рассказать о модулях, которые содержат логику обработки поступающих сообщений. Голосуйте за модули которые интересуют вас в первую очередь.

Посмотреть текущее состояние проекта и поиграть можно перейдя по ссылке.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
О каких модулях вам интересно узнать в первую очередь
28%Авторизация7
0%Аккаунт0
4%Ресурсы1
48%Игра12
0%Звонок0
20%Боты5
Проголосовали 25 пользователей. Воздержались 5 пользователей.