Установка соединения:
- клиент шлёт SYN со своим начальным номером последовательности (ISN, случайный — защита от подделки);
- сервер отвечает SYN-ACK: свой ISN и подтверждение номера клиента + 1;
- клиент подтверждает ACK. Соединение установлено, можно слать данные.
Зачем три шага, а не два: обе стороны должны убедиться, что другая получила их номер последовательности. Цена — один RTT до начала обмена; с TLS 1.3 добавляется ещё один, отсюда интерес к keep-alive, пулам и QUIC.
На сервере полуоткрытые соединения (получен SYN) лежат в очереди SYN, а установленные, но ещё не принятые accept(), — в очереди accept (размер — backlog в listen и somaxconn). Переполнение очереди при всплеске — потерянные соединения. SYN flood — атака множеством SYN без завершения; защита — SYN cookies: сервер не хранит состояние, а кодирует его в своём ISN.
Закрытие. TCP двунаправлен, каждая сторона закрывает свою половину: FIN → ACK, затем FIN → ACK с другой стороны (часто FIN и ACK объединяются). Можно закрыть запись и продолжать читать (half-close).
TIME_WAIT. Сторона, закрывшая первой, после последнего ACK ждёт 2×MSL (максимальное время жизни сегмента; в Linux фиксированно 60 секунд). Зачем: если последний ACK потерялся, другая сторона повторит FIN, и на него надо ответить; и чтобы опоздавшие сегменты старого соединения не попали в новое с теми же адресами и портами.
Практика. Тысячи соединений в TIME_WAIT у клиента — признак того, что на каждый запрос открывается новое соединение. Каждое держит локальный порт, а их около 28 тысяч в диапазоне по умолчанию — при высоком RPS к одному адресу порты кончаются (Cannot assign requested address). Решение — переиспользовать соединения: HTTP keep-alive, пул соединений к базе. tcp_tw_reuse помогает исходящим соединениям; старый tcp_tw_recycle был удалён, потому что ломал клиентов за NAT.
RST — аварийный сброс: порт закрыт (connection refused), приложение закрыло сокет с непрочитанными данными, или соединение неизвестно.