netty series: detailed explanation of channelHandlerContext

brief introduction

We know that ChannelHandler has two very important sub interfaces, channeloutbound handler and channelinbound handler. Basically, these two handler interfaces define the processing logic of all channel inbound and outbound.

Whether ChannelHandler, ChannelOutboundHandler or ChannelInboundHandler, almost all of their methods have a ChannelHandlerContext parameter. What is the purpose of this ChannelHandlerContext? What does it have to do with handler and channel?

ChannelHandlerContext and its application

Friends who are familiar with netty should have contacted ChannelHandlerContext. If not, here is a simple example of handler:

public class ChatServerHandler extends SimpleChannelInboundHandler<String> {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("accepted channel: {}", ctx.channel());
        log.info("accepted channel parent: {}", ctx.channel().parent());
        // channel active
        ctx.write("Channel Active state!\r\n");
        ctx.flush();
    }
}

The handler here inherits SimpleChannelInboundHandler and only needs to implement the corresponding method. What is implemented here is the channelActive method. In the channelActive method, a ChannelHandlerContext parameter is passed in. We can call some of its methods by using ChannelHandlerContext.

Let's take a look at the definition of ChannelHandlerContext:

public interface ChannelHandlerContext extends AttributeMap, ChannelInboundInvoker, ChannelOutboundInvoker {

First, ChannelHandlerContext is an AttributeMap, which can be used to store multiple data.

Then, ChannelHandlerContext inherits ChannelInboundInvoker and channeloutbooundinvoker, which can trigger some methods of inbound and outbound.

In addition to some inherited methods, ChannelHandlerContext can also serve as a communication bridge between channel, handler and pipline, because the corresponding channel, handler and pipline can be obtained from ChannelHandlerContext:

Channel channel();
ChannelHandler handler();
ChannelPipeline pipeline();

It should also be noted that the ChannelHandlerContext also returns an EventExecutor to perform specific tasks:

EventExecutor executor();

Next, let's take a specific look at the implementation of ChannelHandlerContext.

AbstractChannelHandlerContext

AbstractChannelHandlerContext is a very important implementation of ChannelHandlerContext. Although AbstractChannelHandlerContext is an abstract class, it basically implements all the functions of ChannelHandlerContext.

First, let's take a look at the definition of AbstractChannelHandlerContext:

abstract class AbstractChannelHandlerContext implements ChannelHandlerContext, ResourceLeakHint

AbstractChannelHandlerContext is a concrete implementation of ChannelHandlerContext.

Generally speaking, a handler corresponds to a ChannelHandlerContext, but there may be more than one handler in a program, so how to get other handlers in one handler?

In the AbstractChannelHandlerContext, there are two same types of AbstractChannelHandlerContext: next and prev, so that multiple abstractchannelhandlercontexts can build a two-way linked list. Thus, other channelhandlercontexts can be obtained in one ChannelHandlerContext, so as to obtain the handler processing chain.

    volatile AbstractChannelHandlerContext next;
    volatile AbstractChannelHandlerContext prev;

pipeline and executor in AbstractChannelHandlerContext are passed in through constructor:

    AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor,
                                  String name, Class<? extends ChannelHandler> handlerClass) {
        this.name = ObjectUtil.checkNotNull(name, "name");
        this.pipeline = pipeline;
        this.executor = executor;
        this.executionMask = mask(handlerClass);
        // Its ordered if its driven by the EventLoop or the given Executor is an instanceof OrderedEventExecutor.
        ordered = executor == null || executor instanceof OrderedEventExecutor;
    }

Some friends may have questions. How do you get the channel and handler in the ChannelHandlerContext?

For channel, it is obtained through pipeline:

public Channel channel() {
        return pipeline.channel();
    }

For the handler, it is not implemented in the AbstractChannelHandlerContext and needs to be implemented in the class that inherits the AbstractChannelHandlerContext.

For the EventExecutor, you can pass in a new EventExecutor to the abstract channel handlercontext through the constructor. If it is not passed in or is empty, the EventLoop:

    public EventExecutor executor() {
        if (executor == null) {
            return channel().eventLoop();
        } else {
            return executor;
        }
    }

Because EventLoop inherits from OrderedEventExecutor, it is also an EventExecutor.

EventExecutor is mainly used to asynchronously submit tasks for execution. In fact, almost all methods from ChannelInboundInvoker and channeloutbooundinvoker in ChannelHandlerContext are executed through EventExecutor.

For ChannelInboundInvoker, take the method fireChannelRegistered as an example:

    public ChannelHandlerContext fireChannelRegistered() {
        invokeChannelRegistered(findContextInbound(MASK_CHANNEL_REGISTERED));
        return this;
    }

    static void invokeChannelRegistered(final AbstractChannelHandlerContext next) {
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeChannelRegistered();
        } else {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    next.invokeChannelRegistered();
                }
            });
        }
    }

fireChannelRegistered calls the invokeChannelRegistered method, while invokeChannelRegistered calls the execute method of EventExecutor to encapsulate the real calling logic into a runnable class for execution.

Notice that when you call executor The execute method is preceded by a judgment of whether the executor is in the eventLoop. If the executor is already in the eventLoop, you can execute the task directly without enabling a new thread.

For ChannelOutboundInvoker, let's take the bind method as an example to see how the EventExecutor is used:

    public ChannelFuture bind(final SocketAddress localAddress, final ChannelPromise promise) {
        ObjectUtil.checkNotNull(localAddress, "localAddress");
        if (isNotValidPromise(promise, false)) {
            // cancelled
            return promise;
        }

        final AbstractChannelHandlerContext next = findContextOutbound(MASK_BIND);
        EventExecutor executor = next.executor();
        if (executor.inEventLoop()) {
            next.invokeBind(localAddress, promise);
        } else {
            safeExecute(executor, new Runnable() {
                @Override
                public void run() {
                    next.invokeBind(localAddress, promise);
                }
            }, promise, null, false);
        }
        return promise;
    }

You can see that the execution logic is very similar to the invokeChannelRegistered method. It is also to judge whether the executor is in the eventLoop first. If it is, it will be executed directly. If not, it will be executed in the executor.

In the above two examples, the corresponding methods of next are called, which are next Invokechannelregistered and next invokeBind.

We know that ChannelHandlerContext is only an encapsulation, which does not have much business logic, so the corresponding methods called by next are actually the business logic in ChannelInboundHandler and ChannelOutboundHandler encapsulated in Context, as shown below:

    private void invokeUserEventTriggered(Object event) {
        if (invokeHandler()) {
            try {
                ((ChannelInboundHandler) handler()).userEventTriggered(this, event);
            } catch (Throwable t) {
                invokeExceptionCaught(t);
            }
        } else {
            fireUserEventTriggered(event);
        }
    }
    private void invokeBind(SocketAddress localAddress, ChannelPromise promise) {
        if (invokeHandler()) {
            try {
                ((ChannelOutboundHandler) handler()).bind(this, localAddress, promise);
            } catch (Throwable t) {
                notifyOutboundHandlerException(t, promise);
            }
        } else {
            bind(localAddress, promise);
        }
    }

Therefore, it can be seen from AbstractChannelHandlerContext that the methods defined in the ChannelHandlerContext interface are the specific implementation of the called handler, and the Context is only the encapsulation of the handler.

DefaultChannelHandlerContext

DefaultChannelHandlerContext is a concrete implementation of AbstractChannelHandlerContext.

When explaining AbstractChannelHandlerContext, we mentioned that there is no specific handler implementation defined in AbstractChannelHandlerContext, which is implemented in DefaultChannelHandlerContext.

DefaultChannelHandlerContext is very simple. Let's take a look at its specific implementation:

final class DefaultChannelHandlerContext extends AbstractChannelHandlerContext {

    private final ChannelHandler handler;

    DefaultChannelHandlerContext(
            DefaultChannelPipeline pipeline, EventExecutor executor, String name, ChannelHandler handler) {
        super(pipeline, executor, name, handler.getClass());
        this.handler = handler;
    }

    @Override
    public ChannelHandler handler() {
        return handler;
    }
}

DefaultChannelHandlerContext provides an additional ChannelHandler property to store the incoming ChannelHandler.

To this DefaultChannelHandlerContext, you can pass in all necessary handlers, channels, pipeline s and eventexecutors in the ChannelHandlerContext.

summary

In this section, we introduced ChannelHandlerContext and its basic implementations. We learned that ChannelHandlerContext is the encapsulation of handler, channel and pipline. The business logic in ChannelHandlerContext actually calls the corresponding method of the underlying handler. This is also the method we need to implement in the custom handler.

Added by Paul1893 on Wed, 02 Mar 2022 08:53:23 +0200