Немного странно такую заметку писать, вроде что-то очень банальное и возможно многие скажут "да я с пеленок это знаю" - но вот опять сталкиваемся с тем, что такая ошибка в достаточно важной ситуации наглядно портит кровь.
Недавно я писал об идущем соревновании МТС по "программированию роботов" - и упоминал вскользь что пока со стороны организационной наблюдаются проблемы. На днях энтузиасты выявили как раз такую ошибку в коде, используемом организаторами для проверки решений.
Если вы создали сокет, попытались его открыть и отвалились по таймауту - не переиспользуйте его! Для новой попытки обязательно создавайте новый сокет!
Это не вполне очевидно и в документации порой лишь вскользь упомянуто, либо не упомянуто вообще. Ниже немного подробностей с кодом, но в общем вся суть в этой фразе. Не переиспользуйте!
Типичный пример
Поскольку реализация сокетов обычно на уровне системы, проблема не привязан�� к конкретному языку. В упомянутом случае код был на питоне и его можно привести как пример:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2.0)
while True:
try:
sock.connect((HOST, PORT))
break
except Exception as e:
print(f"TCP connect retry to {HOST}:{PORT} ({e})")
time.sleep(0.5)
Что тут происходит? Мы создали клиентский TCP-сокет и решили законнектиться к серверу, но не висеть в одной бесконечной попытке коннекта, а повторять попытки периодически, каждую с небольшим таймаутом.
Ошибка в том что переменная sock
инициализирована (т.е. сокет создан) вне цикла, т.е. мы его один раз создали и пытаемся приконнектить много раз, пока не потерпим успех.
Это выглядит логично, и даже работает на достаточно многих системах. Беда в том, что не на всех. В линуксе по-видимому на современных ядрах проблемы нет. А пользователи windows жалуются что "приходится перезапускать то клиент то сервер" ну и собственно на соревновании это привело к тому что часть команд не могла в течение нескольких дней получить обратную связь по отправленному решению.
В чём дело - понятно. Операция connect
не гарантирует что сокет останется в консистентном ("юзабельном") состоянии, даже если завершилась с ошибкой и коннекта, как такового, не случилось.
К сожалению, в официальной документации на функции сокетов в системных библиотеках разных языков это обычно либо не упомянуто. Может быть потому что это поведение именно на уровне системы а не к самой библиотеке / языку относится.
Правда некоторые языки/библиотке "оборачивают" создание сокета и возвращение коннекта в единую функцию, так что попасть впросак становится затруднительно.
Официальное упоминание можно найти обычно в man 2 connect
, например в виде строчки, где-то в конце:
If connect() fails, consider the state of the socket as unspecified. Portable applications should close the socket and create a new one for reconnecting.
Исправленный пример
Минимальный патч предложенный теми же энтузиастами на соревновании - заключается примерно в трёх строчках - две перенести, одну добавить:
while True:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2.0)
try:
sock.connect((HOST, PORT))
break
except Exception as e:
sock.close()
print(f"TCP connect retry to {HOST}:{PORT} ({e})")
time.sleep(0.5)
Чуть более элегантно будет использовать оператор with
, но м.б. не в любой сиуации удобно.
Немного наивный вопрос - а зачем документация (и пример) хотят закрывать сокет, если он не открылся (а произошла ошибка)? Как и проблема переиспользования это относится к внутренней реализации - в принципе до вызова close()
могут оставаться неосвобождёнными какие-либо ресурсы, то есть в цикле это приведёт к утечке памяти например.
Тут интересна другая строчка из документации, уже по man 2 close
:
close() closes a file descriptor, so that it no longer refers to any file and may be reused.
То бишь по крайней мере если мы пишем прямо на С и используем системные функции без каких-то обёрток сделанных языком более высокого уровня, то сокет (точнее файловый дескриптор созданный вызовом socket(...)
) переиспользовать все-таки можно - главное закрыть его!
Но поскольку не всегда обстоятельства складываются так "прямолинейно" - например на странице документации для соответствующих функций в Python подобных ясных указаний мы не найдём и даже наоборот звучит так как будто после close(...)
объект socket
точно будет невалидным:
socket.close()
Mark the socket closed... Once that happens, all future operations on the socket object will fail.
Впрочем, это уже на уровне библиотеки языка. Про переиспользование сокета в неоткрытом-незакрытом состоянии тут ничего найти не удалось.
Отсюда вывод: даже в 2025 году есть ещё плохо документированные особенности — и с популярными языками, с привычными операциями — в которых несложно сделать ошибку, причём такую что будет трудновато её уловить (ведь «на моей машине все работает»).