DataInputStream.read() "hangs"

Hola comunidad, me encuentro aquí para ver si me pueden aclarar unas cosas sobre el método read(byte b[], int off, int len) de DataInputStream ya que en un escenario multithread usando un ThreadPool, de un Server Socket en el que cada petición se atiende en un thread diferente, utilizo este método y llega a un punto en que se cuelga y no continua.

Esto sucede cuando el cliente corre una aplicación "Antivirus" que se llama Qualys que prácticamente se encarga de bombardear puertos para detectar fallas de seguridad.

Sé de antemano que el método read() se bloquea mientras está leyendo hasta que no exista más que leer, se llegue a el fin de archivo (EOF) o se tire una excepción, pero pareciera que ninguna de estas se está cumpliendo porque el método no retorna nada y no hay excepciones. Cuando se hace el accept y se crea el socket se le asigna un timeout default de 2000 ms.

Además un daemon que tenemos corriendo que se encarga de colectar el estatus de los puertos, cuando este "antivirus" corre se empiezan a detectar muchos puertos (en especial en el que mi Server Socket está a la escucha) en estatus CLOSE_WAIT. Leyendo un poco sobre este estado y de lo que entendí es que en mi caso el cliente que hizo la petición cerro la conexión de su lado antes de que mi server socket contestara a la petición quedando en un estado zombie ya que mi socket no fue cerrado. Leí además sobre como evitar estos estados de close_wait y varios recomiendan usar el header "keep_alive" para forzar a ambos lados (cliente-servidor) a no cerrar la conexión hasta que sea atendida, pero no sé si esto pueda aplicar sobre una tool como lo es el antivirus que utiliza el cliente.

Lo que no sé y quisiera que me ayudaran a entender es ¿por qué el método read no continua?, ¿cómo puedo evitar o prevenir este comportamiento?

En un análisis más profundo tomamos un dump y revisé de forma general el heap y stack de la jvm y detecté que casi 3 hrs después de que el antivirus corriera, un thread con la clase ThreadPool (que no es la de Java) pero que atiende las peticiones del socket seguía en la heap en un estatus TIMED_WAITING, que leyendo sobre eso encontré que es un estatus para un thread de espera con un tiempo de espera definido, lo que se me hace un poco ilógico que teniendo un tiempo de caducidad, este tiempo jamás termine y no arroje una excepción.

¿Alguno de ustedes tiene alguna idea sobre el error o cómo corregirlo/prevenirlo?

Gracias de antemano :)

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de Marce

Olvidé mencionar que por las

Olvidé mencionar que por las circunstancias en las que corre el Server Socket no es posible monitorizarlo por medio de jconsole o alguna herramienta de ese tipo, por eso tenemos un daemon =/ sé que eso lo complica aún más, pero es lo que hay.

Imagen de ezamudio

no es thread-safe

Los streams no son thread-safe. Pero si entendí bien lo que dices, cada nuevo socket lo atiende un hilo nuevo (estás usando el esquema hilo-por-conexión). El timeout default de 2000ms que mencionas... te refieres a socket.setSoTimeout? Ese es el timeout de lectura; si invocas read (cualquier variante) y no se lee nada en 2s, se arrojará excepción. Qué haces cuando se arroja esa excepción? Cierras el socket? Cerrar el socket lo deberías hacer en un bloque finally de ser posible.

Tal vez estás haciendo una lectura en el socket ANTES de fijar el soTimeout; si esa lectura no recibe datos nunca pues nunca se va a arrojar excepción porque tiene un timeout infinito.

Los servers que he hecho en esa modalidad generalmente les pongo el timeout en el mismo hilo donde está el serverSocket.accept para asegurar que cuando ya se lo pase al procesador en un hilo separado, tenga un timeout.

Y si estás usando un thread pool para procesar esas conexiones entrantes, considera que pueden quedarse los procesadores encolados un rato, o que puede arrojarse una RejectedExecutionException si el thread pool está saturado. Y si se queda un procesador encolado un rato y el cliente está enviando datos, se puede llenar el buffer de tu lado y tal vez eso es lo que cause problemas...

Imagen de Marce

Gracias por revisar mi

Gracias por revisar mi problema @ezamudio ahora contesto tus preguntas:

El timeout default de 2000ms que mencionas... te refieres a socket.setSoTimeout? Si me refiero a socket.setSoTimeout, y está implementado como tú lo mencionas, en el mismo hilo donde se hizo el serverSocket.accept .

Qué haces cuando se arroja esa excepción? Cierras el socket? Cerrar el socket lo deberías hacer en un bloque finally de ser posible.
Tengo un throw IOException que delega la excepción a métodos más arriba, y en el punto que se supone es el punto final de la llamada a métodos ahí se cacha la excepción y se cierra la conexión no en un finally como dices (me disculpo por eso) pero se cierra. Revisando más el flujo de ejecución, parece que ya encontré el problema en un método intermedio antes de llegar al punto final donde está el catch que cierra la conexión porque alguien (no se quien) se le ocurrió poner un catch intermedio para poner un mensaje de error, y parece que ese catch se está quedando con la excepción delegada del read y no se está cerrando la conexión nunca, por lo cuál paso a tu segundo comentario acerca del ThreadPool con procesos encolados y el buffer lleno, que suenan muy lógicos si no se cierra la conexión nunca.

No estoy segura si la IOException que estoy delegando cubre la excepción que pueda arrojar un timeout u otro tipo de excepción, y si la lógica que comenté es correcta acerca de ese catch intermedio, pero empezaré por corregir eso. Lo recomendable sería tener un throws Exception en mi método o un throws SocketTimeoutException, o qué?

Mi idea es hacer un catch Exception en el método read del stream, para cerrar el flujo de datos ahí mismo donde leo, y tirar una IOException que vaya a través de los otros métodos, quitar el catch intermedio y dejar que vaya hasta el punto final y cerrar la conexión en un finally. ¿Sería esto un buen approach?

Imagen de ezamudio

Exception

no pongas throws Exception, es horrible. Casi tan malo como catch (Exception e). Con cachar IOException cubres todos los casos de algo que pase con el socket.

No sé bien decirte acerca del catch intermedio, habría ya que ver código pero pues ciertamente huele mal ese catch intermedio, en todo caso si lo dejas, y es solamente para imprimir algo, debería luego arrojar la misma excepción cachada (simplemente throw e dentro del mismo catch).

Imagen de Marce

Ok, gracias @ezamudio seguiré

Ok, gracias @ezamudio seguiré ese path esperando que todo salga bien, jamás usaré un catch (Exception e) y dejaré IOException, haré el trhow e dentro del catch intermedio. Lamento no poder poner código aquí pero no me está permitido hacerlo además de que sería algo complicado ya que son varias clases las involucradas en todo esto, eso hace difícil el rastreo de código.

Agrego un dato más, agregando trace extra para delimitar hasta donde se colgaba la aplicación llegue a que fue hasta el método read y después ya no hubo absolutamente ninguna línea escrita en el log, ninguna excepción y/o trace para esa petición hecha por el antivirus. Hasta 2 hrs después cuando intentan hacer peticiones normales (que no son con el antivirus) notan que el server socket no atiende peticiones.
¿Es posible que jamás se llegue a un timeout aunque haya sido definido? lo pregunto por este comportamiento que veo en el trace en que jamás sale del método read, por lo que no está pasando siquiera por ese "catch intermedio" que comenté, lo que me lleva a pensar en si el ThreadPool está saturadopor peticiones no cerradas y/o el buffer está lleno de igual manera por algún proceso encolado como comentas, la pregunta difícil de contestar sería ¿Por qué no arroja ninguna Excepción? ya sea del timeout, de EOF, o la que mencionas del ThreadPool =/

En un path normal sin que corran ese antivirus de popo, el trace muestra un flujo normal donde read retorna el arreglo de bytes esperado y la petición es atendida satisfactoriamente.

Imagen de ezamudio

aaaah

A ver entonces lo que se cuelga es el serverSocket?

Aunque parezca obvio, vamos a revisar... el patrón hilo-por-conexión es así:

1. Pones el ServerSocket a escuchar en un puerto
2. En un hilo entras a un ciclo donde recibes conexiones con accept
3. Le pasas el socket recibido a algún componente que corra dentro de un hilo separado (ya sea en un thread pool o en un thread dedicado)

algo así:

class Procesador implements Runnable {
  final Socket sock;
  Procesador(Socket socket) { sock=socket; }
  void run() {
    //trabajar con la conexión
    //cerrar el socket al final
  }
}

class Listener implements Runnable {
  ServerSocket server;
  void setup() {
    server = new ServerSocket(1234);
  }
  void run() {
    while (true) {
      Socket s = server.accept();
      new Thread(new Procesador(s)).start();
      //O si es threadpool
      threadPool.execute(new Procesador(s));
    }
  }

Dices que ya no acepta conexiones el server, o sea que ya no está el accept. Probablemente ocurrió una excepción en el accept y se terminó el ciclo y por eso ya no recibe nuevas conexiones...

Si están usando algún protocolo propietario, definan de entrada un handshake corto para desechar rápidamente conexiones inválidas. Por ejemplo que al recibir un socket nuevo le pongas un timeout muy bajo (ni 2s, ponle medio segundo) y leas 8 bytes por decir algo, que sea un saludo aunque sea siempre lo mismo. Si se arroja excepción en esa lectura, cierras el socket. Si lees menos de 8 bytes, cierras el socket. Si los 8 bytes no son los que esperabas, cierras el socket. Si pasó esa prueba entonces ya subes el timeout y comienza el procesamiento normal, tu ciclo de lectura etc.

Java Network Programming

 

Java Network Programming, 4th Edition

Java Network Programming, 4th Edition

▲ Para cualquiera que esté interesando en programación de sockets, este libro será de gran ayuda. Hay una vista previa en Google Books.

¡Por si sirve de algo!

~~~

Imagen de Marce

accept en 2 pasos

Hola, gracias por responder, han sido muy buenos todos tus comentarios Ü

Sobre el repaso del patrón hilo-por-conexión que mencionas, efectivamente está implementado como lo comentas, a excepción de una pequeña variante; sé que el código que pusiste es de ejemplo y no pretende cumplir una implementación completa, y sé a lo que te refieres con el protocolo para un handshake "filtro" que se deshaga de conexiones infructuosas, por lo que aquí te comento la variante en el accept.

Nuestro accept se hace en 2 pasos (si así pudiéramos llamarlo):
1) Accept Cliente ServerSocket.accept() dentro de un ciclo hasta que exista un socket
2) Accept Request Se obtiene el InputStream del socket y se lee la petición.

Nuestro método read funciona así:
- Creamos un DataInputStream a partir de un BufferedInputStream con el InputStream del socket.
- Hacemos una primer lectura con un readInt() que retorna los siguientes 4 bytes del input stream interpretados en un entero.
- Si el readInt retornó algo creamos un arreglo de bytes del tamaño leído, mientras len sea menos que off, comenzamos a leer con read(byte b[], int off, int len)

Si la petición tiene datos para procesar se regresa el arreglo de bytes y ahora si se pasa el request a un objeto en el ThreadPool para que sea procesado por otro hilo, en ese otro hilo se aplica el protocolo propietario que en realidad es muy corto, y si el request del cliente no presenta el protocolo hasta ese punto se rechaza.

Veo un problema potencial en primero leer toda la petición y después aplicar el protocolo, podría modificar eso a la forma en que comentas y hacer un primer filtro con el timeout y el protocolo, de los primero 4 u 8 bytes, de esa forma mensajes como los del antivirus deberían ser descartados de manera inmediata. Me parece una buena opción lo de reducir el timeout para que el handshake sea más rápido.

Intentaré estas buenas sugerencias y comento cualquier resultado. Gracias de antemano.

Imagen de Marce

Gracias

Gracias @jpaul lo revisaré para ayudarme en esto y en futuras implementaciones Ü

Imagen de ezamudio

DataInputStream

No validan la longitud del primer entero que les llega? Qué tal si dice 2GB? Creas un arreglo de 2GB? Porque si solamente lees 4 bytes y los interpretas como un entero, pues el antivirus manda algo (lo que sea) y se puede interpretar como un entero. Pero si manda solamente un byte por ejemplo, puede ser que el DataInputStream se quede esperando más datos para formar un entero porque es lo que le pediste.

Suena como que el DataInputStream solamente lo usas para leer ese entero del principio. Si es así, te recomiendo quitarlo y leer directamente 4 bytes aunque sea un poco más engorroso:

byte[] hlen = new byte[4];
long leidos = bufferedInputStream.read(hlen);
if (leidos != 4) cierraLaConexionYOlvidate();
leidos = ((hlen[0] & 0xff) << 24) | ((hlen[1] & 0xff) << 16) | ((hlen[2] & 0xff) << 8) | (hlen[3] & 0xff);
if (leidos > algunTamañoRazonableParaLoQueEsperasRecibir) cierraLaConexionYOlvidate();
byte[] datos = new byte[(int)leidos];
//y te sigues ya con la lectura y procesamiento
Imagen de Marce

Perfecto

Perfecto muchas gracias :D todo esto pinta bien