How to understand InboundIn InboundOut OutboundOut OutboundIn names

Hi there, as the title said, how to understand these typealias names? I'm a little confusing and the In and Out order.

For example in swift-nio-extras/RequestResponseHandler.swift at main · apple/swift-nio-extras · GitHub,

it has

    public typealias InboundIn = Response
    public typealias InboundOut = Never
    public typealias OutboundIn = (Request, EventLoopPromise<Response>)
    public typealias OutboundOut = Request

From this example I can kind of feel what's the meaning for each, but for InBoundOut, it's Never, which I feel confused. Can someone gives an exmaple?

Thanks in advance.

Think of the syntax as <direction data is going in the pipeline><direction the data is going through the handler>

InboundIn -->   ChannelInboundHandler --> InboundOut --> ...

OutboundOut <-- ChannelOutboundHandler <-- OutboundIn <-- ...

So OutboundIn means the data is coming in to the handler, but it's headed out of the channel.

Each of these typealiases are for converting the type erased NIOAny into the expected type for a particular ChannelHandler to provide type-safety with handling raw bytes.

Say you have a ChannelPipeline with the following: [ChannelInboundHandler] = [SomeHandlerA, SomeHandlerB].

Given that SomeHandlerA is placed first in the pipeline:

SomeHandlerA.InboundOut == SomeHandlerB.InboundIn must be true.


In this case, typealias InboundOut = Never is telling the type system that this ChannelInboundHandler does not "produce" any data, so this handler must be placed at the end of the Inbound pipeline.

Hi thanks for replying!

regards your example, SomeHandlerA.OutboundOut == SomeHandlerB.OutboundIn must be true, why ChannelInBoundHandler can have OutboundOut or OutboundIn?

I was thinking with your diagram, it looks like

InBoundIn --> SomeHandlerA --> InBoundOut == ? --> SomeHandlerB --> InBoundOut?

I'm not a native speaker, so In-Bound-Out is kind of confusing to figure out

for example, any data can flow into a handler and flow out, what does the In and Out stand for when it's on left and on right? Should I split it as In, Bound, Out as 3 components or,

I could interpret InBoundIn as InBound+In, means the data is going In to the InBound handler, while OutBoundIn means, the data is going In to a OutBound handler. But in this case, a InBoundHandler should never have a OutBound-X type

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 ChannelHandlers 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 ChannelHandlers, such that all ChannelHandlers 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: ChannelInboundHandlers have InboundIn and InboundOut, ChannelOutboundHandlers have OutboundIn and OutboundOut, ChannelDuplexHandlers 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 ChannelHandlers 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 ChannelHandlers do exactly this.

These ChannelHandlers 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 ChannelInboundHandlers 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.

4 Likes

@lukasa thanks so much for this super answer!

1 Like

BTW, I think should be put somewhere in the github wiki or guide, so it could help people have same trouble understanding this

2 Likes