RE:NODE

Сеть13 мин чтения

TCP или UDP для игровых серверов: почему игры используют UDP

Почему почти все игры говорят по UDP, во что это обходится в правилах файрвола и прокси и как правильно проверить UDP-порт, когда telnet бесполезен.

0 прочтений

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

Различие, которое имеет значение: head-of-line blocking#

В учебнике сравнение содержит пять различий. Решает что-то только одно.

TCP - это поток. Он нумерует каждый байт, подтверждает полученное, повторно отправляет потерянное и отдаёт приложению идеально упорядоченную последовательность. Чтобы так делать, ему приходится придерживать данные, пришедшие не по порядку, пока не найдётся недостающий кусок. Это и есть head-of-line blocking, и на канале с потерями он превращает 1% потерь пакетов в заметное зависание, потому что принимающему приложению не разрешено видеть пакеты 5, 6 и 7, пока не восстановлен пакет 4. В Linux минимальный таймаут повторной передачи составляет 200 мс, так что одна потеря в неудачный момент стоит как минимум столько.

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

Остальные различия вытекают из этого:

TCPUDP
ЗаголовокМинимум 20 байт8 байт
СоединениеСначала трёхстороннее рукопожатиеНет
ДоставкаГарантированная, по порядкуНи того ни другого
При потереПовторная передача, блокировка потокаНичего не происходит
Адрес источникаПроверяется рукопожатиемПодделывается тривиально
Управление перегрузкойВстроеноРеализуете вы сами

Строка о подделке источника - та, у которой есть последствия помимо производительности: поскольку у UDP нет рукопожатия, злоумышленник может поставить ваш адрес на пакет и заставить третью сторону отвечать вам. Это и есть вся основа атак с отражением, о которых рассказано в статье DDoS-атаки на игровые серверы простыми словами.

Почему шутер не может использовать TCP#

Проследите по времени, и довод возникнет сам собой.

Сервер, работающий на 64 тиках в секунду, выдаёт новый снимок мира каждые 15,6 мс. Каждый снимок описывает, где сейчас все находятся. Допустим, снимок 100 потерялся по дороге к игроку.

По UDP: ничего не происходит. Снимок 101 приходит через 15,6 мс с более свежими позициями, клиент интерполирует через пропуск, и никто ничего не замечает. Потерянный снимок устарел раньше, чем кто-либо успел бы запросить его снова.

По TCP: в ядре клиента лежат в буфере снимки 101, 102 и 103, и оно отказывается отдавать хоть один из них игре, потому что снимок 100 отсутствует, а порядок необходимо сохранить. Оно ждёт повторной передачи, которая занимает как минимум один полный оборот и, возможно, таймаут в 200 мс. Затем игра получает четыре снимка сразу, три из которых описывают прошлое. Игрок видит зависание, а следом - как все телепортируются. Именно так выглядит «rubber-banding».

Никакой настройки, которая бы это исправила, нет. TCP_NODELAY отключает алгоритм Нейгла и убирает другую задержку - ту, где мелкие записи придерживаются ради склеивания, - и его стоит включать на любом игровом TCP-сервере, но head-of-line blocking он не затрагивает, потому что это свойство самой гарантии.

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

Какие игры что используют#

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

ИграПротокол игрового процессаПримечания
Minecraft (Java)TCP 25565Каждое изменение блока важно; UDP-запрос необязателен
TerrariaTCP 7777Те же соображения, мир поменьше
RimWorld multiplayerTCPПошаговая синхронная симуляция, порядок - всё
Counter-Strike 2, TF2, Garry's ModUDP 27015RCON - это TCP на том же номере
ValheimUDP 2456-2457Игра и запросы оба по UDP
FactorioUDP 34197Детерминированная синхронизация шагов, своя надёжность
PalworldUDP 8211Сетевой стек Unreal
Arma 3, DayZUDP 2302 и вышеНесколько последовательных портов
Project ZomboidUDP 16261RCON по TCP на 27015
7 Days to DieTCP и UDP 26900Плюс telnet по TCP
FiveMTCP и UDP 30120HTTP-эндпоинты используют тот же номер
SatisfactoryTCP и UDP 7777После изменения сетевой части в версии 1.0
BeamMPTCP и UDP 30814Оба, на одном номере
Assetto CorsaUDP 9600, TCP 9600 и 8081Последний - веб-интерфейс

Minecraft - поучительное исключение. Это не игра на реакцию: установка блока, перемещение предмета или строка чата - отдельное событие, которое должно прийти ровно один раз и по порядку, а полезного способа интерполировать пропавшее нет. TCP для этого - верный инструмент, и цена в том, что игрок с плохим соединением получает зависание, а не rubber-band. Кодовая база Bedrock - другая программа, использующая UDP на 19132, и именно поэтому они никогда не делили порт.

Всё, что указано как «оба», обычно означает игровой процесс по UDP плюс что-то административное или похожее на HTTP по TCP на том же номере. Поэтому такие игры просят одно выделение, охватывающее оба протокола, а не два.

Надёжность, выстроенная поверх UDP#

Утверждение «у UDP нет надёжности» верно для UDP и неверно для любой игры, которая его использует. Игры на самом деле реализуют выборочную надёжность, потому что разным сообщениям нужны разные гарантии.

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

Библиотеки, которые этим занимаются, полезно узнавать в логе или конфигурационном файле: ENet, RakNet, Steam Networking Sockets и его Datagram Relay, а всё чаще и QUIC - надёжный, шифрованный, мультиплексированный транспорт поверх UDP, на котором работает HTTP/3. QUIC - самое наглядное доказательство этого довода: когда те, кто поддерживает TCP, захотели устранить head-of-line blocking для веба, они не стали чинить TCP, а построили заново поверх UDP.

Практическое следствие для планирования: поскольку игры сами управляют повторной передачей, маршрут с потерями деградирует плавно, а не рушится, но всё же деградирует. 2% потерь на игровом сервере - видимая проблема, тогда как 2% потерь при скачивании файла незаметны. Когда кто-то говорит, что сервер лагает, потери - одно из трёх, что нужно исключить, а найти, на каком узле они возникают, помогает чтение traceroute и mtr.

Что это значит для правил файрвола#

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

bash
$ ufw allow 25565/tcp            # Minecraft Java$ ufw allow 2456:2457/udp        # Valheim game and query$ ufw allow 27015/udp            # a Source game$ ufw allow 27015/tcp            # the same game's RCON$ ufw allow 30814                # both protocols, for BeamMP$ ufw status numbered

Если протокол не указан, как в пятой строке, открываются оба. Для игры, которая действительно использует оба, это правильно, в остальных случаях - небрежно. Остальное есть в руководстве по файрволу ufw, включая оговорку про Docker, на которой попадаются те, у кого наружу торчит база данных.

Два поведения, свойственных именно UDP, вызывают путаницу:

UDP-соединения не существует, но ваш файрвол делает вид, что оно есть. Отслеживание соединений создаёт псевдозапись, когда видит первый пакет потока, и удаляет её по таймауту - в Linux nf_conntrack_udp_timeout по умолчанию равен 30 секундам, а nf_conntrack_udp_timeout_stream - 120 секундам, когда трафик пошёл в обе стороны. Игра, которая ничего не отправляет дольше этого срока, теряет запись, и следующий входящий пакет считается новым. Игры отправляют keepalive-пакеты отчасти по этой причине.

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

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

Почему игру нельзя поставить за обычный обратный прокси#

Этот вопрос приходит примерно раз в неделю, обычно в форме «можно ли поставить мой сервер Minecraft за Cloudflare».

HTTP-обратный прокси - nginx в обычном режиме, Caddy, Traefik или оранжевое облачко перед сайтом - завершает TCP-соединение, читает HTTP-запрос и делает собственный запрос к вашему бэкенду. Каждая часть этого предложения предполагает TCP и HTTP. Игра, отправляющая UDP-датаграммы со своим бинарным протоколом, не предлагает ни того ни другого, так что HTTP-прокси нечего делать. Общее объяснение - в статье что делает обратный прокси, а где именно проходит граница, конкретно сказано в статье Cloudflare для сайтов и игровых серверов.

Что существует:

  • Проксирование TCP на уровне 4. Модуль stream в nginx, HAProxy в режиме TCP или игровой прокси вроде Velocity для Minecraft. Это работает для TCP-игр и по-настоящему полезно: сеть на Velocity ставит несколько бэкендов за один публичный порт.
  • Проксирование UDP. Модуль stream в nginx умеет пересылать UDP через listen 25565 udp; и proxy_pass, и для простых случаев это работает. Загвоздка в адресе источника: без прозрачного проксирования игровой сервер видит адрес прокси вместо адреса каждого игрока, а это ломает баны, логи и всё, что привязано к игроку.
  • Ретрансляционные сети вендоров. Steam Datagram Relay и подобные сервисы несут UDP по собственной магистрали оператора и скрывают адрес сервера. Они - часть платформы игры, а не то, что можно прикрутить сбоку.

Практический итог: веб-фронтенд можно спрятать за прокси, игровой сервер на UDP - нельзя, и если у вас есть и то и другое, общего адреса у них быть не должно. На RE:NODE слот прокси в тарифах для приложений и сайтов - это как раз такая HTTP-штука: направьте запись A на показанный адрес, и сертификат будет выпущен и продлён автоматически, а настоящий адрес клиента придёт в X-Forwarded-For. Он предназначен для вашего сайта и вашего API, а не для игрового порта.

Порты запросов, RCON и смешанный случай#

Большинство игр запускают сразу несколько протоколов, и понимание того, какой есть какой, экономит целый день.

Игровой порт несёт игровой процесс и является таким, какой выбрала игра. Порт запросов отвечает браузерам серверов и сайтам-списками, сообщая число игроков, карту и название. В семействе Source это протокол A2S по UDP, обычно на том же номере, что и игра. В Minecraft их два: TCP-пинг статуса на игровом порту, которым пользуется внутриигровой список серверов, и необязательный UDP-протокол запросов, включаемый через enable-query=true и query.port в server.properties, которым пользуются внешние сайты статуса.

RCON - это удалённое администрирование, и почти всегда он работает по TCP, потому что команда обязана прийти ровно один раз и по порядку. У Minecraft это 25575, игры Source используют номер игрового порта по TCP, Project Zomboid и ещё несколько игр по умолчанию берут 27015. Это единственный порт, к которому стоит относиться как к чувствительному, - см. безопасное использование RCON.

DNS тоже знает об этом различии. Запись A не зависит от протокола: она сопоставляет имя адресу и ничего не говорит о портах и протоколах. Запись SRV включает и то и другое, поэтому запись SRV для Minecraft пишется как _minecraft._tcp.example.com, а её эквивалент для UDP-игры назывался бы _udp. Единственный случай, где это регулярно применяется, разобран в статье SRV-записи для Minecraft.

Как правильно проверить UDP-порт#

Именно здесь большая часть диагностики идёт не туда. К UDP-порту нельзя подключиться через telnet, а nc -z -u почти бесполезен, потому что тишина - нормальный ответ на успешно доставленный UDP-пакет. Отсутствие ответа не доказывает вообще ничего.

Начните на сервере, где ответ однозначен:

bash
$ ss -lunp | grep 27015        # is anything listening on UDP?$ ss -ltnp | grep 25575        # and the TCP side$ ss -lunp

ss -lunp выводит слушающие UDP-сокеты вместе с владеющим ими процессом. Если вашей игры в этом списке нет, проблема в настройке игры, а не в сети, и никакие правки файрвола не помогут. Эта единственная команда решает больше заявок вида «порт не работает», чем что-либо другое.

Если сокет на месте, посмотрите, приходят ли пакеты вообще:

bash
$ tcpdump -n -i any udp port 27015$ tcpdump -n -i any udp port 27015 -c 20 -w /tmp/capture.pcap

Трафик приходит, а ответа нет - значит, игра не отвечает, и это проблема игры. Трафик не приходит - значит, его отбрасывают до вас, и это проблема файрвола или вышестоящей сети.

Снаружи честных вариантов немного:

bash
$ nmap -sU -p 27015 203.0.113.10        # needs root, and is slow

nmap сообщает open|filtered, когда ничего не вернулось, и это означает ровно то, что сказано: либо порт открыт, а служба предпочла не отвечать, либо что-то отбросило пробу. Единственная окончательная проверка снаружи - клиент, говорящий на собственном протоколе игры: сама игра, инструмент запросов к серверу или публичный чекер статуса для этой игры. Если порт запросов достижим, сайт-список видит сервер, а это тот ответ, который вам на самом деле и был нужен.

Ещё одна ловушка, свойственная UDP: закрытый UDP-порт обычно отвечает ICMP-сообщением port-unreachable, так что хост, отбрасывающий ICMP, делает каждый закрытый порт похожим на фильтруемый. Поэтому же некоторые игры ведут себя странно в сетях с агрессивной фильтрацией ICMP, и поэтому фразы «ping проходит, а игра нет» и «ping не проходит, а игра работает» вполне возможны обе.

FAQ#

Почему игры используют UDP, а не TCP?

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

Менее ли надёжен UDP, чем TCP?

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

Нужно ли открывать и TCP, и UDP?

Это зависит от игры, и лучше проверить, чем гадать. Большинству игр нужен UDP для игрового процесса и TCP только для RCON или веб-интерфейса. Несколько, в том числе 7 Days to Die, FiveM, Satisfactory и BeamMP, действительно используют оба на одном номере. Если открыть только TCP для UDP-игры, получится сервер, который работает, но к которому нельзя подключиться.

Можно ли поставить игровой сервер за Cloudflare?

Стандартный прокси - нет: он обрабатывает HTTP и HTTPS поверх TCP. У UDP-игры ему нечего завершать. Используйте его для сайта и держите сайт на другом адресе, чем игру, потому что именно через сайт люди обычно и узнают адрес игры.

Почему telnet говорит, что порт закрыт, хотя сервер работает?

Потому что telnet говорит по TCP, а игровой порт - UDP. Такая проверка бессмысленна. Проверьте ss -lunp на сервере, чтобы узнать, слушает ли что-нибудь, а затем проверьте снаружи инструментом, который говорит на собственном протоколе запросов игры.

Использует ли UDP меньше трафика, чем TCP?

Немного, за счёт меньшего заголовка и отсутствия подтверждений, но экономия невелика на фоне трафика, который порождает сама игра. Трафик на игровом сервере вообще редко бывает ограничением - сколько на самом деле стоит игрок в месяц, разобрано в статье трафик и fair use.


Комментарии

Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.

0/2000