Handler of Netty core concept (6)

1 Introduction

 This section introduces the third important concept in Netty - Handler, which was mentioned in the previous two sections, especially Channel and Handler are closely related. The design of the handler itself is very simple, but it plays a very important role. There are many implementations of handlers in Netty (handler is the business implementation that controls the various life cycles of socket io, netty implements many protocols, and naturally there are many handlers kind). This section does not care about the specific implementation of handlers with different functions, but mainly explains the design, function and usage of handlers.

2. Main Concepts

2.1 ChannelHandler

 This figure shows the basic structure of ChannelHandler, which has two subclass interfaces, ChannelInboundHandler and ChannelOutboundHandler. This is how the handlers I often mentioned before are divided into in and out types for different operations.

  handlerAdded(): Executed when the handler is added to the channel. This action is done by the add handler method of the pipeline. For the server, when the client connects, a handler is added to each channel through the read method of ServerBootstrapAcceptor. This method is the first method triggered by the handler.

  handlerRemoved(): Executed when the handler is removed from the channel. This action also exists in the pipeline. Basically, this step is performed when the channel is disconnected.

  exceptionCaught(): A class that captures exception handling in the channel life cycle, this method has been moved to ChannelInboundHandler. Obsolete.

 The annotation Sharable indicates that the handler can be reused by multiple pipelines.

2.2 ChannelInboundHandler

 This interface is mainly responsible for business logic. The main interface methods are as follows:

  channelRegistered(): Triggered when the channel is registered with the thread pool.

  channelUnregistered(): Triggered when the channel is closed.

  channelActive(): After the registered is completed and the channel processes the active state, the state is registered for the first time. Mainly see the register0(promise) method of AbstractChannel.

  channelnactive(): Executed before unregistered, mainly see the deregister method of AbstractChannel.

  channelRead(): This is mainly seen in the fireChannelRead method of the pipeline, which is called by the channel at the stage of acquiring the data, and then triggers the channelRead method of the handler.

  channelReadComplete(): This is the same as the read method above. The fireChannelReadComplete method is called when the read process is completed during the channel running.

  userEventTriggered(): This is also a method provided by the pipeline as the entry fireUserEventTriggered. This is to trigger an event. Take IdleStateHandler as an example, which is generally used as a heartbeat detection event, which is put into the thread pool for execution. If it is judged to be idle, this method will be triggered and transmitted to Each handler.

  channelWritabilityChanged(): This entry is also in the pipeline, but it doesn't seem to be used much. The channel does not call this method, and generally it is not used much.

  exceptionCaught(): This entry is also in the pipeline and is triggered when an exception is thrown by the reading process of the channel, of course not only this place.

 The above methods are traced to the source through the pipeline to trigger the entire execution chain. This process will be explained in detail when we talk about the context later. Just need to know that the entrance is in this place.

2.3 ChannelOutboundHandler

 Compared with in-type handlers, which tend to handle business logic, out-type handlers tend to handle connection logic. The following is the interface method definition for this type:

  bind(): The operation of binding the port

  connect(): connect to the remote address

  disconnect(): disconnect

  close(): the port to which the port is bound

  deregister(): deregister to the thread pool

  read(): Set to listen for read events

  write(): the operation of writing data

  flush(): flush the buffer and send it immediately.

 The above interfaces are all connection-related processing, and these are triggered by the related methods of the pipeline, and the tail object is finally called. In fact, this handler is rather tasteless, because the channel section has already explained that most of the implementation classes can't do anything, and they are finally handed over to the headContext object to call unsafe related methods and handle them. Most handlers only process the write method, and others are directly handed over to other handlers for processing. In my impression, Netty5 seemed to cancel the difference between in and out before, and outhandler was too tasteless (Netty5 has given up development. The official statement is that the development of the forkjoin framework is very complicated and is not ready for development).

2.4 ChannelHandlerContext

 We briefly mentioned the handlerContext before, this is used in conjunction with the pipeline to manage the handler chain. The handler itself does not hold an order relationship, it is all done through the handlerContext. The handlerContext itself will be called along the formed chain sequence by cooperating with the handler. This process will be explained in detail here.

 The interface methods will not be introduced one by one. What you see are some familiar methods. Call fireXXX() for the start of the business, and other methods will not be introduced. Like the pipeline, handlerContext has few implementation classes. The basic use is DefaultChannelHandlerContext, which inherits from AbstractChannelHandlerContext. Most of the methods are also in the abstract parent class. Here's how it works:

 Or go back to the process of constructing the handler chain of the pipeline:

    protected DefaultChannelPipeline(Channel channel) {
        this.channel = ObjectUtil.checkNotNull(channel, "channel");
        succeededFuture = new SucceededChannelFuture(channel, null);
        voidPromise =  new VoidChannelPromise(channel, true);

        tail = new TailContext(this);
        head = new HeadContext(this);

        head.next = tail;
        tail.prev = head;
    }

 When the pipeline is initialized, two handlers, tail and head, are built. These two are special and cannot be replaced.

    public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
        final AbstractChannelHandlerContext newCtx;
        synchronized (this) {
            checkMultiplicity(handler);

            newCtx = newContext(group, filterName(name, handler), handler);

            addLast0(newCtx);

            // If the registered is false it means that the channel was not registered on an eventloop yet.
            // In this case we add the context to the pipeline and add a task that will call
            // ChannelHandler.handlerAdded(...) once the channel is registered.
            if (!registered) {
                newCtx.setAddPending();
                callHandlerCallbackLater(newCtx, true);
                return this;
            }

            EventExecutor executor = newCtx.executor();
            if (!executor.inEventLoop()) {
                newCtx.setAddPending();
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        callHandlerAdded0(newCtx);
                    }
                });
                return this;
            }
        }
        callHandlerAdded0(newCtx);
        return this;
    }

  private void addLast0(AbstractChannelHandlerContext newCtx) {
      AbstractChannelHandlerContext prev = tail.prev;
      newCtx.prev = prev;
      newCtx.next = tail;
      prev.next = newCtx;
      tail.prev = newCtx;
  }

 First determine whether the handler has been loaded by other pipelines, whether it is a sharable type, and make relevant settings. Create context through handler. Then insert it in front of the tail, followed by the method triggered after adding the handler, which triggers the handlerAdd method. Through the addLast0 method, you can see that the parameters prev and next in the context are initialized. That is to say, the previous and next context can be found through the current context. Let's extract one of the methods of the handler interface, let's study how the handler chain is executed.

    public ChannelHandlerContext fireChannelActive() {
        invokeChannelActive(findContextInbound());
        return this;
    }

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

    private void invokeChannelActive() {
        if (invokeHandler()) {
            try {
                ((ChannelInboundHandler) handler()).channelActive(this);
            } catch (Throwable t) {
                notifyHandlerException(t);
            }
        } else {
            fireChannelActive();
        }
    }

  private AbstractChannelHandlerContext findContextInbound() {
      AbstractChannelHandlerContext ctx = this;
      do {
          ctx = ctx.next;
      } while (!ctx.inbound);
      return ctx;
  }

 Any method of handler is chained by three such methods in the context. The static methods are all called for the pipeline, and their input parameters are head, the head node of the handler chain. The invokeChannelActive() method is called, and here is where the content of our handler method is triggered. At first glance, it seems that it is gone after the execution. This place needs the cooperation of the handler to continue. Just find a handler, such as ChannelInboundHandlerAdapter, its related methods call ctx.fireChannelActive(); here it is connected, the fireChannelActive of ctx is not just returned to the invokeChannelActive() method. Its input parameter is no longer the current context, but finds the next in-type handler through findContextInbound, which also shows that this interface is an in-type interface. So a basic loop is ctx.invokeChannelActive(ctx) ->ctx.invokeChannelActive() -> hander.channelActive()->ctx.fireChannelActive(ctx)->findContextInbound()->ctx.invokeChannelActive(ctx). In such a circular chain, the starting point is invokeChannelActive(head) of the pipeline, and the end point is that when handler.channelActive() does not call ctx.channel, the last tailContext does not perform any operation, so the link is broken when it is executed.

3. Postscript

 This section describes a basic content of the handler, mainly explains the execution process of the handlerContext, and clarifies that the entry of the call chain is the pipeline, and the execution of the chain is guaranteed through the control of the pipeline and the context. The life cycle control pipeline of the channel can control the execution of the entire handler. The following figure gives a specific description:

  The above figure summarizes the content of the previous chapters. This is the entire core calling process, but the threading problem has not been set, so the startup class and thread related are not added to the figure. The next chapter will analyze Netty's threading model.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325021245&siteId=291194637