Клиент-серверные приложения. В чем основные принципы разработки клиент-серверных игр?

«Клиент — сервер» (англ. client–server) — вычислительная или сетевая архитектура, в которой задания или сетевая нагрузка распределены между поставщиками услуг, называемыми серверами, и заказчиками услуг, называемыми клиентами. Фактически клиент и сервер — это программное обеспечение. Обычно эти программы расположены на разных вычислительных машинах и взаимодействуют между собой через вычислительную сеть посредством сетевых протоколов, но они могут быть расположены также и на одной машине. Программы-серверы ожидают от клиентских программ запросы и предоставляют им свои ресурсы в виде данных (например, загрузка файлов посредством HTTPFTPBitTorrentпотоковое мультимедиа или работа с базами данных) или в виде сервисных функций (например, работа с электронной почтой, общение посредством систем мгновенного обмена сообщениями или просмотр web-страниц во всемирной паутине). Поскольку одна программа-сервер может выполнять запросы от множества программ-клиентов, её размещают на специально выделенной вычислительной машине, настроенной особым образом, как правило, совместно с другими программами-серверами, поэтому производительность этой машины должна быть высокой. Из-за особой роли такой машины в сети, специфики её оборудования и программного обеспечения, её также называют сервером, а машины, выполняющие клиентские программы, соответственно, клиентами.

Роль клиента и сервера

Характеристика клиент-сервер описывает отношения взаимодействующих программ в приложении. Серверный компонент предоставляет функцию или услугу одному или нескольким клиентам, которые инициируют запросы на такие услуги. Серверы классифицируются по предоставляемым ими услугам. Например, веб-сервер обслуживает веб-страницы, а файловый сервер обслуживает компьютерные файлы. Общий ресурс может быть любой из программного обеспечения и электронных компонентов компьютера — сервера, от программ и данных в процессорах и запоминающих устройств. Совместное использование ресурсов сервера представляет собой услугу.

Является ли компьютер клиентом, сервером или и тем, и другим, определяется характером приложения, которому требуются сервисные функции. Например, на одном компьютере могут одновременно работать веб-серверы и программное обеспечение файлового сервера, чтобы обслуживать разные данные для клиентов, отправляющих различные типы запросов. Клиентское программное обеспечение также может взаимодействовать с серверным программным обеспечением на том же компьютере. Связь между серверами, например, для синхронизации данных, иногда называется межсерверной.

Взаимодействие клиента и сервера

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

Клиенты и серверы обмениваются сообщениями в шаблоне запрос-ответ. Клиент отправляет запрос, а сервер возвращает ответ. Этот обмен сообщениями является примером межпроцессного взаимодействия. Для взаимодействия компьютеры должны иметь общий язык, и они должны следовать правилам, чтобы и клиент, и сервер знали, чего ожидать. Язык и правила общения определены в протоколе связи. Все протоколы клиент-серверной модели работают на уровне приложений. Протокол прикладного уровня определяет основные шаблоны диалога. Чтобы ещё больше формализовать обмен данными, сервер может реализовать интерфейс прикладного программирования (API). API — это уровень абстракции для доступа к сервису. Ограничивая связь определённым форматом контента, он облегчает синтаксический анализ. Абстрагируя доступ, он облегчает межплатформенный обмен данными.

Сервер может получать запросы от множества различных клиентов за короткий период времени. Компьютер может выполнять только ограниченное количество задач в любой момент и полагается на систему планирования для определения приоритетов входящих запросов от клиентов для их удовлетворения. Чтобы предотвратить злоупотребления и максимизировать доступность серверное программное обеспечение может ограничивать доступность для клиентов. Атаки типа «отказ в обслуживании» используют обязанности сервера обрабатывать запросы, такие атаки действуют путем перегрузки сервера чрезмерной частотой запросов. Шифрование следует применять, если между клиентом и сервером должна передаваться конфиденциальная информация.

Вступление​Приведённая статья актуальная для всех современных многопользовательских игр по интернету, в том числе и для серии Counter-Strike.
Разработка онлайновых игр — не самое простое занятие. Режим мультиплеера ставит перед программистом совершенно новые проблемы, в основе которых лежат физика и человеческая природа.
Читерство. Именно с него всё начинается.
Разработчиков однопользовательских игр не особо волнует, честно играет человек или нет, так как действия читера не влияют на других игроков. Конечно, такая игра проходится не так, как планировалось, но это личное дело каждого.
Другое дело — мультиплеер, в основе которого лежит соревновательный режим. В нём действия читера влияют не только на него, но и на окружающих, что недопустимо. Ведь если одни играют честно, а другие — нет, то результат предсказуем. Более того, не получается реально конкурентной борьбы. Разработчику надо всеми силами избегать подобного состояния дел, так как наличие читеров отталкивает пользователей от игры.
Мы можем сделать многое для предотвращения нечестной игры. Главный принцип: нельзя доверять игроку. Всегда следует предполагать худшее — будет попытка сжульничать.

Авторитарные сервера и немые клиенты​Вышесказанный принцип приводит нас к простому решению — держать всё происходящее под своим контролем на центральном сервере, а клиенты будут выступать в роли привилегированных зрителей. Они смогут посылать на сервер команды(например, нажатие клавиши или выстрел), сервер будет информацию обрабатывать и отсылать на клиент ответ. Подобный сервер называется авторитарным, так как только он отвечает за состояние игрового мира.

В целом использование авторитарного сервера предотвращает ряд читов. Например, нельзя доверить клиенту количество жизней или уровень здоровья. Взломанный клиент отправит ложные данные что у него 10000% здоровья, однако сервер знает, что в реальности у игрока только 10%. Так что при следующем попадании игрок умрёт вне зависимости от того, что рисует клиент.
Нельзя доверить клиенту позицию игрока в мире.
Предположим, игрок находится в точке с координатами (10;10). Взломанный клиент говорит «Сейчас я на (10;10), а через секунду буду на (20;10)». Если сервер поверит, то игрок сможет переместиться сквозь стену или передвигаться быстрее остальных.
Правильным решением является высчитывание на стороне сервера. Клиент говорит «Я на (10;10), хочу переместиться вправо на 1 клетку». Сервер проверяет возможность перемещения, обновляет позицию игрока до (11;10) и отсылает сообщение «Вы на (11;10)».


Подводим первые итоги: игра полностью управляется сервером. Клиент посылает свои действия на сервер, тот периодически обновляет игровой мир в соответствии с полученной информацией и отсылает изменения обратно.

Работа с сетями

​Немой клиент отлично себя показывает в пошаговых играх типа шахмат, покера или Heroes Of Might and Magic. В шутерах немного иначе, результаты неплохи при игре по LAN, где у нас идеальная сеть, но при игре по сети Internet модель ломается.
Поговорим о физике. Предположим, вы находитесь в Сан-Франциско и подключились к серверу в Нью-Йорке. Расстояние между городами 4000 км или 2500 миль. Примерно как между Москвой и Лиссабоном. Ничто не может двигаться быстрее скорости света. Получается, что сигнал проделает путь в 4000 км примерно за 13 мс.
13 мс — звучит неплохо. Даже хорошо. Но это очень оптимистичный подход. В реальности сигнал проходит через разные сети серией прыжков, от роутера к роутеру. Далеко не везде сеть быстра. Где-то оборудование старо/перегружено и идут задержки, где-то пакеты теряются и отправляются повторно.
Условно считаем, что время между передачей данных от клиента к серверу равно не 13 мс, а 50 мс. Не самый худший сценарий, скорее даже лучший. А что если вы в Нью-Йорке, а сервер в Токио? Каково время передачи данных? 100мс, 200мс, 500мс? Совершенно недопустимые задержки.
Возвращаясь к нашему примеру, клиент отправил на сервер «Я нажал кнопку идти направо, иду вправо с (10;10) на 1». Через 50 мс сервер получит запрос. Предположим, что сервер идеально быстр и мгновенно обработает запрос. Ещё через 50 мс клиент получит ответ «Вы на (11;10)».
Игрок на экране видел следующее: он нажал стрелку вправо, но в течение 100мс(50+50) не происходило ничего. Затем игрок резко сместился на 1 клетку направо. Это и есть игровой лаг. Вроде как небольшой, но в то же время весьма заметный в онлайн-шутерах. И конечно же больший лаг, например, в полсекунды, неиграбелен.

Подводя итоги боя

Игры по интернету классные, но ставят перед разработчиком свои вызовы. Авторитарный сервер архитектурно хорош против большинства читов, однако подобная прямая, бесхитростная реализация делает игру неудобной.
Нам надо построить авторитарный сервер и в то же время минимизировать вред от задержек клиент-сервер, дабы приблизить игру по сети Internet к игре по LAN.

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


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

Клиентское предсказание​Некоторые игроки пытаются жульничать и отправляют некорректные данные. Тем не менее в основном отправляются верные запросы как от честных игроков, так и от читеров(далеко не все 100% запросов к серверу содержат хаки).
Большая часть присланной информации на сервер верна и отражается на игре так, как задумано. Если игрок находится в точке (10;10) и нажал кнопку «идти направо», то он закончит движение в (11;10).
Данный факт используется при условии детерминированности игры(мы может предсказать результат, если известно предыдущее игровое состояние и посланный на сервер набор команд).
Предположим, что у нас лаг 100мс, в идеале время перемещения игрока составит 100мс. При использовании наивной реализации время перемещения будет равно 200 мс.

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

На рисунке нет задержки между отправкой команды и получением ответа, притом что сервер всё ещё авторитарный. Если взломанный клиент пошлёт невозможные запросы(перелёт через стену, прыжок в небо), то они ни к чем не приведут. На экране читера будет рендер того, что он хочет, но в реальности на сервере и у других игроков иное положение дел.

Проблемы синхронизации

В предыдущем примере автор подобрал числа так, чтобы в учебном примере всё работало. Усложним сценарий: пусть лаг составляет 250мс, а анимация передвижения на 1 клетку вправо длится 100мс. Нажмём на кнопку вправо 2 раза, то есть сдвинемся направо на 2 клетки с точки (10;10). Произойдёт следующее:


​Мы столкнулись с интересной проблемой на t=250, когда пришло новое состояние мира. Клиент предугадал, что игрок в точке (12;10), а сервер говорит, что в (11;10). Так как сервер авторитарный, клиент перемещает игрока обратно на (11;10). Проходит 100мс, в момент времени t=350 клиент получает ответ, что настоящая координата (12;10) и передвигает игрока туда.
С точки зрения игрока происходит бред. Он нажал дважды на кнопку вправо, персонаж прыгнул на 2 клетки вправо, постоял 50мс, отошёл на 1 клетку влево, подождал 100мс и прыгнул на 1 клетку вправо.


Согласование

Ключом к пониманию проблемы является тот факт, что клиент видит мир в настоящем времени, но из-за лага обновления от сервера актуальны для прошлого времени. Сервер отправил на клиент информацию о состоянии игрового мира, но не успел получить ряд отправленных ему клиентских команд.
Пофиксить подобное вполне можно. Давайте к каждому запросу клиента добавлять порядковый номер. Нажал кнопку 1 раз — запрос №1. Нажал кнопку ещё раз — запрос №2. Ответ от сервера будет включать в себя номер последнего запроса.


Рис.6 Клиент-серверное предсказание + согласование с сервером​
В момент времени t = 250, сервер говорит “Отвечаю на запрос №1, вы в (11;10)”. Позиция становится (11;10).
Клиент хранит копии запросов, отправленных на сервер. Он знает, что сервер обработал и прислал ответ на запрос №1, поэтому удаляет копию запроса №1. Копия запроса №2 остаётся на клиенте, так как ответ ещё не получен. Также клиент знает, что должен прийти ответ на запрос №2.
Игрок переместился с (10;10) на (11;10). Таким образом, клиент может высчитать «более-менее настоящее» состояние игрового мира, основываясь на последних ответах от авторитарного сервера и собственном предсказании.
На t = 250 клиент получил “вы в (11;10), последний запрос №1”, удалил копию запроса №1, однако всё ещё хранит копию запроса №2, на который не было ответа.
Клиент сперва перемещает игрока на (11;10), а уже потом делает новое предсказание о движении на 1 клетку вправо.

Продолжим, в t = 350 от сервера пришло новое состояние мира: “ Вы в (12;10), последний запрос №2”. Клиент удаляет копию запроса №2 и обновляет позицию игрока до (12;10). Необработанных команд нет, процесс завершился корректно.


Промежуточные итоги

При использовании авторитарного сервера нам надо создать у игрока иллюзию мгновенного выполнения команд, пока клиент ждёт от сервера подтверждения запросов. Для этого клиент симулирует на экране результат. Когда от сервера приходит очередное состояние мира, клиент снова начинает делать предсказание.
В примере разговор о движении, однако такой же принцип можно применять ко всему. Например, в пошаговой стратегии, когда один игрок наносит урон другому игроку, можно сразу показать кровь и урон.
В то же время из-за сложностей с трансляцией клиенту верного состояния игры не стоит спешить делать ряд необратимых вещей до подтверждения их сервером. Например, обновлять уровень здоровья персонажей. Допустим, вы выстрелили и у врага здоровье упало до нуля. Стоит ли убивать оппонента на клиенте? Нет. Вдруг перед вашим выстрелом он применил аптечку, но сервер не успел вам об этом сказать?
Даже если мир целиком детерминированный(предсказуемый) и нет читеров, то всё равно есть вероятность, что предсказанное клиентом состояние игрового мира не совпадёт с присланным сервером. Подобный сценарий невозможен если в игре 1 человек, однако легко получается когда игроков несколько.

Частота обновления сервера

​До сего момента работа сервера была проста: читаем запрос от клиента, обновляем игровой мир и отправляем ответ.
Усложним сценарий: есть несколько игроков, очень быстро посылающих запросы на сервер. Зашли в игру и просто жмут все кнопки подряд. Обновлять игровой мир на каждое нажатие и отсылать клиенту результат непродуктивно, мы перегрузим процессор и забьём сеть.
Лучшим решением будет накапливать клиентские запросы по мере получения без исполнения. Игровой мир будем обновлять периодично с определённой частотой. Например, 10 раз в секунду. Задержка между обновлениями составляет 100мс, её называют временной шаг. Во время каждого обновления все накопленные команды применяются и новое состояние игры рассылается по клиентам.
Игровой мир обновляется с определённой частотой, не зависящей от количества клиентских запросов.

Что делать с редкими обновлениями?​С точки зрения клиента всё работает также как и раньше — клиент предсказывает состояние игрового мира и ждёт обновление. Однако теперь эти обновления слишком редки(раз в 100мс), клиент имеет весьма отрывочную информацию о состоянии других игроков.
На практике это приводит к движению соперников не плавно, а рывками, так как частота обновления игрового мира всего 10 раз в секунду.
Себя клиент отрисует плавно на основе клиентского предсказания, а вот других игроков уже нет.


В зависимости от типа игры есть много способов борьбы с этим, причём чем более предсказуема игра(=детерминирован мир), тем проще справиться.

Точный предрасчет. Экстраполяция
Допустим, вы разрабатываете гонки. Быстро едущие автомобили предсказуемы. Если машинка едет со скоростью 100 метров в секунду, то через секунду она будет примерно за 100 метров от точки старта.
Почему примерно? Потому что за 1 секунду машина может замедляться/ускоряться, поворачивать в разные стороны. Однако её манёвренность всё же ограничена высокой скоростью и направлением движения независимо от действий игрока. Например, нельзя мгновенно развернуться на 180 градусов.
Как вышесказанное работает на сервере, отправляющем обновления каждые 100мс? Клиент получает скорость и направление движения каждой машины. Следующее обновление придёт через 100мс, но что-то же надо показать игроку. Простейший метод — предположить, что направление движения машинки и скорость сохранятся неизменными, затем на основе предположения сделать предсказание и отрисовать игроку. Позже позиция автомобиля будет скорректирована в соответствии с очередным обновлением.
Величина коррекция зависит от многих факторов, может быть как весьма большой, так и незначительной. Если игрок едет по прямой и не меняет скорость, то предсказанная клиентом позиция совпадёт с истинной. С другой стороны, если игрок врежется в препятствие, предсказание окажется крайне неточным.
Экстраполяция применима к медленно меняющимся ситуациям — например, к движению грузового корабля.

Интерполяция

Экстраполяция неприменима в динамичных играх с постоянно меняющимся направлением движения — например, в 3D-шутерах. Игрок может резко остановиться, завернуть за угол, делая рассчёты на основе прошлой скорости и направления бессмысленными. + Вы не можете обновлять позицию игрока когда авторитарный сервер пришлёт данные. Игроки на экране будут просто телепортироваться каждые 100 мс.
У сервера есть информация о позиции игроков каждые 100мс. Трюк заключается в том, чтобы показать игроку то, что происходило между обновлениями в прошлом.
Допустим, сервер получил позицию в момент времени t=1000. У него также хранятся данные от t=900. Сервер точно знает, что реально происходило между t=900 и t=1000. Поэтому в отрезке времени с t=1000 до t=1100 следует показать то, что другие игроки делали между t=900 и t=1000.
Сервер показывает настоящее движение других игроков, но на 100мс раньше. Иными словами, на клиенте себя ты видишь в настоящем, а оппонентов — в прошлом. Таким образом, все игроки видят немного разные состояния игрового мира. Задержка незаметна глазу из-за частых обновлений от сервера.

​​
Интерполяция обычно работает достаточно хорошо. Если нет, то можно увеличить подробность данных, отправляемых с каждым обновлением, либо частоту обновлений.
Используя эту технологию, каждый игрок видит мир немного отличающимся от действительности, так как каждый игрок себя видит в настоящем, а остальных — в прошлом. Обновления раз в 100мс делают разницу незаметной, для движения вполне приемлемо.
Исключением является момент, когда требуется высокая точность по времени и пространству. Например, во время стрельбы. Другие игроки видны вам в прошлом, так? Представьте себе, что вы целитесь не в реальное положение игрока, а туда. где он был 100 мс назад! Если оппонент бежит, вы в него никогда не попадёте.

Промежуточный итог

​В клиент-серверной модели с авторитарным сервером, редкими обновлениями и задержками сети мы должны дать игроку иллюзию плавного передвижения. Во 2 части мы поняли как показать игроку движение, используя клиентское предсказание и согласование с сервером. Нажатие на кнопку игроком имеет мгновенный эффект только для конкретно этого игрока, так как предсказание убирает задержку между клиентом и сервером.
Другой проблемой было наличие 2 и более игроков.
Первое решение — интерполяция — основано на симуляции. Мы знаем позицию, скорость, направление, ускорение и можем заранее просчитать дальнейшее движение. Однако этот метод не работает если условия постоянно меняются.
Второе решение — интерполяция — не пытается предсказать. Вместо этого используется реальная информация от сервера, но окружающий мир показывается в недалёком прошлом, в то время как сам игрок — в настоящем.
Однако если на этом остановиться, то модель не заработает там, где нужна высокая точность по пространству и времени. Например в стрельбе.
Игроки будут постоянно промахиваться, так как станут стрелять в прошлое положение оппонента, а не в реальное.

Клиент-серверное взаимодействие выражается в следующем:

  • Сервер получает от клиентов запросы и времена их отправления
  • Сервер обрабатывает команды и обновляет игровой мир
  • Сервер отсылает клиентам состояние игрового мира с определённой периодичностью
  • Клиент посылает запрос + эмулирует успешное выполнение у себя, делая предсказание
  • Клиент получает обновления игрового мира и
    • Синхронизирует предсказанное состояние с авторитарным сервером
    • Интерполирует известные прошлые состояния остальных игроков

С точки зрения игрока здесь присутствуют две важные вещи:

  • Игрок видит себя в настоящем
  • Игрок видит других игроков в прошлом

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

Компенсация лага

Ты прицелился из своей снайперской винтовки в голову врага. Нажал на курок. Промах невозможен, голова точно в перекрестии прицела.
Но ты промазал. Как так?
Потому что видишь игрока в прошлом. Голова была в точке прицела 100мс назад. Это на клиенте противник там, сервер считает иначе.

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

Схема работы такова:

  • Когда ты стреляешь, клиент посылает на сервер точное время выстрела и точное положение прицела.
  • Здесь ключевой момент. Сервер получает от всех клиентов запросы с указанием времени, так что может реконструировать прошлое состояние игры. Можно восстановить мир таким, каков он реально был во время выстрела.
  • Это означает, что сервер знает в кого вы целились и когда. Он оставляет вас в настоящем, а весь игровой мир откатывает назад в прошлое на момент выстрела. Вы же видите всех оппонентов в прошлом, значит, и сервер должен посмотреть также, как вы.
  • Проводится расчёт, попал игрок или нет, после чего мир возвращается в настоящее и клиентам рассылаются обновления.

Все довольны!


Сервер доволен сам по себе. Он всегда доволен.
Игрок доволен потому что выстрелил и попал.
Враг разве что несчастен, но это уже проблемы скила.

Возможна ещё одна коллизия. Враг забегает за угол и по нему стреляет игрок. С точки зрения игрока оппонент не успел свернуть. Каждый же видит мир в прошлом, а себя в настоящем. Выстрел, сервер просчитал с точки зрения стрелявшего игрока и вынес решение о попадании. А вот с точки зрения оппонента он едва-едва успел забежать за угол, но тут несправедливо словил пулю.
Увы и ах, решения этой проблемы нет. Приходится идти на такой компромисс. Так как игрок застрелил врага в прошлом, то противник оказался убит после того, как спрятался.
Звучит нечетно, но лучше так, чем постоянные промахи в ситуациях, когда промахнуться невозможно.

Заключение​На этом рассказ закончен. Стрельба организована довольно хитро, но тем не менее разобраться в причинах иногда странного поведения игрового мира обычному человеку вполне реально, а геймерам даже полезно.

Автор оригинальной статьи gabrielgambetta

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *