Немного странно такую заметку писать, вроде что-то очень банальное и возможно многие скажут "да я с пеленок это знаю" - но вот опять сталкиваемся с тем, что такая ошибка в достаточно важной ситуации наглядно портит кровь.
Недавно я писал об идущем соревновании МТС по "программированию роботов" - и упоминал вскользь что пока со стороны организационной наблюдаются проблемы. На днях энтузиасты выявили как раз такую ошибку в коде, используемом организаторами для проверки решений.
Если вы создали сокет, попытались его открыть и отвалились по таймауту - не переиспользуйте его! Для новой попытки обязательно создавайте новый сокет!
Это не вполне очевидно и в документации порой лишь вскользь упомянуто, либо не упомянуто вообще. Ниже немного подробностей с кодом, но в общем вся суть в этой фразе. Не переиспользуйте!
Типичный пример
Поскольку реализация сокетов обычно на уровне системы, проблема не привязана к конкретному языку. В упомянутом случае код был на питоне и его можно привести как пример:
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 году есть ещё плохо документированные особенности — и с популярными языками, с привычными операциями — в которых несложно сделать ошибку, причём такую что будет трудновато её уловить (ведь «на моей машине все работает»).
Комментарии (17)

apevzner
14.10.2025 06:08Если вы создали сокет, попытались его открыть и отвалились по таймауту - не переиспользуйте его! Для новой попытки обязательно создавайте новый сокет!
Я дико извиняюсь, но создание сокета (вызовом socket) - это и есть его открытие. Что открыли, то надо закрыть.
А connect - это не открытие сокета, а установление соединения - действие, которое пытается изменить состояние сокета, но не открывает и не закрывает его.

RodionGork Автор
14.10.2025 06:08ну, можно так это рассматривать, особенно отталкиваясь от соображения что
socket(...)с точки зрения POSIX возвращает файловый дескрипторно положа руку на сердце сказать "открыли" нельзя - по большому счету здесь ни моё ни ваше утверждение не 100%-корректно. сокет это абстракция и то что он бывает "открытым" или закрытым этого никто не сказал :)
в частности мануал по функции поясняет её действие в таких словах:
socket() creates an endpoint for communication and returns a file descriptor that refers
to that endpoint.в связи со сложностями перевода "endpoint" на русский точный смысл воспроизвести затруднительно но глагол здесь "creates" а не "opens"
Резюмируя - "открытие" не годится в синонимы ни к "созданию" ни к "соединению" - но по большому счету кажется, хоть как назови, только ошибок избегай :) в качестве "мнемонического пояснения" ваше рассуждение, конечно, вполне годится!

apevzner
14.10.2025 06:08Я бы тут хотел не спорить о терминах, а разделить выделение/освобождение ресурса (в данном случае, сокета) и управление состоянием этого ресурса.
Некоторую путаницу в POSIX вносит то, что выделенный но не присоединенный сокет - штука довольно бесполезная, а close объединяет семантику закрытия соединения и освобождения дескриптора.
В этом смысле Go и Plan9 более последовательны, net.Dial и dial основременно создают объект и устанавливают соединение (но усложняют настройку сокета, которую хотелось бы сделать до попытки установления соединения).

domix32
14.10.2025 06:08даже в 2025 году есть ещё плохо документированные особенности - и с популярными языками, с привычными операциями - в которых несложно сделать ошибку, причём такую что будет трудновато её уловить
сначала не читают пепов, а потом живут без
with
RodionGork Автор
14.10.2025 06:08написано же - это не конкретно про питон :)

domix32
14.10.2025 06:08Замените пепы на маны - суть не поменяется. Если язык без RAII, то помнить о противоположных операциях выделения и освобождения ресурсов приходится всегда. Будь-то malloc/free, open/close или connect/disconnect. В противном случае неплохо бы знать как готовить местный RAII, особенно обёртки над библиотеками без RAII - как раз with в питоне про это же.

RodionGork Автор
14.10.2025 06:08суть была в том что выделить можно в начале, высвободить в конце, а между ними долбить connect до опупения на одном и том же сокете, и вот это неправильно.
Вот код с
withно правильнее он от этого не стал.with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: 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) # proceed using connection

alcotel
14.10.2025 06:08А не приходит ли эта проблема от самого принципа работы API сокетов?
Пояснительная бригада говорит, что соединение по TCP в сети идентифицируется как пара пар значений, ((local host, local port), (remote host, remote port)). Цифру "local port" клиент обычно явно не указывают, её выбирает ОС автоматически при вызове bind или connect. Если коннект по каким-то причинам порвался, для переустановки соединения клиент должен поменять "local port". Иначе клиент считает, что переустанавливает соединение, а сервер считает, что продолжается старое.
Сокет просто запоминает цифру local port, когда ОС её назначила. И забыть сокет её просто так не может. Чтобы забыть, приходится создавать новый сокет.

domix32
14.10.2025 06:08А не приходит ли эта проблема от самого принципа работы API сокетов?
id сокета aka sockfd и tcp порт это не одни и те же сущности. Главным образом потому, что сокет не обязан быть именно TCP. bind резолвит локальный порт, только если ему явно был передан нулевой порт в адресе. При connect вы ручками указвыаете к какому порту подключаться, так что нет там никакого автоматически. Если connect провалился, то вас обычно уже ничего не спасёт, ибо указанный удалённый сервер недоступен. Пересоздавайте сокет заново и пробуйте снова.
Если вы описывали разрыв соединения, то там есть некоторый таймаут пока TCP соединение решит, что попытки передачи кончились и клиент/сервер потерялся и не отвечает. TCP пакеты завернуты в пакеты IP и у того уже есть эти четыре значения.
Так что нет, API сокетов тут не причём. По крайней мере не в том виде, в котором вы его описываете.

alcotel
14.10.2025 06:08Если автор кода явно говорит AF_INET и SOCK_STREAM, было бы странно ожидать не TCP, а какой нибудь "PPP over CAN". Хотя... С применением впн скоро и не такое увидим)))
Пересоздавайте сокет заново и пробуйте
В том-то и вопрос. Не все понимают, почему нужно именно пересоздать сокет, а не воспользоваться старым. Я тоже не до конца понимаю, и просто строю предположение.
Но я хорошо понимаю, что творится на физическом уровне, и на уровне IP-пакетов. Поэтому, так сказать, стучусь снизу.

domix32
14.10.2025 06:08а не воспользоваться старым
самый простой ответ - нужно сбросить состояние сокета до некоторого рабочего, для чего требуется занулить одни поля и выставить в дефолтное положение другие и перекинуть из одной очереди в другую. Что по сути на 90+% пересекается с созданием нового сокета, поэтому просто забываем о старом sockfd и генерим новый. Получаем меньше кода в ядре, меньше кода в пространстве пользователя, меньше багов в среднем.
Если автор кода явно говорит AF_INET и SOCK_STREAM, было бы странно ожидать не TCP
Собственно, ничто не мешает прокинуть другие параметры и получить ровно те же проблемы, поэтому и написал, что проблема не специфична для TCP сокета, а в принципе для любого сокета - хоть датаграммами кидайся, хоть голыми IP пакетами, хоть виртуалкам сокеты тереби.

IZh
14.10.2025 06:08close() closes a file descriptor, so that it no longer refers to any file and may be reused.
То бишь по крайней мере если мы пишем прямо на С и используем системные функции без каких-то обёрток сделанных языком более высокого уровня, то сокет (точнее файловый дескриптор созданный вызовом socket(...)) переиспользовать все-таки можно - главное закрыть его!
Тут речь идёт не о переиспользовании переменной в юзерспейсе, а о том, что после закрытия файлового дескриптора в ядре, это место в таблице файловых дескрипторов процесса станет свободным, и при открытии нового файла или сокета, ядро просто переиспользует первый свободный дескриптор.
Например, если вы закроете STDIN (дескриптор которого имеет номер 0), а затем откроете любой файл, то у этого файла дескриптор будет 0 за счёт переиспользования. Но этот дескриптор уже не будет иметь никакого отношения к тому, что было нулём раньше. И в этом плане никакое переиспользование в юзерспейсе не выйдет.
andreykorol
неплохо бы еще проверять, что вернула socket.socket
RodionGork Автор
Это уже конкретно про питон... А что она может возвращать кроме объекта оборачивающего сокет? Вроде же бросать исключение должна если что не так?
andreykorol
на питоне практически не пишу, так что действительно могу ошибаться. Смущает обращение к полученному объекту "sock.settimeout" без проверки и вне try..except
andreykorol
почитал, по идее сам вызов socket.socket в случае неудачи вызовет исключение. сишный стереотип сработал
RodionGork Автор
ну что ж, всё-таки пойнт отчасти валидный - try должен захватывать больше строк :) спасибо!