Manejo sencillo de los tiempos de espera de la red

Muchos programadores temen la idea de manejar los tiempos de espera de la red. Un temor común es que un cliente de red simple de un solo subproceso sin soporte de tiempo de espera se convertirá en una pesadilla compleja de subprocesos múltiples, con subprocesos separados necesarios para detectar tiempos de espera de red y algún tipo de proceso de notificación en funcionamiento entre el subproceso bloqueado y la aplicación principal. Si bien esta es una opción para los desarrolladores, no es la única. Lidiar con los tiempos de espera de la red no tiene por qué ser una tarea difícil y, en muchos casos, puede evitar por completo escribir código para hilos adicionales.

Cuando se trabaja con conexiones de red, o cualquier tipo de dispositivo de E / S, existen dos clasificaciones de operaciones:

  • Operaciones de bloqueo : lectura o escritura se detiene, la operación espera hasta que el dispositivo de E / S esté listo
  • Operaciones sin bloqueo : se realiza un intento de lectura o escritura, la operación se cancela si el dispositivo de E / S no está listo

La red Java es, por defecto, una forma de bloqueo de E / S. Por lo tanto, cuando una aplicación de red Java lee desde una conexión de socket, generalmente esperará indefinidamente si no hay una respuesta inmediata. Si no hay datos disponibles, el programa seguirá esperando y no se podrá realizar más trabajo. Una solución, que resuelve el problema pero introduce un poco de complejidad adicional, es que un segundo subproceso realice la operación; de esta manera, si el segundo hilo se bloquea, la aplicación aún puede responder a los comandos del usuario, o incluso terminar el hilo detenido si es necesario.

Esta solución se emplea a menudo, pero existe una alternativa mucho más sencilla. Java también soporta sin bloqueo de red I / O, que puede ser activado en cualquier Socket, ServerSocketo DatagramSocket. Es posible especificar el tiempo máximo que se detendrá una operación de lectura o escritura antes de devolver el control a la aplicación. Para los clientes de red, esta es la solución más sencilla y ofrece un código más sencillo y manejable.

El único inconveniente de la E / S de red sin bloqueo en Java es que requiere un socket existente. Por lo tanto, si bien este método es perfecto para operaciones normales de lectura o escritura, una operación de conexión puede detenerse durante un período mucho más largo, ya que no existe un método para especificar un período de tiempo de espera para las operaciones de conexión. Muchas aplicaciones requieren esta capacidad; Sin embargo, puede evitar fácilmente el trabajo adicional de escribir código adicional. Escribí una pequeña clase que le permite especificar un valor de tiempo de espera para una conexión. Utiliza un segundo hilo, pero los detalles internos se abstraen. Este enfoque funciona bien, ya que proporciona una interfaz de E / S sin bloqueo y los detalles del segundo hilo están ocultos a la vista.

E / S de red sin bloqueo

La forma más sencilla de hacer algo a menudo resulta ser la mejor. Si bien a veces es necesario utilizar subprocesos y bloquear E / S, en la mayoría de los casos, las E / S sin bloqueo se prestan a una solución mucho más clara y elegante. Con solo unas pocas líneas de código, puede incorporar soportes de tiempo de espera para cualquier aplicación de socket. ¿No me crees? Sigue leyendo.

Cuando se lanzó Java 1.1, incluyó cambios de API en el java.netpaquete que permitieron a los programadores especificar opciones de socket. Estas opciones dan a los programadores un mayor control sobre la comunicación por socket. Una opción en particular SO_TIMEOUT,, es extremadamente útil, porque permite a los programadores especificar la cantidad de tiempo que se bloqueará una operación de lectura. Podemos especificar una demora breve, o ninguna, y hacer que nuestro código de red no se bloquee.

Echemos un vistazo a cómo funciona esto. Se setSoTimeout ( int )ha agregado un nuevo método a las siguientes clases de conectores:

  • java.net.Socket
  • java.net.DatagramSocket
  • java.net.ServerSocket

Este método nos permite especificar una duración máxima del tiempo de espera, en milisegundos, que bloquearán las siguientes operaciones de red:

  • ServerSocket.accept()
  • SocketInputStream.read()
  • DatagramSocket.receive()

Siempre que se llama a uno de estos métodos, el reloj comienza a correr. Si la operación no está bloqueada, se restablecerá y solo se reiniciará una vez que se vuelva a llamar a uno de estos métodos; como resultado, no se puede producir ningún tiempo de espera a menos que realice una operación de E / S de red. El siguiente ejemplo muestra lo fácil que puede ser manejar los tiempos de espera sin tener que recurrir a múltiples subprocesos de ejecución:

// Cree un conector de datagramas en el puerto 2000 para escuchar los paquetes UDP entrantes DatagramSocket dgramSocket = new DatagramSocket (2000); // Desactive el bloqueo de operaciones de E / S, especificando un tiempo de espera de cinco segundos dgramSocket.setSoTimeout (5000);

Asignar un valor de tiempo de espera evita que las operaciones de nuestra red se bloqueen indefinidamente. En este punto, probablemente se esté preguntando qué sucederá cuando se agote el tiempo de operación de una red. En lugar de devolver un código de error, que los desarrolladores no siempre pueden comprobar, java.io.InterruptedIOExceptionse lanza un. El manejo de excepciones es una forma excelente de lidiar con las condiciones de error y nos permite separar nuestro código normal de nuestro código de manejo de errores. Además, ¿quién verifica religiosamente cada valor de retorno para una referencia nula? Al lanzar una excepción, los desarrolladores se ven obligados a proporcionar un controlador de captura para los tiempos de espera.

El siguiente fragmento de código muestra cómo manejar una operación de tiempo de espera cuando se lee desde un socket TCP:

// Establece el tiempo de espera del socket en diez segundos connection.setSoTimeout (10000); try {// Crea un DataInputStream para leer desde el socket DataInputStream din = new DataInputStream (connection.getInputStream ()); // Leer datos hasta el final de los datos para (;;) {String line = din.readLine (); if (línea! = nulo) System.out.println (línea); más romper; }} // Se lanza una excepción cuando se agota el tiempo de espera de la red catch (InterruptedIOException iioe) {System.err.println ("El host remoto agotó el tiempo de espera durante la operación de lectura"); } // Se lanza una excepción cuando se produce un error de E / S de red general catch (IOException ioe) {System.err.println ("Error de E / S de red -" + ioe); }

Con solo unas pocas líneas adicionales de código para un try {}bloque de captura, es extremadamente fácil capturar los tiempos de espera de la red. Entonces, una aplicación puede responder a la situación sin detenerse. Por ejemplo, podría comenzar notificando al usuario o intentando establecer una nueva conexión. Cuando se utilizan sockets de datagramas, que envían paquetes de información sin garantizar la entrega, una aplicación podría responder a un tiempo de espera de la red reenviando un paquete que se había perdido en tránsito. La implementación de este soporte de tiempo de espera lleva muy poco tiempo y conduce a una solución muy limpia. De hecho, la única vez que la E / S sin bloqueo no es la solución óptima es cuando también necesita detectar tiempos de espera en las operaciones de conexión o cuando su entorno de destino no es compatible con Java 1.1.

Manejo del tiempo de espera en operaciones de conexión

Si su objetivo es lograr la detección y el manejo completos del tiempo de espera, deberá considerar las operaciones de conexión. Al crear una instancia de java.net.Socket, se intenta establecer una conexión. Si la máquina host está activa, pero no se está ejecutando ningún servicio en el puerto especificado en el java.net.Socketconstructor, se ConnectionExceptionlanzará un y el control volverá a la aplicación. Sin embargo, si la máquina está inactiva, o si no hay una ruta a ese host, la conexión del socket eventualmente se agotará por sí sola mucho más tarde. Mientras tanto, su aplicación permanece congelada y no hay forma de cambiar el valor de tiempo de espera.

Aunque la llamada al constructor del socket eventualmente regresará, introduce un retraso significativo. Una forma de lidiar con este problema es emplear un segundo hilo, que realizará la conexión potencialmente bloqueante, y sondear continuamente ese hilo para ver si se ha establecido una conexión.

Sin embargo, esto no siempre conduce a una solución elegante. Sí, puede convertir sus clientes de red en aplicaciones multiproceso, pero a menudo la cantidad de trabajo adicional requerido para hacer esto es prohibitiva. Hace que el código sea más complejo, y cuando se escribe solo una aplicación de red simple, la cantidad de esfuerzo requerido es difícil de justificar. Si escribe muchas aplicaciones de red, se encontrará reinventando la rueda con frecuencia. Sin embargo, existe una solución más sencilla.

Escribí una clase simple y reutilizable que puede usar en sus propias aplicaciones. La clase genera una conexión de socket TCP sin detenerse durante largos períodos de tiempo. Simplemente llama a un getSocketmétodo, especifica el nombre de host, el puerto y el tiempo de espera, y recibe un socket. El siguiente ejemplo muestra una solicitud de conexión:

// Conéctese a un servidor remoto por nombre de host, con un tiempo de espera de cuatro segundos Socket connection = TimedSocket.getSocket ("server.my-network.net", 23, 4000); 

Si todo va bien, se devolverá un socket, al igual que los java.net.Socketconstructores estándar . Si la conexión no se puede establecer antes de que ocurra el tiempo de espera especificado, el método se detendrá y lanzará un java.io.InterruptedIOException, tal como lo harían otras operaciones de lectura de socket cuando se haya especificado un tiempo de espera mediante un setSoTimeoutmétodo. Bastante fácil, ¿eh?

Encapsulating multithreaded network code into a single class

While the TimedSocket class is a useful component in itself, it's also a very good learning aid for understanding how to deal with blocking I/O. When a blocking operation is performed, a single-threaded application will become blocked indefinitely. If multiple threads of execution are used, however, only one thread need stall; the other thread can continue to execute. Let's take a look at how the TimedSocket class works.

When an application needs to connect to a remote server, it invokes the TimedSocket.getSocket() method and passes details of the remote host and port. The getSocket() method is overloaded, allowing both a String hostname and an InetAddress to be specified. This range of parameters should be sufficient for the majority of socket operations, though custom overloading could be added for special implementations. Inside the getSocket() method, a second thread is created.

The imaginatively named SocketThread will create an instance of java.net.Socket, which can potentially block for a considerable amount of time. It provides accessor methods to determine if a connection has been established or if an error has occurred (for example, if java.net.SocketException was thrown during the connect).

While the connection is being established, the primary thread waits until a connection is established, for an error to occur, or for a network timeout. Every hundred milliseconds, a check is made to see if the second thread has achieved a connection. If this check fails, a second check must be made to determine whether an error occurred in the connection. If not, and the connection attempt is still continuing, a timer is incremented and, after a small sleep, the connection will be polled again.

This method makes heavy use of exception handling. If an error occurs, then this exception will be read from the SocketThread instance, and it will be thrown again. If a network timeout occurs, the method will throw a java.io.InterruptedIOException.

The following code snippet shows the polling mechanism and error-handling code.

for (;;) { // Check to see if a connection is established if (st.isConnected()) { // Yes ... assign to sock variable, and break out of loop sock = st.getSocket(); break; } else { // Check to see if an error occurred if (st.isError()) { // No connection could be established throw (st.getException()); } try { // Sleep for a short period of time Thread.sleep ( POLL_DELAY ); } catch (InterruptedException ie) {} // Increment timer timer += POLL_DELAY; // Check to see if time limit exceeded if (timer > delay) { // Can't connect to server throw new InterruptedIOException ("Could not connect for " + delay + " milliseconds"); } } } 

Inside the blocked thread

While the connection is regularly polled, the second thread attempts to create a new instance of java.net.Socket. Accessor methods are provided to determine the state of the connection, as well as to get the final socket connection. The SocketThread.isConnected() method returns a boolean value to indicate whether a connection has been established, and the SocketThread.getSocket() method returns a Socket. Similar methods are provided to determine if an error has occurred, and to access the exception that was caught.

Todos estos métodos proporcionan una interfaz controlada a la SocketThreadinstancia, sin permitir la modificación externa de las variables de miembros privados. El siguiente ejemplo de código muestra el run()método del hilo . Cuando, y si, el constructor de socket devuelve un Socket, se asignará a una variable miembro privada, a la que los métodos de acceso proporcionan acceso. La próxima vez que se consulte un estado de conexión, utilizando el SocketThread.isConnected()método, el socket estará disponible para su uso. Se utiliza la misma técnica para detectar errores; si java.io.IOExceptionse detecta un, se almacenará en un miembro privado, al que se puede acceder a través de los métodos de acceso isError()y getException().