Почему невозможно без попытки ввода-вывода обнаружить, что сокет TCP был корректно закрыт одноранговым узлом?

93

В ответ на недавний вопрос , я задаюсь вопросом, почему в Java невозможно без попытки чтения / записи в сокете TCP обнаружить, что сокет был корректно закрыт партнером? Похоже, что это так, независимо от того, используете ли вы pre-NIO Socketили NIO SocketChannel.

Когда одноранговый узел корректно закрывает TCP-соединение, TCP-стеки на обеих сторонах соединения знают об этом факте. Сторона сервера (та, которая инициирует завершение работы) оказывается в состоянии FIN_WAIT2, тогда как сторона клиента (та, которая явно не реагирует на отключение) оказывается в состоянии CLOSE_WAIT. Почему не существует способ , в Socketили SocketChannelчто может запросить стек TCP , чтобы увидеть , было ли прекращено базовое соединение TCP? Может быть, стек TCP не предоставляет такую ​​информацию о состоянии? Или это дизайнерское решение, позволяющее избежать дорогостоящего обращения к ядру?

С помощью пользователей, которые уже опубликовали некоторые ответы на этот вопрос, я думаю, что вижу, откуда может возникнуть проблема. Сторона, которая явно не закрывает соединение, оказывается в состоянии TCP, CLOSE_WAITчто означает, что соединение находится в процессе закрытия и ожидает, пока сторона выполнит свою собственную CLOSEоперацию. Я полагаю, это справедливо, что isConnectedвозвращается trueи isClosedвозвращается false, но почему нет ничего подобного isClosing?

Ниже приведены тестовые классы, в которых используются сокеты до NIO. Но идентичные результаты получены при использовании NIO.

import java.net.ServerSocket;
import java.net.Socket;

public class MyServer {
  public static void main(String[] args) throws Exception {
    final ServerSocket ss = new ServerSocket(12345);
    final Socket cs = ss.accept();
    System.out.println("Accepted connection");
    Thread.sleep(5000);
    cs.close();
    System.out.println("Closed connection");
    ss.close();
    Thread.sleep(100000);
  }
}


import java.net.Socket;

public class MyClient {
  public static void main(String[] args) throws Exception {
    final Socket s = new Socket("localhost", 12345);
    for (int i = 0; i < 10; i++) {
      System.out.println("connected: " + s.isConnected() + 
        ", closed: " + s.isClosed());
      Thread.sleep(1000);
    }
    Thread.sleep(100000);
  }
}

Когда тестовый клиент подключается к тест-серверу, вывод остается неизменным даже после того, как сервер инициирует отключение соединения:

connected: true, closed: false
connected: true, closed: false
...
Александр
источник
Я подумал, что должен упомянуть: протокол SCTP не имеет этой «проблемы». SCTP не имеет полузакрытого состояния, как TCP, другими словами, одна сторона не может продолжать отправлять данные, пока другая сторона закрыла свой отправляющий сокет. Это должно упростить задачу.
Tarnay Kálmán
2
У нас есть два почтовых ящика (розетки) ........................................... ...... Почтовые ящики отправляют почту друг другу, используя RoyalMail (IP), забывая о TCP ............................. .................... Все в порядке, почтовые ящики могут отправлять / получать почту друг другу (в последнее время много задержек), отправляя при этом без проблем. ............. Если один почтовый ящик столкнется с грузовиком и выйдет из строя ... как другой почтовый ящик узнает? Он должен быть уведомлен Королевской Почтой, которая, в свою очередь, не будет знать, пока не попытается в следующий раз отправить / получить почту из этого неудавшегося почтового ящика ... ...... эээ ...
divinci
Если вы не собираетесь читать из сокета или писать в сокет, какое вам дело? А если вы собираетесь читать из сокета или писать в сокет, зачем делать дополнительную проверку? Какой вариант использования?
Дэвид Шварц
2
Socket.closeне изящное закрытие.
user253751
@immibis Это, безусловно, изящное закрытие, если в приемном буфере сокета нет непрочитанных данных или вы не испортили SO_LINGER.
Маркиз Лорн,

Ответы:

29

Я часто использую сокеты, в основном с селекторами, и хотя я не являюсь экспертом по сетевой OSI, насколько я понимаю, вызов shutdownOutput()сокета фактически отправляет что-то в сеть (FIN), что пробуждает мой селектор на другой стороне (такое же поведение в C язык). Здесь у вас есть обнаружение : фактическое обнаружение операции чтения, которая завершится ошибкой при попытке ее выполнения.

В приведенном вами коде закрытие сокета приведет к отключению как входных, так и выходных потоков без возможности чтения данных, которые могут быть доступны, что приведет к их потере. Метод Java Socket.close()выполняет «изящное» отключение (противоположное тому, что я изначально думал) в том смысле, что данные, оставшиеся в выходном потоке, будут отправлены с последующим FIN, чтобы сигнализировать о его закрытии. FIN будет ACK'd другой стороной, как любой обычный пакет будет 1 .

Если вам нужно дождаться, пока другая сторона закроет свой сокет, вам нужно дождаться его FIN. И для этого вы должны обнаружить Socket.getInputStream().read() < 0, что означает, что вы не должны закрывать свой сокет, так как он закроет егоInputStream .

Из того, что я сделал на C, а теперь и на Java, достижение такого синхронизированного закрытия должно выполняться следующим образом:

  1. Вывод сокета выключения (отправляет FIN на другом конце, это последнее, что когда-либо будет отправлено этим сокетом). Вход все еще открыт, поэтому вы можете read()обнаружить удаленныйclose()
  2. Прочтите сокет, InputStreamпока мы не получим ответ-FIN с другого конца (поскольку он обнаружит FIN, он пройдет через тот же процесс постепенного установления соединения). Это важно для некоторых ОС, поскольку они фактически не закрывают сокет, пока один из его буферов все еще содержит данные. Они называются "призрачными" сокетами и используют номера дескрипторов в ОС (это может больше не быть проблемой для современных ОС).
  3. Закройте сокет (путем вызова Socket.close()или закрытия его InputStreamили OutputStream)

Как показано в следующем фрагменте кода Java:

public void synchronizedClose(Socket sok) {
    InputStream is = sok.getInputStream();
    sok.shutdownOutput(); // Sends the 'FIN' on the network
    while (is.read() > 0) ; // "read()" returns '-1' when the 'FIN' is reached
    sok.close(); // or is.close(); Now we can close the Socket
}

Конечно, обе стороны должны использовать один и тот же способ закрытия, иначе отправляющая часть может всегда отправлять достаточно данных, чтобы whileцикл был занят (например, если отправляющая часть только отправляет данные и никогда не читает, чтобы обнаружить завершение соединения. Что неуклюже, но вы можете не контролировать это).

Как отметил @WarrenDew в своем комментарии, отбрасывание данных в программе (прикладной уровень) приводит к необратимому отключению на прикладном уровне: хотя все данные были получены на уровне TCP ( whileцикл), они отбрасываются.

1 : Из « Основы сетевых технологий в Java »: см. Рис. 3.3 с.45, и весь §3.7, стр 43-48

Матье
источник
Java выполняет изящное закрытие. Это не «брутально».
Marquis of Lorne
2
@EJP, «постепенное отключение» - это особый обмен, происходящий на уровне TCP, когда Клиент должен сигнализировать о своем желании отключиться серверу, который, в свою очередь, отправляет оставшиеся данные перед закрытием своей стороны. Часть «отправить оставшиеся данные» должна обрабатываться программой (хотя в большинстве случаев люди ничего не отправляют). Вызов socket.close()"жестокий" в том смысле, что он не учитывает сигнализацию клиент / сервер. Сервер будет уведомлен об отключении клиента только тогда, когда его собственный выходной буфер сокета будет заполнен (поскольку данные не подтверждаются другой стороной, которая была закрыта).
Matthieu
См. MSDN для получения дополнительной информации.
Matthieu
1
@Matthieu Если ваше приложение не считывает все доступные данные, это может быть некорректно на уровне приложения, но на транспортном уровне TCP данные все равно принимаются, и соединение завершается корректно. То же самое было бы, если бы ваше приложение считывало все данные из входного потока и просто отбрасывало их.
Уоррен Дью
2
@LeonidUsov Это просто неверно. Java read()возвращает -1 в конце потока и продолжает делать это, сколько бы раз вы это ни вызывали. AC read()or recv()возвращает ноль в конце потока и продолжает делать это, сколько бы раз вы это ни вызывали.
Marquis of Lorne
18

Я думаю, это скорее вопрос программирования сокетов. Java просто следует традиции программирования сокетов.

Из Википедии :

TCP обеспечивает надежную и упорядоченную доставку потока байтов от одной программы на одном компьютере к другой программе на другом компьютере.

Как только рукопожатие выполнено, TCP не делает различий между двумя конечными точками (клиентом и сервером). Термины «клиент» и «сервер» используются в основном для удобства. Таким образом, «сервер» может отправлять данные, а «клиент» может отправлять некоторые другие данные одновременно друг другу.

Термин «Закрыть» также вводит в заблуждение. Есть только декларация FIN, что означает: «Я больше не буду вам посылать». Но это не значит, что в полете нет пакетов или другому нечего сказать. Если вы реализуете обычную почту в качестве уровня канала передачи данных, или если ваш пакет путешествовал по разным маршрутам, возможно, получатель получает пакеты в неправильном порядке. TCP знает, как это исправить.

Также у вас, как у программы, может не быть времени постоянно проверять, что находится в буфере. Итак, когда вам будет удобно, вы можете проверить, что находится в буфере. В общем, текущая реализация сокета не так уж и плоха. Если бы на самом деле был isPeerClosed (), этот дополнительный вызов вам нужно было бы делать каждый раз, когда вы хотите вызвать read.

Юджин Йокота
источник
1
Я так не думаю, вы можете тестировать состояния в коде C как в Windows, так и в Linux !!! Java по какой-то причине может не раскрывать некоторые вещи, например, было бы неплохо раскрыть функции getsockopt, которые есть в Windows и Linux. Фактически, ответы ниже содержат некоторый код Linux C для стороны Linux.
Дин Хиллер,
Я не думаю, что наличие метода isPeerClosed () каким-то образом заставляет вас вызывать его перед каждой попыткой чтения. Вы можете просто вызвать его только тогда, когда вам это явно нужно. Я согласен с тем, что текущая реализация сокета не так уж плоха, даже если она требует от вас записи в выходной поток, если вы хотите узнать, закрыта ли удаленная часть сокета или нет. Потому что, если это не так, вам придется обрабатывать свои записанные данные и с другой стороны, и это такое же большое удовольствие, как сидение на вертикально ориентированном гвозде;)
Ян Хруби
1
Это действительно означает, что в полете больше нет пакета. FIN получен после любых данных в полете. Однако это не означает, что партнер закрыл сокет для ввода. Вы должны ** отправить что-то * и получить «сброс соединения», чтобы это обнаружить. FIN мог просто означать отключение для вывода.
Marquis of Lorne
11

Базовый API сокетов не имеет такого уведомления.

Отправляющий TCP-стек в любом случае не будет отправлять бит FIN до последнего пакета, поэтому может быть много данных, буферизованных с момента, когда отправляющее приложение логически закрыло свой сокет, прежде чем эти данные даже будут отправлены. Точно так же данные, которые буферизуются, потому что сеть быстрее, чем принимающее приложение (я не знаю, возможно, вы ретранслируете их через более медленное соединение), могут быть важны для получателя, и вы не хотите, чтобы принимающее приложение их отбрасывало. просто потому, что стек получил бит FIN.

Майк Диммик
источник
В моем тестовом примере (возможно, я должен указать здесь ...) нет данных, отправленных / полученных через соединение специально. Итак, я почти уверен, что стек получает FIN (изящный) или RST (в некоторых не изящных сценариях). Это также подтверждает netstat.
Александр
1
Конечно - если ничего не буферизовано, то FIN будет отправлен немедленно, в противном случае пакет будет пустым (без полезной нагрузки). Однако после FIN этот конец соединения больше не отправляет пакеты данных (он все равно будет подтверждать все отправленные ему сообщения).
Майк Диммик,
1
Что происходит, так это то, что стороны соединения оказываются в <code> CLOSE_WAIT </code> и <code> FIN_WAIT_2 </code>, и в этом состоянии находятся <code> isConcected () </code> и <code> isClosed () </code> по-прежнему не видит, что соединение было разорвано.
Александр
Спасибо за ваши предложения! Думаю, теперь я лучше понимаю проблему. Я уточнил вопрос (см. Третий абзац): почему нет Socket.isClosing для проверки полузакрытых соединений?
Александр
8

Поскольку ни один из ответов до сих пор полностью не отвечает на вопрос, я резюмирую свое текущее понимание проблемы.

Когда TCP-соединение установлено и один партнер вызывает close()или использует shutdownOutput()его сокет, сокет на другой стороне соединения переходит в CLOSE_WAITсостояние. В принципе, можно узнать из стека TCP, находится ли сокет в CLOSE_WAITсостоянии без вызова read/recv(например, getsockopt()в Linux: http://www.developerweb.net/forum/showthread.php?t=4395 ), но это не так. портативный.

SocketКажется, что класс Java разработан для обеспечения абстракции, сравнимой с сокетом BSD TCP, вероятно, потому, что это тот уровень абстракции, к которому люди привыкли при программировании приложений TCP / IP. Сокеты BSD - это обобщение, поддерживающее сокеты, отличные от INET (например, TCP), поэтому они не предоставляют переносимого способа определения состояния TCP сокета.

Нет такого метода, как isCloseWait()потому, что люди, привыкшие программировать TCP-приложения на уровне абстракции, предлагаемой сокетами BSD, не ожидают, что Java предоставит какие-либо дополнительные методы.

Александр
источник
3
Java также не может предоставить никаких дополнительных методов переносимо. Может быть, они могли бы создать метод isCloseWait (), который возвращал бы false, если бы платформа его не поддерживала, но сколько программистов пострадали бы от этого, если бы они тестировали только на поддерживаемых платформах?
ephemient
1
похоже, это может быть переносимым для меня ... в Windows есть этот msdn.microsoft.com/en-us/library/windows/desktop/…, а в Linux - это pubs.opengroup.org/onlinepubs/009695399/functions/…
Дин Хиллер
1
Не то чтобы программисты к этому привыкли; Дело в том, что интерфейс сокетов полезен программистам. Имейте в виду, что абстракция сокета используется не только для протокола TCP.
Уоррен Дью
В Java нет такого метода, isCloseWait()потому что он поддерживается не на всех платформах.
Marquis of Lorne
Протокол идентификатора (RFC 1413) позволяет серверу либо сохранять соединение открытым после отправки ответа, либо закрывать его без отправки дополнительных данных. Клиент идентификации Java может оставить соединение открытым, чтобы избежать трехстороннего рукопожатия при следующем поиске, но как он узнает, что соединение все еще открыто? Стоит ли просто попробовать, отреагировать на любую ошибку, повторно открыв соединение? Или это ошибка дизайна протокола?
Дэвид
7

Определить, закрыта ли удаленная сторона сокета (TCP), можно с помощью метода java.net.Socket.sendUrgentData (int) и перехватить исключение IOException, которое он генерирует, если удаленная сторона не работает. Это было проверено между Java-Java и Java-C.

Это позволяет избежать проблемы разработки протокола связи для использования какого-либо механизма проверки связи. При отключении OOBInline на сокете (setOOBInline (false) любые полученные данные OOB автоматически отбрасываются, но данные OOB по-прежнему могут быть отправлены. Если удаленная сторона закрыта, выполняется попытка сброса соединения, сбой и вызывает некоторое исключение IOException .

Если вы действительно используете данные OOB в своем протоколе, ваш опыт может отличаться.

Дядя Пер
источник
4

стек ввода-вывода Java определенно отправляет FIN, когда он разрушается при внезапном разрыве. Нет никакого смысла в том, что вы не можете этого обнаружить, т.к. большинство клиентов отправляют FIN только в том случае, если они закрывают соединение.

... еще одна причина, по которой я действительно начинаю ненавидеть Java-классы NIO. Вроде все ползучие.


источник
1
кроме того, похоже, я получаю и конец потока только при чтении (возврат -1), когда присутствует FIN. Так что это единственный способ обнаружить отключение на стороне чтения.
3
Вы можете это обнаружить. При чтении вы получаете EOS. И Java не отправляет FIN. TCP делает это. Java не реализует TCP / IP, а просто использует реализацию платформы.
Marquis of Lorne
4

Это интересная тема. Я только что покопался в коде Java, чтобы проверить. По моему мнению, есть две отдельные проблемы: первая - это сам TCP RFC, который позволяет удаленно закрытому сокету передавать данные в полудуплексном режиме, поэтому удаленно закрытый сокет остается полуоткрытым. Согласно RFC, RST не закрывает соединение, вам нужно отправить явную команду ABORT; поэтому Java позволяет отправлять данные через полузакрытый сокет

(Существует два метода чтения состояния закрытия на обеих конечных точках.)

Другая проблема в том, что реализация говорит, что это поведение необязательно. Поскольку Java стремится быть портативной, они реализовали лучшую общую функцию. Я полагаю, что сохранение карты (ОС, реализация полудуплекса) было бы проблемой.

Лоренцо Боккачча
источник
1
Я полагаю, вы говорите о RFC 793 ( faqs.org/rfcs/rfc793.html ), раздел 3.5 «Закрытие соединения». Я не уверен, что это объясняет проблему, потому что обе стороны завершают плавное отключение соединения и попадают в состояния, в которых они не должны отправлять / получать какие-либо данные.
Александр
Смотря как. сколько FIN вы видите на розетке? Кроме того, это может быть проблема конкретной платформы: возможно, Windows отвечает на каждый FIN с помощью FIN, и соединение закрывается на обоих концах, но другие операционные системы могут вести себя иначе, и здесь возникает проблема 2
Лоренцо Боккачча,
1
Нет, к сожалению, это не так. isOutputShutdown и isInputShutdown - это первое, что каждый пытается сделать, столкнувшись с этим «обнаружением», но оба метода возвращают false. Я только что протестировал его на Windows XP и Linux 2.6. Возвращаемые значения всех 4 методов остаются неизменными даже после попытки чтения
Александр
1
Для справки, это не полудуплекс. Полудуплекс означает, что только одна сторона может отправлять одновременно; обе стороны все еще могут отправить.
rbp
1
isInputShutdown и isOutputShutdown тестируют локальный конец соединения - это тесты, чтобы выяснить, вызвали ли вы shutdownInput или shutdownOutput для этого Socket. Они ничего не говорят вам об удаленном конце соединения.
Майк Диммик,
3

Это недостаток классов OO-сокетов Java (и всех других, на которые я смотрел) - нет доступа к системному вызову select.

Правильный ответ на C:

struct timeval tp;  
fd_set in;  
fd_set out;  
fd_set err;  

FD_ZERO (in);  
FD_ZERO (out);  
FD_ZERO (err);  

FD_SET(socket_handle, err);  

tp.tv_sec = 0; /* or however long you want to wait */  
tp.tv_usec = 0;  
select(socket_handle + 1, in, out, err, &tp);  

if (FD_ISSET(socket_handle, err) {  
   /* handle closed socket */  
}  
Джошуа
источник
Вы можете сделать то же самое с getsocketop(... SOL_SOCKET, SO_ERROR, ...).
Дэвид Шварц
1
Набор дескрипторов файла ошибок не указывает на закрытое соединение. Прочтите, пожалуйста, выбранное руководство: 'exceptfds - этот набор отслеживается для "исключительных условий". На практике встречается только одно такое исключительное условие: доступность внеполосных (OOB) данных для чтения из сокета TCP. FIN не является данными OOB.
Ян Вробель,
1
Вы можете использовать класс Selector для доступа к системному вызову select (). Однако он использует NIO.
Matthieu
1
Нет ничего необычного в соединении, которое было отключено другой стороной.
Дэвид Шварц
1
Многие платформы, в том числе Java, сделать обеспечивают доступ к избранному () системному вызову.
Marquis of Lorne
2

Вот неудачный обходной путь. Используйте SSL;), и SSL выполняет тесное рукопожатие при разрыве, чтобы вы были уведомлены о закрытии сокета (большинство реализаций, похоже, выполняют разрыв надлежащего рукопожатия).

Дин Хиллер
источник
2
Как получить "уведомление" о закрытии сокета при использовании SSL в java?
Dave B
2

Причина такого поведения (которое не относится к Java) заключается в том, что вы не получаете никакой информации о состоянии из стека TCP. В конце концов, сокет - это просто еще один дескриптор файла, и вы не можете узнать, есть ли реальные данные для чтения из него, не пытаясь на самом деле (здесь select(2)не поможет, он только сигнализирует, что вы можете попробовать без блокировки).

Для получения дополнительной информации см. FAQ по сокетам Unix .

WMR
источник
2
Сокеты REALbasic (в Mac OS X и Linux) основаны на сокетах BSD, но RB умудряется выдать вам приятную ошибку 102, когда соединение разрывается на другом конце. Итак, я согласен с исходным плакатом, это должно быть возможно, и ужасно, что Java (и Какао) не предоставляют этого.
Джо Строут,
1
@JoeStrout RB может сделать это только тогда, когда вы выполняете ввод-вывод. Не существует API, который сообщал бы вам о состоянии соединения без выполнения операций ввода-вывода. Период. Это не недостаток Java. На самом деле это связано с отсутствием «гудка» в TCP, что является преднамеренной особенностью дизайна.
Marquis of Lorne
select() сообщает вам, есть ли данные или EOS, доступные для чтения без блокировки. «Сигналы, которые можно попробовать без блокировки» бессмысленны. Если вы находитесь в неблокирующем режиме, вы всегда можете попробовать без блокировки. select()управляется данными в буфере приема сокета, ожидающим FIN или пробелом в буфере отправки сокета.
Маркиз Лорн,
@EJP А что getsockopt(SO_ERROR)? Фактически, даже getpeernameскажет вам, подключен ли еще сокет.
Дэвид Шварц
0

Только записи требуют обмена пакетами, что позволяет определить потерю соединения. Обычный обходной путь - использовать опцию KEEP ALIVE.

Рэй
источник
Я думаю, что конечной точке разрешено инициировать плавное завершение соединения, отправив пакет с установленным FIN, без записи какой-либо полезной нагрузки.
Александр
@Alexander Конечно, есть, но это не имеет отношения к этому ответу, который касается обнаружения меньшего количества соединений.
Маркиз Лорн,
-3

Когда дело доходит до полуоткрытых сокетов Java, возможно, стоит взглянуть на isInputShutdown () и isOutputShutdown () .

JimmyB
источник
Нет. Это говорит вам только то, что вы звонили, а не то, что звонил друг.
Marquis of Lorne
Не хотите поделиться своим источником этого заявления?
JimmyB
3
Хотите поделиться своим источником для противоположного? Это ваше заявление. Если у вас есть доказательства, давайте их. Я утверждаю, что вы неправы. Проведите эксперимент и докажите, что я ошибаюсь.
Marquis of Lorne
Три года спустя и никаких экспериментов. QED
Marquis of Lorne