¿Por qué es imposible, sin intentar E / S, detectar que el par TCP cerró correctamente el socket?

92

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 Socketo 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 Socketo SocketChannelque 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_WAITlo que significa que la conexión está en proceso de cerrarse y espera a que el lado emita su propia CLOSEoperación. Supongo que es bastante justo que isConnectedregrese truey isClosedregrese 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
...
Alejandro
fuente
Pensé que mencionaría: El protocolo SCTP no tiene este "problema". SCTP no tiene un estado medio cerrado como TCP, en otras palabras, un lado no puede continuar enviando datos mientras que el otro extremo ha cerrado su socket de envío. Eso debería facilitar las cosas.
Tarnay Kálmán
2
Disponemos de dos Buzones (Sockets) ........................................... ...... Los Buzones de Correo se envían correo entre sí usando RoyalMail (IP) olvídate de TCP ............................. .................... Todo está bien y muy bien, los buzones pueden enviarse / recibir correo entre sí (mucho retraso recientemente) enviándose mientras se reciben sin problemas. ............. Si un buzón fuera atropellado por un camión y fallara ... ¿cómo lo sabría el otro buzón? Tendría que ser notificado por Royal Mail, quien a su vez no lo sabría hasta que la próxima vez intentara enviar / recibir correo desde ese Buzón fallido ... ... erm .....
divinci
Si no va a leer desde el zócalo o escribir en el zócalo, ¿por qué le importa? Y si va a leer desde el socket o escribir en el socket, ¿por qué hacer una verificación adicional? ¿Cuál es el caso de uso?
David Schwartz
2
Socket.closeno es un cierre elegante.
user253751
@immibis Ciertamente es un cierre elegante, a menos que haya datos no leídos en el búfer de recepción del socket o que se haya metido con SO_LINGER.
Marqués de Lorne

Respuestas:

29

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í:

  1. Salida del zócalo de apagado (envía FIN al otro extremo, esto es lo último que enviará este zócalo). La entrada todavía está abierta para que pueda read()detectar el control remotoclose()
  2. Lea el socket InputStreamhasta 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)
  3. Cierre el socket (llamando Socket.close()o cerrando su InputStreamo OutputStream)

Como se muestra en el siguiente fragmento de 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 de envío siempre puede estar enviando suficientes datos para mantener el whilebucle 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 whilebucle), se descartan.

1 : De " Redes fundamentales en Java ": consulte la fig. 3.3 p.45, y todo §3.7, pp 43-48

Matthieu
fuente
Java realiza un cierre elegante. No es 'brutal'.
Marqués de Lorne
2
@EJP, "desconexión elegante" es un intercambio específico que tiene lugar a nivel TCP, donde el Cliente debe indicar su voluntad de desconectarse al Servidor, que, a su vez, envía los datos restantes antes de cerrar su lado. La parte "enviar datos restantes" tiene que ser manejada por el programa (aunque la gente no envía nada la mayor parte del tiempo). Llamar 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).
Matthieu
Consulte MSDN para obtener más información.
Matthieu
Java no obliga a las aplicaciones a llamar a Socket.close () en lugar de 'enviar [ing] los datos restantes' primero, y no les impide usar un cierre primero. Socket.close () solo hace lo que hace close (sd). Java no es diferente a este respecto de la propia API de Berkeley Sockets. De hecho, en la mayoría de los aspectos.
Marqués de Lorne
2
@LeonidUsov Eso es simplemente incorrecto. Java read()devuelve -1 al final de la secuencia, y continúa haciéndolo tantas veces como lo llame. AC read()o recv()devuelve cero al final de la transmisión, y continúa haciéndolo tantas veces como lo llame.
Marqués de Lorne
18

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 :

TCP proporciona una entrega ordenada y confiable de un flujo de bytes desde 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" 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.

Eugene Yokota
fuente
1
No lo creo, puedes probar estados en código C tanto en Windows como en Linux. Java por alguna razón puede no exponer algunas de las cosas, como sería bueno exponer las funciones getsockopt que están en Windows y Linux. De hecho, las respuestas a continuación tienen algo de código C de Linux para el lado de Linux.
Dean Hiller
No creo que tener el método 'isPeerClosed ()' de alguna manera te comprometa a llamarlo antes de cada intento de lectura. Simplemente puede llamarlo solo cuando lo necesite explícitamente. Estoy de acuerdo en que la implementación actual del socket no es tan mala, aunque requiere que escriba en el flujo de salida si desea saber si la parte remota del socket está cerrada o no. Porque si no es así, también debe manejar sus datos escritos en el otro lado, y es simplemente tan placentero como sentarse en el clavo orientado verticalmente;)
Jan Hruby
1
Se hace media 'no hay más paquetes en vuelo'. El FIN se recibe después de cualquier dato en vuelo. Sin embargo, lo que no significa es que el par ha cerrado el zócalo para la entrada. Tienes que ** enviar algo * y obtener un 'restablecimiento de conexión' para detectarlo. El FIN podría haber significado simplemente un cierre de la producción.
Marqués de Lorne
11

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.

Mike Dimmick
fuente
En mi ejemplo de prueba (tal vez debería proporcionar uno aquí ...) no hay datos enviados / recibidos a través de la conexión a propósito. Entonces, estoy bastante seguro de que la pila recibe FIN (elegante) o RST (en algunos escenarios no agraciados). Esto también lo confirma netstat.
Alexander
1
Seguro: si no hay nada almacenado en el búfer, el FIN se enviará inmediatamente, en un paquete vacío (sin carga). Sin embargo, después de FIN, no se envían más paquetes de datos al final de la conexión (todavía ACK todo lo que se le envíe).
Mike Dimmick
1
Lo que sucede es que los lados de la conexión terminan en <code> CLOSE_WAIT </code> y <code> FIN_WAIT_2 </code> y es en este estado el <code> isConcected () </code> y <code> isClosed () </code> todavía no ve que la conexión se ha terminado.
Alexander
¡Gracias por tus sugerencias! Creo que ahora entiendo mejor el problema. Hice la pregunta más específica (ver tercer párrafo): ¿por qué no hay "Socket.isClosing" para probar las conexiones medio cerradas?
Alexander
8

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()o shutdownOutput()en su socket, el socket del otro lado de la conexión cambia a CLOSE_WAITestado. En principio, es posible averiguar en la pila TCP si un socket está en CLOSE_WAITestado sin llamar read/recv(por ejemplo, getsockopt()en Linux: http://www.developerweb.net/forum/showthread.php?t=4395 ), pero eso no es portátil.

La Socketclase 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.

Alejandro
fuente
3
Java tampoco puede proporcionar métodos adicionales de forma portátil. Tal vez podrían crear un método isCloseWait () que devolvería falso si la plataforma no lo admitiera, pero ¿cuántos programadores serían mordidos por ese problema si solo probaran en plataformas compatibles?
Ephemient
1
parece que podría ser portátil para mí ... Windows tiene este msdn.microsoft.com/en-us/library/windows/desktop/… y linux este pubs.opengroup.org/onlinepubs/009695399/functions/…
Dean Hiller
1
No es que los programadores estén acostumbrados; es que la interfaz de socket es lo que es útil para los programadores. Tenga en cuenta que la abstracción de socket se usa para más que el protocolo TCP.
Warren Dew
No hay ningún método en Java como isCloseWait()porque no es compatible con todas las plataformas.
Marqués de Lorne
El protocolo ident (RFC 1413) permite al servidor mantener la conexión abierta después de enviar una respuesta o cerrarla sin enviar más datos. Un cliente de identidad de Java puede optar por mantener la conexión abierta para evitar el protocolo de enlace de 3 vías en la próxima búsqueda, pero ¿cómo sabe que la conexión aún está abierta? ¿Debería intentar responder a cualquier error volviendo a abrir la conexión? ¿O es un error de diseño de protocolo?
David
7

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.

Tío per
fuente
4

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
1
Además, parece que solo obtengo un final de flujo en lectura (retorno -1) cuando hay un FIN. Entonces, esta es la única forma que puedo ver para detectar un apagado en el lado de lectura.
3
Puedes detectarlo. Obtienes EOS al leer. Y Java no envía FIN. TCP hace eso. Java no implementa TCP / IP, solo usa la implementación de la plataforma.
Marqués de Lorne
4

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.

Lorenzo Boccaccia
fuente
1
Supongo que está hablando de RFC 793 ( faqs.org/rfcs/rfc793.html ) Sección 3.5 Cerrar una conexión. No estoy seguro de que explique el problema, porque ambas partes completan el cierre elegante de la conexión y terminan en estados en los que no deberían enviar / recibir ningún dato.
Alexander
Depende. ¿cuántas aletas ves en el enchufe? Además, podría ser un problema específico de la plataforma: tal vez Windows responda cada FIN con un FIN y la conexión esté cerrada en ambos extremos, pero es posible que otros sistemas operativos no se comporten de esta manera, y aquí es donde surge el problema 2
Lorenzo Boccaccia
1
No, lamentablemente ese no es el caso. isOutputShutdown e isInputShutdown es lo primero que todos intentan cuando se enfrentan a este "descubrimiento", pero ambos métodos devuelven falso. Lo acabo de probar en Windows XP y Linux 2.6. Los valores de retorno de los 4 métodos siguen siendo los mismos incluso después de un intento de lectura
Alexander
1
Para el registro, esto no es semidúplex. Half-duplex significa que solo un lado puede enviar a la vez; ambos lados todavía pueden enviar.
rbp
1
isInputShutdown e isOutputShutdown prueban el extremo local de la conexión; son pruebas para averiguar si ha llamado shutdownInput o shutdownOutput en este Socket. No le dicen nada sobre el extremo remoto de la conexión.
Mike Dimmick
3

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:

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 */  
}  
Joshua
fuente
Puedes hacer lo mismo con getsocketop(... SOL_SOCKET, SO_ERROR, ...).
David Schwartz
1
El conjunto de descriptores de archivos de error no indicará una conexión cerrada. Por favor, lea el manual de selección: 'exceptfds - Este conjunto se vigila para detectar "condiciones excepcionales". En la práctica, solo una de estas condiciones excepcionales es común: la disponibilidad de datos fuera de banda (OOB) para leer desde un socket TCP '. FIN no son datos OOB.
Jan Wrobel
1
Puede usar la clase 'Selector' para acceder al syscall 'select ()'. Sin embargo, usa NIO.
Matthieu
1
No hay nada excepcional en una conexión que ha sido apagada por el otro lado.
David Schwartz
1
Muchas plataformas, incluyendo Java, hacen proporcionar acceso a la llamada al sistema select ().
Marqués de Lorne
2

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).

Dean Hiller
fuente
2
¿Cómo se "notifica" a uno cuando se cierra el socket cuando se usa SSL en Java?
Dave B
2

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 .

WMR
fuente
2
Los sockets REALbasic (en Mac OS X y Linux) se basan en sockets BSD, sin embargo, RB se las arregla para darle un buen error 102 cuando la conexión se interrumpe por el otro extremo. Así que estoy de acuerdo con el póster original, esto debería ser posible y es lamentable que Java (y Cocoa) no lo proporcionen.
Joe Strout
1
@JoeStrout RB solo puede hacer eso cuando haces algunas E / S. No hay una API que le dé el estado de la conexión sin realizar E / S. Período. Esta no es una deficiencia de Java. De hecho, se debe a la falta de un 'tono de marcado' en TCP, que es una característica de diseño deliberada.
Marqués de Lorne
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.
Marqués de Lorne
@EJP ¿Sobre qué getsockopt(SO_ERROR)? De hecho, incluso getpeernamete dirá si el enchufe sigue conectado.
David Schwartz
0

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.

Rayo
fuente
Creo que un punto final puede iniciar un cierre de conexión elegante enviando un paquete con FIN configurado, sin escribir ninguna carga útil.
Alexander
@Alexander Por supuesto que sí, pero eso no es relevante para esta respuesta, que se trata de detectar menos conexión.
Marqués de Lorne
-3

Cuando se trata de trabajar con sockets Java medio abiertos, es posible que desee echar un vistazo a isInputShutdown () e isOutputShutdown () .

JimmyB
fuente
No. Eso solo te dice lo que has llamado, no lo que ha llamado el compañero.
Marqués de Lorne
¿Le gustaría compartir su fuente para esa declaración?
JimmyB
3
¿Te importaría compartir tu fuente para lo contrario? Es tu declaración. Si tiene una prueba, déjela. Afirmo que estás equivocado. Haz el experimento y demuestra que estoy equivocado.
Marqués de Lorne
Tres años después y sin experimento. QED
Marqués de Lorne