Какой протокол используется в играх
Перейти к содержимому

Какой протокол используется в играх

  • автор:

О сетевой модели в играх для начинающих

image

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

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

В целом существует два основных типа сетевых архитектур: peer-to-peer и клиент-серверная. В архитектуре peer-to-peer (p2p) данные передаются между любыми парами подключенных игроков, а в клиент-серверной архитектуре данные передаются только между игроками и сервером.

Хотя архитектура peer-to-peer по-прежнему используется в некоторых играх, стандартом является клиент-серверная: она проще в реализации, требует канал меньшей ширины и облегчает защиту от читерства. Поэтому в этом руководстве мы сосредоточимся на клиент-серверной архитектуре.

В частности, нас больше всего интересуют авторитарные серверы: в таких системах сервер всегда прав. Например, если игрок думает, что находится в координатах (10, 5), а сервер говорит ему, что он в (5, 3), то клиент должен заменить свою позицию той, которую передаёт сервер, а не наоборот. Использование авторитарных серверов упрощает распознавание читеров.

В игровых сетевых системах есть три основных компонента:

  • Транспортный протокол: как передаются данные между клиентами и сервером.
  • Протокол приложения: что передаётся от клиентов серверу и от сервера клиентам и в каком формате.
  • Логика приложения: как передаваемые данные используются для обновления состояния клиентов и сервера.

Транспортный протокол

Первый шаг заключается в выборе протокола для транспортировки данных между сервером и клиентами. Для этого существует два Интернет-протокола: TCP и UDP. Но вы можете создать и собственный транспортный протокол на основе одного из них или применить библиотеку, в которой они используются.

Сравнение TCP и UDP

И TCP, и UDP основаны на IP. IP позволяет передавать пакет от источника получателю, но не даёт гарантий, что отправленный пакет рано или поздно попадёт к получателю, что он доберётся до него хотя бы раз и что последовательность пакетов придёт в правильном порядке. Более того, пакет может содержать только ограниченный размер данных, задаваемый величиной MTU.

UDP является всего лишь тонким слоем поверх IP. Следовательно, он имеет те же ограничения. В отличие от него, TCP обладает множеством особенностей. Он обеспечивает надёжное упорядоченное соединение между двумя узлами с проверкой на ошибки. Следовательно, TCP очень удобен и используется во множестве других протоколов, например, в HTTP, FTP и SMTP. Но все эти функции имеют свою цену: задержку.

Чтобы понять, почему эти функции могут вызывать задержку, надо разобраться, как работает TCP. Когда узел-отправитель передаёт пакет узлу-получателю, он ожидает получить подтверждение (ACK). Если спустя определённое время он не получает его (потому что пакет или подтверждение было утеряно, или по каким-то другим причинам), то отправляет пакет повторно. Более того, TCP гарантирует получение пакетов в правильном порядке, поэтому пока утерянный пакет не получен, все остальные пакеты не могут быть обработаны, даже если они уже получены узлом-получателем.

Но как вы наверно понимаете, задержка в многопользовательских играх очень важна, особенно в таких активных жанрах, как FPS. Именно поэтому многие игры используют UDP с собственным протоколом.

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

Итак, если TCP такой отстойный, то мы будем создавать свой транспортный протокол на основе UDP?

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

Во многих успешных играх, в том числе World of Warcraft, Minecraft и Terraria, используется TCP. Однако в большинстве FPS применяются собственные протоколы на основе UDP, поэтому ниже мы поговорим о них подробнее.

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

Чтобы подробнее узнать о различиях между UDP и TCP в контексте многопользовательских игр, можно прочитать статью Гленна Фидлера UDP vs. TCP.

Собственный протокол

Итак, вы хотите создать собственный транспортный протокол, но не знаете, с чего начать? Вам повезло, ведь Гленн Фидлер написал об этом две потрясающие статьи. В них вы найдёте множество умных мыслей.

Первая статья, Networking for Game Programmers 2008 года, проще, чем вторая, Building A Game Network Protocol 2016 года. Рекомендую вам начать с более старой.

Учтите, что Гленн Фидлер — большой сторонник использования собственного протокола на основе UDP. И после прочтения его статей вы наверняка переймёте у него мнение о том, что TCP имеет в видеоиграх серьёзные недостатки, и захотите реализовать собственный протокол.

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

Сетевые библиотеки

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

  • yojimbo Гленна Фидлера
  • RakNet, которая больше не поддерживается, но её форк SLikeNet похоже ещё активен.
  • ENet — это библиотека, созданная для многопользовательского FPS Cube
  • GameNetworkingSockets компании Valve

Транспортный протокол: заключение

Подведём итог: существует два основных транспортных протокола: TCP и UDP. TCP обладает множеством полезных особенностей: надёжность, сохранение порядка пакетов, обнаружение ошибок. У UDP всего этого нет, зато TCP по своей природе обладает повышенными задержками, недопустимыми для некоторых игр. То есть для обеспечения низких задержек можно создать собственный протокол на основе UDP или использовать библиотеку, реализующую транспортный протокол на UDP и адаптированную для многопользовательских видеоигр.

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

У меня есть два совета:

  • Максимально абстрагируйте транспортный протокол от остальной части приложения, чтобы его можно было легко заменить, не переписывая весь код.
  • Не занимайтесь преждевременной оптимизацией. Если вы не специалист по сетям и не уверены, нужен ли вам собственный транспортный протокол на основе UDP, то можете начать с TCP или библиотеки, обеспечивающих надёжность, а затем протестировать и измерить производительность. Если возникают проблемы и вы уверены, что причина заключается в транспортном протоколе, то возможно настало время создавать собственный транспортный протокол.

Протокол приложения

Теперь, когда мы можем обмениваться данными между клиентами и сервером, нужно решить, какие именно данные передавать и в каком формате.

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

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

Сериализация

Первым шагом будет преобразование данных, которые мы хотим отправить (ввод или игровое состояние), в подходящий для передачи формат. Этот процесс называется сериализацией.

В голову сразу приходит мысль использовать человекочитаемый формат, например JSON или XML. Но это будет совершенно неэффективно и впустую займёт большую часть канала.

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

Для сериализации данных можно использовать библиотеку, например:

  • FlatBuffers компании Google
  • Cap’n Proto компании Sandstorm
  • cereal Шейна Гранта и Рэндольфа Вурхиса

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

Сжатие

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

Битовая упаковка

Первая техника — это битовая упаковка. Она заключается в использовании ровно того количества битов, которое необходимо для описания нужной величины. Например, если у вас есть перечисление, которое может иметь 16 различных значений, то вместо целого байта (8 бит) можно использовать всего 4 бита.

Гленн Фидлер объясняет, как реализовать это, во второй части статьи Reading and Writing Packets.

Битовая упаковка особенно хорошо работает с дискретизацией, которая будет темой следующего раздела.

Дискретизация

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

Гленн Фидлер (опять!) показывает, как применять дискретизацию на практике, в своей статье Snapshot Compression.

Алгоритмы сжатия

Следующей техникой будут алгоритмы сжатия без потерь.

Вот, на мой взгляд, три самых интересных алгоритма, которые нужно знать:

  • Кодирование Хаффмана с заранее вычисленным кодом, которое чрезвычайно быстро и может давать хорошие результаты. Оно использовалось для сжатия пакетов в сетевом движке Quake3.
  • zlib — алгоритм сжатия общего назначения, который никогда не увеличивает объём данных. Как можно увидеть здесь, он применялся во множестве областей применения. Для обновления состояний он может оказаться избыточным. Но он может и пригодиться, если вам нужно отправлять клиентам с сервера ассеты, длинные тексты или рельеф.
  • Копирование длин серий — это, наверно, простейший алгоритм сжатия, но он очень эффективен для определённых типов данных, и может использоваться как этап предварительной обработки перед zlib. Он особенно подходит для сжатия рельефа, состоящего из тайлов или вокселей, в которых множество соседних элементов повторяется.

Дельта-сжатие

Последняя методика сжатия — это дельта-сжатие. Она заключается в том, что передаются только различия между текущим игровым состоянием и последним состоянием, полученным клиентом.

Впервые она была применена в сетевом движке Quake3. Вот две статьи, объясняющих способ её использования:

  • The Quake3 Networking Model Брайана Хука
  • Quake 3 Source Code Review: Network Model Фабьена Санглара [перевод статьи на Хабре, см. раздел «Сетевая модель»]

Шифрование

Кроме того вам может понадобиться шифровать передачу информации между клиентами и сервером. На это есть несколько причин:

  • приватность/конфиденциальность: сообщения могут быть прочитаны только получателем, и ни одному другому лицу, выполняющему сниффинг сети, не удастся их прочесть.
  • аутентификация: человек, желающий исполнять роль игрока, должен знать его ключ.
  • предотвращение читерства: злонамеренным игрокам будет намного сложнее создавать собственные пакеты для читерства, им придётся воспроизводить схему шифрования и находить ключ (который меняется при каждом соединении).

Протокол приложения: заключение

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

Логика приложения

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

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

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

Техники сглаживания задержек

Все описанные в этом разделе техники подробно рассмотрены в серии Fast-Paced Multiplayer Габриэля Гамбетты. Я настойчиво рекомендую прочитать эту великолепную серию статей. В ней также есть интерактивное демо, позволяющее увидеть, как эти техники работают на практике.

Первая техника заключается в том, чтобы применять результат ввода напрямую, не ожидая ответа от сервера. Это называется прогнозированием на стороне клиента. Однако когда клиент получает обновление от сервера, он должен убедиться, что его прогноз был верным. Если это не так, то ему нужно просто изменить своё состояние согласно полученному от сервера, потому что сервер авторитарен. Эта техника впервые была использована в Quake. Подробнее о ней можно прочитать в статье Quake Engine code review Фабьена Санглара [перевод на Хабре].

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

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

Гленн Фидлер (как всегда!) написал в 2004 году статью Network Physics (2004), в которой заложил фундамент синхронизации симуляции физики между сервером и клиентом. В 2014 году он написал новую серию статей Networking Physics, в которой описал другие техники для синхронизации симуляции физики.

Предотвращение читерства

Существует две основные техники предотвращения читерства.

Первая: усложнение отправки читерами вредоносных пакетов. Как сказано выше, хорошим способом её реализации является шифрование.

Вторая: авторитарный сервер должен получать только команды/ввод/действия. Клиент не должен иметь возможности изменять состояние на сервере, кроме как отправкой ввода. Тогда сервер каждый раз при получении ввода должен перед его применением проверять его на допустимость.

Логика приложения: заключение

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

Другие полезные ресурсы

Если вы хотите изучить другие ресурсы, посвящённые сетевым моделям, то их можно найти здесь:

  • Блог Гленна Фидлера — стоит прочитать весь его блог, в нём есть множество отличных статей. Здесь собраны все статьи по сетевым технологиям.
  • Awesome Game Networking автора M. Fatih MAR — это подробный список статей и видео по сетевым движкам видеоигр.
  • В wiki сабреддита r/gamedev тоже есть множество полезных ссылок.
  • сетевые протоколы
  • сетевые технологии
  • читерство
  • компенсация лагов
  • tcp
  • udp
  • шифрование трафика

Про протоколы TCP и UDP простыми словами

Скорость и надёжность передачи данных зависит от того, какой сетевой протокол вы используете: UDP или TCP. Они выполняют одну и ту же задачу, но по-разному. Один из них более надежный, а другой – более быстрый. Как понять, какой из них подходит вам? Узнайте об этом в нашей статье.

Jan 29, 2023
Время чтения: 5 мин.

Что такое TCP

TCP (Transmission Control Protocol) – это сетевой протокол, передающий данные с устройства пользователя на веб-сервер. Всякий раз, когда вы общаетесь с друзьями по Skype, отправляете электронные письма, смотрите онлайн-видео или просто просматриваете сайты, используется протокол TCP.

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

Что такое UDP

UDP расшифровывается как User Datagram Protocol. По сравнению с TCP, сетевой протокол UDP менее надежен, но быстрее и проще. Он часто используется в ситуациях, когда высокая скорость имеет решающее значение, например, при просмотре потокового телевидения или во время онлайн-игр.

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

Принцип работы TCP и UDP

Протокол TCP работает надежнее, чем UDP. Он передает пакеты данных с устройства на веб-сервер. UDP быстрее и проще, но не даёт гарантии, что пакет по дороге не потеряется.

Принцип работы TCP такой:

  1. TCP присваивает каждому пакету данных уникальный идентификатор и порядковый номер. Это позволяет получателю определить, какой пакет был получен и какой поступает следующим.
  2. Как только пакет данных получен в правильном порядке, получатель отправляет подтверждение отправителю.
  3. Теперь отправитель может отправить следующий пакет.
  4. Если пакет потерян или отправлен в неправильном порядке, получатель молчит, сигнализируя о том, что тот же пакет данных нужно отправить снова.

Последовательная отправка данных решает сразу несколько задач: контроль над перегрузками, больше возможностей управлять потоком, а также легче обнаруживать и исправлять ошибки. К тому же данные, отправленные через TCP, с большей вероятностью достигнут места назначения в полном объеме. Однако наряду с преимуществами у протокола TCP есть и недостаток: из-за постоянной «переклички» между сторонами установление соединения и обмен данными занимает больше времени.

Протокол UDP также передаёт данные, но без уникальных идентификаторов и строгой очередности. Отправляя данные в потоке, он гарантирует доставку только общего количества пакетов. Исправлять ошибки UDP почти не умеет и проблему потерянных пакетов никак не решает. В нем чаще случаются ошибки, зато данные передаются намного быстрее, чем через TCP.

Безопасен ли UDP? Почти невозможно настроить брандмауэр, позволяющий только определенные соединения UDP и блокирующий остальные. Хотя протокол TCP в этом отношении гораздо надежнее, существуют меры защиты и UDP-соединений. Например, для определенных приложений можно использовать прокси или устанавливать туннельное соединение между удаленным пользователем и внутренней сетью компании.

Основное различие между TCP и UDP

UDP быстрее, чем TCP, потому что пользователю не нужно разрешать или подтверждать получение данных перед отправкой следующего пакета. Поэтому через протокол UDP и установка соединения, и передача данных происходит быстрее. В то же время вызывает опасения вопрос безопасности протокола UDP. Если сравнивать протоколы UDP с TCP VPN, OpenVPN лучше всего работает на UDP-порте, хотя его можно настроить на любом порте.

Вот сравнительная таблица их характеристик:

TCP UDP
Надежность Высокая Меньшая
Скорость Меньшая Высокая
Способ передачи Пакеты доставляются в строгой очередности Пакеты доставляются в хаотичном порядке
Обнаружение и исправление ошибок Да Нет
Отслеживание перегрузок TCP Да Нет
Подтверждение Да Только контрольная сумма

Как UDP, так и TCP делят поток данных на меньшие блоки – пакеты данных. К ним относятся IP-адреса отправителя и получателя, различные конфигурации, фактическая отправляемая информация и трейлер – данные, указывающие на конец пакета.

Так какой же протокол лучше: UDP или TCP? Как и в других подобных случаях, все зависит от того, для чего вы их используете. Если нужна быстрая и постоянная передача данных для правильной работы приложения, вам придется использовать UDP. В других же случаях лучше пользоваться TCP, он гарантированно доставит все данные без потерь.

Сетевое программирование для разработчиков игр. Часть 1: UDP vs. TCP

От переводчика: Это перевод первой статьи из цикла «Networking for game programmers». Мне очень нравится весь цикл статей, плюс всегда хотелось попробовать себя в качестве переводчика. Возможно, опытным разработчикам статья покажется слишком очевидной, но, как мне кажется, польза от нее в любом случае будет.

Привет, меня зовут Гленн Фидлер и я приветствую вас в первой статье из моей онлайн-книги “Сетевое программирование для разрабочиков игр”.

image

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

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

Выбор типа сокетов полностью зависит от жанра игры, которую разрабатываете. В данном цикле статей я буду считать, что вы пишете игру в стиле action — наподобие Halo, Battlefield 1942, Quake, Unreal, CounterStrike, Team Fortress и т.п.

Теперь мы более подробно рассмотрим свойства каждого типа сокетов (учитывая тот факт, что мы разрабатыватаем игру в стиле action), и немного углубимся в детали работы сети интернет. После подробного обзора правильный вариант станет очевиден!

TCP расшифровывается как “transmission control protocol” (протокол контроля передачи), а IP — как “internet protocol”. Вместе они лежат в основе практически всего, что вы делаете в сети, начиная от просмотра веб-страниц и кончая общением в IRC и электронной почтой — все это работает на основе TCP/IP.

Если вы когда-либо уже использовали TCP сокеты, то вы должны знать, что TCP — это протокол, использующий принцип надежного соединения. Это означает, что вы устанавливаете соединение между двумя компьютерами, и затем пересылаете данные между ними подобно тому, как если бы вы записывали информацию в файл на одном компьютере, а на другом — считывали бы ее из того же файла.

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

Еще разок — все просто, как обычная запись или чтение из файла. Элементарно, Ватсон!

Но такая простота в обращении совершенно отличается от того, что на самом деле происходит «под капотом», на более низком уровне — уровне протокола IP.

На этом уровне нет понятия соединения — вместо этого отдельные пакеты передаются от одного компьютера к другому. Можно представить этот процесс как передачу записки от одного человека к другому в комнате, полной народу: в конце концов записка попадает к кому надо, но при этом пройдя через множество рук.

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

Так происходит потому, что сеть интернет — это самоорганизующаяся и самовосстанавливающаяся система, которая может “обходить” проблемные участки в сети. На самом деле, то, что происходит на таком низком уровне в сети — реально круто. Вы можете почитать об этом более подробно в уже ставшей классикой книге — “TCP/IP Illustrated”.

А что, если мы захотим пересылать информацию между компьютерами не в стиле чтения/записи в файл, а непосредственно отправляя и получая отдельные пакеты?

Что ж, мы можем сделать это, используя UDP. UDP расшифровывается как “user datagram protocol” (протокол пользовательских датаграмм), и он работает поверх IP (как и TCP), но вместо добавления кучи функциональности он представляет собой лишь небольшую надстройку над IP.

Используя UDP, мы можем отослать пакет по определенному IP адресу (к примеру, 112.140.20.10) и порту (к примеру, 52423), и он будет передаваться от компьютера к компьютеру, пока не достигнет цели (или не потеряется по пути).

При этом, на стороне приемника мы просто сидим и ждем, прослушивая определенный порт (52423 в нашем случае), и, когда на него приходит пакет от кого-либо (помним, что соединения не используются), мы получаем об этом уведомление с адресом и портом компьютера-отправителя, размером пакета, и после этого можем прочитать данные из этого пакета.

Протокол UDP не гарантирует доставку данных. На практике большинство пакетов, конечно, доходят, но всегда имеются потери около 1-5%, а иногда бывают периоды времени, в которые пакеты вообще не доходят (помните, что между отправителем и получателем могут находиться тысячи компьютеров, на любом из которых что-то может отказать или сломаться).

Также UDP не гарантирует порядок доставки пакетов. Вы можете отправить пять пакетов по порядку — 1, 2, 3, 4, 5 — а прийти они могут совершенно в другом порядке — к примеру, 3, 1, 2, 5, 4. Опять же, на практике, они скорее всего придут в правильном порядке в большинстве случаев, но полагаться на это нельзя!

Наконец, хоть UDP и ничего особо не добавляет к IP, одну вещь он все-таки гарантирует. Если вы пересылаете пакет, то он либо дойдет полностью, либо не дойдет вообще. Так, если вы пересылаете пакет в 256 байт другому компьютеру, то он не может получить только первые 100 байт от пакета — он обязательно должен получить все 256 байт. Это реально единственная вещь, которую гарантирует протокол UDP — все остальное ложится на ваши плечи.

Итак, нам нужно решить — использовать TCP или UDP сокеты? Давайте взглянем на их свойства:

  • Использует принцип соединений
  • Гарантирует доставку и очередность
  • Автоматически разбивает информацию на пакеты
  • Следит за тем, чтобы не пересылать данные слишком интенсивно (контроль потока данных)
  • Легко использовать — как запись/чтение из файла
  • Не использует принцип соединений — придется реализовывать это вручную
  • Не гарантирует доставку и порядок доставки пакетов — они могут дойти в неправильном порядке, с дубликатами, или вообще не дойти!
  • Нужно вручную разбивать данные на пакеты и отправлять их
  • Нужно следить за тем, чтобы не пересылать данные слишком интенсивно
  • Если пакет потеряется, то нужно как-то это отследить, и в случае необходимости переслать его заново

Использовать TCP — это наверное, худшая ошибка, которую можно совершить, разрабатывая многопользовательскую игру. Чтобы понять почему, давайте разберемся, что делает TCP таким простым в использовании!

Как работает TCP

TCP и UDP оба работают поверх IP, но по факту они совершенно разные. UDP ведет себя очень похоже на IP, в то время как TCP абстрагирует пользователя от всех проблем с пакетами, делая взаимодействие с ним похожим на чтение/запись в файл.

Итак, как же он это делает?

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

Такое поведение может стать проблемой для нашей многопользовательской игры, если нужно передавать очень маленькие пакеты. Может случиться так, что TCP решит не передавать наши данные, пока их не накопится достаточно, чтобы сформировать пакет определенного размера (скажем, больше ста байт). И это — большая проблема, потому что необходимо передавать данные с клиента (нажатия клавиш игрока) на сервер как можно быстрее, и если при этом будут возникать задержки из-за буферизации данных протоколом, то для игрока на клиентской стороне игра будет происходить далеко не самым приятным образом. При этом обновление объектов игры будет происходить с задержкой и редко — тогда как нам нужно делать обновление объектов вовремя и часто.

В TCP есть опция, призванная исправить это — “TCP_NODELAY”. Она говорит протоколу, чтобы он не ждал накопления данных в очереди на отправку, а отсылал их сразу.

К сожалению, даже с установленной данной опцией, у TCP наблюдается множество проблем при использовании его в сетевых играх.

Корень всех проблем заключается в том, каким образом TCP обрабатывает пакеты, потерянные или пришедшие вне очереди, создавая иллюзию надежного и последовательного соединения.

Как TCP обеспечивает надежность соединения

При передаче TCP разбивает поток данных на отдельные пакеты, пересылает их по сети, используя ненадежный протокол IP, и затем на принимающем компьютере восстанавливает из принятых пакетов первоначальный поток.

Но что будет, если один из пакетов не дойдет? Или если пакеты придут не по порядку, или с дубликатами?

Если особо не углубляться в детали работы TCP (а это реально очень сложная тема — можете почитать в TCP/IP Illustrated), процесс выглядит так: TCP отправляет пакет, определяет, что пакет не дошел, и заново отправляет тот же пакет адресату. Дублирующиеся пакеты отсеиваются на стороне адресата, а пакеты, пришедшие не по порядку — переупорядочиваются, чтобы все было как надо — надежно и по порядку.

Проблема заключается в том, что когда TCP таким образом “синхронизирует” поток данных, в случае потери пакета передача останавливается до тех пор, пока потерянный пакет не будет отправлен заново (и получен адресатом). Если во время ожидания придут новые данные, они будут поставлены в очередь, и вы не сможете прочитать их, пока не дойдет тот самый потерянный пакет. Сколько времени занимает посылка пакета заново? Она занимает как минимум время, равное времени прохождения пакета туда и обратно (когда TCP определяет, какой пакет надо отправить заново), плюс время на повторную доставку потерянного пакета. Так что, если пинг между компьютерами составляет 125 мс, повторная передача пакета займет примерно одну пятую секунды, а в худшем случае — до полсекунды (представьте, если вдруг заново отправленный пакет тоже потеряется). Веселуха!

Почему никогда не стоит использовать TCP для многопользовательских игр

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

Рассмотрим простой пример многопользовательской игры, например, 3d-шутер. Сетевая часть в игре построена очень просто: каждую итерацию цикла игры клиент посылает на сервер описание всех действий игрока (нажатые клавиши, положение мыши и т.п.), и каждую итерацию сервер обрабатывает эти данные, обновляет модель игрового мира и посылает обратно клиенту текущие позиции объектов мира, чтобы тот отрисовал игроку новый кадр.

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

К сожалению, изменить такое поведение TCP никак нельзя, да и не надо, так как в нем и заключается смысл TCP. Это — необходимость, чтобы сделать передачу данных через интернет надежным и последовательным потоком данных.
Но нам не нужен надежный и последовательный поток данных.

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

Но подождите! Почему я не могу использовать и UDP, и TCP вместе?

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

Конечно, велико искушение использовать UDP для передачи данных пользовательского ввода и состояния мира, а TCP — для тех данных, которые должны быть гарантированно доставлены. Возможно, вы даже думаете, что можно сделать несколько “потоков” команд — например, один для загрузки уровней, другой — для команд AI. Вы думаете: “Мне не нужно, чтобы команды AI ждали в очереди, если потеряется пакет с данными для загрузки уровня, ведь они же совершенно не связаны!”. В данном случае вы правы, и вы можете решить создать по TCP сокету на каждый поток команд.

На первый взгляд, это отличная идея. Но проблема в том, что раз TCP и UDP оба работают поверх IP, пакеты обоих протоколов будут влиять друг на друга — уже на уровне IP. Как конкретно будет проявляться это влияние — очень сложный вопрос, и связан он с механизмами обеспечения надежности в TCP. Но, в любом случае, знайте, что использование TCP обычно приводит к увеличению потерь UDP пакетов. Если хотите узнать об этом больше, можете прочитать вот эту статью.

Заключение

Я рекомендую не просто использовать UDP, но и использовать только UDP и больше ничего. Не используйте TCP и UDP вместе — вместо этого лучше узнайте, как реализовать те “фишки” TCP, которые вам нужны, самостоятельно, на основе UDP.

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

Игровой сервер. TCP/IP vs UDP

Собираюсь писать очередной проект. Суть организации игрового пространства следующая — есть набор локаций (достаточно больших, 2-5 км), на каждой локации в реалтайме передвигаются игроки и совершают некие действия.

Какой протокол лучше использовать для серверного решения подобной игры? У меня есть сомнения по поводу «скорострельности» TCP/IP и также есть сомнения по поводу надёжности UDP. Ибо передаваться должны как необязательные данные (например, данные по перемещениям игроков), так и обязательные — нанесённый урон и так далее.

Как рассчитывается необходимая частота обновлений позиций игроков? Как вообще реализуется передача позиций игроков в пределах видимости?

Есть ли где либо готовые решения/исходники на Java для подобных серверов? (DarkStar не надо, пока с этим монстром разберёшься — проще свой написать)

#1
0:49, 16 окт 2010
#2
10:53, 16 окт 2010

Wo2lf
> Какой протокол лучше использовать для серверного решения подобной игры?
И тот и другой.
Где нужна скорость передачи, но не нужна 100% гарантия получения, там UDP.
Где нужна 100% гарантия получения, там TCP.

PS. На Java сервер лучше не писать, лучше на C++ под Windows Server 2008 R2. Другие решение очень медленные и не надёжные.

#3
12:28, 16 окт 2010

всё зависит от степени лени:

средняя лень — используй и TCP и UDP одновременно.
если не лень организовать свой reliable протокол поверх UDP — тогда только UDP.
а если лень максимальна — делай всё на TCP и не заморачивайся, никаких преимуществ в скорости UDP не даёт.

#4
12:41, 16 окт 2010

>PS. На Java сервер лучше не писать, лучше на C++ под Windows Server 2008 R2. Другие решение очень медленные и не надёжные.
Нет, язык выбран уже однозначно, на планируемую нагрузку 3к онлайн и пиковые 10к онлайн вполне хватает, да и опыта в написании таких серверов уже набрано порядком, наработки есть. Всё же мне важны трудозатраты, а на плюсах они на порядок больше. Да и современные версии жабы в серверном исполнении не намного медленней плюсов.

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

#5
12:53, 16 окт 2010

serg_usyp
> Где нужна скорость передачи, но не нужна 100% гарантия получения, там UDP.
fixed

#6
13:35, 16 окт 2010

Кстати, кто что думает по поводу

Работал ли кто с этим продуктом?

#7
14:12, 16 окт 2010

Wo2lf
> Нет, язык выбран уже однозначно, на планируемую нагрузку 3к онлайн и пиковые 10к онлайн вполне хватает
Да, серьёзно, а можно узнать 10к игроков это Java-сервер у тебя при каком количестве сообщений (в том числе общий трафик) в секунду держит?

#8
14:20, 16 окт 2010

serg_usyp
> Да, серьёзно, а можно узнать 10к игроков это Java-сервер у тебя при каком
> количестве сообщений (в том числе общий трафик) в секунду держит?
Железо решает. Плюс написание сервера на С++ дороже, чем покупка более мощного железа и написаниe его на Java.

#9
18:14, 16 окт 2010

batment
> Плюс написание сервера на С++ дороже
Это догма.

> чем покупка более мощного железа и написаниe его на Java.
Использование C++ вместо Java увеличивает производительность в 2+ раза.
Покупать 2+ сервера вместо одного!
Боюсь, на такое готовы лишь пойти эффективные менеджеры.

#10
19:56, 16 окт 2010

serg_usyp
Тогда обьяснюсь. На С++ уже есть множество решений, но практически каждое нужно дорабатывать, поэтому пройдет не один месяц, прежде чем будет что-то готово (это если брать одного программиста). На Java все велосипеды стандартизированы, плюс специфических проблем вроде утечек памяти гораздо меньше, поэтому сроки разработки смело можно поделить на два, а то и больше, а сэкономленные деньги (если речь идет о человеко-месяцах, то это тысячи долларов) потратить на более мощную систему , к тому же код будет работать с аналогичной с C++ скоростью (хватит жить в 95 году и считать, что java — это медленно).

#11
20:19, 16 окт 2010

batment
> На С++ уже есть множество решений, но практически каждое нужно дорабатывать, поэтому пройдет не один месяц, прежде чем будет что-то готово (это если брать одного программиста).
На системном уровне там всё просто и ничего дорабатывать не нужно.
На прикладном уровне (работа с протоколом передачи данных) в C++ столько же работы, что и с Java.

> плюс специфических проблем вроде утечек памяти гораздо меньше, поэтому сроки разработки смело можно поделить на два,
Это тоже догма.
Там утечкам памяти не откуда браться, это не разработка искусственного интеллекта, это если только у программиста руки из опы растут, то будут утечки памяти.
Там могут быть только проблемы с совместным доступом к памяти. Для этого нужно рядом с программистами ставить чувака с бейсбольной битой и обяснять ему, чтобы бил ей по башке программистам, когда они забывают расставлять критические секции.

> к тому же код будет работать с аналогичной с C++ скоростью (хватит жить в 95 году и считать, что java — это медленно).
Если под скорость подразумевается пинг, то он может и до 95 на C++ был таким же, как на Java.
А вот поддерживать игроков Java-сервер сможет в 2+ раз меньше аналогичного на C++ из-зи избыточного кода вызванного архитектурой Java.

#12
21:30, 16 окт 2010

serg_usyp
Хорошо, тогда почему большинство вакансий программистов серверов касаются только Java и C#?

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

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