Какие нюансы бывают при разрыве TCP соединения
Что проверяют
Понимаете ли вы, что соединение может закончиться по-разному и каждый вариант выглядит в коде по-своему. Следом обычно спрашивают про TIME_WAIT и CLOSE_WAIT на сервере или как обнаружить клиента, который исчез молча.
Как отвечать
Нормальное закрытие идёт в четыре шага: одна сторона шлёт FIN, другая подтверждает ACK, потом шлёт свой FIN и получает ACK. Направления закрываются отдельно, поэтому возможно полузакрытие: sock.shutdown(socket.SHUT_WR) сообщает «я всё отправил», но читать ещё можно.
- EOF не ошибка:
recv()вернулb'', значит пришёл FIN. В asyncio так же:reader.read()возвращает пустые байты; - TIME_WAIT: остаётся у стороны, закрывшей первой, на 2×MSL (в Linux фиксированные 60 секунд). Это нормально, а для рестарта сервера на том же порту ставят
SO_REUSEADDR; - CLOSE_WAIT: пир прислал FIN, а приложение не вызвало
close(). Растущее число таких сокетов почти всегда утечка соединений в коде; - RST: жёсткий сброс. Бывает при записи в закрытое соединение, при
close()с непрочитанными данными в буфере илиSO_LINGERс нулевым таймаутом. В Python этоConnectionResetErrorилиBrokenPipeError, SIGPIPE интерпретатор по умолчанию игнорирует; - тихая смерть: пропало питание или NAT забыл запись. Ни FIN, ни RST не придёт, чтение висит бесконечно. Спасают
sock.settimeout(), heartbeat в протоколе или TCP keepalive (SO_KEEPALIVE,TCP_KEEPIDLE; системный порог в Linux два часа, его обычно уменьшают).
Успешный send() значит лишь, что данные легли в буфер ядра. Что приложение на той стороне их обработало, подтверждает только ваш протокол.
Где спотыкаются
Советуют «добавить повторную передачу». TCP делает это сам, а проблема разрыва в другом: неизвестно, успела ли другая сторона обработать запрос, поэтому повторять можно только идемпотентные операции.
Считают TIME_WAIT аварией и ищут tcp_tw_recycle. Этой настройки в Linux давно нет, она ломала клиентов за NAT.
Забывают про пулы соединений в requests и httpx: сервер закрыл keep-alive по таймауту, а следующий запрос берёт мёртвый сокет и падает с RemoteDisconnected или сбросом соединения.