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

- Что такое TCP и UDP
- Как TCP устанавливает соединение: тройное рукопожатие
- Как TCP гарантирует доставку данных и порядок пакетов
- Как работает протокол UDP
- Отличия TCP и UDP: сравнение в таблице
- Порты TCP и UDP
- Где используют TCP, а где UDP
- DNS-запросы
- Голосовые и видеозвонки, потоковое видео
- Онлайн-игры
- QUIC и HTTP/3
- Пример на Python: TCP эхо-сервер и клиент
- Пример на Python: UDP эхо-сервер и клиент
- Почему TCP склеивает сообщения, а UDP нет
- Как проверить TCP и UDP порты в Windows
- Что выбрать: TCP или UDP
- Частые вопросы о TCP и UDP
- Какой протокол быстрее, TCP или UDP?
- Почему HTTP/3 работает поверх UDP, если вебу нужна надёжность?
Что такое TCP и UDP
TCP (Transmission Control Protocol) и UDP (User Datagram Protocol) служат двумя основными протоколами транспортного уровня в стеке TCP/IP. Под ними работает протокол IP, который доставляет пакеты между компьютерами по адресам, над ними прикладные протоколы: HTTP, SMTP, DNS, SSH. Транспортный протокол по номеру порта решает, какой программе отдать пришедшие данные, а TCP ещё и отвечает за надёжность передачи данных. В семиуровневой модели OSI транспортный уровень четвёртый снизу.
Как TCP устанавливает соединение: тройное рукопожатие
Для установления соединения стороны обмениваются тремя служебными пакетами (three-way handshake), и только потом начинается передача данных:
- Клиент отправляет пакет с флагом SYN и своим начальным порядковым номером, например 100.
- Сервер отвечает пакетом SYN-ACK: в нём его собственный начальный номер, например 300, и подтверждение 101. Число в поле подтверждения означает номер, начиная с которого сторона ждёт следующие данные.
- Клиент отправляет 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-клиент, который не дождался ответа, сам отправляет запрос ещё раз и не рассчитывает, что ответы придут в порядке запросов.
Полей для управления потоком и перегрузкой в заголовке 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), и задержка растёт.

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: порт на той стороне никто не слушает. Признаком потери данных она не служит: если пакет потеряется по дороге, ошибки не будет, сработает только тайм-аут.
Почему 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.








