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

Асинхронное приложение для онлайн-игр... на 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 позволила нам в короткие сроки собрать прототип работающего приложения для совместных игр пользователей.



  • Разработка SPA: TypeScript или JavaScript — что выбрать?

    Разработка SPA: TypeScript или JavaScript — что выбрать?

    Разбираем сложность перехода, преимущества и недостатки.

  • Опыт разработки виджетов для сторонних сайтов

    Опыт разработки виджетов для сторонних сайтов

    Если ваш продукт предоставляет услуги для бизнеса, рано или поздно появится задача создать встраиваемый виджет для сайтов клиентов. Это может быть виджет покупки билетов, прогноза погоды, курса валют, отзывов, комментариев и много чего другого.

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