Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
![](https://habrastorage.org/webt/s4/wy/rv/s4wyrvlooidescjouq7uwzztfsi.png)
ПО как услуга, инфраструктура как услуга, платформа как услуга, коммуникационная платформа как услуга, видеоконференции как услуга, а что насчет облачных игр как услуги? Уже было предпринято несколько попыток создания облачных игр (Cloud Gaming), например, Stadia, недавно запущенная компанией Google. Stadia не новичок в WebRTC, но могут ли другие использовать WebRTC так же?
Тхань Нгуен (Thanh Nguyen) решил проверить эту возможность на своем опенсорсном проекте CloudRetro. CloudRetro основан на Pion, популярной WebRTC библиотеке на базе Go (спасибо Шону из группы разработчиков Pion за помощь в подготовке этой статьи). В данной статье Тхань делает обзор архитектуры своего проекта, а также рассказывает, что полезного он узнал и с какими челленджами столкнулся во время работы.
Вступление
В прошлом году, когда Google анонсировал Stadia, мне просто снесло крышу. Идея настолько уникальна и инновационна, что я постоянно задавался вопросом, как такое вообще возможно при существующих технологиях. Желание лучше разобраться в этой теме побудило меня создать свою версию опенсорсной облачной игры. Результат был просто фантастический. Ниже я хотел бы поделиться процессом работы над моим годичным проектом.
TLDR: короткая слайд-версия с основными моментами
Почему за облачными играми будущее
Я верю, что Cloud Gaming скоро станет новым поколением не только игр, но и других областей информатики. Облачные игры – это вершина модели клиент/сервер. Такая модель максимизирует управление бэкэндом и минимизирует работу фронтенда за счет размещения игровой логики на удаленном сервере и потоковой передачи изображений/аудио клиенту. Сервер выполняет тяжелую обработку, поэтому клиент больше не зависит от аппаратных ограничений.
Google Stadia, по сути, позволяет играть в AAA-игры (т.е. высококлассные игры-блокбастеры) на интерфейсе вроде YouTube. Та же методология может быть применена и к другим тяжелым оффлайновым приложениям, таким как операционная система или 2D/3D графический дизайн и т.д. чтобы мы могли стабильно запускать их на устройствах с низкими техническими характеристиками на разных платформах.
![](https://habrastorage.org/webt/6a/kl/2c/6akl2cm7gcrwdeicgmrai2g6gqq.png)
Будущее этой технологии: представляете, если бы Microsoft Windows 10 работал в браузере Chrome?
Облачные игры технически сложны
Гейминг – одна из тех редких областей, где требуется постоянная быстрая реакция пользователя. Если изредка мы встречаемся с задержкой в 2 секунды при клике на странице, это приемлемо. Видеопотоки в прямом эфире, как правило, отстают на несколько секунд, но все равно предлагают достаточное удобство в использовании. Однако, если игра часто задерживается на 500 мс, играть просто невозможно. Наша цель – достичь чрезвычайно низкой задержки, чтобы разрыв между вводом и медиа был как можно меньше. Поэтому традиционный подход к потоковому видео здесь неприменим.
![](https://habrastorage.org/webt/n-/w5/wa/n-w5waro7q4wcpacxxkbm6seeza.png)
Общий шаблон облачной игры
Опенсорсный проект CloudRetro
Я решил создать тестовый образец облачной игры, чтобы проверить, возможно ли всё это при таких жестких сетевых ограничениях. Для проверки концепции я выбрал Golang, поскольку это наиболее знакомый мне язык и хорошо подходящий для данной реализации по многим другим причинам, как выяснилось позже. Go простой и развивается очень быстро; каналы в Go отлично подходят для управления многопоточностью.
Проект CloudRetro.io – облачный игровой сервис с открытым исходным кодом для ретро-игры. Цель проекта – привнести в традиционные ретро-игры наиболее комфортные игровые ощущения и добавить мультиплеер.
Подробно ознакомиться с проектом можно здесь: https://github.com/giongto35/cloud-game.
Функциональность CloudRetro
Для демонстрации всей мощи облачных игр в CloudRetro используются ретро-игры. Что позволяет получить множество уникальных игровых впечатлений.
- Портативность игры
- Мгновенное воспроизведение при открытии страницы; загрузка и установка не нужны
- Работает в мобильном браузере, так что для запуска не нужно никакое программное обеспечение
- Игровые сеансы можно совместно использовать на нескольких устройствах и хранить в облаке для следующего входа
- Игру можно стримить, а можно играть в нее сразу несколькими пользователями:
- Crowdplay типа TwitchPlayPokemon, только более кросплатформенный и более риалтаймовый
- Оффлайн игры в онлайне. Играть могут много пользователей без настройки сети. В Samurai Shodown теперь можно играть 2 игрокам по сети CloudRetro
Демо-версия многопользовательской онлайн-игры на разных устройствах
Инфраструктура
Требования и стек технологий
Ниже приведен список требований, которые я установил перед началом проекта.
1. Один игрок
Это требование может показаться не слишком важным и очевидным здесь, но это один из моих ключевых выводов, это позволяет облачным играм держаться как можно дальше от традиционных потоковых сервисов. Если мы сосредоточимся на однопользовательской игре, мы сможем избавиться от централизованного сервера или CDN, потому что нам не нужно делать потоковую передачу в массы. Вместо того, чтобы загружать потоки на поглощающий сервер или передавать пакеты на централизованный сервер WebSocket, сервисные потоки передаются пользователю напрямую через одноранговое соединение WebRTC.
2. Медиапоток с низкой задержкой
Читая про Stadia, я часто встречаю в некоторых статьях упоминание WebRTC. Я понял, что WebRTC – выдающаяся технология, и она прекрасно подходит для использования в облачных играх. WebRTC – это проект, который предоставляет веб-браузерам и мобильным приложениям связь в реальном времени через простой API. Он обеспечивает одноранговое соединение, оптимизирован для медиа и имеет встроенные стандартные кодеки, такие как VP8 и H264.
Я отдал предпочтение обеспечению максимально комфортной работы пользователей, а не сохранению высокого качества графики. В алгоритме допустимы некоторые потери. В Google Stadia есть дополнительный шаг по уменьшению размера изображения на сервере, и кадры масштабируются до более высокого качества перед передачей одноранговым узлам.
3. Распределенная инфраструктура с географической маршрутизацией
Вне зависимости от того, насколько оптимизирован алгоритм сжатия и код, сеть все равно является решающим фактором, который больше всего способствует задержке. Архитектура должна иметь механизм сопряжения ближайшего к пользователю сервера для сокращения времени приема-передачи (RTT). Архитектура должна иметь 1 координатора и несколько потоковых серверов, распределенных по всему миру: Запад США, Восток США, Европа, Сингапур, Китай. Все потоковые серверы должны быть полностью изолированы. Система может регулировать свое распределение, когда сервер присоединяется к сети или выходит из нее. Таким образом, при большом трафике, добавление дополнительных серверов позволяет осуществлять горизонтальное масштабирование.
4. Браузерная совместимость
Облачные игры предстают в наилучшем свете, когда требует от пользователей по минимуму. Это значит, что есть возможность запуска в браузере. Браузеры помогают сделать игровой процесс максимально комфортным для пользователей, избавив их от установки программного и аппаратного обеспечения. Браузеры также помогают обеспечить кросс-платформенность для мобильных и десктопных версий. К счастью, WebRTC отлично поддерживается в различных браузерах.
5. Четкое разделение игрового интерфейса и сервиса
Я рассматриваю сервис облачных игр как платформу. У каждого должна быть возможность подключать к платформе что угодно. Сейчас я интегрировал LibRetro с сервисом облачных игр, потому что LibRetro предлагает красивый интерфейс игрового эмулятора для ретро-игр, таких как SNES, GBA, PS.
6. Комнаты для мультиплеера, crowd play и внешнее связывание (deep-link) с игрой
CloudRetro поддерживает множество новых геймплеев, таких как CrowdPlay и Online MultiPlayer для ретро-игр. Если несколько пользователей откроют один и тот же deep-link на разных компьютерах, они увидят одну и ту же запущенную игру и даже смогут присоединиться к ней.
Более того, состояния игры хранятся в облачном хранилище. Это позволяет пользователям продолжать игру в любое время на любом другом устройстве.
7. Горизонтальное масштабирование
Как и любой SAAS в настоящее время, облачные игры должны быть спроектированы так, чтобы быть горизонтально масштабируемыми. Конструкция «координатор-воркер» позволяет добавлять больше воркеров, чтобы обслуживать больший трафик.
8. Нет привязки к одному облаку
Инфраструктура CloudRetro размещается на различных облачных провайдерах (Digital Ocean, Alibaba, пользовательский провайдер) для различных регионов. Я активирую запуск в контейнере Docker для инфраструктуры и настраиваю сетевые параметры с помощью bash-скрипта, чтобы избежать зависимости от одного облачного провайдера. Сочетая это с NAT Traversal в WebRTC, мы можем получить гибкость для развертывания CloudRetro на любой облачной платформе и даже на машинах любого пользователя.
Архитектурный дизайн
Воркер: (или потоковый сервер, упомянутый выше) множит игры, запускает кодирующий пайплайн и передает закодированное медиа пользователям. Инстансы воркера распространяются по всему миру, и каждый воркер может обрабатывать несколько пользовательских сессий одновременно.
Координатор: отвечает за сопряжение нового пользователя с наиболее подходящим воркером для потоковой передачи. Координатор взаимодействует с воркерами через WebSocket.
Хранилище игровых состояний: центральное удаленное хранилище для всех состояний игры. Это хранилище обеспечивает такие важные функции, как, например, удаленное сохранение/загрузка.
Верхнеуровневая архитектура CloudRetro
Пользовательский сценарий
Когда новый пользователь открывает CloudRetro на шагах 1 и 2, показанных на рисунке ниже, координатор вместе со списком доступных воркеров запрашивается на первую страницу. После этого на шаге 3 клиент рассчитывает задержки для всех кандидатов с помощью HTTP запроса ping. Этот список задержек затем отправляется обратно координатору, чтобы он мог определить наиболее подходящего воркера для обслуживания пользователя. На шаге 4 ниже создается игра. Между пользователем и назначенным воркером устанавливается потоковое соединение WebRTC.
Пользовательский сценарий после получения доступа
Что внутри воркера
Игровые и потоковые пайплайны хранятся внутри воркера изолированно и обмениваются там информацией через интерфейс. В настоящее время эта связь осуществляется посредством передачи данных в памяти по каналам Golang в том же процессе. Следующей целью является сегрегация, т.е. независимый запуск игры в другом процессе.
Взаимодействие компонентов воркера
Основные составляющие:
- WebRTC: клиентский компонент, принимающий пользовательский ввод и выводящий закодированное медиа с сервера.
- Игровой эмулятор: игровой компонент. Благодаря библиотеке Libretro система способна запускать игру внутри одного и того же процесса и внутренне перехватывать медиа и поток ввода.
- Внутриигровые кадры захватываются и отправляются в кодировщик.
- Изображение/аудио кодировщик: кодирующий пайплайн, который принимает медиакадры, кодирует их в фоновом режиме и выводит закодированные изображения/аудио.
Реализация
CloudRetro полагается на WebRTC как на магистральную технологию, поэтому перед тем, как углубиться в подробности реализации на Golang, я решил рассказать о самом WebRTC. Это потрясающая технология, которая сильно помогла мне в достижении задержки потоковой передачи данных равной всего лишь доле секунды.
WebRTC
WebRTC предназначен для обеспечения высококачественных одноранговых соединений на нативном мобильном приложении и в браузерах с помощью простых API.
NAT Traversal
WebRTC известен своей функциональностью NAT Traversal. WebRTC предназначен для одноранговой коммуникации. Его цель – найти наиболее подходящий прямой маршрут, избегая NAT-шлюзов и брандмауэров для одноранговой связи через процесс под названием ICE. В рамках этого процесса API WebRTC находят ваш публичный IP-адрес с помощью серверов STUN и переадресовывают его на сервер ретрансляции (TURN), когда прямое соединение не может быть установлено.
Однако CloudRetro не полностью использует эту возможность. Его одноранговые соединения существуют не между пользователями, а между пользователями и облачными серверами. Серверная часть модели имеет меньше ограничений на прямую связь, чем обычное пользовательское устройство. Это позволяет делать предварительное открытие входящих портов или использование публичных IP-адресов напрямую, так как сервер не находится за NAT.
Раньше я хотел превратить проект в платформу распространения игр для Cloud Gaming. Идея заключалась в том, чтобы позволить создателям игр предоставлять игры и потоковые ресурсы. А пользователи взаимодействовали бы с провайдерами напрямую. В такой децентрализованной манере CloudRetro является всего лишь средой для подключения сторонних потоковых ресурсов к пользователям, что делает его более масштабируемым, когда на нем больше не висит хостинг. Роль WebRTC NAT Traversal здесь очень важна для облегчения инициализации однорангового соединения на сторонних потоковых ресурсах, что упрощает подключение создателя к сети.
Сжатие видео
Сжатие видео – это незаменимая часть пайплайна, которая в значительной степени способствует плавности потока. Несмотря на то, что не обязательно знать все детали кодирования видео в VP8/H264, понимание концепции помогает разбираться в параметрах скорости потокового видео, отлаживать неожиданное поведение и настраивать задержку.
Сжатие видео для потокового сервиса является сложной задачей, потому что алгоритм должен гарантировать, что общее время кодирования + время передача по сети + время декодирования настолько мало, насколько это возможно. Кроме того, процесс кодирования должен быть последовательным и непрерывным. Некоторые взаимные уступки при кодировании не применимы – например, мы не можем предпочесть длительное время кодирования меньшему размеру файла и времени декодирования, или использовать непоследовательное сжатие.
Идея сжатия видео состоит в том, чтобы исключить ненужные биты информации, сохраняя при этом допустимый уровень точности для пользователей. Кроме кодирования отдельных статических кадров изображения, алгоритм делает вывод для текущего кадра из предыдущего и следующего, поэтому посылается только их разница. Как видно из примера c Pacman’ом, передаются только дифференциальные точки.
Сравнение видеокадров на примере Pacman
Сжатие аудио
Аналогичным образом, алгоритм сжатия звука опускает данные, которые не могут быть восприняты человеком. Opus на данный момент является аудиокодеком с наилучшей производительностью. Он разработан для передачи аудиоволны по протоколу упорядоченной датаграммы, такому как RTP (Real Time Transport Protocol – протокол передачи трафика реального времени). Его задержка меньше, чем у mp3 и aac, а качество выше. Задержка обычно составляет около 5~66,5 мс.
Pion, WebRTC в Golang
Pion – это проект с открытым исходным кодом, который перетаскивает WebRTC на Golang. Вместо обычного врапинга нативных C++ библиотек WebRTC, Pion является нативной Golang-реализацией WebRTC с лучшей производительностью, интеграцией с Go, а также контролем версий на протоколах WebRTC.
Библиотека также обеспечивает потоковую передачу данных с большим количеством отличных встроенных модулей с задержкой менее секунды. Она имеет свою собственную реализацию STUN, DTLS, SCTP и т.д. и некоторые эксперименты с QUIC и WebAssembly. Сама по себе эта опенсорсная библиотека является действительно хорошим источником обучения с отличной документацией, реализацией сетевых протоколов и классными примерами.
Комьюнити Pion, возглавляемое очень страстным создателем, довольно оживленное, там ведется много качественных дискуссий о WebRTC. Если вас интересует эта технология, присоединяйтесь к http://pion.ly/slack – вы узнаете много нового.
Написание CloudRetro на Golang
Реализация воркера на Go
Каналы Go в действии
Благодаря красивому дизайну каналов Go, проблемы потоковой передачи событий и параллелизма значительно упрощаются. Как и на диаграмме, в разных GoRoutines параллельно работают несколько компонентов. Каждый компонент управляет своим состоянием и общается по каналам. Выборочное утверждение Golang заставляет обработать по одному атомарному событию каждый момент времени в игре (game tick). Это означает, что для такого дизайна блокировка не нужна. Например, когда пользователь сохраняется, требуется полный снэпшот состояния игры. Это состояние должно оставаться непрерывным, выполняя вход до тех пор, пока сохранение не будет завершено. Во время каждого game tick’а бэкэнд может обрабатывать только операцию сохранения или ввода, что делает процесс потокобезопасным.
func (e *gameEmulator) gameUpdate() { for { select { case <-e.saveOperation: e.saveGameState() case key := <-e.input: e.updateGameState(key) case <-e.done: e.close() return } } }
Fan-in / Fan-out
Этот шаблон Golang отлично подходит для моего сценария использования CrowdPlay и Multiple Player. Следуя этому шаблону, все пользовательские входы в одной комнате встраиваются в центральный входной канал. Игровые медиа затем разворачиваются на всех пользователей в одной комнате. Таким образом, мы достигаем разделения состояния игры между несколькими игровыми сессиями разных пользователей.
Синхронизация между различными сеансами
Недостатки Golang
Golang не совершенен. Канал медленный. По сравнению с блокировкой канал Go – это просто более простой способ обработки параллельных и потоковых событий, но канал не дает наилучшей производительности. Под каналом есть сложная логика блокировки. Поэтому я внес некоторые коррективы в реализацию, повторно применив блокировки и атомарные значения при замене каналов для оптимизации производительности.
Кроме того, garbage collector в Golang неуправляем, из-за чего иногда возникают подозрительные длинные паузы. Это сильно мешает работе потокового приложения в реальном времени.
CGO
В проекте используется существующая VP8/H264 библиотека Golang с открытым исходным кодом для сжатия медиа и Libretro для игровых эмуляторов. Все эти библиотеки являются просто обертками библиотеки C в Go с использованием CGO. Некоторые из недостатков перечислены в этом посте Dave Cheney. Проблемы, с которыми я столкнулся:
- невозможность поймать краш в CGO, даже с помощью Golang RecoveryCrash;
- невозможность определить узкое место в производительности, когда мы не можем обнаружить детализированные проблемы в CGO.
Заключение
Я достиг своей цели – разобрался в облачных игровых сервисах и создал платформу, которая помогает играть в ностальгические ретро-игры с моими друзьями онлайн. Создание этого проекта было бы невозможным без библиотеки Pion и поддержки сообщества Pion. Я чрезвычайно благодарен за его интенсивное развитие. Простые API, предоставленные WebRTC и Pion, обеспечили плавную интеграцию. Мое первое доказательство концепции было выпущено на той же неделе, несмотря на то, что я заранее не знал об одноранговой связи (P2P).
Несмотря на простоту интеграции, P2P-потоковое вещание действительно является очень сложной областью в компьютерной науке. Ей приходится иметь дело со сложностью многолетних сетевых архитектур, таких как IP и NAT для создания одноранговой сессии. За время работы над этим проектом я накопил много ценных знаний о сети и оптимизации производительности, поэтому рекомендую всем попробовать построить P2P-продукты с помощью WebRTC.
CloudRetro обслуживает все сценарии использования, которые я ожидал, с моей точки зрения, как ретро-геймера. Тем не менее, я думаю, что есть много областей в проекте, которые я могу улучшить, например, сделать сеть более надежной и производительной, обеспечить более высокое качество графики игр, или возможность делиться играми между пользователями. Я упорно работаю над этим. Пожалуйста, следите за проектом и поддержите его, если он вам нравится.