TCP и UDP: чем отличаются протоколы и когда какой выбрать

Два компьютера на столе, между ними ровный ряд конвертов и россыпь конвертов Технологии

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

Два компьютера на столе, между ними ровный ряд конвертов и россыпь конвертов

Что такое TCP и UDP

TCP (Transmission Control Protocol) и UDP (User Datagram Protocol) служат двумя основными протоколами транспортного уровня в стеке TCP/IP. Под ними работает протокол IP, который доставляет пакеты между компьютерами по адресам, над ними прикладные протоколы: HTTP, SMTP, DNS, SSH. Транспортный протокол по номеру порта решает, какой программе отдать пришедшие данные, а TCP ещё и отвечает за надёжность передачи данных. В семиуровневой модели OSI транспортный уровень четвёртый снизу.

Как TCP устанавливает соединение: тройное рукопожатие

Для установления соединения стороны обмениваются тремя служебными пакетами (three-way handshake), и только потом начинается передача данных:

  1. Клиент отправляет пакет с флагом SYN и своим начальным порядковым номером, например 100.
  2. Сервер отвечает пакетом SYN-ACK: в нём его собственный начальный номер, например 300, и подтверждение 101. Число в поле подтверждения означает номер, начиная с которого сторона ждёт следующие данные.
  3. Клиент отправляет ACK с подтверждением 301. Соединение установлено.

Сам ACK номер не занимает, поэтому первый пакет с данными у клиента тоже идёт с номером 101. Три шага нужны, чтобы задержавшиеся в сети старые дубли запросов на соединение не сбили стороны с толку. В простом случае, как в этой схеме, данные идут не раньше, чем SYN дойдёт до сервера и SYN-ACK вернётся обратно. У протокола UDP рукопожатия нет: первая же датаграмма несёт данные. Закрывают соединение TCP тоже явно: флаг FIN означает, что отправителю больше нечего отправлять.

Ноутбук, сервер и три стрелки между ними: вправо, влево и снова вправо

Как TCP гарантирует доставку данных и порядок пакетов

Надёжность TCP складывается из нескольких механизмов:

Механизм Что делает
Порядковые номера у каждого байта данных свой номер, по номерам получатель восстанавливает порядок пакетов и отбрасывает дубли
Подтверждения подтверждение с номером X значит, что получены все данные до X
Повторная передача пакет без подтверждения отправляется снова; тайм-аут вычисляется по ходу передачи данных
Контрольная сумма ловит повреждённые пакеты, их передача повторяется
Управление потоком в поле Window получатель сообщает, сколько данных готов принять сейчас
Контроль перегрузки медленный старт, предотвращение перегрузки и растущий тайм-аут повторов, чтобы не перегрузить сеть

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

Заголовок TCP-пакета без опций занимает 20 байт: два порта по 2 байта, порядковый номер и номер подтверждения по 4 байта, 2 байта на длину заголовка и флаги, по 2 байта на окно, контрольную сумму и указатель срочности. С опциями заголовок вырастает максимум до 60 байт: его длина хранится в 4 битах и считается в словах по 4 байта.

Программе TCP отдаёт сплошной поток байтов без деления на сообщения: границ между вызовами отправки в этом потоке нет.

Ровный ряд конвертов один за другим на ленте конвейера, на конвертах метки-точки

Как работает протокол UDP

UDP добавляет к IP почти только порты и контрольную сумму. Заголовок UDP-пакета состоит из четырёх полей по 2 байта: порт отправителя, порт получателя, длина и контрольная сумма, итого 8 байт. Длина считается вместе с заголовком, поэтому меньше 8 она не бывает. В IPv4 нулевая контрольная сумма означает, что отправитель её не считал; в IPv6 сумма обязательна.

Сам протокол не обещает ни доставки данных, ни защиты от дублей. Программам, которым нужна упорядоченная надёжная доставка, подходит TCP. Потерю пакетов или повтор замечает только приложение: например, DNS-клиент, который не дождался ответа, сам отправляет запрос ещё раз и не рассчитывает, что ответы придут в порядке запросов.

Читайте также:  REST API

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

Конверты рассыпаны по столу в беспорядке, один упал на пол

Отличия TCP и UDP: сравнение в таблице

TCP UDP
Соединение устанавливается тройным рукопожатием нет, пакеты уходят сразу
Гарантия доставки данных есть: подтверждения и повторная передача нет
Порядок пакетов данные приходят в порядке отправки не гарантирован
Дубли отсеиваются по порядковым номерам возможны
Единица передачи данных поток байтов, границ сообщений нет датаграмма, границы сохраняются
Заголовок 20-60 байт 8 байт
Управление потоком и перегрузкой есть, контроль перегрузки обязателен нет в самом протоколе
Контрольная сумма в каждом пакете в IPv4 можно не считать (ноль), в IPv6 обязательна
Тип сокета в Python socket.SOCK_STREAM socket.SOCK_DGRAM
Примеры HTTP/1.1 и HTTP/2, SMTP, SSH DNS-запросы, DHCP, RTP для звонков, QUIC

Порты TCP и UDP

Порт задаётся 16-битным числом в заголовке пакета, от 0 до 65535. По нему система понимает, какой программе отдать пришедшие данные. Диапазоны поделены так:

  • 0-1023: системные, или хорошо известные порты, их закрепляет реестр портов;
  • 1024-49151: пользовательские (зарегистрированные), тоже по реестру;
  • 49152-65535: динамические, или временные, их никому не закрепляют. Временный порт нашего TCP-клиента из примера ниже (58141) попал как раз сюда.

Пространства портов у TCP и UDP раздельные: TCP-сокет и UDP-сокет могут одновременно занять один и тот же адрес и порт, а второй TCP-сокет на тот же адрес на Windows падает с OSError: [WinError 10048].

Порт Служба в реестре Записан в реестре для Какой протокол на деле
22 ssh TCP и UDP TCP
25 smtp TCP и UDP TCP
53 domain (DNS) TCP и UDP UDP для обычных запросов, TCP для больших ответов и передачи зоны
67, 68 bootps, bootpc (DHCP) TCP и UDP UDP
80 http TCP и UDP TCP (HTTP/1.1)
443 https TCP и UDP TCP; HTTP/3 идёт по UDP на порту, который сервер объявляет в заголовке Alt-Svc

Запись в реестре не говорит, какой протокол у службы. До 2011 года номер выдавали сразу для TCP и UDP, даже если служба пользовалась одним протоколом. Теперь номер дают только под протоколы из заявки, а на остальных его помечают как зарезервированный. В реестре есть и такие записи: у IMAP порт 143 записан за TCP, а 143/udp стоит как Reserved. Какой протокол нужен службе, написано в её спецификации.

Стена почтовых ящиков с одинаковыми дверцами, одна дверца открыта

Где используют TCP, а где UDP

TCP применяют там, где потеря одного байта портит весь результат: загрузка страниц по HTTP/1.1 и HTTP/2, электронная почта по SMTP, удалённый доступ по SSH. Что HTTP передаёт внутри такого соединения, разобрано в статьях про HTTP заголовки и REST API.

UDP используется в DNS, в DHCP (через него устройство получает сетевые настройки), в звонках, играх и под HTTP/3.

DNS-запросы

Обычный запрос идёт по UDP на порт 53. Без расширения EDNS0 ответ по UDP ограничен 512 байтами; если ответ больше, сервер обрезает его и ставит флаг TC, а клиент повторяет запрос по TCP. Полная передача зоны (AXFR) между серверами всегда идёт по TCP. Сейчас DNS-серверы и системные DNS-клиенты обязаны уметь оба протокола, а TCP можно использовать сразу, без попытки по UDP.

Голосовые и видеозвонки, потоковое видео

Аудио и видео в реальном времени, например в видеоконференции, обычно передают по протоколу RTP, а RTP чаще всего работает поверх UDP. Видео по запросу устроено иначе: HLS передаёт куски ролика по HTTP, то есть по TCP или по QUIC. Расхожее мнение, что видео всегда идёт по UDP, верно для звонков, но не для ролика, который вы смотрите в плеере.

Онлайн-игры

Сетевая библиотека для мультиплеера Unity Transport по умолчанию отправляет ненадёжные датаграммы, а надёжную доставку с порядком включают отдельно. Надёжную доставку включают только для важного трафика, который нельзя потерять, например действий персонажа: надёжный поток страдает от блокировки очереди (head-of-line blocking), и задержка растёт.

Читайте также:  HTTP заголовки: что это, какие бывают и как их посмотреть

Гарнитура с микрофоном и геймпад на рабочем столе

QUIC и HTTP/3

Протокол QUIC передаёт свои пакеты внутри UDP-датаграмм, но сам делает подтверждения, повторную передачу данных, контроль перегрузки и шифрование на TLS 1.3. HTTP/3 работает поверх QUIC. В HTTP/2 поверх TCP один потерянный пакет задерживает все запросы соединения, а в QUIC надёжность устроена по отдельным потокам данных.

Пример на Python: TCP эхо-сервер и клиент

Примеры проверены на Windows 10, Python 3.14.3 и Windows PowerShell 5.1, только на адресе 127.0.0.1: интернет и другие устройства в сети для них не нужны. Сервер запускают в одном окне PowerShell, клиента во втором. Сохраните сервер в файл tcp_server.py:

# tcp_server.py
import socket

ADDRESS = ("127.0.0.1", 50007)

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as listener:
    listener.bind(ADDRESS)
    listener.listen()
    listener.settimeout(1.0)  # раз в секунду accept() отпускает цикл, и Ctrl+C срабатывает
    print("TCP-сервер слушает", ADDRESS)
    try:
        while True:
            try:
                connection, remote_address = listener.accept()
            except TimeoutError:
                continue
            with connection:
                print("Подключился клиент", remote_address)
                while True:
                    data = connection.recv(1024)
                    if not data:  # пустые байты: клиент закрыл соединение
                        break
                    connection.sendall(data)
            print("Клиент отключился", remote_address)
    except KeyboardInterrupt:
        print("Сервер остановлен")

SOCK_STREAM означает протокол TCP. Метод accept() возвращает новый сокет для соединения с клиентом, а listener ждёт следующих. Когда клиент закрывает соединение, recv() возвращает пустые байты и внутренний цикл заканчивается.

На Windows тайм-аут settimeout(1.0) нужен ради Ctrl+C. Мы запускали сервер в отдельной консоли Windows и посылали ей Ctrl+C: вариант без тайм-аута, где accept() ждёт клиента без ограничения, не остановился и за 8 секунд, а вариант с тайм-аутом сразу напечатал «Сервер остановлен» и завершился. Раз в секунду accept() выбрасывает TimeoutError, цикл идёт на следующий круг, и Python успевает обработать нажатие.

Клиент в файле tcp_client.py:

# tcp_client.py
import socket

ADDRESS = ("127.0.0.1", 50007)

try:
    with socket.create_connection(ADDRESS, timeout=5) as connection:
        print("Соединение:", connection.getsockname(), "->", connection.getpeername())
        for message in ["привет", "второе сообщение"]:
            connection.sendall(message.encode("utf-8"))
            reply = connection.recv(1024)
            print("Отправили:", message, "| получили:", reply.decode("utf-8"))
except ConnectionRefusedError as error:
    print("Сервер не принял соединение:", error)

В первом окне запускаем python tcp_server.py, во втором python tcp_client.py. Вывод клиента:

Соединение: ('127.0.0.1', 58141) -> ('127.0.0.1', 50007)
Отправили: привет | получили: привет
Отправили: второе сообщение | получили: второе сообщение

Сервер в своём окне печатает адрес клиента при подключении и отключении. Если сервер не запущен, рукопожатие не состоится, и клиент получит отказ. В наших прогонах на Windows ошибка приходила через 2,2-2,3 секунды:

Сервер не принял соединение: [WinError 10061] Подключение не установлено, т.к. конечный компьютер отверг запрос на подключение

Два бумажных стаканчика, соединённые натянутой нитью

Пример на Python: UDP эхо-сервер и клиент

UDP-сервер в файле udp_server.py не принимает соединений, он просто ждёт датаграммы:

# udp_server.py
import socket

ADDRESS = ("127.0.0.1", 50008)

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server:
    server.bind(ADDRESS)
    server.settimeout(1.0)  # на Windows без тайм-аута Ctrl+C не срабатывает
    print("UDP-сервер ждёт датаграммы на", ADDRESS)
    try:
        while True:
            try:
                data, remote_address = server.recvfrom(65535)
            except TimeoutError:
                continue
            print("Датаграмма от", remote_address, ":", data.decode("utf-8"))
            server.sendto(data, remote_address)
    except KeyboardInterrupt:
        print("Сервер остановлен")

Здесь нет listen() и accept(): recvfrom() возвращает данные вместе с адресом отправителя, и ответ уходит туда через sendto().

Клиент в файле udp_client.py:

# udp_client.py
import socket

ADDRESS = ("127.0.0.1", 50008)

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client:
    client.settimeout(2)
    for message in ["пакет 1", "пакет 2", "пакет 3"]:
        client.sendto(message.encode("utf-8"), ADDRESS)
        try:
            reply, _ = client.recvfrom(65535)
            print("Отправили:", message, "| ответ:", reply.decode("utf-8"))
        except TimeoutError:
            print("Ответа нет за 2 секунды:", message)
        except ConnectionResetError as error:
            print("Ошибка от системы:", error)
            break

Тайм-аут обязателен: UDP не сообщает о потере пакетов, и без settimeout(2) клиент ждал бы потерянные данные вечно. С Python 3.10 исключение тайм-аута сокета совпадает с обычным TimeoutError. Вывод клиента при запущенном сервере:

Отправили: пакет 1 | ответ: пакет 1
Отправили: пакет 2 | ответ: пакет 2
Отправили: пакет 3 | ответ: пакет 3

Потерь пакетов на 127.0.0.1 в наших прогонах не было ни разу. Ветка TimeoutError нужна на случай потерь в реальной сети, на одном компьютере мы её не воспроизвели.

Без сервера Windows 10 повела себя так: recvfrom() упал с ошибкой вместо тайм-аута (без break то же повторяется на каждом сообщении).

Ошибка от системы: [WinError 10054] Удаленный хост принудительно разорвал существующее подключение

Соединения в UDP нет, так что название ошибки сбивает с толку. Так Windows сообщает при следующем recvfrom(), что на одну из прошлых отправок пришёл ответ ICMP Port Unreachable: порт на той стороне никто не слушает. Признаком потери данных она не служит: если пакет потеряется по дороге, ошибки не будет, сработает только тайм-аут.

Читайте также:  OAuth 2.0: что это и как работает протокол авторизации

Почему TCP склеивает сообщения, а UDP нет

Скрипт отправляет три строки по TCP и по UDP, а сервер читает их уже после отправки:

# message_boundaries.py
import socket

HOST = "127.0.0.1"
MESSAGES = ["раз", "два", "три"]

# TCP: отправитель шлёт три сообщения, получатель читает их одним вызовом recv
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as tcp_listener:
    tcp_listener.bind((HOST, 0))  # порт 0: свободный порт выберет система
    tcp_listener.listen()
    with socket.create_connection(tcp_listener.getsockname()) as tcp_sender:
        for message in MESSAGES:
            tcp_sender.sendall(message.encode("utf-8"))
        connection, _ = tcp_listener.accept()
        with connection:
            data = connection.recv(1024)
            print("TCP, один вызов recv:", repr(data.decode("utf-8")))

# UDP: те же три сообщения, каждое отдельной датаграммой
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_receiver:
    udp_receiver.bind((HOST, 0))
    udp_receiver.settimeout(1)
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as udp_sender:
        for message in MESSAGES:
            udp_sender.sendto(message.encode("utf-8"), udp_receiver.getsockname())
    for _ in MESSAGES:
        data, _ = udp_receiver.recvfrom(1024)
        print("UDP, один вызов recvfrom:", repr(data.decode("utf-8")))

Отправителю не нужно ждать accept(): после listen() система держит очередь ещё не принятых соединений. Вывод (десять прогонов подряд дали одно и то же):

TCP, один вызов recv: 'раздватри'
UDP, один вызов recvfrom: 'раз'
UDP, один вызов recvfrom: 'два'
UDP, один вызов recvfrom: 'три'

Три вызова sendall() превратились в одну строку: в потоке TCP нет границ между сообщениями. В реальной сети возможно и обратное, когда одно сообщение приходит по частям за несколько вызовов recv(). Протокол UDP вернул три пакета, каждый целиком.

Поэтому в своём протоколе поверх TCP сообщение должно само говорить, где кончается: фиксированной длиной, разделителем (например, переводом строки) или длиной в начале. Наш эхо-клиент обходится без этого только потому, что сообщения короткие и ответ читается сразу после отправки.

Слева три конверта склеены в одну пачку, справа три отдельных конверта

Как проверить TCP и UDP порты в Windows

Пока оба эхо-сервера запущены, их порты видно в третьем окне PowerShell (как сохранить команды в файл, написано в статье про скрипты PowerShell).

Get-NetTCPConnection -LocalPort 50007 | Select-Object LocalAddress, LocalPort, State, OwningProcess
LocalAddress LocalPort  State OwningProcess
------------ ---------  ----- -------------
127.0.0.1        50007 Listen         31312
Get-NetUDPEndpoint -LocalPort 50008 | Select-Object LocalAddress, LocalPort, OwningProcess
LocalAddress LocalPort OwningProcess
------------ --------- -------------
127.0.0.1        50008         26816

Listen значит, что TCP-сервер ждёт подключений. У UDP-точки состояния нет: соединений в UDP нет. В OwningProcess стоит PID процесса, который держит порт.

Test-NetConnection с параметром -Port проверяет только TCP. Для работающего UDP-сервера он вернёт False:

Test-NetConnection -ComputerName 127.0.0.1 -Port 50007 -InformationLevel Quiet
Test-NetConnection -ComputerName 127.0.0.1 -Port 50008 -InformationLevel Quiet
True
ПРЕДУПРЕЖДЕНИЕ: TCP connect to (127.0.0.1 : 50008) failed
False

UDP-порт этой командой не проверить: нужен ответ от самой службы, как в нашем udp_client.py.

Что выбрать: TCP или UDP

В своём коде начинайте с TCP или с готового протокола поверх него, например HTTP. У UDP нет ни повторной передачи, ни контроля перегрузки, и всё это пришлось бы писать самому.

UDP стоит использовать для голоса и видео в звонке, для игр, где данные идут в реальном времени, или когда запрос короткий и приложение само повторит его, как в DNS. Если же поверх UDP понадобилась надёжная доставка, мы бы не советовали писать свои подтверждения и повторы: возьмите готовую библиотеку QUIC, например aioquic для Python, где это уже сделано вместе с контролем перегрузки и шифрованием.

Частые вопросы о TCP и UDP

Какой протокол быстрее, TCP или UDP?

До первых данных TCP тратит рукопожатие: пакет должен сходить до сервера и вернуться обратно. UDP отправляет данные сразу, и первые данные приходят раньше на один путь туда и обратно. Заголовок UDP 8 байт против 20-60 у TCP, на каждом пакете это экономит трафик. Но повторы и порядок данных тогда делает само приложение, как это сделано в QUIC.

Почему HTTP/3 работает поверх UDP, если вебу нужна надёжность?

Надёжность в HTTP/3 даёт QUIC: подтверждения, повторы и контроль перегрузки сделаны в нём. Пакеты QUIC кладёт в UDP-датаграммы, чтобы его было проще развернуть в существующих системах и сетях. Если UDP заблокирован, клиенту следует перейти на HTTP поверх TCP.

Оцените статью
bestprogrammer.ru
Добавить комментарий