Где UDP лучше TCP — DNS, видео, QUIC, игры
В прошлом уроке мы посмотрели на UDP как на минималистичный транспорт. Если ты сейчас думаешь «это всё круто, но я никогда в жизни не сяду писать UDP-приложение» — это иллюзия. Когда ты открываешь YouTube, делаешь голосовой звонок в Discord, играешь в любой онлайн-шутер, идёшь на сайт через HTTP/3 — ты используешь UDP. Почти всегда не напрямую, а через какой-то протокол поверх (QUIC, RTP, WebRTC). Но фундамент везде один.
В этом уроке разберём по случаям: где UDP реально выигрывает у TCP, почему именно там, и как современные системы выжимают из него максимум.
Случай 1: DNS — короткие запрос/ответ
DNS — самый классический и самый распространённый кейс использования UDP. Каждый раз, когда твой браузер хочет открыть сайт, он сначала резолвит имя в IP через DNS. Это происходит сотни раз в день только на твоей машине.
Почему DNS идёт через UDP по умолчанию:
- Запрос маленький. Типичный DNS-запрос — это 30-80 байт.
- Ответ обычно тоже маленький. Для A-записи — 50-200 байт.
- Latency критичен. Каждый сайт открывается медленнее, если DNS-резолв медленный.
- Retry — это нормально. Если ответ не пришёл за 1-2 секунды, клиент сам пошлёт повторный запрос. Никакого сложного протокола не нужно.
Если бы DNS использовал TCP, каждый запрос требовал бы:
- 3-way handshake (1 RTT)
- Запрос (0.5 RTT)
- Ответ (0.5 RTT)
- Опционально FIN handshake
Итого минимум 2 RTT вместо 1 RTT. На каждой странице десятки DNS-резолвов — лишний RTT моментально превращается в секунды задержки. Плюс TCP-сервер на 53-м порту обслуживал бы в десятки раз меньше клиентов из-за state-памяти.
# Посмотрим DNS-запрос вживую:
dig +short google.com
# Что-то вроде: 142.250.190.78
# С подробностями -- через какой сервер, сколько мс заняло:
dig google.com
# В выводе строка ';; Query time: 12 msec' -- это и есть тот самый RTT
# Захвати трафик и убедись, что это UDP:
sudo tcpdump -i any -n 'udp port 53' &
dig google.com
# Ты увидишь два пакета: запрос и ответ. Один RTT
DNS, кстати, может переключиться на TCP, если ответ не помещается в один UDP-пакет (труъ-фолбэк: бит TC в заголовке, потом повтор по TCP). Это случается с длинными TXT-записями, DNSSEC-подписями, AXFR (zone transfers). Подробнее в модуле 9.
Случай 2: Видеостриминг — устаревший кадр бесполезен
Открой YouTube или Twitch. Видишь поток видео. Что произойдёт, если на 5-й секунде один пакет с куском кадра потеряется?
Если бы это был TCP: TCP не отдаст приложению следующие байты, пока не получит потерянный (in-order delivery). Видео заморозится, через 100-200 мс TCP пошлёт повторный запрос, получит пропущенный пакет — и тогда продолжит. Зритель видит фриз на 200 мс. Это плохо.
С UDP: Видео-плеер получает оставшиеся пакеты в реальном времени. Один кадр получился порченый — ладно, отрендерим его частично или пропустим, покажем следующий. Зритель видит небольшой артефакт на одну сотую секунды. Это норма.
Для видео главное — realtime. Устаревший кадр уже не нужен. Зритель не хочет «получить тот пропущенный кадр через 500 мс» — он хочет видеть актуальную картинку прямо сейчас. UDP даёт эту возможность.
Современные стриминг-протоколы поверх UDP:
- RTP (Real-time Transport Protocol). Стандарт для аудио/видео-передачи. Используется в WebRTC, SIP, IPTV.
- WebRTC. Видеозвонки в браузере. Поверх UDP + DTLS + SRTP.
- SRT (Secure Reliable Transport). Используется для трансляций (broadcast). UDP + опциональная надёжность.
- HLS/DASH через UDP. Adaptive bitrate streaming. На самом деле часто идут через TCP/HTTPS, но мобильные операторы экспериментируют с QUIC.
Случай 3: Мультиплеерные игры — низкая latency или ничего
Counter-Strike, Fortnite, любой шутер. Игрок нажимает ЛКМ. Этот input должен дойти до сервера и обратно ответом (хит/мисс) за 50-100 мс, иначе игра ощущается «лагающей». TCP здесь — катастрофа.
Игровой протокол шлёт state-update на 20-60 раз в секунду. Каждое сообщение — это «вот текущее положение твоего персонажа и видимых противников». Если один такой пакет потерялся, ничего страшного: следующий придёт через 16 мс с актуальной картиной. Не нужно ждать повторной отправки потерянного — он уже не нужен.
# Упрощённая модель игрового UDP-протокола
import socket, struct, time
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect(("game-server", 7777))
# Клиент шлёт input каждые 16 мс (60 раз в секунду)
sequence = 0
while True:
keys = read_keyboard()
# 4 байта sequence + 4 байта input bitmask
packet = struct.pack("!II", sequence, keys)
sock.send(packet)
sequence += 1
time.sleep(0.016)
# Сервер не подтверждает приём. Если пакет потерялся --
# следующий через 16 мс перезатрёт состояние. Старый input
# уже бесполезен -- игрок мог отпустить кнопку
Игры часто реализуют поверх UDP свои механики:
- Reliable packets для важных событий. Покупка предмета, попадание в цель — это нельзя терять. Клиент посылает с ACK + retry до подтверждения.
- Unreliable для state. Position updates можно терять — следующий снапшот заменит.
- Lag compensation. Сервер «откатывает» состояние мира на N мс назад, чтобы попадание клиента засчиталось.
- Prediction. Клиент предсказывает движение, а потом корректирует, когда приходит «истинное» состояние от сервера.
Этого всего не сделать на TCP — он не даёт нужной гибкости и слишком жёсткий по latency.
Случай 4: QUIC и HTTP/3 — UDP как фундамент будущего
Самый интересный случай — это то, как индустрия в последние 10 лет начала строить новые протоколы поверх UDP, фактически отказываясь от TCP в high-performance сценариях.
QUIC (Quick UDP Internet Connections) — это протокол транспортного уровня, разработанный Google, теперь стандарт IETF. Это «TCP++»: даёт надёжность, порядок, congestion control, но поверх UDP. Зачем так?
Преимущества QUIC над TCP:
- 0-RTT resumption. Если ты уже подключался к серверу, повторное подключение начинается с первого пакета содержать данные. TCP+TLS обычно требует 2-3 RTT (TCP handshake + TLS handshake).
- No head-of-line blocking. В TCP+HTTP/2 потеря одного пакета блокирует все streams (потому что TCP — это один поток байт). В QUIC streams независимы — потеря в одном не блокирует другие.
- Connection migration. TCP-соединение идентифицируется по (src_ip, src_port, dst_ip, dst_port). Если у тебя поменялся IP (Wi-Fi -> 4G), соединение рвётся. QUIC использует connection ID, не зависящий от IP — соединение переживает смену сети.
- Faster congestion control evolution. QUIC живёт в user-space (в каждом приложении), а не в ядре. Это значит, что можно деплоить новые алгоритмы congestion control без обновления ОС.
# Проверим, что HTTP/3 работает на каком-нибудь сайте:
curl --http3 -I https://www.cloudflare.com 2>&1 | head -5
# Заметь, что протокол сверху -- HTTP/3, под ним UDP, под ним IP
# Захвати трафик:
sudo tcpdump -i any -n 'udp port 443' &
curl --http3 https://www.cloudflare.com > /dev/null
QUIC — это, пожалуй, самый яркий пример того, что UDP — не «недо-TCP». UDP — это базис для построения чего угодно поверх. Хочешь надёжность? Реализуй сам. Хочешь свой алгоритм congestion control? Пожалуйста. UDP не мешает, не навязывает свою модель.
HTTP/3 поверх QUIC/UDP — как меняется стекСлучай 5: Метрики, телеметрия, IoT
Когда у тебя 10 000 IoT-устройств (термостаты, датчики, светофоры), которые шлют метрики на центральный сервер раз в секунду — что разумно использовать?
TCP — overkill. Каждое устройство держит соединение, сервер должен помнить state каждого. На 10 000 устройств — десятки MB RAM на сервере просто на TCP-таблицы.
UDP — идеально. Устройство «выстрелило» один пакет с измерением и забыло. Сервер получил, агрегировал в timeseries-БД. Потерялся один пакет из тысячи — ну и ладно, статистика этого не заметит.
Классические протоколы для этого:
- StatsD. Метрики приложений (counters, gauges, timers) через UDP на порт 8125. Используется в Datadog, Graphite.
- Syslog. Логи серверов в формате UDP на порт 514 (можно и TCP, но UDP исторически).
- SNMP. Мониторинг сетевого оборудования (запросы к роутерам/свитчам через UDP/161).
- NetFlow / IPFIX. Роутеры отправляют статистику по трафику на collector через UDP.
- CoAP. Constrained Application Protocol — HTTP-подобный протокол для IoT поверх UDP.
# Простой StatsD-клиент:
import socket
def metric(name, value):
msg = f"{name}:{value}|c".encode()
sock.sendto(msg, ("metrics-server", 8125))
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
metric("api.requests", 1)
# Отправили и забыли. Если метрика-сервер недоступен --
# мы это даже не узнаем, и это правильно: метрика не критична
# для бизнес-логики, нельзя задерживать обработку запроса
# ради записи метрики
Принцип здесь: «не блокируй критический путь приложения ради вторичных задач». Если запись метрики через TCP может зависнуть на 5 секунд, она задержит обработку настоящего запроса от пользователя. Через UDP — sendto() неблокирующий, моментальный, и приложение продолжает работу.
Случай 6: Service discovery в локальной сети — mDNS, SSDP
Когда твой Mac или iPhone «видит» AirPlay-устройство на телевизоре или принтер в локальной сети — это работает через mDNS (Multicast DNS) поверх UDP. Когда умные колонки находят телефон — SSDP (Simple Service Discovery Protocol), тоже UDP.
Почему UDP здесь идеален? Потому что нужен multicast — отправить одно сообщение всем устройствам в сети одновременно. TCP не умеет multicast принципиально (это point-to-point протокол). UDP может отправлять на multicast-адреса (224.0.0.0/4 в IPv4), и все, кто подписан на этот адрес, получат пакет.
# Посмотрим mDNS-запросы вживую:
# macOS:
sudo tcpdump -i any -n 'udp port 5353'
# Linux:
sudo tcpdump -i any -n 'udp port 5353'
# В отдельном окне:
# macOS:
dns-sd -B _http._tcp local.
# Linux:
avahi-browse -a
# Ты увидишь UDP-пакеты на multicast-адрес 224.0.0.251:5353,
# где разные устройства анонсируют свои сервисы
Случай 7: NTP — синхронизация времени
Когда твой компьютер обновляет системное время — это NTP (Network Time Protocol). Идёт через UDP порт 123.
Почему UDP? Потому что для точного измерения времени важна симметричность задержки. NTP-клиент шлёт запрос, замеряет, когда ушёл; сервер отвечает с timestamp, когда получил и когда ответил; клиент замеряет, когда пришёл ответ. Из этих 4 timestamps вычисляется offset с точностью до миллисекунд.
TCP добавляет переменную задержку из-за retransmission и congestion control, что портит точность измерения. UDP — простой, прогнозируемый, минимальный оверхед.
Где UDP — НЕ лучше, и не надо тащить
Иногда junior читает про «UDP быстрее» и хочет применять везде. Это плохо. UDP лучше TCP только когда:
- Сообщения короткие (DNS, метрики). Если у тебя 10 МБ файл — UDP плох, потому что фрагментация и отсутствие congestion control приведёт к катастрофическим потерям.
- Допустима потеря данных (видео, игры). Если ты передаёшь банковскую транзакцию — UDP опасен.
- Нужен low latency или multicast (live стриминг, service discovery).
- Сервер обслуживает огромное количество клиентов (DNS, метрики) и не может позволить себе stateful соединения.
В остальных 80% случаев TCP — правильный выбор. HTTP, базы данных, очереди сообщений (Kafka, RabbitMQ), SSH — всё TCP, и это нормально.
Kafka — брокер сообщений поверх TCPНе пытайся «оптимизировать» обычный API на UDP. Если у тебя REST-сервис, который шлёт JSON-ответы — UDP даст тебе только проблемы. Ты потеряешь TLS, надёжность, упорядоченность, и потратишь недели на изобретение TCP заново. Если уверен, что нужен UDP — используй готовое: QUIC, gRPC поверх QUIC, WebRTC.
Попробуй сам
Несколько hands-on упражнений, чтобы прочувствовать сценарии:
# 1. DNS через UDP (классика):
dig +short www.github.com
# Ставь WireShark/tcpdump и смотри: один пакет туда, один обратно. Один RTT.
# 2. HTTP/3 (QUIC поверх UDP) на современном сайте:
curl --http3 -v https://cloudflare-quic.com 2>&1 | grep -i 'http\|quic\|udp'
# Видно, что используется HTTP/3 over QUIC over UDP
# 3. mDNS multicast (если на macOS или с avahi на Linux):
sudo tcpdump -i any -n 'udp port 5353' &
# В другой консоли подключи AirPods или включи AirPlay-приёмник --
# увидишь поток UDP multicast пакетов
# 4. Имитация game-сценария -- fire-and-forget input:
python3 -c "
import socket, time
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
for i in range(60): # 60 packets/sec
sock.sendto(b'input', ('127.0.0.1', 9999))
time.sleep(1/60)
"
# Никто на 9999 не слушает -- но программа не падает.
# Это и есть fire-and-forget. На реальном игровом сервере
# каждый такой пакет был бы input-ом от игрока
# 5. StatsD-метрики через UDP:
echo 'page.views:1|c' | nc -u -w0 127.0.0.1 8125
# Один UDP-пакет с метрикой. Никаких подтверждений, никакого state