Ввідна інформація
Робота веб-браузерів заснована на протоколі HTTP (протоколі запитів і відповідей без збереження стану). Спочатку він був призначений для обслуговування статичних веб-сторінок. HTTP працює поверх TCP, низкоуровневого протоколу, що гарантує надійну доставку і правильний порядок переданих по Інтернету даних.
Все це відмінно працювало багато років, але нещодавно веб-сайти стали більш інтерактивними і перестали відповідати парадигмі «запит-відповідь» протоколу HTTP. Для вирішення цієї проблеми винайдені сучасні веб-протоколи, такі як WebSockets, WebRTC, HTTP 2.0 і QUIC, що мають потенціал значного поліпшення інтерактивності мережі.
На жаль, новий комплект стандартів веб-розробки не відповідає потребам багатокористувацьких ігор або занадто складний в реалізації.
Це викликає розчарування у розробників ігор, адже вони просто хочуть мати можливість відправляти і приймати UDP-пакети через браузер.
Веб створений поверх TCP, який є протоколом зі збереженням порядку пакетів. Для надійної доставки даних в потрібному порядку в умовах втрати пакетів TCP повинен зберігати найновіші дані в черзі, чекаючи повторного відправлення загублених пакетів. В іншому випадку дані будуть доставлені в неправильному порядку.
Цей принцип називається блокуванням початку черги. Він створює дратівливу розробників і майже трагікомічну ситуацію. Найновіші дані, які їм потрібні, чекають повторної пересилки старих даних, але на момент отримання пересланих даних вони вже застарівають і стають марними.
На жаль, цей процес неможливо виправити в рамках TCP, в ньому всі дані повинні виходити безвідмовно і в потрібному порядку. Тому стандартним рішенням для ігрової індустрії протягом останніх 20 років була передача даних по UDP.
На практиці це означало, що для кожної гри розроблявся власний протокол поверх UDP, який реалізує весь необхідний функціонал, і відправляє більшу частину даних ненадійним способом без збереження порядку. Це забезпечувало максимально швидку доставку тимчасових даних без очікування повторної передачі втрачених пакетів.
А що ж потрібно зробити у випадку з веб-іграми?
Головна проблема веб-ігор сьогодні полягає в тому, що розробники ігор не мають можливості скористатися в браузері найкращим рішенням, обраним ігровою індустрією. Замість цього веб-ігри відправляють ігрові дані по TCP, що призводить до низької чуйності.
Використання TCP зовсім необов'язково, цю проблему можна було вирішити «по клацанню пальців», якби у веб-ігор з'явилася можливість відправляти і приймати UDP-пакети.
А що таке WebSockets?
WebSockets - це розширення протоколу HTTP, модифікуючу HTTP-з'єднання таким чином, що дані можуть передаватися в обидві сторони. При цьому не використовується стандартний патерн «запит-відповідь».
Така техніка дозволяє елегантно вирішити проблему веб-сайтів, яким потрібно відображати динамічно змінюється контент, тому що після установки websocket-з'єднання сервер може надсилати дані в браузер без відповідного запиту.
На жаль, оскільки WebSockets реалізований поверх TCP, дані все одно схильні до блокування початку черги.
Що таке QUIC?
QUIC - це експериментальний протокол, створений поверх UDP і розроблений як заміняє транспортного шару для HTTP. В даний час він підтримується тільки в Google Chrome.
Найважливіша риса QUIC - підтримка множинних потоків даних. Клієнт або сервер може неявним чином створювати нові канали, збільшуючи ідентифікатор каналу (channel id).
Концепція каналів забезпечує дві великі переваги:
- Дозволяє уникнути відправки запитів підтвердження з'єднання щоразу, коли створення нового запиту.
- Усуває блокування початку черги між непов'язаними потоками даних.
На жаль, хоч ми і усуваємо проблему блокування початку черги у окремих потоків, але вона як і раніше існує всередині кожного потоку.
Що таке WebRTC?
Зауважу, що WebRTC підтримує канал даних, який можна налаштувати на «ненадійний» режим, що дозволяє здійснювати через браузер ненадійну передачу даних без збереження порядку.
Причина полягає в тому, що в багатокористувацьких іграх існує тенденція переходу від передачі peer-to-peer до клієнт-серверної моделі. І хоча WebRTC дозволяє зручно відправляти ненадійні «безладні» дані з браузера в браузер, він терпить крах, коли потрібна передача даних між браузером і виділеним сервером.
Проблема виникає через надзвичайну складність WebRTC. Причини цієї складності зрозумілі: WebRTC в першу чергу був розроблений для обміну даними peer-to-peer між браузерами, тому для обходу NAT і передачі пакетів він в гіршому випадку вимагає підтримки STUN, ICE і TURN.
«Я відчував, що нам потрібна UDP-версія WebSockets. Це єдине, про що ми мріяли ».
Матеус Валадарес (Matheus Valadares), творець agar.io
Якщо коротко, то розробники ігор люблять простоту, і рішення типу «WebSockets for UDP» залучає їх набагато більше, ніж складність WebRTC.
Чому б просто не дозволити відправляти UDP?
Останній варіант вирішення проблеми - просто дозволити користувачам відправляти і отримувати UDP-пакети безпосередньо через браузер. Зрозуміло, це абсолютно жахлива ідея і є вагомі причини того, чому цього ніколи не можна допустити.
Яким може бути рішення?
Але що, якщо підійти з іншого кінця? Замість спроб навести мости від світу інтернету до ігор ми можемо почати з потрібних ігор технік і доопрацювати їх до рішення, добре працює в Інтернеті.
Мене звуть Гленн Фідлер (Glenn Fiedler), я займаюся розробкою ігор протягом останніх 15 років. Більшу частину цього часу я спеціалізувався в мережевому програмуванні. Я отримав величезний досвід, працюючи над динамічними екшн-іграми. Останньою грою, над якою я працював, була Titanfall 2.
Близько місяця я прочитав цю статтю на Hacker News: WebRTC: майбутнє веб-ігор.
У ній автор agar.io Матеус Валадарес розповідав, що WebRTC занадто складний для нього, і він продовжує використовувати в своїх іграх WebSockets.
Я задумався: адже напевно має бути більш просте рішення, ніж WebRTC?
Мені стало цікаво, як би виглядало таке рішення?
На мою думку рішення має володіти такими властивостями:
Я хочу представити мій варіант рішення. Чи не тішу себе ілюзіями, що він буде повністю прийнятий як стандарт для браузерів, я не веб-програміст, я пишу гри. Але я сподіваюся, що воно, по крайней мере, допоможе творцям браузерів і веб-розробникам побачити, що ж насправді потрібно клієнт-серверних ігор. Я хочу, щоб запропоноване мною рішення хоча б частково допомогло навести мости між іграми та Інтернетом.
Сподіваюся, в результаті ми отримаємо в найближчому майбутньому набагато кращу роботу багатокористувацьких браузерних ігор.
netcode.io
Рішення до якого я прийшов - це netcode.io
Він призначений для таких ігор, як agar.io. яким потрібно рознести гравців з основного веб-сайту на екземпляри виділених серверів. Кожен з серверів має обмеження на максимальну кількість гравців (в базовій реалізації - до 256 гравців на екземпляр сервера).
netcode.io виграє у WebRTC в простоті. У ньому застосовується схема тільки з виділеними серверами, тому використовувати ICE, STUN і TURN не потрібно. Завдяки реалізації шифрування, підписів і аутентифікації за допомогою libsodium він дозволяє уникнути складнощів повної реалізації DTLS, при цьому забезпечуючи той же рівень безпеки.
За минулий місяць я створив базову реалізацію netcode.io на C. Вона випущена під ліцензією BSD з трьох пунктів. За кілька місяців я сподіваюся вдосконалити цю реалізацію, написати специфікацію і попрацювати з іншими розробниками над портированием netcode.io на різні мови.
Як це працює
Токен підключення складається з двох частин:
При підключенні до виділеного сервера клієнт періодично відправляє по UDP пакет запиту на підключення. Цей пакет містить приватні дані токена підключення, а також додаткові дані для AEAD, наприклад, інформацію про версії netcode.io, ідентифікатор протоколу (64-бітове число, унікальне для кожної конкретної гри), тимчасову мітку терміну дії токена підключення і порядковий номер примітиву AEAD .
Коли виділений сервер отримує по UDP запит на підключення, він спочатку за допомогою примітиву AEAD перевіряє валідність вмісту пакета. Якщо якісь публічні дані в пакеті запиту на підключення були змінені, то перевірка сигнатури видасть помилку. Це не дозволить клієнтам змінювати тимчасову мітку терміну дії токена підключення, а також дозволить швидко відхиляти токени з закінченим терміном.
Сервер також перевіряє, чи не був токен підключення вже використаний, виконуючи пошук короткої історії HMAC токена. Якщо збіг знайдено, то запит на підключення ігнорується. Завдяки цьому один токен не можна використовувати для підключення декількох клієнтів.
Потім сервер перевіряє, чи є на сервері місце для клієнта. Кожен сервер підтримує певний максимум клієнтів. Наприклад, в грі на 64 гравців буде 64 місця для підключення клієнтів. Якщо сервер заповнений, він відповідає пакетом відмови на запит підключення. Це дозволяє клієнтам швидко дізнаватися про те, що сервер заповнений і потрібно переміститися на наступний сервер в списку.
Рандомизация ключа гарантує відсутність проблем з безпекою, що виникають при шифруванні токенов виклику декількох серверів одним порядковим числом (сервери не координуються між собою). Крім того, пакет виклику підключення значно менше пакета запиту на підключення, що дозволяє уникнути використання протоколу для DDoS-атак типу «посилення».
Коли сервер отримує пакет відповіді на підключення. він шукає відповідний запис про виклик клієнта, і якщо вона існує, він знову виконує пошук місця для підключення клієнта. Якщо вільних місць немає, він відповідає пакетом відмови на запит підключення. тому що місце, колишнє вільним на момент першого отримання запиту на підключення, вже зайнято.
В іншому випадку сервер призначає клієнту вільне місце на сервері і відповідає пакетом підтримки підключення. який повідомляє клієнту, що йому виділено місце на сервері. Таке місце називається індексом клієнта. У багатокористувацьких іграх він зазвичай використовується для ідентифікації клієнтів, підключених до сервера. Наприклад, клієнти 0, 1, 2, 3 в грі на чотирьох гравців відповідають гравцям 1, 2, 3 і 4.
Тепер сервер вважає, що клієнт підключений і що йому можна відправляти пакети корисного навантаження. У цих пакетах містяться дані, що відносяться до гри. Пакети доставляються без збереження порядку. Єдиний недолік такого способу в тому, що оскільки клієнт перед тим, як отримати індекс клієнта і переконатися в повному підключенні, він повинен спочатку отримати пакет підтримки підключення. а сервер відстежує, підтверджений чи клієнт, перевіряючи місце для кожного клієнта.
Прапор підтвердження для кожного клієнта спочатку має значення false і стає true, коли сервер отримує від клієнта пакет підтримки підключення або пакет корисного навантаження. Поки покупець не підтверджений, при кожній відправці пакета корисного навантаження цього клієнту попередньо відправляється і пакет підтримки з'єднання. Це гарантує статистичну вірогідність того, що клієнт знає свій індекс і буде повністю підключений до отримання першого пакету корисного навантаження, що мінімізує кількість циклів установки підключення.
Після того, як підключення клієнта і сервера повністю виконано, вони можуть обмінюватися UDP-пакетами в обох напрямках. Зазвичай ігрові протоколи відправляють введену гравцем інформацію від клієнта до сервера з великою швидкістю, наприклад, 60 разів на секунду, а стан світу від сервера до клієнта трохи рідше, наприклад, 20 разів на секунду. Однак в найсучасніших AAA-іграх швидкість відновлення даних сервера збільшена.
Якщо сервер або покупець не передає стабільний потік пакетів, то автоматично генеруються пакети підтримки підключення, щоб підключення не перервалося по тайм-ауту. Якщо протягом короткого проміжку часу, наприклад, п'яти секунд, з обох сторін не отримано жодного пакета, підключення обривається по тайм-ауту.
Якщо будь-яка зі сторін явно хоче перервати підключення, то відправляється надмірна кількість пакетів завершення підключення. для забезпечення високої статистичної ймовірності отримання пакетів навіть у разі їх часткової втрати. Це дозволяє швидко завершити підключення, щоб інша сторона не очікувала тайм-ауту.
висновок
У популярних веб-іграх типу agar.io передача даних здійснюється через WebSockets поверх TCP, оскільки в контексті клієнт-серверної структури з виділеними серверами WebRTC використовувати складно.
Один з варіантів рішень для Google - зробити інтеграцію підтримки каналів даних WebRTC для виділених серверів набагато простіший для розробників ігор.
Або ж можна використовувати netcode.io. застосовує набагато простіше рішення типу «WebSockets для UDP». Якщо стандартизувати його і вбудувати в браузери, це теж може вирішити проблему.
Гленн Фідлер (Glenn Fiedler) - засновник і президент The Network Protocol Company. Він надає послуги з налаштування мережевої частини ігор. До заснування компанії Гленн був провідним програмістом Respawn Entertainment, де працював над Titanfall 1 і 2.