Модуль: Сети и протоколы · Уровень: Middle+/Senior

TL;DR#

  • UDP — простой датаграммный транспорт: нет соединения, нет гарантий доставки/порядка, нет flow/congestion control. Только порты + контрольная сумма.
  • Плюсы: минимальная латентность и накладные расходы, отсутствие head-of-line blocking, broadcast/multicast, контроль над семантикой надёжности на стороне приложения.
  • Применять: DNS, DHCP, NTP, VoIP/видео (RTP), realtime-игры, метрики (statsd), service discovery, QUIC.
  • “Надёжность поверх UDP” = QUIC (HTTP/3), а также кастомные протоколы (ACK, ретрансмиты, FEC, sequence numbers) — вы переносите логику TCP в userspace.
  • В Go: net.UDPConn, ReadFromUDP/WriteToUDP, net.ListenPacket. Один сокет обслуживает множество “пиров”.

Теория#

UDP в двух словах#

Заголовок UDP — всего 8 байт: source port, dest port, length, checksum. Никакого состояния. Каждый WriteToUDP = одна датаграмма = (обычно) один IP-пакет.

UDP vs TCP#

СвойствоTCPUDP
Соединениеда (handshake)нет
Надёжностьгарантировананет
Порядокгарантированнет
Дедупликацияданет
Flow controlда (rwnd)нет
Congestion controlда (cwnd)нет (приложение само)
Граница сообщенийбайт-потоксохраняется (датаграмма)
Overhead заголовка20+ байт8 байт
Multicast/broadcastнетда
Head-of-line blockingданет

Когда выбирать UDP#

  • Латентность важнее надёжности: голос/видео — потерянный пакет лучше дропнуть, чем ждать ретрансмит.
  • Запрос-ответ малого размера: DNS — одна датаграмма туда, одна обратно, без накладных на handshake/teardown.
  • Multicast/broadcast: service discovery (mDNS), потоковое вещание.
  • Свой протокол надёжности: когда нужна частичная надёжность, приоритеты, FEC, мультиплексирование без HoL — например QUIC.

Границы датаграмм и размер#

  • UDP сохраняет границы: одно Read = одна датаграмма. Если буфер меньше датаграммы — остаток теряется (в отличие от TCP).
  • Практический безопасный payload без фрагментации: ~508 байт (576 MTU IPv4 − заголовки) или ~1200 байт под типичный Ethernet MTU 1500 (так делает QUIC). Большие датаграммы фрагментируются на IP-уровне → потеря одного фрагмента губит всю датаграмму.

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

Чтобы получить “TCP-подобные” гарантии, приложение реализует:

  • Sequence numbers — порядок и детект потерь.
  • ACK / NACK — подтверждения.
  • Ретрансмиссии по таймауту (RTO) или по NACK.
  • Congestion/flow control — иначе забьёте сеть.
  • FEC (forward error correction) — избыточность вместо ретрансмита (хорошо для realtime).

QUIC — канонический пример#

QUIC (RFC 9000) — транспорт от Google, основа HTTP/3:

  • Работает поверх UDP, но даёт надёжность, упорядоченность, congestion control.
  • Мультиплексирование без HoL: независимые streams; потеря в одном не блокирует другие (в отличие от TCP под HTTP/2).
  • 0-RTT / 1-RTT handshake с встроенным TLS 1.3 — handshake транспорта и крипто слиты.
  • Connection ID: соединение переживает смену IP (миграция сети — Wi-Fi → LTE) без реконнекта.
  • Живёт в userspace → можно деплоить новые версии без обновления ядра.

UDP в Go#

// Сервер
addr, _ := net.ResolveUDPAddr("udp", ":9000")
conn, _ := net.ListenUDP("udp", addr)
defer conn.Close()

buf := make([]byte, 1500) // под MTU
for {
    n, remote, err := conn.ReadFromUDP(buf)
    if err != nil { continue }
    // обработать buf[:n], remote — кто прислал
    conn.WriteToUDP([]byte("pong"), remote)
}
// Клиент: можно Dial для "connected UDP" — фиксирует peer,
// позволяет получать ICMP-ошибки (port unreachable) на Read/Write.
conn, _ := net.Dial("udp", "1.2.3.4:9000")
conn.Write([]byte("ping"))
  • net.ListenPacket("udp", ...) — generic интерфейс PacketConn.
  • Для QUIC в Go: библиотека quic-go (она же даёт HTTP/3 через http3.Transport/http3.Server).

Подводные камни / gotchas#

  • Маленький буфер на Read обрезает датаграмму молча — выделяйте буфер под максимальный ожидаемый размер (или MTU).
  • Нет congestion control “из коробки” — наивный UDP-флуд убивает сеть и сам себя (потери растут). Всегда думайте о rate limiting / backpressure.
  • Фрагментация IP — датаграммы >MTU фрагментируются; потеря одного фрагмента = потеря всей датаграммы. Держите payload ≤ ~1200 байт.
  • “Connected UDP” vs unconnected: Dial фиксирует peer и доставляет ICMP-ошибки; ListenUDP+ReadFromUDP обслуживает много пиров, но ICMP вы не увидите.
  • NAT timeout: UDP-маппинги в NAT короткоживущие (десятки секунд). Нужны keep-alive пакеты, иначе “дыра” закроется.
  • Спуфинг source IP тривиален (нет handshake) → UDP-протоколы (DNS, NTP, memcached) часто используются для amplification DDoS. На сервере проверяйте ratio запрос/ответ.
  • Один сокет — много горутин на Read: ReadFromUDP потокобезопасен, но для масштабирования используют SO_REUSEPORT (несколько сокетов, балансировка ядром).

Вопросы на собеседовании#

В: Почему DNS использует UDP, а не TCP? О: Малый запрос/ответ, латентность критична, нет смысла в handshake ради одного пакета. При ответе >512 байт (или с EDNS — >объявленного размера) или при zone transfer DNS переключается на TCP (флаг TC — truncated).

В: Какие гарантии НЕ даёт UDP и что придётся делать самому? О: Нет доставки, порядка, дедупликации, flow/congestion control. Чтобы получить надёжность — sequence numbers, ACK/NACK, ретрансмиты по RTO, congestion control, опционально FEC. Это, по сути, реимплементация TCP в userspace (что и делает QUIC).

В: Что произойдёт, если датаграмма больше размера буфера в Read? О: В UDP границы сохраняются: лишнее обрезается и теряется (не дочитывается следующим Read, как в TCP). Поэтому буфер выделяют под максимальный размер.

В: Зачем QUIC, если есть TCP+TLS? О: QUIC убирает HoL blocking на уровне транспорта (независимые streams), сливает TLS 1.3 в handshake (0/1-RTT), переживает смену IP через Connection ID, живёт в userspace (быстрая эволюция). TCP+TLS страдает от HoL на L4 и более дорогого handshake.

В: Что такое “connected UDP” и зачем он? О: Dial("udp", ...) ассоциирует сокет с конкретным peer. Тогда ядро доставляет ICMP-ошибки (например port unreachable) как ошибку на Read/Write, и можно использовать Write/Read без указания адреса. Для unconnected сокета ICMP вы не получите.

В: Как UDP-сервис может стать вектором amplification DDoS? О: Source IP не верифицируется (нет handshake). Атакующий шлёт мелкий запрос со spoofed IP жертвы, сервис отвечает крупным ответом жертве. Защита: ограничивать коэффициент усиления, rate limiting, response rate limiting, требовать токены/cookie.

В: Почему наивный UDP-протокол может работать хуже TCP под потерями? О: Без congestion control он не сбавляет темп при потерях, увеличивая заторы и собственные потери; без ретрансмитов теряет данные. TCP адаптивно реагирует на потери. Поэтому “просто UDP для скорости” без контроля перегрузки — антипаттерн.

В: Как масштабировать приём UDP на многих ядрах? О: SO_REUSEPORT — несколько сокетов на одном порту, ядро балансирует датаграммы между ними по 4-tuple, каждый сокет читает своей горутиной/тредом, избегая контеншена на одном сокете.

На что копают на senior+#

  • Детали QUIC: streams vs connection, 0-RTT и его replay-риски, connection migration, как congestion control (BBR/CUBIC) реализован в userspace.
  • Почему QUIC выбрал UDP, а не новый L4-протокол (middlebox ossification — файрволлы/NAT режут всё кроме TCP/UDP).
  • FEC vs ретрансмиты: trade-off для realtime (доп. трафик vs доп. латентность).
  • GSO/GRO и offloading для высокопроизводительного UDP (важно для QUIC-серверов, исторически медленных).
  • Точный расчёт безопасного payload под Path MTU, поведение DF-бита и PMTUD для UDP.
  • mDNS/SSDP/multicast-группы и проблемы их работы в облаке (часто multicast недоступен).