10. TCP. Устройство, гарантии и жизненный цикл соединения
«Серьезные проблемы, с которыми мы сталкиваемся, невозможно решить на том же уровне мышления, на котором мы их создали».
— приписывается Альберту Эйнштейну
Представьте, что вы хотите отправить другу многотомное собрание сочинений, но в вашем распоряжении есть только почтовые открытки. Каждая открытка может потеряться в пути, задержаться на таможне на неделю или прийти раньше тех, что были отправлены за несколько дней до нее. Это суровая реальность сетевого уровня IP: доставка отдельных пакетов без каких-либо гарантий целостности, порядка и сохранности (Best-Effort Delivery).
Как поверх этого ненадежного хаоса построить иллюзию идеальной, непрерывной телефонной трубы — виртуального двунаправленного потока, куда вы отправляете байты на одной стороне, а на другой стороне они появляются в строго идентичном порядке, без пропусков, искажений и дубликатов?
Этим триумфом инженерной мысли является TCP (Transmission Control Protocol) — протокол, разработанный Винтоном Серфом и Робертом Каном в далеком 1974 году.
В повседневной разработке на Go протокол TCP прячется за элегантным интерфейсом net.Conn. Инженер пишет conn.Write(data) и conn.Read(buf), словно работает с обычным файлом на диске. Но с точки зрения концепции Mechanical Sympathy за этим лаконичным фасадом скрывается огромный айсберг: сложнейший конечный автомат ядра Linux, структуры данных tcp_sock, кольцевые буферы сокетов, синхронизация кэш-линий процессора по протоколу MESI и асинхронный сетевой поллер Go (netpoller).
В этой лекции мы заглянем под капот TCP, разберем анатомию его заголовка, изучим гарантийный контракт и проследим жизненный цикл соединения от первого флага SYN до финального TIME_WAIT.
Введение: Что такое TCP на самом деле?
TCP — это не просто абстрактный «надежный протокол». Для операционной системы TCP — это высоконагруженный конечный автомат с состоянием (Stateful Finite State Machine), работающий целиком в пространстве ядра.
В отличие от протокола UDP, где сетевой стек просто «выстреливает» датаграмму в провод и мгновенно забывает о ней, TCP требует от обеих сторон постоянного поддержания и синхронизации контекста соединения:
- Аллокация памяти в ядре: Для каждого активного TCP-сокета операционная система выделяет внутреннюю структуру ядра
struct tcp_sock, дескрипторstruct file, а также буферы отправки (sndbuf) и приема (rcvbuf). - Синхронизация многоядерных процессоров: При отправке и приеме каждого сетевого сегмента ядро обновляет переменные последовательностей и окон. При конкурентном доступе сетевых прерываний и системных потоков это приводит к атомарным операциям
cmpxchgи инвалидации процессорных кэш-линий (эффект Cache Line Ping-Pong по протоколу когерентности MESI). - Контроль перегрузок и очередей: Ядро динамически вычисляет задержку прохождения пакетов (RTT) и адаптирует скорость передачи данных под физическое состояние линии.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
subgraph Userspace ["Пространство пользователя (Go Runtime)"]
GoApp["Go-приложение (горутины)"]
NetFD["Интерфейс net.Conn (netFD)"]
GoApp <-->|"conn.Read() / conn.Write()"| NetFD
end
subgraph KernelSpace ["Пространство ядра Linux (TCP/IP Stack)"]
SockStruct["struct tcp_sock (Состояние сокета)"]
SndBuf["Очередь отправки: sndbuf (sk_buff)"]
RcvBuf["Очередь приема: rcvbuf (sk_buff)"]
FSM["Конечный автомат TCP (ESTABLISHED, etc.)"]
NetFD <-->|"Системные вызовы / Netpoller epoll"| SockStruct
SockStruct --> SndBuf
SockStruct --> RcvBuf
SockStruct --> FSM
end
subgraph Hardware ["Аппаратный уровень"]
NIC["Сетевой адаптер (NIC DMA / Ring Buffers)"]
SndBuf --> NIC
NIC --> RcvBuf
end
Устройство TCP-сегмента (Under the Hood)
Поскольку протокол сетевого уровня IP не предоставляет никаких гарантий, протокол TCP вынужден нести в собственном заголовке всю исчерпывающую информацию, необходимую для сборки разрозненных фрагментов в упорядоченный байтовый поток.
Минимальный размер заголовка TCP без дополнительных опций составляет 20 байт. При использовании расширений поле опций (Options) может увеличивать заголовок до 60 байт.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data Offset | Reserved |C|E|U|A|P|R|S|F| Window Size |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options (variable length) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Ключевые поля TCP-заголовка для бэкенд-инженера:
- Source Port & Destination Port (по 16 бит): Идентифицируют сокеты отправляющего и принимающего процессов. Вместе с IP-адресами источника и назначения они образуют так называемый 5-tuple (Src IP, Dst IP, Src Port, Dst Port, Protocol), однозначно идентифицирующий TCP-сессию.
Sequence Number(32 бита): Порядковый номер первого байта данных в данном сегменте. В отличие от наивных представлений, отсчет начинается не с нуля, а со случайного стартового числа (Initial Sequence Number, ISN) для защиты от атак подмены пакетов.Acknowledgment Number(32 бита): Номер подтверждения. Указывает порядковый номер байта, который отправитель ожидает получить следующим. TCP использует принцип кумулятивного подтверждения (Cumulative ACK): подтверждение номераNозначает, что абсолютно все байты в диапазоне от начала сессии доN-1успешно получены и собраны без ошибок.- Data Offset (4 бита): Длина заголовка TCP в 32-битных словах. Указывает, где заканчиваются опции и начинаются полезные данные приложения.
- Флаги управления (Control Flags, 9 бит):
SYN(Synchronize): Инициирует установку TCP-соединения и синхронизирует стартовые номера ISN;ACK(Acknowledgment): Подтверждает получение данных;FIN(Finish): Инициирует корректное полузакрытие соединения со стороны отправителя;RST(Reset): Мгновенный сброс и разрыв соединения при фатальной ошибке или отказе в подключении;- PSH (Push): Просьба немедленно передать накопленные в буфере приема байты приложению без ожидания заполнения пакета;
- URG (Urgent): Признак срочных данных (устарело, на практике почти не используется);
- ECE & CWR: Флаги явного уведомления о перегрузке маршрутизаторов в сети (Explicit Congestion Notification, ECN).
Window Size(16 бит): Размер окна приема (Flow Control). Указывает, сколько байт данных приемник готов принять в свой приемный буферrcvbufпрямо сейчас без подтверждения.- Checksum (16 бит): Контрольная сумма, вычисляемая по заголовку TCP, псевдозаголовку IP и полезной нагрузке.
Options(переменная длина): Критически важный блок согласования параметров при рукопожатии:- MSS (Maximum Segment Size): Максимальный размер полезной нагрузки сегмента (обычно 1460 байт для Ethernet);
- Window Scaling: Масштабирование окна (сдвиг до 14 бит), позволяющее увеличить размер окна приема с 64 КБ до 1 ГБ для высокоскоростных гигабитных каналов;
- SACK (Selective Acknowledgment): Выборочные подтверждения, позволяющие повторно отправлять только потерянные сегменты, а не всю цепочку;
- Timestamps: Метки времени для ювелирного измерения времени круговой задержки RTT и защиты от зацикливания номеров (PAWS).
Гарантийный контракт TCP
Протокол TCP предлагает прикладному коду три строгие гарантии надежности, за каждую из которых разработчик платит системными ресурсами и сетевыми задержками:
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
Contract["Гарантийный контракт TCP"]:::network
Rel["1. Надежная доставка (Reliability)"]
Ord["2. Строгий порядок (Ordering)"]
NoDup["3. Отсутствие дубликатов (No Duplicates)"]
Contract --> Rel
Contract --> Ord
Contract --> NoDup
Rel --> CostRel["Плата: Retransmission Timers, задержки RTT, зависание conn.Read()"]
Ord --> CostOrd["Плата: Head-of-Line Blocking, буферизация в ядре, потоковая природа"]
NoDup --> CostNoDup["Плата: Накладные расходы на Cumulative ACK и отслеживание Sequence"]
- Гарантия доставки (Reliability):
Если сетевой пакет теряется на промежуточном маршрутизаторе, ядро обнаруживает потерю по таймеру повторной передачи (Retransmission Timeout, RTO) или через тройной дублирующий ACK и отправляет дубликат.
Важнейший вывод для Go: Вызовconn.Write()возвращает управление со статусом успеха (nil, nil) вовсе не тогда, когда данные дошли до удаленного сервера! Он возвращает успех в тот момент, когда байты успешно скопированы из пространства пользователя Go в буфер отправки ядраsndbuf. Если через секунду провод оборвется, данные пропадут, а приложение на Go узнает об этом только при следующей попытке записи. С другой стороны, вызовconn.Read()может заблокироваться надолго, если удаленный клиент молча отключил питание: Go по умолчанию включает TCP keep-alive (15 с простоя, интервал 15 с, 9 проб — около 150 с на простаивающем соединении), но при неподтверждённых данных в сокете keep-alive не срабатывает, поэтому всё равно нужны явные дедлайны (SetReadDeadline) или Heartbeat. - Гарантия порядка (Ordering):
Благодаря полюSequence Numberядро Linux сортирует и пересобирает перемешанные в сети IP-пакеты перед тем, как отдать их в сокет. Для программиста на Go это означает работу с непрерывным потоком байт (Stream), а не с отдельными сообщениями. В TCP принципиально отсутствуют границы сообщений! - Отсутствие дубликатов (No Duplicates):
Благодаря кумулятивным подтверждениям Cumulative ACK подтвержденный байтNгарантирует, что байты в диапазоне1..N-1не будут доставлены прикладному коду повторно. Любые запоздавшие дубликаты сегментов ядро фильтрует и отбрасывает на аппаратном и системном уровне.
Жизненный цикл соединения: От SYN до FIN
Протокол TCP полностью управляется конечным автоматом состояний. Жизненный цикл классического соединения проходит через три обязательные фазы: установка (Handshake), передача данных и разрыв (Teardown).
sequenceDiagram
autonumber
participant Client as Клиент Go (net.Dial)
participant CKernel as Ядро Клиента
participant SKernel as Ядро Сервера
participant Server as Сервер Go (net.Listen)
Note over Client,Server: Фаза 1: 3-Way Handshake (Установка соединения)
Client->>CKernel: socket() + connect()
CKernel->>SKernel: 1. SYN (Seq=X, Options: MSS, WScale)
Note over SKernel: Сокет в SYN_RCVD
SKernel->>CKernel: 2. SYN-ACK (Seq=Y, Ack=X+1)
Note over CKernel: Сокет в ESTABLISHED
CKernel-->>Client: net.Dial() завершается -> conn
CKernel->>SKernel: 3. ACK (Ack=Y+1)
Note over SKernel: Сокет в ESTABLISHED -> попадает в Accept Queue
SKernel-->>Server: listener.Accept() -> conn
Note over Client,Server: Фаза 2: Передача данных (Data Transfer)
Client->>CKernel: conn.Write(данные)
CKernel->>SKernel: PSH-ACK (Seq=X+1, Data)
SKernel->>CKernel: ACK (Ack=X+1+Len)
SKernel-->>Server: conn.Read() возвращает данные
Note over Client,Server: Фаза 3: 4-Way Teardown (Корректное закрытие)
Client->>CKernel: conn.Close()
CKernel->>SKernel: 1. FIN (Seq=X_final)
Note over CKernel: FIN_WAIT_1
SKernel->>CKernel: 2. ACK (Ack=X_final+1)
Note over CKernel: FIN_WAIT_2
Note over SKernel: CLOSE_WAIT (Сервер уведомлен через io.EOF)
Server->>SKernel: conn.Close()
SKernel->>CKernel: 3. FIN (Seq=Y_final)
Note over SKernel: LAST_ACK
CKernel->>SKernel: 4. ACK (Ack=Y_final+1)
Note over SKernel: CLOSED
Note over CKernel: TIME_WAIT (Длится 2 * MSL = 60 секунд)
Note over CKernel: По истечении таймера -> CLOSED
Подробный разбор стадий жизненного цикла:
- 3-Way Handshake (Тройное рукопожатие):
- Клиент отправляет пакет
SYNсо своим начальным номеромSeq=Xи набором поддерживаемых опций (MSS, SACK, Window Scale). - Сервер отвечает пакетом
SYN-ACKсо своим собственным номеромSeq=Yи подтверждениемAck=X+1. - Клиент завершает рукопожатие отправкой пакета
ACK(Ack=Y+1). В Go блокирующий вызовnet.Dial()возвращает управление горутине ровно в этот момент.
- Клиент отправляет пакет
- Фаза передачи данных (ESTABLISHED):
Данные непрерывно курсируют в обоих направлениях. Стороны используют скользящее окно (Sliding Window), отправляя группы пакетов без ожидания ответа на каждый отдельный байт. - Корректное закрытие (Tear Down):
TCP является дуплексным протоколом: каждая сторона закрывает свой исходящий канал независимо.- Инициатор закрытия (вызов
conn.Close()) отправляет флагFINи переходит в состояниеFIN_WAIT_1. - Вторая сторона отвечает пакетом
ACKи переходит в состояниеCLOSE_WAIT. Прикладной код на сервере получает ошибкуio.EOFпри чтении из сокета. - Сервер досылает остатки данных, вызывает свой
conn.Close()и отправляет собственный флагFIN(или комбинированный пакетFIN-ACK), переходя вLAST_ACK. - Клиент отправляет финальный
ACKи переводит сокет в специальное состояниеTIME_WAIT.
- Инициатор закрытия (вызов
- Состояние TIME_WAIT:
Сторона, которая первой инициировала закрытие сокета, обязана удерживать его в состоянииTIME_WAITв течение фиксированного времени:
$$\mathbf{2 \times MSL \ (Maximum \ Segment \ Lifetime)}$$
В ядре Linux значение MSL жестко зашито и равно 30 секундам, поэтомуTIME_WAITпродолжается ровно 60 секунд.
Зачем это нужно?- Гарантировать доставку финального ACK: если он потеряется, сервер повторно пришлет FIN, и клиент в состоянии
TIME_WAITсможет переотправить ACK; - Предотвратить смешивание данных: гарантировать, что все задержавшиеся в сетевых буферах блуждающие пакеты старого соединения окончательно погибнут в сети (истечет их IP TTL) и не будут ошибочно восприняты новым сокетом, созданным на тех же самых портах.
- Гарантировать доставку финального ACK: если он потеряется, сервер повторно пришлет FIN, и клиент в состоянии
Под капотом: Как Go общается с TCP стеком ОС
Среда исполнения языка Go не содержит собственной изолированной реализации протокола TCP. Рантайм Go всецело делегирует сетевую логику ядру Linux через стандартные системные вызовы сокетов.
Однако гениальность архитектуры Go заключается в том, как язык связывает синхронный код горутин с асинхронным ядром операционной системы:
// Стандартный вызов подключения в Go выглядит синхронным и блокирующим:
conn, err := net.Dial("tcp", "192.168.1.1:8080")
if err != nil {
log.Fatalf("Ошибка подключения: %v", err)
}
defer conn.Close()
Пошаговый путь системных вызовов внутри net.Dial:
- Создание сокета: Рантайм обращается к ядру через системный вызов
sys_socket:
socket(AF_INET, SOCK_STREAM, 0)(для IPv4-адреса; Go добавляетSOCK_NONBLOCK|SOCK_CLOEXEC). - Перевод в неблокирующий режим: Сокет создается или переводится в неблокирующий режим флагом
SOCK_NONBLOCKлибо системным вызовомfcntl(fd, F_SETFL, O_NONBLOCK). В Go все сетевые сокеты всегда неблокирующие! - Инициация подключения: Вызывается системный вызов
sys_connect:
connect(). Поскольку сокет неблокирующий, ядро инициирует отправку SYN-пакета и мгновенно возвращает специальный код ошибкиEINPROGRESS(«Операция выполняется в фоне»). - Сетевой поллер (Netpoller):
Вместо того чтобы блокировать тяжелый системный поток операционной системы (тред M), планировщик Go передает дескриптор сокета во внутреннюю подсистемуnetpoller.
Netpoller регистрирует файловый дескриптор в системном мультиплексореepollс помощью вызоваepoll_ctl(EPOLL_CTL_ADD, fd, EPOLLIN|EPOLLOUT). - Парковка горутины: Текущая горутина (G) переводится в состояние сна (
_Gwaiting), освобождая системный тред M для исполнения других полезных горутин. - Пробуждение по прерыванию:
Когда удаленный сервер возвращает SYN-ACK, сетевая карта генерирует аппаратное прерывание. ПотокMрантайма Go, который в этот момент опрашивает сетевой поллер (findRunnableили блокирующийnetpollна потоке без работы; выделенного «фонового потока поллера» в Go нет), получает изepoll_pwaitуведомление о готовности дескриптора на запись, находит заснувшую горутину G, возвращает ее в очередь планировщика и передает приложению готовый интерфейсnet.Conn.
flowchart TD
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef error fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef aux fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
classDef system fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
classDef data fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
classDef network fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
GoCode["Горутина G: net.Dial('tcp', ...)"]:::network --> Syscall["Ядро: socket() + неблокирующий режим (SOCK_NONBLOCK)"]
Syscall --> Connect["connect() -> возвращает EINPROGRESS"]
Connect --> Netpoller["Регистрация дескриптора fd в Netpoller (epoll)"]
Netpoller --> Park["Парковка горутины: состояние _Gwaiting<br/>(Тред M не блокируется!)"]
NIC["Сетевая карта: получен SYN-ACK"]:::success --> KernelEvent["Событие готовности сокета в ядре"]
KernelEvent --> EpollWait["epoll_wait возвращает событие в Netpoller"]
EpollWait --> Wake["Пробуждение горутины G -> статус _Grunnable"]
Wake --> ReturnConn["Возврат интерфейса net.Conn прикладному коду"]
Ловушки и инженерные компромиссы (Gotchas)
В реальных высоконагруженных распределенных системах идеальный фасад TCP сталкивается с жесткими физическими ограничениями:
1. Блокировка начала очереди (Head-of-Line / HOL Blocking на L4)
Поскольку TCP гарантирует строгий порядок байт, потеря хотя бы одного пакета в сети приводит к тому, что ядро Linux обязано удерживать в буфере все последующие успешно доставленные пакеты. Пока потерянный сегмент не будет переотправлен и подтвержден, ядро не отдаст приложению на Go ни одного байта!
В протоколах HTTP/1.1 и HTTP/2 стандартного пакета net/http потеря пакета для одного стрима замораживает все остальные параллельные потоки на этом соединении, приводя к деградации производительности HTTP-серверов.
Решение: Переход на протокол HTTP/3 поверх QUIC (UDP), где каждый поток данных независим на транспортном уровне.
2. Конфликт алгоритма Нейгла и задержки (Nagle vs TCP_NODELAY)
По умолчанию ядро Linux для обычного сокета объединяет мелкие порции исходящих данных в один большой пакет, ожидая подтверждения ACK на предыдущий пакет (алгоритм Нейгла). Но в Go алгоритм Нейгла уже выключен: пакет net ставит TCP_NODELAY=1 на каждое TCP-соединение (и клиентское, и принятое через Accept). В сочетании с механизмом отложенных подтверждений (Delayed ACK) на стороне приемника это приводит к искусственным «зависаниям» сетевых RPC-запросов на 40 миллисекунд!
Поэтому для соединений net.Conn вызывать SetNoDelay(true) не обязательно — это значение по умолчанию. Явный вызов нужен, чтобы подчеркнуть намерение или вернуть флаг после SetNoDelay(false); если же вам понадобится Нейгл (например, для пакетной отправки мелких записей), его включают как SetNoDelay(false):
if tcpConn, ok := conn.(*net.TCPConn); ok {
// В Go это значение по умолчанию (Нейгл отключён); вызов лишь фиксирует намерение
_ = tcpConn.SetNoDelay(true)
}
3. Исчерпание портов (Port Exhaustion)
При создании сотен тысяч кратковременных исходящих TCP-подключений ядро перебирает доступные порты из диапазона ip_local_port_range (обычно 32768–60999). Из-за задержки TIME_WAIT (60 сек) порты не успевают освобождаться, и net.Dial начинает падать с ошибкой dial tcp: cannot assign requested address. Решение — строгое использование пулов соединений.
4. Поведение дедлайнов в Go (Deadlines vs Network Drops)
Методы SetReadDeadline и SetWriteDeadline интерфейса net.Conn не вызывают системных таймаутов ядра. Они устанавливают таймеры внутри рантайма Go. Если таймер истекает, сокет переводится в состояние ошибки, а вызовы возвращают системную ошибку i/o timeout. Не путайте ошибку дедлайна с физическим разрывом сетевого кабеля!
Итог
Протокол TCP — это надежнейший фундамент современного бэкенда, превращающий непредсказуемый хаос IP-пакетов в строгий и упорядоченный поток байт:
- Сущность TCP: Это сложный конечный автомат ядра Linux, оперирующий структурами
tcp_sock, буферамиsndbuf/rcvbufи требующий бережного отношения к кэш-линиям процессора. - Гарантийный контракт: TCP гарантирует строгую доставку, упорядоченность и отсутствие дубликатов, но оперирует непрерывным потоком байт. Разделение сообщений (фрейминг) полностью лежит на прикладном коде.
- Жизненный цикл: Соединение проходит через 3-Way Handshake, передачу данных и 4-Way Teardown. Состояние
TIME_WAIT(длительностью $2 \times \text{MSL} = 60$ секунд) защищает сеть от блуждающих пакетов-призраков. - Сетевой поллер Go: Стандартный пакет
netиспользует асинхронный механизмnetpollerповерх системного вызоваepoll_wait, позволяя одной машине удерживать сотни тысяч соединений без блокировки системных тредов. - Инженерные компромиссы: Блокировка начала очереди (HOL Blocking), задержки алгоритма Нейгла (
TCP_NODELAY), риск исчерпания портов и опасность утечек дескрипторов в состоянииCLOSE_WAIT.
Мы разобрали фундаментальное устройство и жизненный цикл TCP-соединения. Но как в деталях устроены флаги TCP-заголовка, как происходит трехстороннее рукопожатие на уровне системных очередей SYN Queue и Accept Queue, и что делать при SYN-флуд атаках?
В следующей лекции мы детально изучим низкоуровневую механику установки и разрыва сессий в 11. TCP Handshake, Flow Control и Congestion Control.