Netty-Pipeline Depth Analysis

Keywords: PHP Netty socket JDK network

First of all, we know that in NIO network programming model, IO operations are directly related to channel, such as client request connection, or sending data to the server, the server must obtain this data from the client channel.

So what is Channel Pipeline?

In fact, this channel Pepiline is a component added by netty to the native channel. Annotations on the Channel Pipeline interface illustrate the role of channel Pipeline. This channel Pipeline is the implementation of advanced filters. netty directs the data in channel Pipeline and gives users 100% control over the data in channel. In addition, the channelPipeline data structure is a two-way linked list. Each node is ChannelContext. ChannelContext maintains the reference of the corresponding handler and pipeline. To sum up, through channelPipeline, users can easily write data to Channel and read data from Channel.

Create pipeline

Through the tracing of previous blogs, we know that whether we create the channel of the server through reflection or directly create the channel of the client by new, as the parent constructor is invoked layer by layer, we will eventually create a large component C of Channel in AbstractChannel, the top abstract class of Channel system. Hannel Pipeline

So at the entrance of our program, pipeline of AbstractChannel = new Channel Pipeline ();, follow up and see his source code as follows:

protected DefaultChannelPipeline newChannelPipeline() {
    // todo follow up
    return new DefaultChannelPipeline(this);
}

As you can see, it creates a Default Channel Pipeline (this Channel)
Default Channel Pipeline is the default implementation of Channel Pipeline. It plays an important role. Let's take a look at the following inheritance system diagram of Channel ChannelContext Channel Pipeline. We can see two classes in the diagram, which are very important in fact.

What's the relationship between them?

When we have finished looking at what we have done in the Default Channel Pipeline () construct, it's natural to know.

// todo is here
protected DefaultChannelPipeline(Channel channel) {
    this.channel = ObjectUtil.checkNotNull(channel, "channel");
    // todo saves the current Channel
    succeededFuture = new SucceededChannelFuture(channel, null);
    voidPromise =  new VoidChannelPromise(channel, true);

    // The two methods of todo are very important
    // todo setup tail
    tail = new TailContext(this);
    // todo setup header
    head = new HeadContext(this);

    // todo bidirectional linked list Association
    head.next = tail;
    tail.prev = head;
}

The following main things have been done:

  • Initialize succeeded future
  • Initialize voidPromise
  • Create tail nodes
  • Create header nodes
  • Associated Head and Tail Nodes

Actually, so far, the initialization of pipiline has been completed. Let's go on and look at it.

In addition, let's look at the internal classes and methods of Default Channel Pipeline, as shown below ()

We focus on the parts I circle.

  • Two important internal classes
    • Header Context of Header Node
    • Tail Context
    • Tasks processed after adding handler to Pending Handler AddedTask
    • Pending Handler CallBack adds handler callbacks
    • Tasks after PengdingHandler RemovedTask removes Handler
  • A large number of addXXX methods,
 final AbstractChannelHandlerContext head;
 final AbstractChannelHandlerContext tail;

Follow up its packaging methods:

TailContext(DefaultChannelPipeline pipeline) {
        super(pipeline, null, TAIL_NAME, true, false);
        setAddComplete();
    }
// todo is here
AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, String name,
                              boolean inbound, boolean outbound) {
    this.name = ObjectUtil.checkNotNull(name, "name");
    // todo attaches a value to the pipeline of ChannelContext
    this.pipeline = pipeline;
    this.executor = executor;
    this.inbound = inbound;
    this.outbound = outbound;
    // Its ordered if its driven by the EventLoop or the given Executor is an instanceof OrderedEventExecutor.
    ordered = executor == null || executor instanceof OrderedEventExecutor;
}

As we can see, this tail node is an inbound processor. At first, I was really puzzled. Shouldn't header be an inbound processor? I won't buy it anymore. Why?

Because, for netty, the data transmitted from the header node will start to propagate backwards, how to propagate it? Because it is a two-way linked list, directly find the latter node, what kind of node? Inbound type, so the data MSG propagates backwards from the first node after the header, if so, until the end, it is only propagating. Data will be propagated to tail node without any processing, because tail is also inbound type, tail node will release this msg for us to prevent memory leak. Of course, if we use MSG ourselves, and do not propagate back, nor release, memory leak is sooner or later, this is why tail is Inbound type, header node Contrary to it, here's how

ok, now you know the creation of Channel Pipeline

The relationship between Channel pipeline and Channel Handler and Channel Handler Context

In the process of pipeline creation, the head and tail nodes in Default Channel Pipeline are ChannelHandlerContext, which means that in the structure of pipeline bidirectional list, each node is a ChannelHandlerContext, and each ChannelHandlerContext maintains a han. If you don't believe it, you can see the figure above. The implementation class of ChannelHandlerContext, DefaultChannelHandlerContext, has the following source code:

final class DefaultChannelHandlerContext extends AbstractChannelHandlerContext {
// There is a reference to handler in todo Context
private final ChannelHandler handler;

// todo creates the default ChannelHandlerContext,
DefaultChannelHandlerContext(
        DefaultChannelPipeline pipeline, EventExecutor executor, String name, ChannelHandler handler) {
    super(pipeline, executor, name, isInbound(handler), isOutbound(handler));
    if (handler == null) {
        throw new NullPointerException("handler");
    }
    this.handler = handler;

ChannelHandlerContext interface inherits both ChannelOutBoundInvoker and ChannelInBoundInvoker so that it has both inbound and outbound events to propagate. After ChannelHandlerContext propagates events, who handles them? Of course, the inheritance system diagram of ChannelHandler is given below. You can see that it is for inbound events. Stand-out and outbound processing ChannelHandler have different inheritance branch responses

Add a new node:

Generally, we add multiple handler s dynamically at one time through Chanel Initialezer. Let's see the init() of ServerBootStrap in the process of server startup. The following source code: parse me and write it under the code.

// todo This is the implementation of ServerBootStrapt to initialize channel for his parent class to initialize NioServerSocket Channel
@Override
void init(Channel channel) throws Exception {
// todo ChannelOption is the ChannelConfig information for configuring Channel
final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
    // todo passes NioserverSocketChannel and options Map in, assigning values to the properties in Channel
    // todo constants are all about information related to protocols such as TCP
    setChannelOptions(channel, options, logger);
}
    // Another wave of todo assigning attrs0() to attributes in Channel is to get user-defined business logic attributes -- AttributeKey
final Map<AttributeKey<?>, Object> attrs = attrs0();
// todo is a map that maintains dynamic business data while the program is running. It can realize that business data can be retrieved from the original stored data along with the operation of netty.
synchronized (attrs) {
    for (Entry<AttributeKey<?>, Object> e : attrs.entrySet()) {
        @SuppressWarnings("unchecked")
        AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
        channel.attr(key).set(e.getValue());
    }
}
// Todo - --- options attrs: can be dynamically passed in when creating BootStrap
// Todo Channel Pipeline is an important component in itself. It contains a processor one by one. It says that he is a high-level filter. Interactive data passes through it layer by layer.
// p is called directly below todo, which means that pipeline has been created before channel calls pipeline method.
// When exactly was todo created? In fact, when NioServer SocketChannel was created as a channel object, a default pipeline object was created in his top Abstract parent class (AbstractChannel).
/// todo adds: ChannelHandlerContext is the bridge between ChannelHandler and Pipline
ChannelPipeline p = channel.pipeline();

// Todo workerGroup handles IO threads
final EventLoopGroup currentChildGroup = childGroup;
// Initializer we added to todo
final ChannelHandler currentChildHandler = childHandler;

final Entry<ChannelOption<?>, Object>[] currentChildOptions;
final Entry<AttributeKey<?>, Object>[] currentChildAttrs;


// todo Here are some of the property settings we added to the Server class for the new connection channel, which are used by acceptor!!!
synchronized (childOptions) {
    currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
}
synchronized (childAttrs) {
    currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
}

// todo adds a Channel Initializer to NioServer Socket Channel by default.
// todo (the Channel Initializer that we later added ourselves inherited from the Channel Initializer, which we inherited from the Channel Initializer implements ChannelHandler)
p.addLast(new ChannelInitializer<Channel>() { // todo enters addlast
    // todo is a Channel Initializer that allows us to add multiple processors to the pipeline at once.
    @Override
        public void initChannel(final Channel ch) throws Exception {
            final ChannelPipeline pipeline = ch.pipeline();
            // todo gets the handler object of bootStrap without returning null
            // To do this handler for the Channel of the bossgroup, add the handler() we added to the server class to add the processor
            ChannelHandler handler = config.handler();
            if (handler != null) {
                pipeline.addLast(handler);
            }

            // The todo Server Bootstrap Acceptor receiver is a special Chanel handler
             ch.eventLoop().execute(new Runnable() {
                @Override
                public void run() {
                    // todo!!! - This is very important. In Server BootStrap, netty has generated receivers for us!!!
                    // todo specializes in handling access to new connections, binding channel s of new connections to a thread in workerGroup
                    // todo is used to process user requests, but it's not clear how it triggers execution
                    pipeline.addLast(new ServerBootstrapAcceptor(
                            // todo These parameters are user-defined parameters
                            // Todo NioServer Socket Channel, Worker Thread Group Processor Relations Events
                            ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                }
            });
    }
});

This function is really long, but our focus is on channel Initializer. At this stage, channel has not registered with Selector on EventLoop.

Is there any analysis of how to add handler? How to come here? In fact, the following Server Bootstrap Acceptor is a handler

Let's see what the above code does.

ch.eventLoop().execute(new Runnable() {
    @Override
    public void run() {
        // todo!!! - This is very important. In Server BootStrap, netty has generated receivers for us!!!
        // todo specializes in handling access to new connections, binding channel s of new connections to a thread in workerGroup
        // todo is used to process user requests, but it's not clear how it triggers execution
        pipeline.addLast(new ServerBootstrapAcceptor(
                // todo These parameters are user-defined parameters
                // Todo NioServer Socket Channel, Worker Thread Group Processor Relations Events
                ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
    }
});

No? It was really a cover-up for me at that time, but it was not related to EventLoop!!! Where did ch. EvetLoop come from?.

Later, it became clear that this was actually a callback. netty provided users with the means to add handler to pipeline at any time.

So where is the callback? Actually, it's called back immediately after the selection in EventLoop from jdk's native channel group. The source code is as follows

private void register0(ChannelPromise promise) {
    try {
        // check if the channel is still open as it could be closed in the mean time when the register
        // call was outside of the eventLoop
        if (!promise.setUncancellable() || !ensureOpen(promise)) {
            return;
        }
        boolean firstRegistration = neverRegistered;
        // todo enters the method doRegister()
        // todo registers Server Socket Channel created by the system into the selector
        doRegister();
        neverRegistered = false;
        registered = true;
        
        // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
        // user may already fire events through the pipeline in the ChannelFutureListener.
        // todo ensures that handler Added (...) is called before notify the promise
        // todo is necessary because the user may have triggered the event through a pipeline in Channel FutureListener.
        // todo executes the HandlerAdded() method if necessary
        // todo is just this method, which calls back the important way we added Accpter to Initializer
        pipeline.invokeHandlerAddedIfNeeded();

The callback function is pipeline. invokeHandler AddedIfNeeded ();, look at its name, if necessary, the execution handler has added the operation Haha, we certainly need now, just added a Server BootstrapAcceptor.

Between looking at the source code, notice that the method is called by pipeline. What pipeline is it? That's what we said above: Default Channel Pipeline, ok, follow up the source code and go to Default Channel Pipeline.

// todo performs the addition of handler, if necessary
final void invokeHandlerAddedIfNeeded() {
    assert channel.eventLoop().inEventLoop();
    if (firstRegistration) {
        firstRegistration = false;
        // todo Now that our channel is registered with EvetLoop in the bossGroup, it's time to call back the handler s that were added before registration.
        callHandlerAddedForAllHandlers();
    }
}

Call this class method callHandlerAddedForAllHandlers(); follow up

// todo callback handler added before registration was completed
private void callHandlerAddedForAllHandlers() {
    final PendingHandlerCallback pendingHandlerCallbackHead;
    synchronized (this) {
        assert !registered;

        // This Channel itself was registered.
        registered = true;

        pendingHandlerCallbackHead = this.pendingHandlerCallbackHead;
        // Null out so it can be GC'ed.
        this.pendingHandlerCallbackHead = null;
    }
  PendingHandlerCallback task = pendingHandlerCallbackHead;
    while (task != null) {
        task.execute();
        task = task.next;
    }
}

Our action task.execute();

The pending handler CallbackHead is an internal class of Default Channel Pipeline, which assists in completing callbacks after adding handlers, and the source code is as follows:

private abstract static class PendingHandlerCallback implements Runnable {
    final AbstractChannelHandlerContext ctx;
    PendingHandlerCallback next;

    PendingHandlerCallback(AbstractChannelHandlerContext ctx) {
        this.ctx = ctx;
    }

    abstract void execute();
}

As we follow up on task.execute() in the previous step, we will see its compact approach, so who implemented it? The implementation class is PendingHandlerAddedTask, which is also the internal class of DefaultChannelPipeline. Since it is not an abstract class, we have to implement the abstract method of its parent class PendingHandlerCallback at the same time. In fact, there are two ways: excute(). The other is run() --Runable

Let's go in and see how it implements excute. The source code is as follows:

@Override
void execute() {
EventExecutor executor = ctx.executor();
if (executor.inEventLoop()) {
    callHandlerAdded0(ctx);
} else {
    try {
        executor.execute(this);
    } catch (RejectedExecutionException e) {
        if (logger.isWarnEnabled()) {
            logger.warn(
                    "Can't invoke handlerAdded() as the EventExecutor {} rejected it, removing handler {}.",
                    executor, ctx.name(), e);
        }
        remove0(ctx);
        ctx.setRemoved();
    }
}

Callback timing of HandlerAdded()

We trace down and call the class method callHandlerAdded0(ctx); the source code is as follows:

// todo focuses on this method, and the entry is the Context that was just added.
private void callHandlerAdded0(final AbstractChannelHandlerContext ctx) {
try {
    // todo calls after handler is associated with channel and Context is added to Pipeline!!!
    ctx.handler().handlerAdded(ctx); // todo is the first of many callback methods to be called
    ctx.setAddComplete();  // todo modification status
}
...

Keep tracking down

  • ctx.handler() -- Gets the current channel
  • Call channel's. handler Added (ctx);

This handler Added () is a callback method defined in ChannelHandler. When is the callback? When handler is added, callback is made because we know that when the server-side channel is started, the Server Bootstrap Acceptor is added through the channel Initializer, so when is the handler Added () callback of the Server Bootstrap Acceptor? The machine is ctx. handler (). handler Added (ctx) in the code above.

If you click on this function directly, it must be in the ChannelHandler interface; then the new question comes, who is the implementation class? The answer is the abstract class ChannelInitializer `, on which we add the ServerBootstrapAcceptor and create an anonymous object of ChannelInitializer `.

Its inheritance system chart is as follows:

Introduce this Channel Initializer, which is an auxiliary class provided by Netty to provide initialization work for channel. What work? Batch initialization of channel?

There are three important ways to do this, as follows

  • Handler Added () of the rewritten channel
  • My own initChannel()
  • Own remove()

Follow up on our handler Added (Channel Handler Context ctx) source code as follows:

   @Override
        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
                initChannel(ctx); // The todo method is above, and the logic to remove Initializer can be found in final.
            }
    }

Call initChannel(ctx) of this class; the source code is as follows:

  private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
        if (initMap.putIfAbsent(ctx, Boolean.TRUE) == null) { // Guard against re-entrance.
            try {
                initChannel((C) ctx.channel());
            } catch (Throwable cause) {
                // Explicitly call exceptionCaught(...) as we removed the handler before calling initChannel(...).
                // We do so to prevent multiple calls to initChannel(...).
                exceptionCaught(ctx, cause);
            } finally {
                // Todo remove (ctx); delete Channel Initializer
                remove(ctx);
            }
            return true;
        }
        return false;
    }

Two points

  • First, continue to call the abstract method initChannel((C) ctx.channel()) of this class.
  • Second, remove(ctx);

Take the first step separately

InitChannel ((C) ctx. channel (); Initialization channel, this function is designed to be abstract, the question arises, who is the implementation class? In fact, the implementation class just said that netty created the anonymous internal class when adding ServerBootStrapAcceptor. Let's follow up to see its implementation: The source code is as follows:

 @Override
    public void initChannel(final Channel ch) throws Exception {
        final ChannelPipeline pipeline = ch.pipeline();
        // todo gets the handler object of bootStrap without returning null
        // To do this handler for the Channel of the bossgroup, add the handler() we added to the server class to add the processor
        ChannelHandler handler = config.handler();
        if (handler != null) {
            pipeline.addLast(handler);
        }

        // The todo Server Bootstrap Acceptor receiver is a special Chanel handler
         ch.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                // todo!!! - This is very important. In Server BootStrap, netty has generated receivers for us!!!
                // todo specializes in handling access to new connections, binding channel s of new connections to a thread in workerGroup
                // todo is used to process user requests, but it's not clear how it triggers execution
                pipeline.addLast(new ServerBootstrapAcceptor(
                        // todo These parameters are user-defined parameters
                        // Todo NioServer Socket Channel, Worker Thread Group Processor Relations Events
                        ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
            }
        });
}

In fact, it completes a method callback and successfully adds a Server Bootstrap Acceptor processor.

Delete a node

Come back to step two

remove(ctx); delete a node, delete Initializer? Yes, delete this initializer, why delete it, said many times, in fact, he is an auxiliary class, the purpose is to add multiple handlers to the channel at one time through him, and now the handler has been added, leaving him useless, move directly. except

Let's look at the source code.

 // todo deletes the current ctx node
    private void remove(ChannelHandlerContext ctx) {
        try {
            ChannelPipeline pipeline = ctx.pipeline();
            if (pipeline.context(this) != null) {
                pipeline.remove(this);
            }
        } finally {
            initMap.remove(ctx);
        }
    }

Remove from pipeline. Looking all the way through, you will find that the dungeon deletes linked list nodes.

private static void remove0(AbstractChannelHandlerContext ctx) {
    AbstractChannelHandlerContext prev = ctx.prev;
    AbstractChannelHandlerContext next = ctx.next;
    prev.next = next;
    next.prev = prev;
}

Dissemination of inbound events

What is an inbound event

Inbound event is actually an event initiated by the client actively, such as client requesting connection, client sending valid data to server actively after connection, and so on. As long as the event initiated by client actively is an Inbound event, it is characterized by event triggering type. When channel is in a node, it triggers service. What actions do end-to-end propagate?

How netty Treats inbound

Netty adds pipeline components to jdk's native channel in order to better handle the data in channel. netty will direct the data in the channel of jdk's native channel to the pipeline, and start to spread downward from the head of the pipeline. Users have 100% control over the process. They can take the data out for processing or download it. Broadcast, spread to the tail node, tail node will be recycled, if in the process of transmission, the end node did not reach, and they did not recycle, they will face the problem of memory leak.

In conclusion, in the face of Inbound data, passive communication

Does netty know what type of data the client sends?

For example, a chat program, the client may send a heartbeat package, or may send chat content, netty is not human, he does not know what the data is, he only knows the data, need to be further processed, how to deal with it? Directing the data to the user-specified handler chain.

Start reading source code

Here the book is followed by the end of a blog, the dissemination of events.
The key steps are as follows

Step 1: Wait for the server to start and finish

Step 2: Use telnet to simulate the access logic for sending requests - > new connections

Step 3: The channel Promise Promise method propagates the channel Activation Event - > for the purpose of re-registering the port.

The third is the starting point of our program: fireChannelActive() source code is as follows:

@Override
public final ChannelPipeline fireChannelActive() {
    // Todo Channel Active spreads from head
    AbstractChannelHandlerContext.invokeChannelActive(head);
    return this;
}

Called the invokeChannelActive method of AbstractChannelHandlerContext

Here, I think it's especially necessary to tell myself the importance of AbstractChannelHandlerContext. Every node in Default Channel Pipeline, including header,tail and our own additions, is of the AbstractChannelHandlerContext type. Events are propagated around AbstractChannelHandlerContext. To begin with, review its inheritance system as follows

Then go back to AbstractChannelHandlerContext.invokeChannelActive(head); obviously, this is a static method, follow up, the source code is as follows:

// todo is here
static void invokeChannelActive(final AbstractChannelHandlerContext next) {
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
    next.invokeChannelActive();
...
}
  • First point: events of inbound type are propagated from header, next - > HeaderContext
  • Second point: HeaderContext is actually the AbstractChannelHandlerContext type, so invokeChannelActive() is actually the method of the current class.

ok, follow up and see what he did, source code:

// todo makes Channel active
private void invokeChannelActive() {
// todo keeps going in
((ChannelInboundHandler) handler()).channelActive(this);
}

Let's see, the above code does the following things

  • handler() -- Return to the current handler, which is to take handler out of the Handler Context
  • Strongly converted to ChannelInboundHandler type because he is an InBound type processor

If we click on Channel Active (this) with our mouse, we will undoubtedly enter Channel InboundHandler and see the abstract method.

So the question arises, who achieved it?

Actually, the headerContext header node did it. As I said before, the Inbound event started to spread from the header. Follow it up and see the source code:

// todo comes here in two steps. 1. Continue to spread Channel Active. After the dissemination is over, do the second thing.
// todo                  2.     readIfIsAutoRead();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// Todo FireChannel Active triggers callbacks only after the actual port bindings have been made
ctx.fireChannelActive();

// todo registers a read event by default
// To do follow up, readIfIsAutoRead is used to register events that have been registered on selector, and to re-register the Accept events added to the binding when initializing NioServer Socket Channel
// The goal of todo is to poll for accept events when new connections come in, so that netty can further handle this event.
readIfIsAutoRead();
}

In fact, there are two important things, as we have seen above:

  • The purpose of propagating channelActive() downward is for the channelActive() in the handler added by the user after the header to be called back.
  • readIfIsAutoRead(); that is, to test two interesting events Netty can understand.

Now let's see how its events spread down, and we're back to AbstractChannelHandlerContext, with the following source code:

public ChannelHandlerContext fireChannelActive() {
        invokeChannelActive(findContextInbound());
        return this;
    }
  • findContextInbound() finds the next processor of Inbound type. Let's see its implementation. The source code is as follows:
 private AbstractChannelHandlerContext findContextInbound() {
    AbstractChannelHandlerContext ctx = this;
    do {
        ctx = ctx.next;
    } while (!ctx.inbound);
    return ctx;
}

Is it clear? From the current node, the whole list of variables will be changed later, and who will be the next node? In the logic of new link access, I manually added three InboundHandler s in batch. In the order I added, they will be found at one time.

Continue to follow up the invokeChannelActive(findContextInbound()) method with the following source code

  // todo is here
static void invokeChannelActive(final AbstractChannelHandlerContext next) {
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        next.invokeChannelActive();
...

Beginning next - > HeaderContext
Now next is the handler of Inbound that I added manually after header.

Similarly, invokeChannelActive() is called with the following source code:

// todo makes Channel active
private void invokeChannelActive() {
    // todo keeps going in
    ((ChannelInboundHandler) handler()).channelActive(this);

See again, callback, handler.channelActive(this);, go to view

public class MyServerHandlerA extends ChannelInboundHandlerAdapter {
// When the channel on the server is bound to the upper port, todo propagates the channelActive event.
// After the todo event is propagated below, we manually propagate a channelRead event
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
    ctx.channel().pipeline().fireChannelRead("hello MyServerHandlerA");
}

In my processor, continue to propagate the manually added data "hello MyServer Handler A"

Similarly, she will spread it in the order in which she finds it.

Eventually she will come to tail and do the following work in tail. The source code is as follows

 @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    // todo channelRead
    onUnhandledInboundMessage(msg);
}

protected void onUnhandledInboundException(Throwable cause) {
try {
    logger.warn(
            "An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " +
                    "It usually means the last handler in the pipeline did not handle the exception.",
            cause);
} finally {
    ReferenceCountUtil.release(cause);
}
}  

Why is the Tail node an Inbound processor?

The last step explains why tail is designed as Inbound. The data in channel, whether used or not by the server, will eventually be released. tail can finish the work and clean up the memory.

Dissemination of outbound events

What is an outBound event

outbound events created such as: connect,disconnect,bind,write,flush,read,close,register,deregister, out type events are more initiative events initiated by the server, such as binding the upper port to the active channel, initiatively writing data to the channel, initiatively closing the user's connection.

Start reading source code

The most typical outbound event is when the server writes data to the client and prepares for testing, such as the following:

public class OutBoundHandlerB extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println( "hello OutBoundHandlerB");
        ctx.write(ctx, promise);
    }
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        ctx.executor().schedule(()->{
            // todo simulates a response to the client
            ctx.channel().write("Hello World");
            // Writing 2: ctx.write("Hello World");
        },3, TimeUnit.SECONDS);
    }
}
public class OutBoundHandlerA extends ChannelOutboundHandlerAdapter {
    // When the channel on the server is bound to the upper port, todo propagates the channelActive event.
    // After the todo event is propagated below, we manually propagate a channelRead event
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println( "hello OutBoundHandlerA");
        ctx.write(ctx, promise);
    }
}
public class OutBoundHandlerC extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println( "hello OutBoundHandlerC");
        ctx.write(ctx, promise);
    }
}

Next, we debug the breakpoint, hit the breakpoint on the handler Added of OutBoundHandlerB, simulate sending data to the client, start the program, the process is as follows.

  • Waiting for the startup of the server
  • Server channel Polls Possible Interesting Events on Server channel
  • Sending requests to the server using tennet
  • The server creates a channel for the client and registers the native chanenl for the client on the Selector
  • Add the handler we added to the Initializer to the pipeline by invokeChannelAddedIfNeeded()
    • Callback the channelAdded() method in these handler s one by one
      • Contrary to the order we added in
      • C --> B --->A
    • These child handlers are added to the pipeline of channel on each client
  • Propagating channel Registration Completion Events
  • Propagating channelActive events
    • readIfAutoRead() completes the second registration of events of interest that netty can handle

In addition, our above writes are submitted in the form of timed tasks. When the only thread executor in CTX is used to execute tasks in three seconds, the program will continue to bind the ports, and after three seconds, the timed tasks will be aggregated into the common task queue, then the ctx. channel (). write ("Hello Wo") in our Outbound Handler B will be executed. Rld ";

What does the order of handler addition and execution of outBound type have to do with it?

Because events of the Outbound type are propagated from tail of the linked list, the order of execution is contrary to the order in which we add them.

It's too long. Rewrite and fill in a picture.

From ctx.channel().write("Hello World"); start with the source code, the mouse directly follow up, enter Channel Outbound Invoker, write to channel, we enter the implementation of Default Channel Pipeline, the source code is as follows

@Override
public final ChannelFuture write(Object msg) {
    return tail.write(msg);
}

Once again, the outbound event is passed forward from the tail. We know that the tail node is of Default Channel Handler Context type, so let's see how its write() method is implemented.

@Override
public ChannelFuture write(Object msg) {
    return write(msg, newPromise());
}

Among them, MSG - > we want to write the content of the client, the default promise() of newPromise().
Continue to follow up on this method write (msg, new Promise ()), the source code is as follows:

@Override
public ChannelFuture write(final Object msg, final ChannelPromise promise) {
    if (msg == null) {
        throw new NullPointerException("msg");
    }

    try {
        if (isNotValidPromise(promise, true)) {
            ReferenceCountUtil.release(msg);
            // cancelled
            return promise;
        }
    } catch (RuntimeException e) {
        ReferenceCountUtil.release(msg);
        throw e;
    }
    write(msg, false, promise);

    return promise;
}

A lot of judgments have been made, of which we only care about write(msg, false, promise); the source code is as follows:

private void write(Object msg, boolean flush, ChannelPromise promise) {
    AbstractChannelHandlerContext next = findContextOutbound();
    final Object m = pipeline.touch(msg, next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        if (flush) {
            next.invokeWriteAndFlush(m, promise);
        } else {
            next.invokeWrite(m, promise);
        }

As you can see, the important logic findContextOutbound(); its source code is as follows, traversing the list from the tail node to find the handler of the previous outbound type

private AbstractChannelHandlerContext findContextOutbound() {
    AbstractChannelHandlerContext ctx = this;
    do {
        ctx = ctx.prev;
    } while (!ctx.outbound);
    return ctx;
}

When we find it, because we use the function write instead of write AndFlush, we go to the above else block invokeWrite.

private void invokeWrite(Object msg, ChannelPromise promise) {
    if (invokeHandler()) {
        invokeWrite0(msg, promise);
    } else {
        write(msg, promise);
    }
}

Follow up on invokeWrite0(msg, promise); finally see handler's write logic

private void invokeWrite0(Object msg, ChannelPromise promise) {
    try {
        ((ChannelOutboundHandler) handler()).write(this, msg, promise);
    } catch (Throwable t) {
        notifyOutboundHandlerException(t, promise);
    }
}

Among them:

  • (Channel Outbound Handler) Handler () -- The node in front of tail
  • Call the write function of the current node

In fact, we call back our own add handler write function, we follow up, the source code is as follows:

public class OutBoundHandlerC extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println( "hello OutBoundHandlerC");
        ctx.write(msg, promise);
    }
}

We continue to call write, and msg will continue to pass forward with the same logic

Pass it all the way to the HeadContext node, because this node is also of Outbound type, which is the propagation of Outbound events. Let's see how the HeaderContext ends. The source code is as follows:

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    unsafe.write(msg, promise);
}

Header uses unsafe class, which is not a problem, and the operation related to data reading and writing is ultimately inseparable from unsafe.

Why is the Header node an outBound processor?

Take the above write event for example, msg is processed by so many handler s that the ultimate goal is to pass it to the client, so netty designs the header as an outBound type node, which completes the writing to the client.

The difference between context.write() and context.channel().write()

  • context.write(), which propagates forward from the current node
  • context.channel().write() propagates forward in turn from the tail node

Abnormal propagation

If an exception occurs in netty, the propagation of the exception event has nothing to do with whether the current node is an inbound or outbound processor. It propagates all the way to the next node. If there is no handler handling the exception, it is finally handled by the tail node.

The Best Solution to Exception Handling

Since the propagation of exceptions has nothing to do with the inbound and outbound processors, we can add our Unified Exception Processor at the end of the pipeline, that is, tail, just like the following:

public class MyServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
      // Best practices for todo exception handling, with exception handler added at the end of the pipeline
      channelPipeline.addLast(new myExceptionCaughtHandler());
    }
}

public class myExceptionCaughtHandler extends ChannelInboundHandlerAdapter {
// Eventually all the anomalies will come here.
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    if (cause instanceof Custom exception 1){
    }else if(cause instanceof  Custom exception 2){
    }
    // Don't propagate down below todo
     // super.exceptionCaught(ctx, cause);
}
}

Characteristics of SimpleChannel InboundHandler

Through the previous analysis, we know that if the msg of the client blindly propagates back, it will eventually propagate to the tail node, which will be released by the tail node, thus avoiding memory leak.

If our handler uses msg and does not pass it back, it will be unlucky. Memory leaks will occur over time.

The generic SimpleChannelInboundHandler <T> provided by netty's wayward words can automatically release memory for us. Let's see how he can do it.

/ todo Direct inheritance ChanelInboundHandlerAdapter Implementing abstract classes
// todo's own processor can also inherit the SimpleChannel InboundHandler adapter to achieve the same effect
public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter {

    private final TypeParameterMatcher matcher;
    private final boolean autoRelease;
    protected SimpleChannelInboundHandler() {
        this(true);
    }
    protected SimpleChannelInboundHandler(boolean autoRelease) {
        matcher = TypeParameterMatcher.find(this, SimpleChannelInboundHandler.class, "I");
        this.autoRelease = autoRelease;
    }

    protected SimpleChannelInboundHandler(Class<? extends I> inboundMessageType) {
        this(inboundMessageType, true);
    }

    protected SimpleChannelInboundHandler(Class<? extends I> inboundMessageType, boolean autoRelease) {
        matcher = TypeParameterMatcher.get(inboundMessageType);
        this.autoRelease = autoRelease;
    }

    public boolean acceptInboundMessage(Object msg) throws Exception {
        return matcher.match(msg);
    }

    // Todo ChannelRead has been completely rewritten
    // todo is actually a design pattern, template method design pattern.
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        boolean release = true;
        try {
            if (acceptInboundMessage(msg)) {
                @SuppressWarnings("unchecked")
                // todo turned the message around
                I imsg = (I) msg;
                //  todo channelRead0() is abstract in its parent class, so when we write handler ourselves, we need to rewrite this abstract method, as follows
                // todo is actually a design pattern, template method design pattern.
                channelRead0(ctx, imsg);
            } else {
                release = false;
                ctx.fireChannelRead(msg);
            }
        } finally {// todo decreases the msg count by one, which means that the reference to the message is decreases by one. That means we don't want to do anything about it.
            if (autoRelease && release) {
                ReferenceCountUtil.release(msg);
            }
        }
    }
    protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception;
}
  • It's an abstract class itself, and the abstract method is channelRead0, which means we need to rewrite this method.
  • He inherited ChannelInboundHandlerAdapter, an adapter class that allows him to implement only some of the methods he needs.

Let's look at the channelRead implementation. The template method design pattern mainly does the following three things.

  • Converting msg strongly to data of a specific generic type
  • Pass CTX and msg to your chanenlRead0 using MSG and ctx(ctx,msg)
    • chanenlRead0 uses msg and ctx
  • In the final code block, release msg

Posted by vladj on Fri, 19 Jul 2019 20:41:59 -0700