I don’t understand how to connect channel handlers

i hate channel handlers.

not because they stole my bike or insulted my cat, but because i have no clue how to use them aside from what i can deduce from NIO’s incredibly sparse examples.

they are untyped, crash with cryptic messages when you connect them in the wrong order, and the diagram in the documentation doesn’t match the three-layer formation that the NIO developers say they take in reality.

so i (and i don’t know how many others) have simply resigned to only writing “final” channel handlers, one that only implement channelRead and forward incoming requests to something that is not part of the channel pipeline.

but now, i have been advised to implement “intermediate” channel handlers that go in the fan-outs between the three-layer pipeline:

  • a channel handler that lives in the root pipeline and “reads” smaller pipelines (what?) and… well… i don’t know what it does with the smaller pipelines after it has read them. (forward them to the next handler? drop them? what is the pattern here?) there are no type hints. everything is NIOAny!

  • a channel handler that lives in the smaller pipelines, and well… same deal.

how do you implement a channel handler that goes in the middle of a pipeline? and can we update the docs with an explanation of how the “hierarchical” pipelines work? the existing documentation is not exactly helpful in this respect.


For example, a typical server will have the following handlers in each channel’s pipeline, but your mileage may vary depending on the complexity and characteristics of the protocol and business logic:

Protocol Decoder - translates binary data (e.g. ByteBuffer) into a struct / class

Protocol Encoder - translates a struct / class into binary data (e.g. ByteBuffer)

Business Logic Handler - performs the actual business logic (e.g. database access)

if only!

These first two problems are the same problem.

ChannelHandlers aren't untyped, they're just not compile-time-type-checked. ChannelHandlers have 4 relevant associated types: InboundIn, InboundOut, OutboundIn, OutboundOut. The first two belong to inbound handlers, the third to outbound handlers, and any channel handler can have the fourth.

ChannelHandlers are linked together in a list, as shown in the diagram you've linked. The head of this list is the network side. On the inbound path, each handler's InboundOut must match the InboundIn of the handler after it (after meaning "closer to the tail". On the outbound path, each handler's OutboundOut must match the OutboundIn of the handler before it (before meaning "closer to the head").

These types are required to align, one handler to another, in order. If they do not align, there will be runtime crashes. We have investigated getting this to be compile-time type checked, and this is still a thing we want to do, but we're trying to balance this with the freedom to rearrange the pipeline at runtime.

The three-layer formation doesn't apply to ChannelHandlers: they aren't layered. It applies to Channels. In general, a ChannelHandler is in one-and-only-one Channel, and so the layering is completely invisible to it.

To your "what": NIO's ChannelPipeline can send any kind of structured data. In our case, that allows us to build a useful abstraction on top of the listening socket by sending channels down the pipeline.

Forward them.

We know this sucks. We've known for years. We want to fix it.

The following rough algorithm works:

  1. Decide if you need to transform reads, writes, or both.
  2. Depending on the answer to the above, conform to ChannelInboundHandler (reads), ChannelOutboundHandler (writes), or ChannelDuplexHandler (both).
  3. Provide the required associated types that the conformance asks for. This means deciding about your Inbound/Outbound- in and out types.
  4. Write your logic.

NIO itself does have a wide range of examples that you might crib from. For example, WebSocketFrameEncoder.

Yes, though there isn't only one way they're required to work.

2 Likes