The ChannelPipeline is the core of Netty’s extensibility — a chain of handlers that process inbound and outbound data, enabling protocol encoding/decoding, business logic, and cross-cutting concerns.

Pipeline Structure

  Inbound:  Socket → [Decoder] → [Business Handler] → [Inbound Adapter]
Outbound: Application → [Encoder] → [Outbound Adapter] → Socket
  

Each handler processes specific event types and passes to the next handler.

Handler Types

Type Direction Purpose
ChannelInboundHandlerAdapter Inbound Read events
ChannelOutboundHandlerAdapter Outbound Write events
SimpleChannelInboundHandler<T> Inbound Typed message handling
MessageToMessageDecoder Inbound Transform messages
MessageToByteEncoder Outbound Serialize to bytes

Custom Protocol Example

Protocol: [4-byte length][JSON payload]

  // Decoder: bytes → JSON object
public class JsonFrameDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        if (in.readableBytes() < 4) return;

        in.markReaderIndex();
        int length = in.readInt();
        if (in.readableBytes() < length) {
            in.resetReaderIndex();
            return;
        }

        byte[] data = new byte[length];
        in.readBytes(data);
        OrderMessage msg = objectMapper.readValue(data, OrderMessage.class);
        out.add(msg);
    }
}

// Encoder: JSON object → bytes
public class JsonFrameEncoder extends MessageToByteEncoder<OrderMessage> {
    @Override
    protected void encode(ChannelHandlerContext ctx, OrderMessage msg, ByteBuf out)
            throws Exception {
        byte[] data = objectMapper.writeValueAsBytes(msg);
        out.writeInt(data.length);
        out.writeBytes(data);
    }
}
  

Pipeline Setup

  ch.pipeline()
    .addLast("frameDecoder", new JsonFrameDecoder())
    .addLast("frameEncoder", new JsonFrameEncoder())
    .addLast("idleStateHandler", new IdleStateHandler(60, 0, 0))
    .addLast("businessHandler", new OrderHandler());
  

Business Handler

  public class OrderHandler extends SimpleChannelInboundHandler<OrderMessage> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, OrderMessage msg) {
        switch (msg.getType()) {
            case CREATE -> handleCreate(ctx, msg);
            case CANCEL -> handleCancel(ctx, msg);
            default -> ctx.writeAndFlush(new OrderMessage("ERROR", "Unknown type"));
        }
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        if (evt instanceof IdleStateEvent) {
            ctx.writeAndFlush(new OrderMessage("PING", ""));
        }
    }
}
  

Built-in Codecs

  // HTTP
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpObjectAggregator(65536));

// WebSocket
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));

// Protobuf
pipeline.addLast(new ProtobufVarint32FrameDecoder());
pipeline.addLast(new ProtobufDecoder(OrderMessage.getDefaultInstance()));

// SSL/TLS
pipeline.addFirst("ssl", SslContextBuilder.forServer(cert, key).build().newHandler(ch.alloc()));

// Logging
pipeline.addFirst("logging", new LoggingHandler(LogLevel.DEBUG));
  

Sharable Handlers

Stateless handlers can be shared across channels:

  @ChannelHandler.Sharable
public class AuthenticationHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        if (isAuthenticated(ctx)) {
            ctx.fireChannelRead(msg);  // pass to next handler
        } else {
            ctx.close();
        }
    }
}

// Reuse same instance
AuthenticationHandler authHandler = new AuthenticationHandler();
bootstrap.childHandler(new ChannelInitializer<>() {
    protected void initChannel(SocketChannel ch) {
        ch.pipeline().addLast(authHandler);  // shared instance OK
    }
});
  

Dynamic Pipeline Modification

Add/remove handlers at runtime:

  // After authentication, add encryption
ChannelPipeline pipeline = ctx.pipeline();
pipeline.addAfter("auth", "encryption", new EncryptionHandler());

// Remove handler after handshake
pipeline.remove("handshakeHandler");
  

Best Practices

  • Order matters — decoders before business logic, encoders after
  • Use @Sharable only for truly stateless handlers
  • Add idle state handler to detect and close dead connections
  • Use SimpleChannelInboundHandler for typed messages — automatic release
  • Separate protocol handling (codec) from business logic (handler)
  • Add SSL handler first in the pipeline (before any data processing)