Асинхронное приложение для онлайн-игр… на PHP. Серьёзно? Да!

Наш заказчик из отрасли онлайн-игр (игр для вечеринок). Festa — игровая платформа в формате онлайн-конференции: https://festa.games/.

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

Скриншот интерфейса платформы festa.games:

Разработчики поймут вопрос «… на PHP. Серьёзно?», заданный в заголовке.

Самое разумное решение такой задачи — использование WebSocket, протокола обмена сообщениями между браузером и веб-сервером в режиме реального времени. Для работы с вебсокет-сообщениями все обычно обращаются к Node.js. Использовать PHP в таком ключе боятся, т.к.у него нет встроенных механизмов для асинхронной работы.

Задача нашего заказчика была срочной, и у нас не было времени на изучение «ноды» и всего, что с ней связано. Поэтому мы разрабатывали с помощью библиотеки ReactPHP, с которой уже «собаку съели».

Было принято решение писать бэкенд на асинхронном PHP. В качестве реализации мы выбрали Ratchet, библиотеку для асинхронной обработки вебсокет-сообщений. «Под капотом» здесь как раз ReactPHP.

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

Как вели разработку и в чём её особенности

В «классическом» PHP каждый запрос обрабатывается отдельным процессом. Но Ratchet для сервера запускает только один процесс, который слушает все вебсокет-сообщения с порта, который мы указываем в настройках. Из-за этого при программировании сервера нужно учитывать ряд особенностей.

Вот основные из них.

1 — Память ограничена

Работая с одним постоянно запущенным процессом, нужно постоянно следить за большим объемом данных.

Для примера расскажем про список подключений.

Если их просто добавлять, не контролировать активность и другие важные параметры, то в какой-то момент можно упереться в нехватку памяти.

Из-за этого приложение будет работать медленно или вообще перестанет работать.

В «классическом» случае при разработке на PHP об этом даже не задумываешься. Создаётся процесс для PHP, обрабатывает запрос (например, сервер «отдаёт» страницу сайта), после чего процесс удаляется, очищая память.

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

Поэтому тут нужен более внимательный подход.

2 — Блокировки — это плохо

При работе асинхронного приложения нельзя использовать блокирующие функции.

Классический пример — запрос информации из базы данных. Если использовать стандартные функции для работы с БД, наш сервер сформирует запрос в БД, отправит его, и пока ответ не вернётся, все наши вебсокет-сообщения будут висеть. А тем временем пользователи не будут понимать, почему их сообщения никто не получил.

Из этого пункта вытекает следующая особенность программирования сервера.

3 — Нестандартная работа с базой данных

Как мы писали выше: блокировать ничего нельзя, а запросы к БД делать нужно.

Для решения данной проблемы мы используем асинхронного клиента для работы с БД PgAsync.

Основное отличие асинхронных приложений — сделав запрос, не стоит ожидать, что в следующей строчке кода (как это работает обычно) все данные у вас уже будут. Не будут. Тут на помощь приходят callback-функции, в которые «заворачиваем» всё, что хотим сделать при загрузке необходимых данных:

/** * Получение данных "классическим" способом */ private function getData(): void { $data = $this->getDataFromDB(); var_dump($data); // Сформированные данные из БД } /** * Получение данных асинхронно */ private function getDataAsync(): void { $onDataLoaded = function ($data) { var_dump($data); // Сформированные данные из БД }; $data = $this->getDataFromDBAsync($onDataLoaded); var_dump($data); // null }

4 — Фатальные ошибки фатальны

В целом, данное утверждение актуально для разработки на любом языке программирования и на любой технологии.

Проблема в Ratchet заключается в том, что любое исключение или ошибка сопровождаются падением «демона» с нашим сервером. Из-за чего все наши подключения к серверу будут разорваны, пользователям придётся каким-то образом переподключаться к серверу, а нам восстанавливать все данные.

В таких приложениях очень важно поддерживать качество кода:

  • соблюдать Code Style (у всех разработчиков должен быть настроен IDE с code style по PSR-12)
  • придерживаться Type Hinting (правильное описание и использование типов позволяет существенно уменьшить количество ошибок)
  • регламентировать PHPDoc (правильное документирование кода также позволяет уменьшить количество ошибок)

Всем понятно, написать на 100% чистый код очень сложно, поэтому периодически ошибки всё равно могут всплывать. Их нужно ловить.

Для отлова и минимизации ущерба от ошибок, которые всё-таки проскочили, мы используем:

  • Supervisor как инструмент управления процессами нашего сервера. Он запускает сервер отдельным процессом, и при его падении автоматически перезапускает. Все логи из потока вывода он складывает в файлы, в соответствии с конфигурацией.
  • Sentry как сервис мониторинга ошибок различных языков программирования, в том числе JavaScript и PHP. С его помощью можно отлавливать ошибки, возникающие как в браузере пользователя, так и на самом сервере.

У асинхронного сервера есть и плюсы!

1 — Быстрый ответ сервера

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

2 — Уменьшение нагрузки на базу данных

Получив данные из БД, можно хранить их в самом сервере, не делая повторных запросов. Однако тут проходит тонкая грань с пунктом про ограничение памяти, описанным выше. Поэтому нужно осознанно решать, что именно хранить на сервере, а за какими данными лучше лишний раз «сходить» в базу.

3 — Оптимизация цикла событий

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

Вывод

Не бойтесь пробовать новые технологии или применять для новых задач хорошо вам известные инструменты. На нашем примере оказалось, что писать асинхронные приложения даже на PHP вполне реально. И в этом есть даже преимущества. «Серьёзно?» — Серьёзно :)

Радует, что и сам язык развивается в этом направлении. Надеемся, Fibers в версии 8.1 дадут новый виток развития асинхронных приложений на PHP.

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

Автор статьи: Константин Василенко, backend-разработчик, Студия Валерия Комягина.

0
3 комментария
Роман Московский

Круто! А как зайти поиграть? Мне, как первому комментарию, код-приглашение будет?)

Ответить
Развернуть ветку
Valeri Komyagin

Привет, Роман! Спасибо большое за Ваш интерес!

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

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

Напишите нам пожалуйста на [email protected] - я предупредил нашего продуктолога, он обязательно пришлёт Вам инвайт, как только мы откроемся хотя бы для более широкого тестирования. 

Ответить
Развернуть ветку
Alexander D

Джоб сейфти девелопмент.
А вообще молодцы.

Ответить
Развернуть ветку
0 комментариев
Раскрывать всегда