E/S de sockets Java: bloqueo, no bloqueo y asíncrono

Foto de Evi Radauscher en Unsplash

Al describir E / S, los términos no bloqueante y asincrónico a menudo se usan indistintamente, pero hay una diferencia significativa entre ellos. En este artículo se describen las diferencias teóricas y prácticas entre las operaciones de E/S de sockets asíncronos y sin bloqueo en Java.

Los sockets son terminales para realizar comunicaciones bidireccionales mediante protocolos TCP y UDP. Las API de sockets Java son adaptadores para la funcionalidad correspondiente de los sistemas operativos. Comunicación de sockets en sistemas operativos compatibles con POSIX (Unix, Linux, Mac OS X, BSD, Solaris,AIX, etc.) es realizado por Berkeley sockets. La comunicación de sockets en Windows es realizada por Winsock que también se basa en sockets Berkeley con funcionalidad adicional para cumplir con el modelo de programación de Windows.

Las definiciones POSIX

En este artículo se utilizan definiciones simplificadas de la especificación POSIX.

Hilo bloqueado: un hilo que está esperando alguna condición antes de que pueda continuar la ejecución.

Bloqueo: una propiedad de un conector que hace que las llamadas a él esperen a que se realice la acción solicitada antes de regresar.

Sin bloqueo: una propiedad de un socket que hace que las llamadas a él regresen sin demora, cuando se detecta que la acción solicitada no se puede completar sin un retraso desconocido.

Operación de E/S síncrona: una operación de E/S que hace que el hilo solicitante se bloquee hasta que se complete la operación de E/S.

Operación de E/S asíncrona: una operación de E/S que por sí sola no causa que se bloquee el subproceso solicitante; esto implica que el subproceso y la operación de E/S pueden ejecutarse simultáneamente.

Por lo tanto, de acuerdo con la especificación POSIX, la diferencia entre los términos no bloqueante y asíncrono es obvia:

  • sin bloqueo: una propiedad de un conector que hace que las llamadas a él regresen sin demora
  • E/S asincrónicas: una propiedad en una operación de E/S (lectura o escritura) que se ejecuta simultáneamente con el subproceso solicitante

Modelos de E/S

Los siguientes modelos de E/S son los más comunes para los sistemas operativos compatibles con POSIX:

  • modelo de E/S de bloqueo
  • modelo de E/S sin bloqueo
  • Modelo de multiplexación de E/S
  • modelo de E/S impulsada por señal
  • modelo de E/S asíncrona

Modelo de E/S de bloqueo

En el modelo de E/S de bloqueo, la aplicación realiza una llamada al sistema de bloqueo hasta que los datos se reciben en el núcleo y se copian del espacio del núcleo al espacio del usuario.

el bloqueo de e/S modelo

bloqueo de e/S modelo

Pros:

  • El modelo de E/S más simple para implementar

Contras:

  • La aplicación está bloqueada

Modelo de E/S sin bloqueo

En el modelo de E / S sin bloqueo, la aplicación realiza una llamada al sistema que devuelve inmediatamente una de dos respuestas:

  • si la operación de E/S se puede completar de inmediato, se devuelven los datos
  • si la operación de E/S no se puede completar de inmediato, se devuelve un código de error que indica que la operación de E/S se bloquearía o que el dispositivo no está disponible temporalmente

Para completar la operación de E/S, la aplicación debe esperar (realizar llamadas repetidas al sistema) hasta que se complete.

sin bloqueo de e/S modelo

no-bloqueo de e/S modelo

Pros:

  • La aplicación no está bloqueada

Contras:

  • La aplicación debería esperar hasta su finalización, lo que causaría muchos cambios de contexto del kernel de usuario
  • Este modelo puede introducir latencia de E/S porque puede haber un espacio entre la disponibilidad de datos en el kernel y la lectura de datos por parte de la aplicación

Modelo de multiplexación de E/S

En el modelo de multiplexación de E/S-modelo de E/S de bloqueo con notificaciones de bloqueo), la aplicación realiza una llamada al sistema de selección de bloqueo para comenzar a monitorear la actividad en muchos descriptores. Para cada descriptor, es posible solicitar la notificación de su disponibilidad para ciertas operaciones de E / S (conexión, lectura o escritura, ocurrencia de errores, etc.).). Cuando la llamada al sistema select devuelve que al menos un descriptor está listo, la aplicación realiza una llamada sin bloqueo y copia los datos del espacio del núcleo al espacio de usuario.

I/O multiplexación modelo

I/O multiplexación modelo

Pros:

  • Es posible realizar operaciones de e/S de múltiples descriptores en un hilo

Contras:

  • La aplicación sigue bloqueada en la llamada al sistema seleccionada
  • No todos los sistemas operativos admiten este modelo de manera eficiente

Modelo de E/S con control de señal

En el modelo de E/S con control de señal, la aplicación realiza una llamada sin bloqueo y registra un controlador de señal. Cuando un descriptor está listo para una operación de E/S, se genera una señal para la aplicación. Luego, el manejador de señales copia los datos del espacio del núcleo al espacio de usuario.

modelo de E/S impulsada por señal

modelo de E/S impulsada por señal

Pros:

  • La aplicación no está bloqueada
  • Las señales pueden proporcionar un buen rendimiento

Contras:

  • No todos los sistemas operativos admiten señales

Modelo de E/S asíncrona

En el modelo de E/S asíncrona (también conocido como modelo de E/S superpuestas), la aplicación realiza la llamada sin bloqueo e inicia una operación en segundo plano en el núcleo. Cuando se completa la operación (los datos se reciben en el núcleo y se copian del espacio del núcleo al espacio del usuario), se genera una devolución de llamada de finalización para finalizar la operación de E/S.

Una diferencia entre el modelo de E/S asíncrona y el modelo de E/S accionada por señal es que con E/S accionadas por señal, el núcleo le dice a la aplicación cuando se puede iniciar una operación de E/S, pero con el modelo de E/S asíncrona, el núcleo le dice a la aplicación cuando se completa una operación de E/S.

modelo de E/S asíncrona

modelo de E/S asíncrona

Pros:

  • La aplicación no está bloqueada
  • Este modelo puede proporcionar el mejor rendimiento

Contras:

  • El modelo de E/S más complicado de implementar
  • No todos los sistemas operativos admiten este modelo de manera eficiente

API de E/S de Java

La API de E / S de Java se basa en flujos (Flujo de entrada, flujo de salida) que representan un flujo de datos unidireccional de bloqueo.

API Java NIO

La API Java NIO se basa en las clases de Canal, Búfer y Selector, que son adaptadores para operaciones de E/S de bajo nivel de sistemas operativos.

La clase de canal representa una conexión a una entidad (dispositivo de hardware, archivo, socket, componente de software, etc.) que es capaz de realizar operaciones de E/S (lectura o escritura).

En comparación con los flujos unidireccionales, los canales son bidireccionales.

La clase de búfer es un contenedor de datos de tamaño fijo con métodos adicionales para leer y escribir datos. Todos los datos del canal se manejan a través del Búfer, pero nunca directamente: todos los datos que se envían a un canal se escriben en un Búfer, todos los datos que se reciben de un canal se leen en un Búfer.

En comparación con los flujos, que están orientados a bytes, los canales están orientados a bloques. La E/S orientada a bytes es más simple, pero para algunas entidades de E/S puede ser bastante lenta. La E/S orientada a bloques puede ser mucho más rápida, pero es más complicada.

La clase Selector permite suscribirse a eventos de muchos objetos SelectableChannel registrados en una sola llamada. Cuando llegan eventos, un objeto Selector los envía a los controladores de eventos correspondientes.

API de Java NIO2

La API de Java NIO2 se basa en canales asíncronos (canal asíncrono, canal asíncrono, canal asíncrono, etc.) que admiten operaciones de E/S asíncronas (conexión, lectura o escritura, manejo de errores).

Los canales asíncronos proporcionan dos mecanismos para controlar las operaciones de E/S asíncronas. El primer mecanismo es devolver un java.útil.concurrente.Objeto futuro, que modela una operación pendiente y se puede usar para consultar el estado y obtener el resultado. El segundo mecanismo es pasar a la operación un java.nio.canal.Objeto de controlador de terminación, que define los métodos de controlador que se ejecutan después de que la operación se haya completado o haya fallado. Las API proporcionadas para ambos mecanismos son equivalentes.

Los canales asíncronos proporcionan una forma estándar de realizar operaciones asíncronas de forma independiente. Sin embargo, la cantidad que la API de Java sockets puede explotar las capacidades asíncronas nativas de un sistema operativo dependerá del soporte para esa plataforma.

Socket echo server

La mayoría de los modelos de E/S mencionados anteriormente se implementan aquí en servidores echo y clientes con API de sockets Java. Los servidores y clientes de echo funcionan con el siguiente algoritmo:

  1. un servidor escucha un socket en un puerto TCP registrado 7000
  2. un cliente se conecta desde un socket en un puerto TCP dinámico al socket del servidor
  3. el cliente lee una cadena de entrada de la consola y envía los bytes de su socket al socket del servidor
  4. el servidor recibe los bytes de su socket y los envía de vuelta al socket del cliente
  5. el cliente recibe los bytes de su socket y escribe la cadena de eco en la consola
  6. cuando el cliente recibe el mismo número de bytes que ha enviado, se desconecta del servidor
  7. cuando el servidor recibe una cadena especial, deja de escuchar

La conversión entre cadenas y bytes aquí se realiza explícitamente en codificación UTF-8.

Además, solo se proporcionan códigos simplificados para servidores echo. El enlace a los códigos completos para los servidores y clientes de echo se proporciona en la conclusión.

Servidor de E / S de bloqueo

En el siguiente ejemplo, el modelo de E/S de bloqueo se implementa en un servidor echo con API de E / S de Java.

El ServerSocket.aceptar bloques de método hasta que se acepte una conexión. El InputStream.el método de lectura se bloquea hasta que los datos de entrada estén disponibles o se desconecte un cliente. El OutputStream.escribir bloques de método hasta que se escriban todos los datos de salida.

public class IoEchoServer { public static void main(String args) throws IOException {
ServerSocket serverSocket = new ServerSocket(7000); while (active) {
Socket socket = serverSocket.accept(); // blocking InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream(); int read;
byte bytes = new byte;
while ((read = is.read(bytes)) != -1) { // blocking
os.write(bytes, 0, read); // blocking
} socket.close();
} serverSocket.close();
}
}

Bloquear servidor echo NIO

En el siguiente ejemplo, el modelo de E/S de bloqueo se implementa en un servidor echo con API NIO de Java.

Los objetos ServerSocketChannel y SocketChannel están configurados de forma predeterminada en el modo de bloqueo. El canal ServerSocket.el método accept bloquea y devuelve un objeto SocketChannel cuando se acepta una conexión. El ServerSocket.el método de lectura se bloquea hasta que los datos de entrada estén disponibles o se desconecte un cliente. El ServerSocket.escribir bloques de método hasta que se escriban todos los datos de salida.

public class NioBlockingEchoServer { public static void main(String args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress("localhost", 7000)); while (active) {
SocketChannel socketChannel = serverSocketChannel.accept(); // blocking ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int read = socketChannel.read(buffer); // blocking
if (read < 0) {
break;
} buffer.flip();
socketChannel.write(buffer); // blocking
} socketChannel.close();
} serverSocketChannel.close();
}
}

Servidor echo NIO sin bloqueo

En el siguiente ejemplo, el modelo de E/S sin bloqueo se implementa en un servidor echo con API NIO de Java.

Los objetos ServerSocketChannel y SocketChannel se configuran explícitamente en el modo sin bloqueo. El canal ServerSocket.el método accept no bloquea y devuelve null si aún no se acepta ninguna conexión o un objeto SocketChannel de lo contrario. El ServerSocket.read no bloquea y devuelve 0 si no hay datos disponibles o un número positivo de bytes leídos de otro modo. El ServerSocket.el método de escritura no se bloquea si hay espacio libre en el búfer de salida del socket.

public class NioNonBlockingEchoServer { public static void main(String args) throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(7000)); while (active) {
SocketChannel socketChannel = serverSocketChannel.accept(); // non-blocking
if (socketChannel != null) {
socketChannel.configureBlocking(false); ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int read = socketChannel.read(buffer); // non-blocking
if (read < 0) {
break;
} buffer.flip();
socketChannel.write(buffer); // can be non-blocking
} socketChannel.close();
}
} serverSocketChannel.close();
}
}

Servidor de multiplexación NIO echo

En el siguiente ejemplo, el modelo de E/S de multiplexación se implementa en una API Java NIO de servidor echo.

Durante la inicialización, varios objetos ServerSocketChannel, que están configurados en el modo sin bloqueo, se registran en el mismo objeto Selector con la tecla de selección.Argumento OP_ACCEPT para especificar que un evento de aceptación de conexión es interesante.

En el bucle principal, el Selector.seleccione bloques de métodos hasta que se produzca al menos uno de los eventos registrados. Luego el Selector.El método selectedKeys devuelve un conjunto de objetos SelectionKey para los que se han producido eventos. Iterando a través de los objetos SelectionKey, es posible determinar qué evento de E/S (conectar, aceptar, leer, escribir) ha ocurrido y qué objetos de sockets (ServerSocketChannel, SocketChannel) se han asociado con ese evento.

La indicación de una tecla de selección de que un canal está listo para alguna operación es una pista, no una garantía.

public class NioMultiplexingEchoServer { public static void main(String args) throws IOException {
final int ports = 8;
ServerSocketChannel serverSocketChannels = new ServerSocketChannel; Selector selector = Selector.open(); for (int p = 0; p < ports; p++) {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannels = serverSocketChannel;
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress("localhost", 7000 + p)); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} while (active) {
selector.select(); // blocking Iterator<SelectionKey> keysIterator = selector.selectedKeys().iterator();
while (keysIterator.hasNext()) {
SelectionKey key = keysIterator.next(); if (key.isAcceptable()) {
accept(selector, key);
}
if (key.isReadable()) {
keysIterator.remove();
read(selector, key);
}
if (key.isWritable()) {
keysIterator.remove();
write(key);
}
}
} for (ServerSocketChannel serverSocketChannel : serverSocketChannels) {
serverSocketChannel.close();
}
}
}

Cuando un objeto SelectionKey indica que ha ocurrido un evento de aceptación de conexión, se convierte en el ServerSocketChannel.aceptar llamada (que puede ser sin bloqueo) para aceptar la conexión. Después de eso, se configura un nuevo objeto SocketChannel en el modo sin bloqueo y se registra en el mismo objeto Selector con la tecla de selección.Argumento OP_READ para especificar que ahora un evento de lectura es interesante.

private static void accept(Selector selector, SelectionKey key) throws IOException {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverSocketChannel.accept(); // can be non-blocking
if (socketChannel != null) {
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
}

Cuando un objeto SelectionKey indica que ha ocurrido un evento de lectura, se convierte en el canal del calcetín.llamada de lectura (que puede ser sin bloqueo) para leer datos del objeto SocketChannel en un nuevo objeto ByteByffer. Después de eso, el objeto SocketChannel se registra en el mismo objeto Selector con la tecla de selección.Argumento OP_WRITE para especificar que ahora un evento de escritura es interesante. Además, este objeto ByteBuffer se utiliza durante el registro como un archivo adjunto.

private static void read(Selector selector, SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.read(buffer); // can be non-blocking buffer.flip();
socketChannel.register(selector, SelectionKey.OP_WRITE, buffer);
}

Cuando un objeto SelectionKeys indica que ha ocurrido un evento de escritura, se convierte en el canal de calcetines.llamada de escritura (que puede ser sin bloqueo) para escribir datos en el objeto SocketChannel desde el objeto ByteByffer, extraído de la tecla de selección.método de fijación. Después de eso, el canal de calcetines.la llamada de cloase cierra la conexión.

private static void write(SelectionKey key) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel(); ByteBuffer buffer = (ByteBuffer) key.attachment(); socketChannel.write(buffer); // can be non-blocking
socketChannel.close();
}

Después de cada lectura o escritura, el objeto SelectionKey se elimina del conjunto de objetos SelectionKey para evitar su reutilización. Pero el objeto SelectionKey para la aceptación de la conexión no se elimina para tener la capacidad de realizar la siguiente operación similar.

Servidor de eco NIO2 asíncrono

En el siguiente ejemplo, el modelo de E/S asíncrono se implementa en un servidor de eco con API Java NIO2. Las clases AsynchronousServerSocketChannel, AsynchronousSocketChannel aquí se utilizan con el mecanismo de controladores de finalización.

El canal asincrónico del servidor de la bolsa.el método accept inicia una operación de aceptación de conexión asíncrona.

public class Nio2CompletionHandlerEchoServer { public static void main(String args) throws IOException {
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(7000)); AcceptCompletionHandler acceptCompletionHandler = new AcceptCompletionHandler(serverSocketChannel);
serverSocketChannel.accept(null, acceptCompletionHandler); System.in.read();
}
}

Cuando se acepta una conexión (o la operación falla), se llama a la clase AcceptCompletionHandler, que por el canal Asynchronoussocket.leer (Destino de ByteBuffer, Un archivo adjunto,Controlador de terminación < Entero,? el método super A> handler) inicia una operación de lectura asincrónica desde el objeto Asincronoussocketchannel a un nuevo objeto ByteBuffer.

class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, Void> { private final AsynchronousServerSocketChannel serverSocketChannel; AcceptCompletionHandler(AsynchronousServerSocketChannel serverSocketChannel) {
this.serverSocketChannel = serverSocketChannel;
} @Override
public void completed(AsynchronousSocketChannel socketChannel, Void attachment) {
serverSocketChannel.accept(null, this); // non-blocking ByteBuffer buffer = ByteBuffer.allocate(1024);
ReadCompletionHandler readCompletionHandler = new ReadCompletionHandler(socketChannel, buffer);
socketChannel.read(buffer, null, readCompletionHandler); // non-blocking
} @Override
public void failed(Throwable t, Void attachment) {
// exception handling
}
}

Cuando la operación de lectura se completa (o falla), se llama a la clase ReadCompletionHandler, que por el canal Asynchronoussocket.write (fuente de ByteBuffer, Un adjunto,Controlador de terminación < Entero,? el método super A> handler) inicia una operación de escritura asincrónica en el objeto Asincronoussocketchannel desde el objeto ByteBuffer.

class ReadCompletionHandler implements CompletionHandler<Integer, Void> { private final AsynchronousSocketChannel socketChannel;
private final ByteBuffer buffer; ReadCompletionHandler(AsynchronousSocketChannel socketChannel, ByteBuffer buffer) {
this.socketChannel = socketChannel;
this.buffer = buffer;
} @Override
public void completed(Integer bytesRead, Void attachment) {
WriteCompletionHandler writeCompletionHandler = new WriteCompletionHandler(socketChannel);
buffer.flip();
socketChannel.write(buffer, null, writeCompletionHandler); // non-blocking
} @Override
public void failed(Throwable t, Void attachment) {
// exception handling
}
}

Cuando la operación de escritura se completa (o falla), se llama a la clase WriteCompletionHandler, que por el canal Asynchronoussocket.el método close cierra la conexión.

class WriteCompletionHandler implements CompletionHandler<Integer, Void> { private final AsynchronousSocketChannel socketChannel; WriteCompletionHandler(AsynchronousSocketChannel socketChannel) {
this.socketChannel = socketChannel;
} @Override
public void completed(Integer bytesWritten, Void attachment) {
try {
socketChannel.close();
} catch (IOException e) {
// exception handling
}
} @Override
public void failed(Throwable t, Void attachment) {
// exception handling
}
}

En este ejemplo, las operaciones de E/S asincrónicas se realizan sin adjuntos, porque todos los objetos necesarios (AsynchronousSocketChannel, ByteBuffer) se pasan como argumentos de constructor para los controladores de finalización apropiados.

Conclusión

La elección del modelo de E/S para la comunicación de sockets depende de los parámetros del tráfico. Si las solicitudes de E/S son largas e infrecuentes, las E / S asíncronas generalmente son una buena opción. Sin embargo, si las solicitudes de E/S son cortas y rápidas, la sobrecarga de procesamiento de llamadas al núcleo puede hacer que las E/S síncronas sean mucho mejores.

A pesar de que Java proporciona una forma estándar de realizar E/S de sockets en los diferentes sistemas operativos, el rendimiento real puede variar significativamente dependiendo de su implementación. Es posible comenzar a estudiar estas diferencias con el conocido artículo de Dan Kegel The C10K problem.

Los ejemplos de código completos están disponibles en el repositorio de GitHub.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.