¡Una visita obligada para los amantes de Netty! ¡Este artículo explica la familia ChannelHandler en detalle para ayudarlo a dominar rápidamente las habilidades de desarrollo de Netty!

1 Ciclo de vida de la interfaz del canal

Channel define un modelo de estado simple pero poderoso estrechamente relacionado con la API ChannelInboundHandler

1.1 Estado del canal

estado describir
CanalNo registrado El canal se ha creado, pero aún no se ha registrado en EventLoop
CanalRegistrado El canal se ha registrado en EventLoop
CanalActivo El canal está activo (conectado a sus nodos remotos). Ahora puede recibir y enviar datos.
CanalInactivo El canal no está conectado al nodo remoto

1.1.1 Modelo de estado del canal

El ciclo de vida normal de un canal se muestra en la siguiente figura. Cuando estos estados cambien, se generarán los eventos correspondientes.

Estos eventos se reenviarán al ChannelHandler en ChannelPipeline, que luego puede responder a ellos.

2 El ciclo de vida de ChannelHandler

Operaciones de ciclo de vida de la interfaz ChannelHandler que se llaman cuando se agrega o elimina un ChannelHandler de ChannelPipeline. Cada uno de estos métodos acepta un parámetro ChannelHandlerContext.

tipo describir
handlerAdded Llamado cuando ChannelHandler se agrega a ChannelPipeline
controladorEliminado Llamado cuando ChannelHandler se elimina de ChannelPipeline
excepciónAtrapado Llamado cuando ocurre un error en ChannelPipeline durante el procesamiento

Netty define las siguientes subinterfaces ChannelHandler:

  • ChannelInboundHandler, maneja los datos entrantes y varios cambios de estado
  • ChannelOutboundHandler, maneja los datos salientes y permite la interceptación de todas las operaciones

Controlador de entrada de 3 canales

El ciclo de vida de la interfaz ChannelInboundHandler.

3.1 Cuándo ser llamado

  • cuando se reciben los datos
  • o cuando cambia el estado del Canal correspondiente

Estos métodos están fuertemente relacionados con el ciclo de vida del canal.

① Cuando se hayan leído todos los bytes legibles del canal, se llamará al método de devolución de llamada; por lo tanto, puede estar en channelRead

Ve varias llamadas a channelRead(...) antes de llamar a Complete().

Cuando una implementación de ChannelInboundHandler anula el método channelRead(), será responsable de liberar explícitamente la memoria asociada con la instancia de ByteBuf agrupada. Netty proporciona ReferenceCountUtil.release() para esto

代码清单 6-1

@Sharable
// 扩展了 ChannelInboundHandlerAdapter
public class DiscardHandler extends ChannelInboundHandlerAdapter {
    
    
  @Override
  public void channelRead(ChannelHandlerContext ctx, Object msg) {
    
    
    // 丢弃已接收的消息
    ReferenceCountUtil.release(msg);
  }
}

Netty registrará recursos inéditos con mensajes de registro de nivel WARN, lo que facilita la detección de instancias de violaciones en su código. Pero administrar los recursos de esta manera puede ser engorroso. Más fácil de usar SimpleChannelInboundHandler. El Listado 6-2 es una variación del Listado 6-1 que ilustra este punto:

代码清单 6-2

@Sharable
public class SimpleDiscardHandler extends SimpleChannelInboundHandler<Object> {
    
    
  @Override
  public void channelRead0(ChannelHandlerContext ctx, Object msg) {
    
    
    // No need to do anything special
  }
}

SimpleChannelInboundHandler liberará recursos automáticamente, por lo que no debe almacenar referencias a ningún mensaje para uso futuro, ya que estas referencias se invalidarán.

La Sección 6.1.6 proporciona una discusión más detallada del manejo de referencias.

Controlador de salida de 4 canales

Las operaciones y los datos salientes son manejados por ChannelOutboundHandler. Sus métodos son llamados por Channel, ChannelPipeline y ChannelHandlerContext.

4.1 Posponer operaciones o eventos a pedido

Una característica poderosa de ChannelOutboundHandler, que hace posible manejar solicitudes de formas complejas. Si se suspenden las escrituras en los nodos remotos, puede diferir la operación de vaciado y reanudarla más tarde.

Sí, ChannelOutboundHandler en Netty tiene la capacidad de diferir operaciones o eventos. Esto generalmente se logra a través de los métodos de escritura y descarga de ChannelHandlerContext.

ejemplo

Muestre cómo usar ChannelOutboundHandler para posponer la operación de vaciado:

public class MyOutboundHandler extends ChannelOutboundHandlerAdapter {
    
    
    private boolean isWritePending = false;

  	// 当write方法被调用时,它将isWritePending标记设置为true,并调用ctx.write
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    
    
        isWritePending = true;
        ctx.write(msg, promise);
    }

  	// 当flush方法被调用时,如果isWritePending标记为true,则将它设置为false,并调用ctx.flush
    @Override
    public void flush(ChannelHandlerContext ctx) throws Exception {
    
    
        if (isWritePending) {
    
    
            isWritePending = false;
            ctx.flush();
        }
    }
}

Se puede modificar según sea necesario para operaciones más complejas.

4.2 API ChannelOutboundHandler

La Tabla 6-4 muestra todos los métodos definidos por ChannelOutboundHandler (ignorando los heredados de ChannelHandler):

4.3 ChannelPromise VS ChannelFuture

La mayoría de los métodos en ChannelOutboundHandler toman un parámetro ChannelPromise para recibir una notificación cuando se completa la operación. ChannelPromise es una subclase de ChannelFuture, que define algunos métodos de escritura, como setSuccess() y setFailure(), lo que hace que ChannelFuture sea inmutable. La referencia aquí es el diseño de Promesa y Futuro en Scala, cuando una Promesa se cumple, el valor del Futuro correspondiente no se puede modificar de ninguna manera.

Adaptador de controlador de 5 canales

5.1 Importancia

Clase que simplifica la tarea de escribir ChannelHandlers.

Puede usar las clases ChannelInboundHandlerAdapter y ChannelOutboundHandlerAdapter como punto de partida para su propio ChannelHandler. Estos dos adaptadores proporcionan la implementación básica de ChannelInboundHandler y ChannelOutboundHandler respectivamente. Al extender la clase abstracta ChannelHandlerAdapter, obtienen los métodos de la interfaz principal común ChannelHandler.

Figura 6-2 La jerarquía de la clase ChannelHandlerAdapter:

ChannelHandlerAdapter también proporciona el método de utilidad isSharable(). Si su implementación correspondiente está marcada como Compartible, este método devuelve verdadero, lo que indica que se puede agregar a múltiples ChannelPipelines (como se explica en la Sección 2.3.1).

El cuerpo del método proporcionado por ChannelInboundHandlerAdapter y ChannelOutboundHandlerAdapter invoca el método equivalente en su ChannelHandlerContext asociado para reenviar el evento al siguiente ChannelHandler en ChannelPipeline.

Para usar estas clases de adaptadores en su propio ChannelHandler, simplemente extienda y anule los métodos que requieren implementaciones personalizadas.

6 Gestión de recursos

Siempre que llame a los siguientes métodos para procesar datos, debe asegurarse de que no haya fugas de recursos:

  • ChannelInboundHandler.channelRead()
  • 或ChannelOutboundHandler.write()

Netty usa el conteo de referencias para ByteBufs agrupados. Entonces, después de que un ByteBuf se usa por completo, es muy importante ajustar su recuento de referencia. Para ayudarlo a diagnosticar problemas potenciales (fugas de recursos), Netty proporciona la clase ResourceLeakDetector, que muestrea aproximadamente el 1 % de las asignaciones de búfer de su aplicación para detectar fugas de memoria. La sobrecarga asociada es muy pequeña. Si se detecta una pérdida de memoria, se generará un mensaje de registro similar a este:

LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option
'-Dio.netty.leakDetectionLevel=ADVANCED' or call
ResourceLeakDetector.setLevel().

6.1 Nivel de detección de fugas

Netty actualmente define 4 niveles de detección de fugas, como se muestra en la Tabla 6-5:

tipo describir
DESACTIVADO Deshabilite la detección de fugas. Solo debe establecerse en este valor después de pruebas exhaustivas.
SIMPLE Detecta e informa cualquier fuga encontrada utilizando una tasa de muestreo predeterminada del 1%. Este es el nivel predeterminado, adecuado para la mayoría de las situaciones
AVANZADO Usando la tasa de muestreo predeterminada, informe cualquier fuga encontrada y dónde se accedió a los mensajes correspondientes
PARANOICO Similar a AVANZADO, pero probará cada acceso (a un mensaje). Esto tendrá un gran impacto en el rendimiento y solo debe usarse durante la fase de depuración.

El nivel de detección de fugas se puede definir estableciendo la siguiente propiedad del sistema Java en un valor de la tabla:

java -Dio.netty.leakDetectionLevel=ADVANCED

Reinicie la aplicación con esta opción de JVM y verá dónde se accedió al búfer filtrado más recientemente de la aplicación. El siguiente es un informe de fuga típico generado por una prueba unitaria:

Running io.netty.handler.codec.xml.XmlFrameDecoderTest

15:03:36.886 [main] ERROR io.netty.util.ResourceLeakDetector - LEAK: ByteBuf.release() was not called before it's garbage-collected.

Recent access records: 1

\#1: io.netty.buffer.AdvancedLeakAwareByteBuf.toString( AdvancedLeakAwareByteBuf.java:697)

io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml( XmlFrameDecoderTest.java:157)

io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages( XmlFrameDecoderTest.java:133)

...

¿Cómo utilizar esta herramienta de diagnóstico para evitar fugas al implementar los métodos ChannelInboundHandler.channelRead(), ChannelOutboundHandler.write()? Vea si su operación channelRead() consume el mensaje entrante directamente, es decir, no reenvía el mensaje entrante al siguiente ChannelInboundHandler llamando al método ChannelHandlerContext.fireChannelRead(). El Listado 6-3 muestra cómo liberar el mensaje:

@Sharable
public class DiscardInboundHandler extends ChannelInboundHandlerAdapter {
    
    
  @Override
  public void channelRead(ChannelHandlerContext ctx, Object msg) {
    
    
    // 释放资源
    ReferenceCountUtil.release(msg);
  }
}

6.2 Manera simple de consumir mensajes entrantes

El consumo de datos entrantes es una tarea rutinaria, por lo que Netty proporciona una implementación ChannelInboundHandler especial de SimpleChannelInboundHandler. La implementación liberará automáticamente el mensaje después de que el método channelRead0() consuma el mensaje.

El consumo de datos entrantes se refiere al procesamiento de datos de red recibidos en la aplicación Netty. Cuando el cliente envía datos al servidor, el servidor recibe y lee los datos. Estos datos son datos entrantes porque fluyen hacia el servidor desde la red externa.

Los datos entrantes en Netty generalmente los maneja ChannelInboundHandler. Estos controladores son responsables de decodificar los datos recibidos, convertirlos a un formato que la aplicación comprenda y pasarlos al siguiente controlador o a la propia aplicación.

Pasos de consumo para datos entrantes

  1. Leer datos: use ChannelHandlerContext#read para leer datos de la red
  2. Decodificación de datos: use ChannelInboundHandlerAdapter#channelRead para decodificar los datos leídos
  3. Procesar datos: use controladores de lógica empresarial para procesar datos decodificados
  4. Pasar datos: use ChannelHandlerContext#fireChannelRead para pasar datos procesados ​​al siguiente controlador o a la propia aplicación

En la dirección de salida, si maneja la operación write() y descarta un mensaje, también debe ser responsable de liberarlo. El listado 6-4 muestra una implementación que descarta todos los datos escritos.

@Sharable
public class DiscardOutboundHandler extends ChannelOutboundHandlerAdapter {
    
    
  
  @Override
  public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
    
    
    // 使用 ReferenceCountUtil.realse(...)释放资源
    ReferenceCountUtil.release(msg);
    promise.setSuccess();
  }
}

No solo para liberar recursos, sino también para notificar a ChannelPromise. De lo contrario, puede ocurrir que ChannelFutureListener no pueda recibir la notificación de que se ha procesado un determinado mensaje.

En resumen, si un mensaje se consume o se descarta y no se entrega al siguiente en ChannelPipeline

ChannelOutboundHandler, es responsabilidad del usuario llamar a ReferenceCountUtil.release(). Si el mensaje llega a la capa de transporte real, se liberará automáticamente cuando se escriba o cuando se cierre el Canal.

Supongo que te gusta

Origin blog.csdn.net/qq_33589510/article/details/130661484
Recomendado
Clasificación