java - socketclient - ¿Por qué es imposible, sin intentar E/S, detectar que el socket TCP se cerró grácilmente por pares?
socketclient java (12)
Aquí hay una solución coja. Use SSL;) y SSL hace un estrecho apretón de manos en el desmontaje para que se le notifique que el socket está cerrado (la mayoría de las implementaciones parecen hacer un aterrizaje de handshake que es).
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 pilas 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 pila TCP para ver si la conexión TCP subyacente ha finalizado? ¿Es que la pila 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
...
Creo que esto es más una pregunta de programación de socket. Java solo sigue la tradición de programación de socket.
De la Wikipedia :
TCP proporciona entrega confiable y ordenada de una secuencia de bytes de un programa en una computadora a otro programa 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 declaración 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 programa, 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.
Cuando se trata de ocuparse de los zócalos Java medio abiertos, uno puede querer echar un vistazo a isInputShutdown() y isOutputShutdown() .
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 pila 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 programar 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 programar aplicaciones TCP en el nivel de abstracción ofrecido por los conectores BSD no esperan que Java proporcione ningún método adicional.
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 pila 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 .
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.
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 */
}
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í:
- 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 elclose()
remotoclose()
- 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) - Cierre el socket (llamando a
Socket.close()
o cerrando suInputStream
oOutputStream
)
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 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 ciclo while), se descartan.
1 : De " Redes Fundamentales en Java ": ver fig. 3.3 p.45, y el total §3.7, pp 43-48
La API de sockets subyacente no tiene esa 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 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 pila.
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.
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.
la pila Java IO definitivamente envía FIN cuando se destruye en un desmontaje 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.