Что делает UDP
port плюс контрольная сумма - вот и весь UDP. В отличие от [[tcp-handshake|TCP]], UDP не открывает соединение, не нумерует пакеты, не подтверждает доставку. Просто берёт твои данные, оборачивает в datagram с src/dst-портами и кидает в [[ipv4-addressing|IP]]-сеть.
Заголовок UDP - 8 байт
0 7 8 15 16 23 24 31
+--------+--------+--------+--------+
| src port | dst port |
+--------+--------+--------+--------+
| length | checksum |
+--------+--------+--------+--------+
| data ... |
- src/dst port - 16 бит, 0-65535 (как у TCP)
- length - длина UDP-заголовка + payload в байтах
- checksum - 16-битная сумма заголовка + payload (опциональна в IPv4, обязательна в IPv6)
Сравни с TCP, где заголовок минимум 20 байт + опции - UDP легче в 2.5 раза.
Когда выбирать UDP
| Сценарий | Почему UDP |
|---|---|
| DNS-запрос (один query, один ответ) | TCP-handshake = 1.5×RTT накладных, не нужен |
| DHCP (broadcast, новый клиент) | TCP не работает по broadcast |
| VoIP / видеозвонки | потеря 1-2 пакета < перезапрос; ретрансмит вреден |
| NTP (sync времени) | ретрансмит ломает точность |
| QUIC (HTTP/3) | контроль доставки в user-space, не в ядре |
| Стриминг (RTP) | потерю кадра проще пропустить |
| Игры (real-time) | актуальное состояние важнее старого |
Когда нельзя UDP
- Большие данные где порядок важен (HTTP/1-2, SSH, базы) - нужен TCP
- Reliability на уровне приложения - либо ты пишешь свой ack/retry поверх UDP (как QUIC), либо не используй UDP
Размер пакета и фрагментация
Теоретический максимум UDP-payload = 65 507 байт (65535 - IP-заголовок 20 - UDP-заголовок 8). Но MTU Ethernet = 1500, и UDP > MTU → IP-фрагментация на уровне сети. Это плохо:
- Если хоть один фрагмент потерялся - весь datagram дропается
- Многие firewall'ы дропают фрагменты по дефолту
- На пути MTU может быть меньше 1500 (тоннели, VPN)
Поэтому в UDP-приложениях держат payload под 1472 байта (1500 - IP - UDP). DNS исторически ограничен 512 байтами в UDP, для больших ответов переключается на TCP (или EDNS0 расширяет до 4096).
UDP-сокет на Linux
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP = SOCK_DGRAM
s.bind(("0.0.0.0", 5353))data, addr = s.recvfrom(4096) # blocking read одного датаграма
s.sendto(b"pong", addr) # ответ конкретному клиенту
Никаких listen()/accept()/connect() - сразу bind() и recvfrom().
Что видно в tcpdump
IP 10.0.0.1.55321 > 8.8.8.8.53: UDP, length 32
IP 8.8.8.8.53 > 10.0.0.1.55321: UDP, length 64
Никаких флагов SYN/ACK - просто два независимых датаграма. По одному пакету tcpdump не скажет, связаны они или нет (нет state).
Заметки про conntrack
Хотя у UDP нет state, [[conntrack|netfilter conntrack]] всё равно создаёт
«псевдо-соединение» по 5-tuple (src-ip, src-port, dst-ip, dst-port,
proto=UDP) и держит его 30 секунд после последнего пакета. Это нужно
чтобы [[nat|NAT]] работал и чтобы ответный пакет проходил RELATED,ESTABLISHED.
Когда что-то пошло не так
- No response - UDP не сообщит об ошибке. Если порт закрыт - ядро может прислать ICMP unreachable, но это best-effort
- Out-of-order delivery - UDP не сортирует. Приложение должно само разбираться через свой sequence-number
- Duplicate packets - могут быть. Опять, приложение само
- Большой пакет → TIMEOUT - вероятно фрагмент потерялся; шли меньше