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

В качестве ответа на недавний вопрос , я удивляюсь, почему в 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
...

Ответы [ 12 ]

29 голосов
/ 22 февраля 2012

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

В коде, который вы даете, закрытие сокета отключит как входной, так и выходной потоки, без возможности чтения данных, которые могут быть доступны, и, следовательно, потерю их. Метод 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

18 голосов
/ 01 октября 2008

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

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

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

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

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

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

11 голосов
/ 01 октября 2008

В базовом API сокетов такого уведомления нет.

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

8 голосов
/ 01 октября 2008

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

Когда установлено соединение 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 предоставит какие-либо дополнительные методы.

7 голосов
/ 10 августа 2011

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

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

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

4 голосов
/ 06 июля 2009

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

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

4 голосов
/ 01 октября 2008

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

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

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

3 голосов
/ 25 января 2009

Это недостаток классов 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 */  
}  
2 голосов
/ 24 августа 2012

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

2 голосов
/ 01 октября 2008

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

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

...