Como continuación de una pregunta reciente , me pregunto por qué es imposible en Java, sin intentar leer / escribir en un socket TCP, detectar que el par ha cerrado el socket con gracia. Este parece ser el caso independientemente de si se usa el pre-NIO Socket
o el NIO SocketChannel
.
Cuando un par cierra con gracia una conexión TCP, las pilas de TCP en ambos lados de la conexión conocen el hecho. El lado del servidor (el que inicia el apagado) termina en estado FIN_WAIT2
, mientras que el lado del cliente (el que no responde explícitamente al apagado) termina en estado CLOSE_WAIT
. ¿Por qué no hay un método en Socket
o SocketChannel
que pueda consultar la pila TCP para ver si la conexión TCP subyacente se ha terminado? ¿Es que la pila TCP no proporciona tal información de estado? ¿O es una decisión de diseño para 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 el estado TCP, CLOSE_WAIT
lo que significa que la conexión está en proceso de cerrarse y espera a que el lado emita su propia CLOSE
operación. Supongo que es bastante justo que isConnected
regrese true
y isClosed
regrese false
, pero ¿por qué no hay algo así isClosing
?
A continuación se muestran las clases de prueba que utilizan sockets pre-NIO. Pero se obtienen resultados idénticos utilizando 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
...
fuente
Socket.close
no es un cierre elegante.Respuestas:
He estado usando Sockets a menudo, principalmente con Selectores, y aunque no soy un experto en Network OSI, según tengo entendido, llamar
shutdownOutput()
a un Socket en realidad envía algo en la red (FIN) que despierta mi Selector en el otro lado (el mismo comportamiento en C idioma). Aquí tienes la detección : detectar realmente una operación de lectura que fallará cuando la pruebes.En el código que proporcione, cerrar el socket apagará los flujos de entrada y salida, sin posibilidades de leer los datos que puedan estar disponibles, por lo que los perderá. El
Socket.close()
método Java realiza una desconexión "elegante" (opuesta a lo que pensé inicialmente) en el sentido de que los datos que quedan en el flujo de salida se enviarán seguidos de un FIN para señalar su cierre. El FIN será ACK por el otro lado, como lo haría cualquier paquete regular 1 .Si necesita esperar a que el otro lado cierre su zócalo, debe esperar su FIN. Y para lograr eso, se tiene que detectar
Socket.getInputStream().read() < 0
, lo que significa que debe no tan cerca de su toma de corriente, ya que cerrará suInputStream
.Por lo que hice en C, y ahora en Java, lograr un cierre sincronizado debería hacerse así:
read()
detectar el control remotoclose()
InputStream
hasta que recibamos la respuesta-FIN del otro extremo (ya que detectará el FIN, pasará por el mismo proceso de conexión elegante). Esto es importante en algunos sistemas operativos, ya que en realidad no cierran el socket siempre que uno de sus búfer todavía contenga datos. Se llaman socket "fantasma" y usan números de descriptor en el sistema operativo (eso puede que ya no sea un problema con el sistema operativo moderno)Socket.close()
o cerrando suInputStream
oOutputStream
)Como se muestra en el siguiente fragmento de Java:
Por supuesto, ambos lados tienen que usar la misma forma de cierre, o la parte de envío siempre puede estar enviando suficientes datos para mantener el
while
bucle ocupado (por ejemplo, si la parte de envío solo envía datos y nunca lee para detectar la terminación de la conexión. Lo cual es torpe, pero es posible que no tengas control sobre eso).Como señaló @WarrenDew en su comentario, descartar los datos en el programa (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
while
bucle), se descartan.1 : De " Redes fundamentales en Java ": consulte la fig. 3.3 p.45, y todo §3.7, pp 43-48
fuente
socket.close()
es "brutal" en la forma en que no respeta esta señalización Cliente / Servidor. El servidor sería notificado de la desconexión de un Cliente solo cuando su propio búfer de salida de socket esté lleno (porque el otro lado, que estaba cerrado, no está recibiendo ningún dato).read()
devuelve -1 al final de la secuencia, y continúa haciéndolo tantas veces como lo llame. ACread()
orecv()
devuelve cero al final de la transmisión, y continúa haciéndolo tantas veces como lo llame.Creo que esta es más una cuestión de programación de sockets. Java simplemente sigue la tradición de la programación de sockets.
De Wikipedia :
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" se utiliza principalmente por conveniencia. Por tanto, el "servidor" podría estar enviando datos y el "cliente" podría estar enviando algunos otros datos simultáneamente entre sí.
El término "Cerrar" también es engañoso. Solo hay una declaración FIN, que significa "No te voy a enviar 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 el correo postal como capa de enlace de datos, o si su paquete viajó por diferentes rutas, es posible que el receptor reciba los paquetes en el orden incorrecto. TCP sabe cómo solucionarlo por usted.
Además, es posible que usted, como programa, no tenga tiempo para seguir comprobando lo que hay en el búfer. Por lo tanto, a su conveniencia, puede verificar qué hay en el búfer. Con todo, la implementación de socket actual no es tan mala. Si realmente hubiera isPeerClosed (), esa es una llamada adicional que debe realizar cada vez que desee llamar a read.
fuente
La API de sockets subyacente no tiene tal notificación.
La pila 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 desde que la aplicación emisora cerró lógicamente su socket antes de que se envíen esos datos. Del mismo modo, los datos almacenados en búfer porque la red es más rápida que la aplicación receptora (no lo sé, tal vez los esté transmitiendo a través de una conexión más lenta) podrían ser importantes para el receptor y no querría que la aplicación receptora los descartara. simplemente porque la pila ha recibido el bit FIN.
fuente
Dado que ninguna de las respuestas hasta ahora responde completamente a la pregunta, estoy resumiendo mi comprensión actual del problema.
Cuando se establece una conexión TCP y un par llama
close()
oshutdownOutput()
en su socket, el socket del otro lado de la conexión cambia aCLOSE_WAIT
estado. En principio, es posible averiguar en la pila TCP si un socket está enCLOSE_WAIT
estado sin llamarread/recv
(por ejemplo,getsockopt()
en Linux: http://www.developerweb.net/forum/showthread.php?t=4395 ), pero eso no es portátil.La
Socket
clase 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 la gente está acostumbrada al programar aplicaciones TCP / IP. Los sockets BSD son una generalización que admite sockets distintos de los INET (por ejemplo, TCP), por lo que no proporcionan una forma portátil de averiguar el estado TCP de un socket.No existe un método así
isCloseWait()
porque las personas acostumbradas a programar aplicaciones TCP al nivel de abstracción que ofrecen los sockets BSD no esperan que Java proporcione métodos adicionales.fuente
isCloseWait()
porque no es compatible con todas las plataformas.Se puede detectar si el lado remoto de una conexión de socket (TCP) se ha cerrado con el método java.net.Socket.sendUrgentData (int) y detectar la IOException que lanza si el lado remoto está inactivo. Esto se ha 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), todos los datos OOB recibidos se descartan silenciosamente, pero los datos OOB aún se pueden enviar. Si el lado remoto está cerrado, se intenta restablecer la conexión, falla y provoca que se genere alguna IOException .
Si realmente usa datos OOB en su protocolo, entonces su millaje puede variar.
fuente
la pila Java IO definitivamente envía FIN cuando se destruye en un desmontaje abrupto. Simplemente no tiene sentido que no pueda detectar esto, porque la mayoría de los clientes solo envían el FIN si están cerrando la conexión.
... otra razón por la que realmente estoy empezando a odiar las clases de Java NIO. Parece que todo es un poco tonto.
fuente
Es un tema interesante. Acabo de buscar en el código java para comprobarlo. Según mi hallazgo, hay dos problemas distintos: el primero es el propio TCP RFC, que permite que un socket cerrado de forma remota transmita datos en semidúplex, por lo que un socket cerrado de forma remota todavía está medio abierto. Según el RFC, RST no cierra la conexión, debe enviar un comando ABORT explícito; por lo que Java permite enviar 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) habría sido un problema, supongo.
fuente
Esta es una falla de las clases de socket OO de Java (y de todas las demás que he visto): no hay acceso a la llamada al sistema seleccionada.
Respuesta correcta en C:
fuente
getsocketop(... SOL_SOCKET, SO_ERROR, ...)
.Aquí hay una solución poco convincente. Use SSL;) y SSL hace un apretón de manos cercano en el desmontaje para que se le notifique que el socket se cierra (la mayoría de las implementaciones parecen hacer un desmontaje de protocolo de enlace de propiedad).
fuente
La razón de este comportamiento (que no es específico de Java) es el hecho de que no obtiene ninguna información de estado de la pila TCP. Después de todo, un socket es solo otro identificador de archivo y no puede averiguar si hay datos reales para leer sin intentarlo (
select(2)
no ayudará allí, solo indica que puede intentarlo sin bloquear).Para obtener más información, consulte las preguntas frecuentes sobre sockets Unix .
fuente
select()
le dice si hay datos o EOS disponibles para leer sin bloquear. "Señales que puede probar sin bloquear" no tiene sentido. Si está en modo sin bloqueo, siempre puede intentar sin bloquear.select()
es impulsado por datos en el búfer de recepción del socket o un FIN o espacio pendiente en el búfer de envío del socket.getsockopt(SO_ERROR)
? De hecho, inclusogetpeername
te dirá si el enchufe sigue conectado.Solo las escrituras requieren que se intercambien paquetes, lo que permite determinar la pérdida de conexión. Una solución común es utilizar la opción KEEP ALIVE.
fuente
Cuando se trata de trabajar con sockets Java medio abiertos, es posible que desee echar un vistazo a isInputShutdown () e isOutputShutdown () .
fuente