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:
- Se obtiene un ByteBufAllocator para reservar memoria.
- Se leen los bytes desde el búfer de recepción TCP hacia un objeto
ByteBufgestionado por Netty. - Se inicia la propagación del evento a través del pipeline mediante
pipeline.fireChannelRead(byteBuf), comenzando desde el nodoHeadContext.
// 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:
- Validación: El codificador verifica si puede procesar el tipo de objeto enviado.
- Asginación: Se reserva un
ByteBufde salida (preferiblemente de memoria directa). - Codificación: El método abstracto
encode()convierte el objeto en bytes. - 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.