This is not quite right: a ChannelInboundHandler
does not have OutboundIn
. To understand the relationship here, let's step back and define some terms and then talk about the inheritance hierarchy. (Much of this is covered in the README, but it will not hurt to have this information in more places.)
All ChannelHandler
s deal with two directions of messages. In NIO we call these "inbound" and "outbound". A light-hearted way to think about this is that "inbound" events are things that the network does to you, whereas "outbound" events are things you do to the network.
There are a wide variety of inbound and outbound events, but the type aliases we're discussing here apply only to the data-carrying events: ChannelInboundHandler.channelRead
and ChannelOutboundHandler.write
. As @Mordil noted above, the ChannelPipeline
is conceptually a doubly-linked list of ChannelHandler
s, such that all ChannelHandler
s in a pipeline are well-ordered with regard to every other handler.
That means that when a ChannelHandler
A
calls ChannelHandlerContext.fireChannelRead
, that will find the next ChannelInboundHandler
between A
and the tail of the ChannelPipeline
and pass the data to that handler's ChannelInboundHandler.channelRead
method. Similarly, when ChannelHandler
A
calls ChannelHandlerContext.write
, that data will be passed to the next ChannelOutboundHandler
between A
and the head of the ChannelPipeline
.
To understand how this works, it first helps to see the ChannelHandler
protocol hierarchy:
┌────────────────────────┐
│ │
┌──│_EmittingChannelHandler │───┐
│ │ │ │
│ └────────────────────────┘ │
│ │
▼ ▼
┌────────────────────────┐ ┌────────────────────────┐
│ │ │ │
│ ChannelInboundHandler │ │ ChannelOutboundHandler │
│ │ │ │
└────────────────────────┘ └────────────────────────┘
│ │
│ │
│ ┌────────────────────────┐ │
│ │ │ │
└─▶│ ChannelDuplexHandler │◀──┘
│ │
└────────────────────────┘
In our case there are only 3 ChannelHandler
types that are immediately used by users: ChannelInboundHandler
, ChannelOutboundHandler
, and ChannelDuplexHandler
. ChannelDuplexHandler
is very straightforward, so straightforward that I will simply reproduce its declaration here:
/// A combination of `ChannelInboundHandler` and `ChannelOutboundHandler`.
public typealias ChannelDuplexHandler = ChannelInboundHandler & ChannelOutboundHandler
So there are 3 kinds of ChannelHandler
: Inbound
, Outbound
, and Duplex
. What kinds of data can they send and receive? It helps to look at a pipeline diagram, so here's a simple one containing one Duplex
(A
), two Inbound
(B
and D
), and an Outbound
(C
). I'll annotate this pipeline diagram with the data flows that each ChannelHandler
participates in. In this diagram, the head of the pipeline (the part closest to the network) is on the left, and the tail is on the right: this is how the NIO team usually draw pipelines.
┌──────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ │ │ │ │ │
│ │ │ChannelInboundHandler│ │ChannelInboundHandler│
InboundIn(A) ────────▶│ │─── InboundOut(A) ──▶│ │─── InboundOut(B) ───▶│ │─────▶ InboundOut(D)
│ │ InboundIn(B) │ B │ InboundIn(D) │ D │
│ │ │ │ │ │
│ ChannelDuplexHandler │ └─────────────────────┘ └─────────────────────┘
│ │ │ │
│ A │ OutboundOut(B) OutboundOut(D)
│ │ ┌ ─ OutboundIn(A) ┘ ┌──────────────────────┐ │ OutboundIn(C)
│ │ │ │
│ │ │ │ChannelOutboundHandler│ │
OutboundOut(A) ◀────────│ │◀───────────◀────────── OutboundOut(C) ────│ │◀─────────◀───────────────── OutboundIn(C)
│ │ OutboundIn(A) │ C │ Channel.write()
│ │ │ │
└──────────────────────┘ └──────────────────────┘
Here I have drawn "inbound" data on the top, and "outbound" data on the bottom, and annotated every line with the typealiases that apply to the data making that transition.
This is a complex diagram, so let's call out a few things.
Firstly, let's look at A
. Because A
is a Duplex
handler, it needs all 4 type aliases. The reason for that is that Duplex
handlers need to process both write
and channelRead
calls, meaning they receive and transform data coming from the network and data going to the network. Notice that InboundIn(A)
is on the left side of A
, which indicates that A
's InboundIn
type is the type passed to it in channelRead
, and InboundOut(A)
is on the right side of A
, which indicates that InboundOut(A)
is the type that A
passes to ChannelHandlerContext.fireChannelRead
.
You can see this pattern in the two Inbound
handlers B
and D
. Each of those has an InboundIn
and an InboundOut
, and their InboundIn
applies to the data they receive from the handler to their left (towards the head) and their InboundOut
applies to the data they send to the handler on their right (towards the tail).
What about outbound? Again, we can start with A
. This time, OutboundIn
is on A
's right, closer to the tail, but it applies once again to the data A
is receiving. This is the type that A
will receive on a write
function. OutboundOut
is on A
's left, closer to the head, but this is still data that A
is emitting, this time from ChannelHandlerContext.write
.
So far so clear: ChannelInboundHandler
s have InboundIn
and InboundOut
, ChannelOutboundHandler
s have OutboundIn
and OutboundOut
, ChannelDuplexHandler
s have both. Except there are those pesky lines crossing from the top to the bottom in my diagram: what's up with that?
This is where _EmittingChannelHandler
comes in. This exists to solve a simple problem: what happens if you want to write a ChannelHandler
that only wants to receive reads, but sometimes wants to send a write? This is more common than you might think: for example, most ChannelHandler
s at the end of a ChannelPipeline
fall into this category. This is common when you have a server that generates responses: most of NIO's example server ChannelHandler
s do exactly this.
These ChannelHandler
s don't want to see the data being passed along the pipeline for writes: that is, they don't want to have a write
function that gets called, but they do want to be able to call a write
function themselves. This means they don't want to have an OutboundIn
, but they do want to have an OutboundOut
.
This is what _EmittingChannelHandler
provides. You can see this by looking at the definitions of the protocols. For example, while ChannelInboundHandler
has a somewhat tricky definition, the relevant parts for us look like this:
public protocol ChannelInboundHandler: _EmittingChannelHandler {
/// The type of the inbound data which is wrapped in `NIOAny`.
associatedtype InboundIn
/// The type of the inbound data which will be forwarded to the next `ChannelInboundHandler` in the `ChannelPipeline`.
associatedtype InboundOut = Never
}
Similarly for ChannelOutboundHandler
:
public protocol ChannelOutboundHandler: _EmittingChannelHandler {
/// The type of the outbound data which is wrapped in `NIOAny`.
associatedtype OutboundIn
}
And finally _EmittingChannelHandler
defines one extra associatedtype
:
public protocol _EmittingChannelHandler {
/// The type of the outbound data which will be forwarded to the next `ChannelOutboundHandler` in the `ChannelPipeline`.
associatedtype OutboundOut = Never
}
So how should you think about the words Inbound
and Outbound
? In all cases they apply to the direction of the event in question: inbound events are triggered by the network, outbound events are emitted to the network. The only wrinkle here is that ChannelInboundHandler
s are allowed to send data to the network, even though they only receive events for inbound data. This is a programmer convenience and nothing more.