Netty's responsibility chain model filter chain implementation source code analysis

Keywords: Netty

2021SC@SDUSC

Let's analyze the details of outbound data propagation. Let's start with the write method of ChannelOutboundHandlerAdapter:

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

If you want to implement your own business processing logic, you need to inherit the ChannelOutboundHandlerAdapter and override the write method. After processing the data, you need to continue to transfer the data downward, that is, you need to call the ctx.write(msg, promise) method, which will call the equivalent method of AbstractChannelHandlerContext. The following is a method that calls the link:

    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);
           }
       } else {
           AbstractWriteTask task;
           if (flush) {
               task = WriteAndFlushTask.newInstance(next, m, promise);
           }  else {
               task = WriteTask.newInstance(next, m, promise);
           }
           safeExecute(executor, task, promise, m);
       }
   }

 

The key here is to find the next qualified handler through the findContextOutbound method:

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

Here, go back from the current handler along the prev to find the next outbound handler that is true. You may feel familiar with this method because it is very similar to the findContextInbound(MASK_CHANNEL_READ) method analyzed in the last blog. This method is used to find the next qualified ChannelHandler during inbound event propagation:

    private AbstractChannelHandlerContext findContextInbound(int mask) {
        AbstractChannelHandlerContext ctx = this;
        EventExecutor currentExecutor = executor();
        do {
            ctx = ctx.next;
        } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_INBOUND));
        return ctx;
    }

In fact, the principle of the write event is similar to the read event analyzed last time. The difference is that the direction and goal are different. The direction of the inbound event is next and the target is InboundHandler. The direction of outbound events is opposite to that of inbound events. It is prev direction, that is, back propagation. The target is OutboundHandler. Therefore, we can see from the code that the search is to look back in the direction of prev, that is, the propagation direction of outbound events is from back to front.

Here we understand the event propagation direction of ChannelPipeline. Now let's consider how to handle the inbound data if it is passed to the tail node and ends? What happens to outbound data if it is transferred to the head node?

Let's take a look at the invokeWriteAndFlush method. It has a method to judge the invokeHandler(). Here are its details:

    private boolean invokeHandler() {
       int handlerState = this.handlerState;
       return handlerState == ADD_COMPLETE || (!ordered && handlerState == ADD_PENDING);
   }

We focus on if handlerstate = = add_ When complete is true, the following method will be executed:

           invokeWrite0(msg, promise);


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

Let's consider when this judgment condition will hold. We need to come back here   On the constructors of HeadContext and TailContext:

        HeadContext(DefaultChannelPipeline pipeline) {
           super(pipeline, null, HEAD_NAME, false, true);
           unsafe = pipeline.channel().unsafe();
           setAddComplete();

       }
       
       TailContext(DefaultChannelPipeline pipeline) {
           super(pipeline, null, TAIL_NAME, true, false);
           setAddComplete();

       }        
    final void setAddComplete() {
       for (;;) {
           int oldState = handlerState;
           // Ensure we never update when the handlerState is REMOVE_COMPLETE already.
           // oldState is usually ADD_PENDING but can also be REMOVE_COMPLETE when an EventExecutor is used that is not
           // exposing ordering guarantees.
           if (oldState == REMOVE_COMPLETE || HANDLER_STATE_UPDATER.compareAndSet(this, oldState, ADD_COMPLETE)) {
               return;
           }
       }
   }

Here, we can see that the setAddComplete() method will be called after the two construction methods, and the code of this method will be pasted directly later. The meaning expressed here is to enter an infinite loop, and we will not change the update until the state of the oldState changes to REMOVE_COMPLETE. Then it will be executed   HANDLER_STATE_UPDATER.compareAndSet(this, oldState, ADD_COMPLETE) is an atomic operation that sets the handlerState to add_ COMPLETE. In this way, the tenable conditions of the above function are explained clearly.

Then let's look at the following operations:

​

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

​

Let's first look at the handler() method, which returns the current context. That is, the equivalent method of TailContext will be executed. For example, read will execute the channelRead method of TailContext:

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

    protected void onUnhandledInboundMessage(Object msg) {
       try {
           logger.debug(
                   "Discarded inbound message {} that reached at the tail of the pipeline. " +
                           "Please check your pipeline configuration.", msg);
       } finally {
           ReferenceCountUtil.release(msg);
       }
   }

You can see that after the message is delivered to the tail, it will not be delivered, and it will be released. The outbound event is similar. When the event is passed to the head, the equivalent method of HeadContext will be called, and then it will not be passed on.

Posted by alexszilagyi on Fri, 19 Nov 2021 11:45:54 -0800