Модуль: Сети и протоколы · Уровень: 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#
| Свойство | TCP | UDP |
|---|---|---|
| Соединение | да (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 недоступен).