¿Por qué es imposible, sin intentar E / S, detectar que el socket TCP se cerró grácilmente por pares?

Como seguimiento de una pregunta reciente , me pregunto por qué es imposible en Java, sin intentar leer / escribir en un socket TCP, detectar que el socket ha sido cerrado con gracia por el par. Este parece ser el caso independientemente de si se usa el Socket anterior a NIO o el SocketChannel NIO.

Cuando un par cierra con gracia una conexión TCP, las stacks de TCP en ambos lados de la conexión lo saben. El lado del servidor (el que inicia el cierre) termina en el estado FIN_WAIT2 , mientras que el lado del cliente (el que no responde explícitamente al apagado) termina en el estado CLOSE_WAIT . ¿Por qué no hay un método en Socket o SocketChannel que pueda consultar la stack TCP para ver si la conexión TCP subyacente ha finalizado? ¿Es que la stack TCP no proporciona esa información de estado? ¿O es una decisión de diseño evitar una llamada costosa al kernel?

Con la ayuda de los usuarios que ya han publicado algunas respuestas a esta pregunta, creo que veo de dónde podría venir el problema. El lado que no cierra explícitamente la conexión termina en estado TCP CLOSE_WAIT lo que significa que la conexión se está cerrando y espera a que el lado emita su propia operación CLOSE . Supongo que es justo que isConnected devuelva true e isClosed devuelva false , pero ¿por qué no hay algo así como isClosing ?

A continuación se encuentran las clases de prueba que usan sockets anteriores a NIO. Pero se obtienen resultados idénticos usando 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); } } 

Cuando el cliente de prueba se conecta al servidor de prueba, la salida permanece sin cambios incluso después de que el servidor inicia el cierre de la conexión:

 connected: true, closed: false connected: true, closed: false ... 

He estado usando Sockets a menudo, principalmente con Selectors, y aunque no soy un experto en OSI de red, desde mi entendimiento, llamar a shutdownOutput() en un Socket realmente envía algo en la red (FIN) que despierta mi Selector en el otro lado (mismo comportamiento en lenguaje C). Aquí tiene detección : realmente detecta una operación de lectura que fallará cuando la pruebe.

En el código que proporcione, al cerrar el socket se apagarán los flujos de entrada y salida, sin posibilidad de leer los datos que podrían estar disponibles, por lo tanto, perderlos. El Socket.close() Java Socket.close() realiza una desconexión “elegante” (al contrario de lo que inicialmente pensé) en que los datos que quedan en la secuencia de salida se enviarán seguidos por un FIN para señalar su cierre. La FIN será ACK’d por el otro lado, ya que cualquier paquete regular sería 1 .

Si necesita esperar que el otro lado cierre su socket, debe esperar su FIN. Y para lograr eso, debe detectar Socket.getInputStream().read() < 0 , lo que significa que no debe cerrar el socket, ya que cerraría su InputStream .

De lo que hice en C, y ahora en Java, lograr ese cierre sincronizado debería hacerse así:

  1. Salida de socket de cierre (envía FIN en el otro extremo, esto es lo último que enviará este socket). La entrada todavía está abierta para que pueda read() y detectar el close() remoto close()
  2. Lee el socket InputStream hasta que recibamos la respuesta FIN desde el otro extremo (ya que detectará el FIN, pasará por el mismo proceso de pronunciación). Esto es importante en algunos sistemas operativos ya que en realidad no cierran el socket siempre que uno de sus búferes aún contenga datos. Se llaman socket "fantasma" y usan números de descriptores en el sistema operativo (que ya no es un problema con el sistema operativo moderno)
  3. Cierre el socket (llamando a Socket.close() o cerrando su InputStream o OutputStream )

Como se muestra en el siguiente fragmento de código 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 } 

Por supuesto, ambos lados tienen que usar la misma forma de cierre, o la parte emisora ​​siempre estará enviando datos suficientes para mantener ocupado el bucle while (por ejemplo, si la parte emisora ​​solo envía datos y nunca lee para detectar la terminación de la conexión. , pero puede que no tengas control sobre eso).

Como señaló @WarrenDew en su comentario, descartar los datos en el progtwig (capa de aplicación) induce una desconexión no elegante en la capa de aplicación: aunque todos los datos se recibieron en la capa TCP (el ciclo while), se descartan.

1 : De " Redes Fundamentales en Java ": ver fig. 3.3 p.45, y el total §3.7, pp 43-48

Creo que esto es más una pregunta de progtwigción de socket. Java solo sigue la tradición de progtwigción de socket.

De la Wikipedia :

TCP proporciona entrega confiable y ordenada de una secuencia de bytes de un progtwig en una computadora a otro progtwig en otra computadora.

Una vez que se realiza el protocolo de enlace, TCP no hace ninguna distinción entre dos puntos finales (cliente y servidor). El término “cliente” y “servidor” es principalmente por conveniencia. Entonces, el “servidor” podría estar enviando datos y “el cliente” podría estar enviando otros datos simultáneamente.

El término “Cerrar” también es engañoso. Solo hay una statement FIN, lo que significa “No voy a enviarte más cosas”. Pero esto no significa que no haya paquetes en vuelo o que el otro no tenga más que decir. Si implementa Snail Mail como la capa de enlace de datos, o si su paquete viajó por rutas diferentes, es posible que el receptor reciba paquetes en orden incorrecto. TCP sabe cómo solucionar esto por ti.

También usted, como progtwig, puede no tener tiempo para seguir comprobando lo que hay en el búfer. Entonces, a su conveniencia, puede verificar qué hay en el búfer. En general, la implementación del socket actual no es tan mala. Si realmente hay isPeerClosed (), esa es una llamada adicional que debe realizar cada vez que quiera llamar a read.

La API de sockets subyacente no tiene esa notificación.

La stack TCP emisora ​​no enviará el bit FIN hasta el último paquete de todos modos, por lo que podría haber una gran cantidad de datos almacenados en búfer cuando la aplicación emisora ​​cerró lógicamente su socket antes de que incluso se envíen los datos. Del mismo modo, los datos almacenados en búfer porque la red es más rápida que la aplicación receptora (no sé, tal vez lo estés retransmitiendo a través de una conexión más lenta) podrían ser importantes para el receptor y no querrás que la aplicación receptora lo descarte solo porque el bit FIN ha sido recibido por la stack.

Dado que ninguna de las respuestas hasta ahora responde completamente la pregunta, estoy resumiendo mi comprensión actual del problema.

Cuando se establece una conexión TCP y un par llama a close() o shutdownOutput() en su socket, el socket en el otro lado de la conexión pasa al estado CLOSE_WAIT . En principio, es posible descubrir a partir de la stack TCP si un socket está en estado CLOSE_WAIT sin llamar a read/recv (por ejemplo, getsockopt() en Linux: http://www.developerweb.net/forum/showthread.php?t = 4395 ), pero eso no es portátil.

La clase Socket de Java parece estar diseñada para proporcionar una abstracción comparable a un socket BSD TCP, probablemente porque este es el nivel de abstracción al que las personas están acostumbradas al progtwigr aplicaciones TCP / IP. Los zócalos BSD son una generalización que admite zócalos que no sean solo INET (por ejemplo, TCP), por lo que no proporcionan una forma portátil de averiguar el estado TCP de un zócalo.

No existe un método como isCloseWait() porque las personas que solían progtwigr aplicaciones TCP en el nivel de abstracción ofrecido por los conectores BSD no esperan que Java proporcione ningún método adicional.

La detección de si el lado remoto de una conexión de socket (TCP) se ha cerrado se puede hacer con el método java.net.Socket.sendUrgentData (int) y capturando la IOException que lanza si el lado remoto está inactivo. Esto ha sido probado entre Java-Java y Java-C.

Esto evita el problema de diseñar el protocolo de comunicación para utilizar algún tipo de mecanismo de ping. Al deshabilitar OOBInline en un socket (setOOBInline (false), cualquier dato OOB recibido se descarta silenciosamente, pero aún se pueden enviar datos OOB. Si el lado remoto está cerrado, se intenta restablecer la conexión, falla y causa que se genere alguna IOException .

Si realmente usa datos OOB en su protocolo, entonces su millaje puede variar.

Es un tema interesante He buscado el código de Java justo ahora para comprobar. A partir de mi hallazgo, hay dos problemas distintos: el primero es el TCP RFC en sí mismo, que permite que el socket cerrado remotamente transmita datos en semidúplex, por lo que un socket remoto cerrado sigue estando medio abierto. De acuerdo con el RFC, RST no cierra la conexión, debe enviar un comando ABORT explícito; para que Java permita el envío de datos a través de un socket medio cerrado

(Hay dos métodos para leer el estado de cierre en ambos extremos).

El otro problema es que la implementación dice que este comportamiento es opcional. Como Java se esfuerza por ser portátil, implementaron la mejor característica común. Mantener un mapa de (SO, implementación de half duplex) hubiera sido un problema, supongo.

la stack Java IO definitivamente envía FIN cuando se destruye en un desassembly abrupto. Simplemente no tiene sentido que no pueda detectar esto, b / c la mayoría de los clientes solo envían el FIN si cierran la conexión.

… otra razón por la que realmente estoy empezando a odiar las clases de NIO Java. Parece que todo es un poco mediocre.

Este es un defecto de las clases de socket OO de Java (y de todos los demás que he analizado), sin acceso a la llamada al sistema de selección.

Respuesta correcta en 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 */ } 

Aquí hay una solución coja. Use SSL;) y SSL hace un estrecho apretón de manos en el desassembly para que se le notifique que el socket está cerrado (la mayoría de las implementaciones parecen hacer un aterrizaje de handshake que es).

El motivo de este comportamiento (que no es específico de Java) es el hecho de que no se obtiene ninguna información de estado de la stack TCP. Después de todo, un socket es simplemente otro manejador de archivo y no se puede averiguar si hay datos reales para leer sin intentarlo ( select(2) no ayudará, solo indica que se puede intentar sin bloquear) .

Para obtener más información, consulte las Preguntas frecuentes sobre el conector Unix .

Solo las escrituras requieren que los paquetes se intercambien, lo que permite determinar la pérdida de conexión. Un trabajo común es usar la opción KEEP ALIVE.

Cuando se trata de ocuparse de los zócalos Java medio abiertos, uno puede querer echar un vistazo a isInputShutdown () y isOutputShutdown () .