Наш заказчик из отрасли онлайн-игр (игр для вечеринок). Festa — игровая платформа в формате онлайн-конференции: https://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 позволила нам в короткие сроки собрать прототип работающего приложения для совместных игр пользователей.