Прямой ответ
Закрытие соединения выполняется обменом завершающими сегментами, при этом сторона может закрыть передачу, продолжая приём. Аварийное закрытие сбросом прерывает обмен и теряет непереданные данные.
| Способ | Поведение |
|---|---|
| Штатное закрытие | Обмен завершающими сегментами, данные передаются |
| Полузакрытие | Передача закрыта, приём продолжается |
| Сброс соединения | Обмен прерывается, непереданные данные теряются |
import socket
s = socket.create_connection(("host", 8080))
s.sendall(b"GET / HTTP/1.1\r\nHost: host\r\n\r\n")
s.shutdown(socket.SHUT_WR) # полузакрытие: передача закрыта
data = s.recv(4096) # приём продолжается
s.close()
Полузакрытие позволяет сообщить собеседнику об окончании передачи, не прерывая приём ответа. О жизненном цикле соединения — в статье про TCP-рукопожатие.
Потеря данных при сбросе и таймауты ожидания
Два аспекта определяют корректность закрытия соединения.
Сброс соединения. Пакет сброса прерывает обмен немедленно, поэтому данные, находящиеся в буфере отправки, теряются. Сброс применяется при ошибке либо при закрытии сокета с непереданными данными.
Таймаут ожидания закрытия. Приложение ожидает завершения обмена в течение таймаута, поэтому зависший собеседник не удерживает соединение неограниченно. По истечении таймаута соединение закрывается принудительно.
Практическое следствие: штатное закрытие требует ожидания подтверждения, поэтому таймаут ограничивает время ожидания.
Отдельный аспект — непереданные данные. Закрытие сокета с данными в буфере отправки может привести к сбросу, поэтому данные теряются. Ожидание завершения передачи перед закрытием исключает потерю.
Три признака некорректного закрытия.
Закрытие без ожидания передачи. Данные в буфере отправки теряются, поэтому собеседник получает неполный ответ.
Отсутствие таймаута ожидания. Соединение удерживается неограниченно при зависшем собеседнике, поэтому ресурсы не освобождаются.
Закрытие в потоке без синхронизации. Два потока закрывают соединение одновременно, поэтому состояние сокета не определено.
Об обнаружении разрыва без обмена данными — в статье про TCP keepalive; о таймаутах операций — в статье про сетевые таймауты.
Закрытие в пуле соединений и возврат в пул
Соединение, возвращаемое в пул, не закрывается, поэтому состояние обмена проверяется до возврата.
Возврат в пул вместо закрытия. Соединение возвращается в пул для переиспользования, поэтому обмен завершающими сегментами не выполняется. Соединение остаётся установленным.
Проверка перед возвратом. Соединение с непереданными данными либо с признаком ошибки не возвращается в пул, а закрывается, поэтому следующее обращение не получает непригодное соединение.
Закрытие по превышению срока. Соединение, удерживаемое дольше установленного срока, закрывается, поэтому накопление устаревших соединений исключается.
Практическое следствие: соединение закрывается при ошибке либо по превышении срока, а при штатном завершении обмена возвращается в пул для переиспользования.
Отдельный аспект — полузакрытие в пуле. Соединение в пуле не закрывает передачу, поскольку ожидает следующего запроса, поэтому полузакрытие применяется только при окончательном завершении работы.
Закрытие при аварийном завершении процесса
Процесс, завершившийся аварийно, не выполняет штатное закрытие, поэтому состояние соединения определяется системой.
Закрытие дескрипторов системой. Дескрипторы сокета закрываются при завершении процесса, поэтому система выполняет обмен завершающими сегментами. Данные в буфере отправки могут быть потеряны.
Обнаружение разрыва собеседником. Собеседник обнаруживает закрытие при попытке передачи либо по механизму контроля доступности, поэтому уведомление не всегда поступает немедленно.
Состояние соединения после аварийного завершения. Соединение переходит в состояние ожидания либо закрывается сбросом в зависимости от наличия непереданных данных.
Практическое следствие: штатное закрытие в коде приложения обеспечивает полноту передачи, тогда как аварийное завершение приводит к закрытию системой с возможной потерей данных.
Отдельный аспект — перезапуск процесса. Соединения, установленные прежним процессом, закрываются, поэтому клиент выполняет повторное подключение к новому процессу.
Практика: корректное закрытие соединения
1. Дождитесь завершения передачи перед закрытием. Данные в буфере отправки должны быть переданы, иначе они теряются.
2. Используйте полузакрытие для сообщения об окончании. Передача закрывается, при этом приём ответа продолжается.
3. Задайте таймаут ожидания закрытия. Соединение не удерживается неограниченно при зависшем собеседнике.
4. Закрывайте сокет явно. Соединение, не закрытое приложением, удерживается до обнаружения разрыва.
5. Синхронизируйте закрытие при работе в потоках. Одновременное закрытие из двух потоков приводит к неопределённому состоянию.
6. Проверяйте состояние после закрытия. Соединение переходит в состояние ожидания, поэтому порт освобождается не сразу.
7. Обрабатывайте ошибку при закрытии. Закрытие может завершиться ошибкой, поэтому факт закрытия проверяется.
Рекомендации:
- Различайте штатное закрытие и сброс. Штатное закрытие передаёт данные, сброс прерывает обмен с потерей непереданного.
- Учитывайте длительность состояния ожидания. Порт не освобождается сразу после закрытия, поэтому частое закрытие накапливает состояния.
- Проверяйте завершение передачи. Закрытие сокета с данными в буфере приводит к потере, поэтому ожидание обязательно.
- Фиксируйте таймаут ожидания. Значение определяет, как долго приложение ждёт завершения обмена.
- Пересматривайте при изменении протокола обмена. Изменение порядка закрытия влияет на полноту передаваемых данных.
Практический сценарий: клиент получал неполный ответ
Контекст: часть ответов обрезалась, при этом сервер завершал обработку запроса без ошибок.
- Диагностика. Сервер закрывал сокет сразу после записи ответа, не ожидая завершения передачи. Данные, находящиеся в буфере отправки, терялись при закрытии, поэтому клиент получал неполный ответ.
- Причина. Закрытие выполнялось без ожидания подтверждения передачи. Операция записи помещает данные в буфер, поэтому фактическая передача выполняется позже, а закрытие прерывает её.
- Действия. (а) Добавили ожидание завершения передачи перед закрытием сокета; (б) установили таймаут ожидания, чтобы зависший клиент не удерживал соединение; (в) перевели закрытие на полузакрытие, чтобы клиент мог дочитать ответ; (г) проверили полноту ответа на стороне клиента после изменения; (д) добавили контроль числа соединений в состоянии ожидания.
- Результат. Клиент получает полный ответ, поскольку передача завершается до закрытия соединения.
Вывод: закрытие сокета сразу после записи прерывает передачу данных из буфера отправки. Ожидание завершения передачи перед закрытием обеспечивает полноту ответа.
FAQ
Что такое полузакрытие соединения? Закрытие передачи при сохранённом приёме. Собеседник получает уведомление об окончании передачи и может передать оставшиеся данные.
Теряются ли данные при сбросе соединения? Да, данные в буфере отправки не передаются, поскольку обмен прерывается немедленно.
Нужен ли таймаут ожидания закрытия? Да, иначе соединение удерживается неограниченно при зависшем собеседнике и ресурсы не освобождаются.
Почему порт не освобождается сразу после закрытия? Соединение переходит в состояние ожидания на время удвоенного максимального времени жизни сегмента, поэтому порт занят до истечения срока.
Источники: RFC 9293, shutdown man page, socket man page — ссылки в шапке.