Mecanismos de Codificación y Decodificación en Netty

El Proceso de Lectura de Datos

Cuando el motor de eventos de Netty (NioEventLoop) detecta que hay datos listos en el socket del cliente, se activa el método read() dentro de la implementación NioByteUnsafe. Este componente es el responsable de interactuar directamente con el transporte subyacente.

El flujo técnico sigue estos pasos fundamentales:

  1. Se obtiene un ByteBufAllocator para reservar memoria.
  2. Se leen los bytes desde el búfer de recepción TCP hacia un objeto ByteBuf gestionado por Netty.
  3. Se inicia la propagación del evento a través del pipeline mediante pipeline.fireChannelRead(byteBuf), comenzando desde el nodo HeadContext.
// Representación simplificada de la lectura en NioByteUnsafe
public void ejecutarLectura() {
    final ChannelPipeline ducto = pipeline();
    final ByteBufAllocator asignador = config.getAllocator();
    final RecvByteBufAllocator.Handle manejadorRecv = recvBufAllocHandle();

    do {
        // Reservar espacio en memoria
        ByteBuf contenedor = manejadorRecv.allocate(asignador);
        // Transferir bytes del socket al ByteBuf
        manejadorRecv.lastBytesRead(doReadBytes(contenedor));
        
        if (manejadorRecv.lastBytesRead() <= 0) {
            contenedor.release();
            break;
        }
        
        // Notificar a los handlers en el pipeline
        ducto.fireChannelRead(contenedor);
    } while (manejadorRecv.continueReading());

    ducto.fireChannelReadComplete();
}

Decodificación y el Problema de Fragmentación

En redes TCP, los datos no siempre llegan en paquetes completos que correspondan exactamente a un mensaje de negocio. Netty soluciona esto mediante la clase base ByteToMessageDecoder, que actúa como un acumulador de bytes.

El ciclo de vida de la decodificación en Netty consta de:

  • Acumulación: Los bytes entrantes se añaden a un contenedor interno (cumulation).
  • Fragmentación: Se intenta extraer uno o varios mensajes completos del contenedor llamando al método decode().
  • Limpieza: Se liberan los bytes ya procesados para evitar fugas de memoria o desbordamientos (OOM).
  • Propagación: Los objetos ya decodificados se envían al siguiente ChannelInboundHandler.

Tipos de Decodificadores Predefinidos

Netty ofrece soluciones listas para los protocolos más comunes:

  • FixedLengthFrameDecoder: Divide los bytes entrantes en tamaños fijos.
  • LineBasedFrameDecoder: Utiliza saltos de línea (\n o \r\n) como delimitadores.
  • DelimiterBasedFrameDecoder: Permite definir caracteres personalizados para separar mensajes.
  • LengthFieldBasedFrameDecoder: El más versátil; lee un campo de longitud al inicio del paquete para determinar cuántos bytes siguen.
// Ejemplo de un decodificador de longitud variable personalizado
public class DecodificadorPersonalizado extends LengthFieldBasedFrameDecoder {
    public DecodificadorPersonalizado() {
        // maxFrameLength: 2048, lengthFieldOffset: 0, lengthFieldLength: 4
        super(2048, 0, 4, 0, 4);
    }

    @Override
    protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        ByteBuf frame = (ByteBuf) super.decode(ctx, in);
        if (frame == null) return null;
        
        // Lógica adicional de transformación
        return frame;
    }
}

Escritura y Codificación de Salida

El proceso de enviar datos (writeAndFlush) recorre el pipeline en sentido inverso, desde el final (tail) hacia el inicio (head). Durante este trayecto, los objetos de negocio deben transformarse en bytes mediante un MessageToByteEncoder.

El flujo de escritura técnica es el siguiente:

  1. Validación: El codificador verifica si puede procesar el tipo de objeto enviado.
  2. Asginación: Se reserva un ByteBuf de salida (preferiblemente de memoria directa).
  3. Codificación: El método abstracto encode() convierte el objeto en bytes.
  4. Gestión de Búfer: Los bytes se colocan en el ChannelOutboundBuffer (una cola pendiente de escritura).
// Ejemplo de codificación de un objeto de respuesta
public class CodificadorRespuesta extends MessageToByteEncoder<RespuestaPrototipo> {
    @Override
    protected void encode(ChannelHandlerContext ctx, RespuestaPrototipo msg, ByteBuf out) {
        // Escribir el código de estado (byte)
        out.writeByte(msg.getEstado());
        // Escribir la longitud del cuerpo (int)
        byte[] cuerpo = msg.getCuerpo();
        out.writeInt(cuerpo.length);
        // Escribir los datos
        out.writeBytes(cuerpo);
    }
}

Gestión de la Cola de Escritura (ChannelOutboundBuffer)

Netty no escribe directamente en el socket de forma síncrona. En su lugar, utiliza el ChannelOutboundBuffer, que es una lista enlazada de objetos pendientes. Este componente gestiona la "contrapresión" (backpressure):

  • Si la cola supera el High Water Mark (por defecto 64KB), el canal cambia su estado a isWritable = false.
  • Cuando los datos se envían físicamente y la cola baja del Low Water Mark (por defecto 32KB), el canal vuelve a ser escribible.

Al ejecutar flush(), Netty recorre esta lista enlazada, convierte las entradas en ByteBuffer de Java NIO y las transfiere al canal del sistema operativo mediante doWriteBytes.

Etiquetas: Netty java NIO Protocols Decoders

Publicado el 7-26 14:21