[Из песочницы] Особенности установления соединения между участниками сетевой игры типа «равный к равному»

Это сборник информации, которая мне понадобилась, чтобы реализовать этап установления соединения между участниками сетевой игры типа «равный к равному» (peer-to-peer) с использованием протокола UDP.

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

Опытные геймдевелоперы вряд ли найдут тут для себя что-то новое. Но буду благодарен за замечания и комментарии.

pgwy7h2hgc0rws8_5-btx0brno8.png


Сетевая игра с архитектурой «равный к равному» (peer-to-peer)


  • Каждый игрок хранит всё состояние игрового мира и обрабатывает его синхронно с другими игроками. Каждый игрок передаёт действия пользователя всем другим игрокам. Ведущий игрок, который собирает других игроков, называется сервером, а остальные клиентами. Сервер является главным только на этапе сбора игроков. А во время игры нет главного компьютера.
  • Этот подход имеет следующие особенности:
    • Трафик не зависит от сложности игрового мира, а только от количества игроков. В этом режиме обычно работают стратегии в реальном времени, где нужно обрабатывать тысячи юнитов.
    • Объём трафика имеет порядок N², где N количество игроков. Поэтому этот подход применим только для игр с небольшим количеством игроков.
    • Так как данные передаются напрямую между игроками без промежуточного сервера, то задержки передачи (лаги) минимальные. Но если хотя бы один из игроков имеет проблемы со связью, то это повлияет на всех игроков.
    • Необходимо установить каналы связи между всеми игроками. Но если игроки находятся в разных локальных сетях, то это не всегда возможно.
  • Для передачи данных в игре можно использовать TCP или UDP.
    • TCP (Transmission Control Protocol) обеспечивает надежную доставку потока байтов. Это упрощает реализацию игры, но нет контроля над задержками передачи данных.
    • UDP (User Datagram Protocol) это простой протокол передачи пакетов без гарантии их доставки. Но благодаря его простоте UDP используют в системах реального времени, когда неприемлемо ждать задержавшиеся или потерянные пакеты. Использование UDP позволяет уменьшить задержки в игре, но усложняет реализацию игры.
    В этой статье описано использование UDP.


Установление соединения в локальной сети


  • Чтобы установить соединение между игроками клиенту нужно знать IP-адрес сервера и порт, который слушает программа игры.
    n3di2usgoz4bj4cyqcmbk3txocy.png

    • Например, компьютер игрока A имеет в локальной сети IP-адрес 192.168.1.2. Игрок A запускает программу игры на своём компьютере в режиме сервера, и программа слушает порт 50120. Игрок A каким-либо образом сообщает эту информацию игроку B.
    • Компьютер игрока B имеет в локальной сети IP-адрес 192.168.1.5. Игрок B запускает программу игры на своём компьютере в режиме клиента, и программа занимает порт 50150. Игрок B вводит в программе игры адрес сервера 192.168.1.2:50120, и программа отправляет запрос к серверу по заданному адресу.
    • Сервер находится в режиме ожидания, и получив запрос от игрока B узнаёт его адрес 192.168.1.5:50150. Таким образом клиент и сервер установили соединение и могут начать игру.
  • Если к серверу подключилось несколько клиентов, то сервер должен отправить каждому из них адреса других клиентов.
    f9zuzgexmd5bqe8fxgypsynspyu.png

    В примере на картинке сервер A отправляет клиенту B адрес клиента C (192.168.1.6:50160), а клиенту C отправляет адрес клиента B (192.168.1.5:50150). Таким образом все игроки смогут установить соединения «каждый с каждым».
  • При каждом запуске игры сервер может запрашивать у операционной системы любой свободный порт. Но удобнее каждый раз использовать один и тот же порт, чтобы клиенту не приходилось каждый раз вводить новый порт.

    Все порты разделены на три диапазона:

    • [0, 1023] общеизвестные (системные).
    • [1024, 49151] зарегистрированные (пользовательские). Порт для сервера нужно выбрать в этом диапазоне.
    • [49152, 65535] динамические (частные). Тут ОС выделяет временные порты для программ.

    В Википедии можно посмотреть список зарезервированных портов. Далее для примера выбран порт 49094 для сервера игры.
  • Компьютер может иметь несколько сетевых интерфейсов, как реальных (Ethernet, WiFi), так и виртуальных (VPN). Каждый сетевой интерфейс имеет свой IP-адрес. Программа может предоставить пользователю сервера возможность выбрать сетевой интерфейс по которому он будет ожидать запросы клиентов. Но удобно использовать специальный wildcard IP-адрес 0.0.0.0. Если программа открывает сокет с этим IP-адресом, то она будет слушать заданный порт для всех сетевых интерфейсов данного компьютера. Таким образом игроку не придётся думать какой сетевой интерфейс выбрать.
  • Можно ещё помочь клиенту и автоматически определить IP-адрес сервера. Если клиент отправит запрос на специальный широковещательный IP-адрес (broadcast), то его получат все компьютеры в данной локальной сети.

    Например, если адрес сети равен 192.168.1.0, маска подсети 255.255.255.0, то широковещательный IP-адрес будет 192.168.1.255.

    4yyjotxuzbtirqk2pewwm_npmd8.png

    Если все игровые сервера слушают порт 49094, то пакет отправленный по адресу 192.168.1.255:49094 получат все игровые сервера в данной сети. Каждый сервер пошлёт подтверждение отправителю. И таким образом клиент получит список всех игровых серверов в своей сети, и сможет выбрать нужный ему сервер.
  • Если все игроки находятся в одной локальной сети, то установить соединения «каждый с каждым» достаточно просто. Могут быть проблемы с доступом клиента к порту сервера из-за Firewall. Но это зависит от ОС и настроек безопасности.


Установление соединения из локальной сети к серверу в Интернет


  • Как правило, компьютеры подключены к Интернет не напрямую, а через роутер. И, как правило, на роутере выполняется преобразование сетевых адресов (NAT, Network Address Translation).

    Например, клиент C находится в локальной сети, и имеет доступ к Интернет через роутер B, а сервер A имеет публичный IP-адрес в Интернет. Пусть они имеют следующие адреса:

    gqcfqtu-vneghva8qq7v2do7lxo.png

    • Адрес сервера A в Интернет 203.0.113.2. Программа сервера слушает порт 49094.
    • Адрес роутера B в Интернет 203.0.113.5. Адрес роутера B в локальной сети 192.168.1.1.
    • Адрес клиента C в локальной сети 192.168.1.5. Программа клиента занимает порт 50150, и посылает пакет к серверу по адресу 203.0.113.2:49094.
    • Роутер B является шлюзом по умолчанию (default gateway) в своей локальной сети. То есть на него отправляются все пакеты с адресами не из текущей локальной сети.
    • Роутер имеет таблицу преобразования адресов: (внутренний адрес: внутренний порт) (внешний адрес: внешний порт). Получив от клиента пакет к серверу 203.0.113.2:49094, роутер выделяет любой свободный внешний порт, например 52050, и создаёт запись в таблице преобразования адресов: 192.168.1.5:50150 203.0.113.5:52050.
    • Роутер заменяет в пакете внутренний адрес отправителя 192.168.1.5:50150 на внешний адрес 203.0.113.5:52050, и передаёт изменённый пакет серверу.
    • Сервер получает пакет с адресом отправителя 203.0.113.5:52050, и посылает ответ на этот адрес, то есть роутеру.
    • Получив пакет от сервера, роутер ищет адрес получателя в своей таблице преобразования адресов, выполняет обратную замену внешнего адреса получателя 203.0.113.5:52050 на внутренний адрес 192.168.1.5:50150, и передаёт пакет по этому адресу, то есть клиенту.
    • Если клиент отправляет серверу последующие пакеты, то роутер ищет по адресу отправителя есть ли уже в таблице такая запись, и если есть, то использует ранее выделенный внешний порт, в данном случае 52050.
    • Таким образом клиент и сервер установили соединение через NAT, и могут начать игру. Но сервер не знает внутренний адрес клиента в его локальной сети, и считает что клиентом является роутер.
    • Запись в таблице преобразования адресов действует определенный промежуток времени, как правило, 1–3 минуты. Поэтому клиенту и серверу нужно периодически обмениваться пакетами, чтобы связывающая их запись не была удалена. Если клиент пошлёт серверу новый пакет после удаления записи, то для новой записи на роутере может быть выделен другой внешний порт, и для сервера это будет уже другой клиент с другим адресом.
  • Как правило, роутер пользователя не подключен напрямую к Интернет, а находится во внутренней сети интернет-провайдера. То есть клиент находится за двумя NAT.

    Например так:

    7uhtcwvuy-lojgg6_sxifcd_ucc.png

    То есть выполняется двойное преобразование сетевых адресов.
  • Существуют разные типы NAT:
    • Полный конус (Full cone NAT).
      gnar8u7m0xlyigrhgihadyqe0d0.png

      • После того как в таблице преобразования адресов создана запись (внутренний адрес: внутренний порт) (внешний адрес: внешний порт), все пакеты от отправителя (внутренний адрес: внутренний порт) передаются через (внешний адрес: внешний порт) на любой адрес получателя.
      • Любой внешний сервер может послать пакеты на (внутренний адрес: внутренний порт), отправляя пакеты на (внешний адрес: внешний порт).
      Например, клиент C отправляет пакеты серверам A и D используя один и тот же внешний адрес 203.0.113.5:52050. Если сервер E отправит пакет на этот адрес 203.0.113.5:52050, то роутер передаст его клиенту C.
    • Address-restricted cone NAT.
      i6onaojluqotmemktgufswzg-ia.png

      • После того как в таблице преобразования адресов создана запись (внутренний адрес: внутренний порт) (внешний адрес: внешний порт), все пакеты от отправителя (внутренний адрес: внутренний порт) передаются через (внешний адрес: внешний порт) на любой адрес получателя.
      • Внешний сервер (адрес сервера: порт сервера) может послать пакеты на (внутренний адрес: внутренний порт), отправляя пакеты на (внешний адрес: внешний порт), только если (внутренний адрес: внутренний порт) ранее посылал пакеты на (адрес сервера: любой порт).
      Например, клиент C отправляет пакеты серверам A и D используя один и тот же внешний адрес 203.0.113.5:52050. Сервер D может послать пакет клиенту C через адрес роутера 203.0.113.5:52050 с любого своего порта. Но роутер не пропустит пакеты от сервера E.
    • Port-restricted cone NAT.
      kr6vdi2regppcywwajtz-1basdy.png

      • После того как в таблице преобразования адресов создана запись (внутренний адрес: внутренний порт) (внешний адрес: внешний порт), все пакеты от отправителя (внутренний адрес: внутренний порт) передаются через (внешний адрес: внешний порт) на любой адрес получателя.
      • Внешний сервер (адрес сервера: порт сервера) может послать пакеты на (внутренний адрес: внутренний порт), отправляя пакеты на (внешний адрес: внешний порт), только если (внутренний адрес: внутренний порт) ранее посылал пакеты на (адрес сервера: порт сервера).
      Например, клиент C отправляет пакеты серверам A и D используя один и тот же внешний адрес 203.0.113.5:52050. Роутер не пропустит пакеты ни от сервера E ни с другого порта сервера D.
    • Симметричный NAT (Symmetric NAT).
      98halr7veiq_ibj_irj2lxixcec.png

      • Если один и тот же внутренний отправитель (внутренний адрес: внутренний порт) отправляет пакеты к разным получателям (адрес сервера: порт сервера), то для каждого адреса получателя будет выделен отдельный внешний порт, и будет использоваться отдельная запись в таблице преобразования адресов.
      • Только внешний сервер (адрес сервера: порт сервера), который получил пакет от внутреннего отправителя (внутренний адрес: внутренний порт), может послать пакет обратно.
      Например, клиент C отправляет пакеты серверу A используя внешний адрес 203.0.113.5:52050, а серверу D используя другой внешний адрес 203.0.113.5:52051. Роутер не пропустит пакеты ни от сервера E ни с другого порта сервера D.
  • Если к игровому серверу подключилось несколько клиентов, то для игры типа «равный к равному» (peer-to-peer) нужно установить соединение напрямую между каждой парой клиентов минуя сервер.

    Например, к серверу A подключились два клиента C и E:

    aiifymcc-epffu0da8rkksj_4es.png

    • Клиент C имеет внутренний адрес 192.168.1.5:50150 и внешний адрес 203.0.113.5:52050.
    • Клиент E имеет внутренний адрес 192.168.2.5:50250 и внешний адрес 203.0.113.6:52060.
    • Сервер должен сообщить каждому клиенту внешний адрес другого клиента.
    • Обычно в роутерах используется NAT типа «Port-restricted cone NAT». Роутер пропускает клиенту пакеты только от того отправителя (IP-адрес и порт), которому клиент посылал пакеты ранее. И если клиент C пошлёт пакет клиенту E, то роутер D не пропустит этот пакет.
    • В этом случае используется метод UDP hole punching. Оба клиента должны слать UDP-пакеты друг другу. Как только клиент C отправит пакет клиенту E, то роутер B будет готов принимать пакеты от клиента E. Аналогично, как только клиент E отправит пакет клиенту C, то роутер D будет готов принимать пакеты от клиента C. Первые пакеты того из клиентов, кто начал передавать первым, будут потеряны. Но как только второй клиент тоже начнёт передавать пакеты навстречу, то оба клиента смогут обмениваться пакетами.
    • Но если на роутере используется «Симметричный NAT» (Symmetric NAT), то для каждого получателя будет выделен новый внешний порт. И, как правило, нет возможности узнать какой именно порт выделил роутер. Поэтому если хотя бы у одного из роутеров используется «Симметричный NAT» (Symmetric NAT), то установить соединение между клиентами будет невозможно.
  • Если два клиента находятся в одной локальной сети, то они будут знать только внешние адреса друг друга.
    jjsia4y7k50gk3esxa_5hvzx3ks.png

    Например, клиент C шлёт пакет клиенту D по его внешнему адресу 203.0.113.5:52051. Роутер B должен обработать этот пакет как будто он принят из внешней сети, заменить адрес получателя 203.0.113.5:52051 на внутренний адрес клиента D 192.168.1.6:50060, и послать пакет клиенту D обратно в локальную сеть. Эта функция роутера называется NAT loopback (NAT hairpinning). Если NAT loopback выключен на роутере, то установить соединение между клиентами будет невозможно.
  • Клиенты могут находится в разных локальных сетях, но иметь доступ к Интернет через общего интернет-провайдера.
    q8djyoaqbhaq6z3_wxc7_afbphk.png

    Если NAT loopback выключен на оборудовании интернет-провайдера, то установить соединение между клиентами будет невозможно.
  • Что делать если на роутере используется «Симметричный NAT» (Symmetric NAT), или выключен NAT loopback?

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

    Или можно использовать сторонние программы для создания VPN, например LogMeIn Hamachi.


Установление соединения между локальными сетями в Интернет


  • Как правило, ни один из игроков не имеет публичного IP-адреса, и игроки находятся в разных локальных сетях за NAT.
    56crup-geun05fnbtaokha2vqia.png

    Например, на данной схеме клиент C будет посылать пакеты серверу A на его внешний адрес 203.0.113.2:52020, в котором порт выбран роутером B. Поэтому выбор внутреннего порта сервера A не имеет значения, и можно выбрать любой порт. В данном случае вместо порта 49094 сервер A может запросить у ОС любой свободный порт, например 50120.
  • Чтобы сервер A и клиент C смогли установить соединение, сперва каждому из них нужно как-то узнать свой внешний адрес.

    Для этого можно использовать протокол STUN (Session Traversal Utilities for NAT).

    skljcmi1c5ztbt6law8nf_ylsy0.png

    Протокол STUN позволяет клиенту, находящемуся за NAT, определить свой внешний IP-адрес и порт.

    STUN сообщения передаются в UDP пакетах.

    Клиент может обратиться к любому из публичных STUN-сервером. Список публичных STUN-серверов можно взять в Википедии. Или найти по запросу «public STUN servers list».

    Этот метод не применим если хотя бы у одного из игроков на роутере используется «Симметричный NAT» (Symmetric NAT).

  • После того как каждый из игроков узнал свой внешний адрес, он должен как-либо сообщить свой адрес всем другим игрокам.

    Чтобы игроки могли обмениваться своими адресами можно использовать свой публичный сервер. На схеме он называется сервер адресов (Address-server).

    eve_fiqezp76wwmjfexsb0vlzka.png

    Для этого не обязательно нужен выделенный сервер. Можно использовать любой хостинг с веб-сервером и с поддержкой какого-либо скриптового языка, например PHP. Используя протокол HTTP каждый игрок должен передать на сервер адресов свой внешний адрес. А затем запросить адреса всех других игроков.
  • Например, каждый игровой сервер может зарегистровать на сервере адресов свой уникальный идентификатор (game-id). В качестве game-id можно использовать любую строку, например ник (псевдоним) пользователя сервера. Пользователь сервера как-либо сообщает game-id тем игрокам, которых он хочет пригласить в свою игру. И эти игроки смогут присоединиться к игре, запросив по game-id у сервера адресоввнешние адреса всех участников этой игры.
  • После того как каждый игрок получил внешние адреса всех других игроков, они могут установить соединения «каждый с каждым» используя метод UDP hole punching.

© Habrahabr.ru