API de Java socket: ¿Cómo saber si se ha cerrado una conexión?

92

Me encuentro con algunos problemas con la API de socket de Java. Estoy tratando de mostrar la cantidad de jugadores conectados actualmente a mi juego. Es fácil determinar cuándo se ha conectado un jugador. Sin embargo, parece innecesariamente difícil determinar cuándo un jugador se ha desconectado usando la API de socket.

Llamar isConnected()a un enchufe que se ha desconectado de forma remota siempre parece volver true. Del mismo modo, la llamada isClosed()a un socket que se ha cerrado de forma remota siempre parece volver false. He leído que para determinar realmente si un socket se ha cerrado o no, los datos deben escribirse en el flujo de salida y debe detectarse una excepción. Esta parece una forma realmente sucia de manejar esta situación. Tendríamos que enviar constantemente un mensaje basura a través de la red para saber cuándo se había cerrado un socket.

hay alguna otra solucion?

Dan Brouwer
fuente

Respuestas:

183

No hay una API de TCP que le diga el estado actual de la conexión. isConnected()y le isClosed()dirá el estado actual de su enchufe . No es lo mismo.

  1. isConnected()le indica si se ha conectado esta toma . Tienes, por lo que vuelve verdadero.

  2. isClosed()le indica si se ha cerrado esta toma. Hasta que lo tenga, devuelve falso.

  3. Si el par ha cerrado la conexión de forma ordenada

    • read() devuelve -1
    • readLine() devoluciones null
    • readXXX()lanza EOFExceptionpara cualquier otro XXX.

    • Una escritura arrojará un IOException: 'restablecimiento de conexión por par', eventualmente, sujeto a retrasos en el almacenamiento en búfer.

  4. Si la conexión se ha interrumpido por cualquier otro motivo, una escritura arrojará un IOException, eventualmente, como se indicó anteriormente, y una lectura puede hacer lo mismo.

  5. Si el par todavía está conectado pero no usa la conexión, se puede usar un tiempo de espera de lectura.

  6. Al contrario de lo que puede leer en otros lugares, ClosedChannelExceptionno le dice esto. [Tampoco SocketException: socket closed.] Sólo se le dice que se cerró el canal, y luego continuó a utilizarlo. En otras palabras, un error de programación de tu parte. No indica una conexión cerrada .

  7. Como resultado de algunos experimentos con Java 7 en Windows XP, también parece que si:

    • estás seleccionando en OP_READ
    • select() devuelve un valor mayor que cero
    • el asociado SelectionKeyya no es válido ( key.isValid() == false)

    significa que el par ha restablecido la conexión. Sin embargo, esto puede ser peculiar de la versión o plataforma de JRE.

Marqués de Lorne
fuente
18
Es difícil creer que el protocolo TCP, que está orientado a la conexión, ni siquiera pueda saber el estado de su conexión ... ¿Los tipos que crean estos protocolos conducen sus autos con los ojos cerrados?
PedroD
31
@PedroD Al contrario: fue deliberado. Las suites de protocolos anteriores, como SNA, tenían un 'tono de marcado'. TCP fue diseñado para sobrevivir a una guerra nuclear y, más trivialmente, a bajas y subidas de enrutadores: de ahí la ausencia total de cualquier cosa como tono de marcado, estado de conexión, etc .; y también es la razón por la que TCP keepalive se describe en las RFC como una característica controvertida y por qué siempre está desactivada de forma predeterminada. TCP todavía está con nosotros. SNA? IPX? ¿YO ASI? No. Lo hicieron bien.
Marqués de Lorne
3
No creo que sea una buena excusa para ocultarnos esta información. Saber que la conexión se perdió no significa necesariamente que el protocolo sea menos resistente a fallas, siempre depende de lo que hagamos con ese conocimiento ... Para mí, el método isBound e isConnected de java son métodos puros simulados, no tienen ninguna utilidad , pero expresan la necesidad de un detector de eventos de conexión ... Pero reitero: saber que se perdió la conexión no empeora el protocolo. Ahora bien, si esos protocolos que está diciendo mataron la conexión tan pronto como detectaron que se perdió, esa es una historia diferente.
PedroD
21
@Pedro No lo entiendes. No es una 'excusa'. No hay información que retener. No hay tono de marcado. TCP no sabe si la conexión ha fallado hasta que intentas hacerle algo. Ese fue el criterio de diseño fundamental.
Marqués de Lorne
2
@EJP gracias por tu respuesta. ¿Quizás sabe cómo comprobar si un enchufe se cerró sin "bloquear"? Usar read o readLine va a bloquear. ¿Hay alguna manera de darse cuenta de que si el otro socket se cerró con el método disponible, parece regresar en 0lugar de -1.
insumity
9

Es una práctica general en varios protocolos de mensajería mantener latidos entre sí (seguir enviando paquetes de ping), el paquete no necesita ser muy grande. El mecanismo de sondeo le permitirá detectar el cliente desconectado incluso antes de que TCP se dé cuenta en general (el tiempo de espera de TCP es mucho mayor) Envíe un sondeo y espere 5 segundos para recibir una respuesta, si no ve la respuesta para, digamos 2-3 sondas posteriores, su reproductor está desconectado.

Además, pregunta relacionada

Kalpak Gadre
fuente
6
Es una "práctica general" en algunos protocolos. Observo que HTTP, el protocolo de aplicación más utilizado en el planeta, no tiene operación PING.
Marqués de Lorne
2
@ user207421 porque HTTP / 1 es un protocolo de una sola operación. Envías una solicitud, recibes una respuesta. Y ya está. El enchufe está cerrado. La extensión Websocket tiene operación PING, al igual que HTTP / 2.
Michał Zabielski
Recomendé latidos con un solo byte si es posible :)
Stefan Reich
@ MichałZabielski Es porque los diseñadores de HTTP no lo especificaron. No sabes por qué no. HTTP / 1.1 no es un protocolo de una sola vez. El socket no está cerrado: las conexiones HTTP son persistentes de forma predeterminada. FTP, SMTP, POP3, IMAP, TLS, ... no tienen latidos.
Marqués de Lorne
2

Veo la otra respuesta que se acaba de publicar, pero creo que es interactivo con los clientes que juegan su juego, por lo que puedo plantear otro enfoque (mientras que BufferedReader es definitivamente válido en algunos casos).

Si quisiera ... podría delegar la responsabilidad de "registro" en el cliente. Es decir, tendría una colección de usuarios conectados con una marca de tiempo en el último mensaje recibido de cada uno ... si un cliente agota el tiempo de espera, forzaría un nuevo registro del cliente, pero eso lleva a la cita y la idea a continuación.

He leído que para determinar realmente si un socket se ha cerrado o no, los datos deben escribirse en el flujo de salida y se debe detectar una excepción. Esta parece una forma realmente sucia de manejar esta situación.

Si su código Java no cerró / desconectó el Socket, ¿de qué otra manera se le notificaría que el host remoto cerró su conexión? En última instancia, su try / catch está haciendo más o menos lo mismo que haría un encuestador que escucha eventos en el socket ACTUAL. Considera lo siguiente:

  • su sistema local podría cerrar su socket sin notificarle ... eso es solo la implementación de Socket (es decir, no sondea el hardware / controlador / firmware / lo que sea para el cambio de estado).
  • nuevo Socket (Proxy p) ... hay varias partes (6 puntos finales en realidad) que podrían estar cerrando la conexión con usted ...

Creo que una de las características de los lenguajes abstractos es que estás abstraído de las minucias. Piense en la palabra clave using en C # (probar / finalmente) para SqlConnection so lo que sea ... es solo el costo de hacer negocios ... Creo que probar / atrapar / finalmente es el patrón aceptado y necesario para el uso de Socket.

Scottley
fuente
Su sistema local no puede 'cerrar su enchufe', con o sin notificarle. Su pregunta comienza 'si su código Java no cerró / desconectó el enchufe ...?' tampoco tiene ningún sentido.
Marqués de Lorne
1
¿Qué impide exactamente que el sistema (Linux, por ejemplo) fuerce el cierre del socket? ¿Todavía es posible hacerlo desde 'gdb' usando el comando 'call close'?
Andrey Lebedenko
@AndreyLebedenko Nada 'lo previene exactamente', pero no lo hace.
Marqués de Lorne
@EJB ¿quién lo hace entonces?
Andrey Lebedenko
@AndreyLebedenko Nadie lo hace. Solo la aplicación puede cerrar sus propios sockets, a menos que se cierre sin hacerlo, en cuyo caso el sistema operativo se limpiará.
Marqués de Lorne
1

Creo que esta es la naturaleza de las conexiones tcp, en esos estándares, se necesitan aproximadamente 6 minutos de silencio en la transmisión antes de concluir que la conexión se ha ido. Así que no creo que puedas encontrar una solución exacta para este problema. Quizás la mejor manera es escribir un código útil para adivinar cuándo el servidor debería suponer que la conexión de un usuario está cerrada.

Ehsan Khodarahmi
fuente
2
Puede llevar mucho más que eso, hasta dos horas.
Marqués de Lorne
0

Como dice @ user207421, no hay forma de saber el estado actual de la conexión debido al modelo de arquitectura del protocolo TCP / IP. Por lo tanto, el servidor debe advertirlo antes de cerrar la conexión o usted mismo lo verifica.
Este es un ejemplo simple que muestra cómo saber que el servidor cierra el socket:

sockAdr = new InetSocketAddress(SERVER_HOSTNAME, SERVER_PORT);
socket = new Socket();
timeout = 5000;
socket.connect(sockAdr, timeout);
reader = new BufferedReader(new InputStreamReader(socket.getInputStream());
while ((data = reader.readLine())!=null) 
      log.e(TAG, "received -> " + data);
log.e(TAG, "Socket closed !");
ucMedia
fuente
0

Me enfrenté a un problema similar. En mi caso el cliente debe enviar datos periódicamente. Espero que tengas el mismo requisito. Luego configuro SO_TIMEOUT, socket.setSoTimeout(1000 * 60 * 5);que se lanza java.net.SocketTimeoutExceptioncuando expira el tiempo especificado. Entonces puedo detectar fácilmente al cliente muerto.

hurelhuyag
fuente
-2

Así es como lo manejo

 while(true) {
        if((receiveMessage = receiveRead.readLine()) != null ) {  

        System.out.println("first message same :"+receiveMessage);
        System.out.println(receiveMessage);      

        }
        else if(receiveRead.readLine()==null)
        {

        System.out.println("Client has disconected: "+sock.isClosed()); 
        System.exit(1);
         }    } 

si el código de resultado == nulo

Petar Ceho
fuente
No es necesario que llames readLine()dos veces. Ya sabes que era nulo en el elsebloque.
Marqués de Lorne
-4

En Linux, cuando se escribe () en un socket que el otro lado, desconocido para usted, cerrado provocará una señal / excepción SIGPIPE como quiera llamarlo. Sin embargo, si no desea que SIGPIPE lo atrape, puede usar send () con el indicador MSG_NOSIGNAL. La llamada send () regresará con -1 y en este caso puede verificar errno que le dirá que intentó escribir una tubería rota (en este caso un socket) con el valor EPIPE que según errno.h es equivalente a 32. Como reacción al EPIPE, podría retroceder e intentar reabrir el enchufe e intentar enviar su información nuevamente.

MikeK
fuente
La send()llamada devolverá -1 solo si los datos salientes se almacenaron en búfer durante el tiempo suficiente para que expiren los temporizadores de envío. Es casi seguro que no sucederá en el primer envío después de la desconexión, debido al almacenamiento en búfer en ambos extremos y la naturaleza asincrónica de send()debajo del capó.
Marqués de Lorne