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

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

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

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

Ниже приведены тестовые классы, в которых используются сокеты pre-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
...

Ответ 1

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

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

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

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

  • Выход выключения Shutdown (посылает FIN на другом конце, это последнее, что когда-либо будет отправлено этим сокетом). Вход по-прежнему открыт, поэтому вы можете read() и обнаружить удаленный close()
  • Прочитайте сокет InputStream, пока мы не получим ответ-FIN с другого конца (так как он обнаружит FIN, он будет проходить через тот же грациозный процесс diconnection). Это важно для некоторых ОС, поскольку они фактически не закрывают сокет, если один из его буфера по-прежнему содержит данные. Они называются "призрачными" сокетами и используют дескрипторные номера в ОС (это может быть больше не проблема с современной ОС)
  • Закройте сокет (путем вызова 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

Ответ 2

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

От Wikipedia:

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

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

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

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

Ответ 3

В API-интерфейсе сокетов нет такого уведомления.

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

Ответ 4

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

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

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

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

Ответ 5

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

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

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

Ответ 6

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

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

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

Ответ 7

Это недостаток Java (и всех других, на которые я смотрел) классов OO-сокетов - нет доступа к системному вызову 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 */  
}  

Ответ 8

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

... еще одна причина, по которой я действительно ненавижу классы Java NIO. Похоже, что все немного полузапад.

Ответ 9

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

Для получения дополнительной информации см. Часто задаваемые вопросы о сокетах Unix.

Ответ 10

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

Ответ 11

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

Ответ 12

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