ECONNRESET в Send Linux C
Согласно сетевому программированию Unix, когда сокет записывает дважды в закрытый сокет (после пакета FIN), то в первый раз он успешно отправил, но получил пакет RST от другого хоста. Поскольку хост получает RST, сокет уничтожается. Таким образом, во второй раз, когда он пишет, сигнал SIGPIPE принимается, и возвращается ошибка EPIPE.
Однако в справочных страницах отправки может быть возвращено ECONNRESET, что означает получение пакета RST. Когда он возвращает ECONNRESET, сигнал не возвращается.
В каких случаях ECONNRESET можно вернуть? и почему в этом случае нет сигнала SIGPIPE?
Примечание: я проверил у меня подобный вопрос здесь. Однако, когда я запускаю на своем компьютере с Linux, send возвращает ошибку EPIPE, а не ECONNRESET.
1 ответ
Если узел закрыл соединение, пока в буфере сокета еще остались необработанные данные, он отправит обратно пакет RST. Это приведет к тому, что на сокете будет установлен флаг, и при следующей отправке будет возвращено значение ECONNRESET. Вместо этого EPIPE возвращается (или запускается SIGPIPE) при отправке, если одноранговое соединение было закрыто без ожидающих данных. В обоих случаях локальный сокет все еще открыт (то есть дескриптор файла действителен), но основное соединение закрыто.
Пример. Представьте себе сервер, который читает один байт, а затем закрывает соединение:
- EPIPE: клиент отправляет первый байт. После того, как сервер прочитал байт и закрыл соединение, клиент отправит еще некоторые данные, а затем снова некоторые данные. При последнем отправлении вызова будет запущен EPIPE / SIGPIPE.
- ECONNRESET: клиент отправляет сначала более одного байта. Сервер прочитает один байт и закроет соединение с большим количеством байтов в буфере приема сокетов. Это инициирует RST-пакет соединения с сервера, и при следующей отправке клиент получит ECONNRESET.
TCP-соединение можно рассматривать как два конвейера данных между двумя конечными точками. Один конвейер данных для отправки данных из A в B и один конвейер данных для отправки данных из B в A. Эти два конвейера принадлежат одному соединению, но в остальном они не влияют друг на друга. Отправка данных по одному конвейеру не влияет на данные, отправляемые по другому конвейеру. Если данные в одном конвейере являются данными ответа на данные, отправленные ранее в другом конвейере, это будет известно только вашему приложению, TCP ничего об этом не знает. Задача TCP состоит в том, чтобы обеспечить надежную передачу данных с одного конца конвейера на другой и как можно быстрее, вот и все, что заботит TCP.
Как только одна сторона закончила посылку данных, она сообщает другой стороне, что это было сделано, передав ей пакет с FIN
флаг установлен. Отправка FIN
флаг означает "Я отправил все данные, которые хотел отправить вам, поэтому мой конвейер отправки теперь закрыт". Вы можете вызвать это намеренно в своем коде, вызвав shutdown(socketfd, SHUT_WR)
, Если другая сторона тогда позвонит recv()
в сокете он не получит ошибку, но при приеме будет сказано, что он прочитал ноль байтов, что означает "конец потока". Конец потока не является ошибкой, это означает только то, что туда больше не поступит никаких данных, независимо от того, как часто вы будете звонить. recv()
на этой розетке.
Конечно, это не влияет на другой конвейер, поэтому, когда A -> B
закрыто, B -> A
все еще можно использовать. Вы все еще можете получать из этого сокета, даже если вы закрыли свой конвейер отправки. В какой-то момент, однако, также будет сделано B с отправкой данных, а также передать FIN
, Как только оба конвейера закрыты, соединение в целом закрывается, и это будет постепенное отключение, поскольку обе стороны смогли отправить все данные, которые они хотели отправить, и никакие данные не должны были быть потеряны, так как пока неподтвержденные данные в полете, другая сторона не сказала бы, что это сделано, но подождите, пока эти данные будут надежно переданы в первую очередь.
В качестве альтернативы есть RST
флаг, который закрывает все соединение сразу, независимо от того, была ли отправлена другая сторона, и независимо от того, были ли неподтвержденные данные в полете, поэтому RST
имеет высокий потенциал потери данных. Поскольку это исключительная ситуация, которая может потребовать специальной обработки, программистам было бы полезно узнать, так ли это, поэтому существует две ошибки:
EPIPE
- Вы не можете отправить через этот канал, так как этот канал больше не действителен. Однако все данные, которые вы отправляли до его взлома, все еще были надежно доставлены, вы просто не можете отправлять новые данные.
ECONNRESET
- Ваш канал сломан, и это может быть тот случай, когда данные, которые вы пытались отправить до того, как были потеряны в середине передачи. Если это проблема, вам лучше как-нибудь справиться с этим.
Но эти две ошибки не отображают одно в тон FIN
а также RST
флаг. Если вы получаете RST
в ситуации, когда система не видит риска потери данных, нет никаких оснований ни с чем не сталкиваться. Таким образом, если все данные, отправленные вами ранее, были подтверждены для правильного получения, а затем соединение было закрыто RST
Когда вы пытались отправить новые данные, данные не были потеряны. Это включает в себя текущие данные, которые вы пытались отправить, так как эти данные не были потеряны, они никогда не отправлялись в пути, это различие, поскольку они у вас все еще есть, тогда как данные, которые вы отправляли раньше, могут больше не быть. Если ваш автомобиль выходит из строя в середине дорожной поездки, то это совсем другая ситуация, чем если бы вы все еще были дома, поскольку двигатель вашего автомобиля отказывался даже запускаться. Так что, в конце концов, ваша система решает, RST
запускает ECONNRESET
или EPIPE
,
Хорошо, но зачем другой стороне отправлять вам RST
на первом месте? Почему не всегда в заключение FIN
? Ну, есть несколько причин, но две наиболее известные из них:
Одна сторона может только сигнализировать другой, что она закончила отправку, но единственный способ сообщить, что это сделано со всем соединением, - это отправить
RST
, Поэтому, если одна сторона хочет закрыть соединение и хочет закрыть его изящно, она сначала отправитFIN
сигнализировать о том, что он больше не будет отправлять новые данные, а затем дать другой стороне некоторое время, чтобы прекратить отправку данных, что позволит передавать данные в полете и наконец отправитьFIN
также. Однако что, если другая сторона не хочет останавливаться и продолжает отправлять и отправлять? Такое поведение является законным какFIN
не означает, что соединение должно быть закрыто, это означает, что выполняется только одна сторона. Результатом является то, чтоFIN
сопровождаетсяRST
чтобы наконец закрыть эту связь. Это могло привести к потере данных в полете, а может и нет, только получательRST
будет точно знать, как если бы данные были потеряны, это должно быть на его стороне, так как отправительRST
наверняка не отправлял больше данных послеFIN
, Дляrecv()
позвони, этоRST
не имеет никакого эффекта, так как былFIN
до сигнализации "конец потока", такrecv()
сообщит о прочтении нулевых байтов.Одна сторона должна закрыть соединение, но у него все еще есть неотправленные данные. В идеале это будет ждать, пока все неотправленные данные будут отправлены, а затем передать
FIN
однако время ожидания может быть ограничено, и по истечении этого времени все еще остаются неотправленные данные. В этом случае он не может отправитьFIN
какFIN
было бы ложью. Это сообщит другой стороне: "Эй, я отправил все данные, которые хотел отправить", но это не так. Были данные, которые должны были быть отправлены, но, поскольку закрытие требовало немедленного закрытия, эти данные пришлось отбросить, и в результате эта сторона будет напрямую отправлятьRST
, Будь этоRST
запускаетECONNRESET
дляsend()
Звонок снова зависит от того, является ли получательRST
были неотправленные данные в полете или нет. Тем не менее, это наверняка вызоветECONNRESET
ошибка на следующемrecv()
позвоните, чтобы сообщить программе: "Другая сторона действительно хотела отправить вам больше данных, но не смогла, и, таким образом, некоторые из этих данных были потеряны", так как это может снова быть ситуацией, которая каким-то образом обрабатывает, как данные, которые вы получили был наверняка неполным, и это то, что вы должны знать.
Если вы хотите, чтобы сокет всегда был закрыт напрямую RST
и никогда с FIN
/FIN
или же FIN
/RST
Вы можете просто установить время задержки в ноль.
struct linger l = { .l_onoff = 1, .l_linger = 0 };
setsockopt(socketfd, SOL_SOCKET, SO_LINGER, &l, sizeof(l));
Теперь сокет должен закрываться мгновенно и без каких-либо задержек, независимо от того, как мало и единственный способ мгновенно закрыть сокет TCP - это отправить RST
, Некоторые люди думают: "Зачем включать его и устанавливать нулевое время? Почему бы просто не отключить его вместо этого?", Но отключение имеет другое значение.
Задержка - это время close()
Вызов может блокировать выполнение ожидающих действий отправки, чтобы корректно закрыть сокет. Если включено (.l_onoff != 0
), призыв к close()
может блокировать до .l_linger
секунд. Если вы установите время на ноль, оно может вообще не блокироваться и, таким образом, мгновенно завершается (RST
). Однако, если вы отключите его, то close()
никогда не будет блокироваться, но тогда система может все еще задерживаться при закрытии, но это задержка происходит в фоновом режиме, так что ваш процесс не будет больше это замечать и, следовательно, также не может знать, когда сокет действительно закрылся, так как socketfd
становится недействительным сразу, даже если базовый сокет в ядре все еще существует.