Channel pipeline of netty source code analysis — propagation of outbound events

Time:2020-6-29

In the previous article, we combed the propagation of inbound events in channelpipeline. In this article, we looked at the propagation of outbound events, that is, the implementation of the channeloutbound handler interface.

1. Example of propagation of outbound events

We modified the sample code in the previous article, and added the channel outbound handler outbound implementation in the channel pipeline

public class ServerApp {
    public static void main(String[] args) {
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup work = new NioEventLoopGroup(2);
        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(boss, work).channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ChannelPipeline p = ch.pipeline();
                            // p.addLast(new LoggingHandler(LogLevel.INFO));
                            //Add custom channelhandler to channelpipeline
                            p.addLast(new OutHandlerA());
                            p.addLast(new ServerHandlerA());
                            p.addLast(new ServerHandlerB());
                            p.addLast(new ServerHandlerC());
                            p.addLast(new OutHandlerB());
                            p.addLast(new OutHandlerC());
                        
                        }
                    });
            bootstrap.bind(8050).sync();

        } catch (Exception e) {
            // TODO: handle exception
        }

    }

}

public class OutHandlerA extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        System.err.println(this.getClass().getName()+msg);
        ctx.writeAndFlush((ByteBuf)msg);
    }
}

public class OutHandlerB extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx,Object msg,ChannelPromise promise) {
        System.out.println(this.getClass().getName()+msg);    
        ctx.write((ByteBuf)msg);
    }
}

public class OutHandlerC extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx,Object msg,ChannelPromise promise) {
        System.out.println(this.getClass().getName()+"--"+msg);    
        ctx.write((ByteBuf)msg);
    }
}

Then we execute the CTX write method in the channelread method of serverhandler a to simulate the occurrence of message outbound events.

public class ServerHandlerA  extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object object) {
        ByteBuf byteBuf = PooledByteBufAllocator.DEFAULT.buffer();
        byteBuf.writeByte(1);
        byteBuf.writeByte(2);
        ctx.channel().write(byteBuf);    
        //ctx.write(byteBuf);
    }
}

There are two ways to call the write method in the channelread method above ctx.channel (). Write and ctx.write What is the difference between the two methods? Let’s first look at the results of the two methods

ctx.channel().write

io.netty.example.echo.my.OutHandlerC--PooledUnsafeDirectByteBuf(ridx: 0, widx: 2, cap: 256)
io.netty.example.echo.my.OutHandlerB--PooledUnsafeDirectByteBuf(ridx: 0, widx: 2, cap: 256)
io.netty.example.echo.my.OutHandlerA--PooledUnsafeDirectByteBuf(ridx: 0, widx: 2, cap: 256)

 ctx.write

io.netty.example.echo.my.OutHandlerA--PooledUnsafeDirectByteBuf(ridx: 0, widx: 2, cap: 256)

You can see that when the ctx.channel (). Write, the order in which messages are propagated in the pipeline is from the tail all the way to the top-level outboundhandler; and ctx.write It looks forward to the outboundhandler from its handler.

So, is the difference between the two methods as shown in the results? Let’s start to analyze the internal implementation of these two methods

2. Analysis of outbound event propagation

ctx.channel (). Write and ctx.write The write methods of abstractchannel and abstractchannelhandlercontext are used respectively

Write method of abstractchannel

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

Write method of abstractchannelhandlercontext

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

In the above code, the wirete method of abstractchannel finally calls the write method of pipeline. When we enter the pipeline, we can see that the write method of pipeline starts to call from the tail abstractchannelhandlercontext node by default.

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

Continue to trace down and finally they call the write method of abstractchannelhandlercontext. Let’s look at the specific implementation inside the method.

private void write(Object msg, boolean flush, ChannelPromise promise) {
        ObjectUtil.checkNotNull(msg, "msg");
        try {
            If (isnotvalidpromise (promise, true)) {// check if channelpromise is valid
                ReferenceCountUtil.release(msg);
                // cancelled
                return;
            }
        } catch (RuntimeException e) {
            ReferenceCountUtil.release(msg);
            throw e;
        }

        //Find the previous abstractchannelhandlercontext node
        AbstractChannelHandlerContext next = findContextOutbound();
        final Object m = pipeline.touch(msg, next);
        EventExecutor executor = next.executor();
        if ( executor.inEventLoop ()) {// is it consistent with the current thread
            If (flush) {// determine whether to flush the data to the remote node
                next.invokeWriteAndFlush(m, promise);
            } else {
                next.invokeWrite(m, promise);
            }
        }Else {// if it is not consistently encapsulated as a writetask task thread
            final AbstractWriteTask task;
            if (flush) {
                task = WriteAndFlushTask.newInstance(next, m, promise);
            }  else {
                task = WriteTask.newInstance(next, m, promise);
            }
            //The thread task is handed over to the corresponding event executor for execution
            if (!safeExecute(executor, task, promise, m)) {
                // We failed to submit the AbstractWriteTask. We need to cancel it so we decrement the pending bytes
                // and put it back in the Recycler for re-use later.
                //
                // See https://github.com/netty/netty/issues/8343.
                task.cancel();
            }
        }
    }

Focus on findcontextoutbound(), which is used to get prev of the previous node of the current abstractchannelhandlercontext node

private AbstractChannelHandlerContext findContextOutbound() {
        AbstractChannelHandlerContext ctx = this;
        do {
            ctx =  ctx.prev ; // gets the previous node of the current node
        } while (! ctx.outbound ); // determine whether it is an outbound node
        return ctx;
    }

Finally passed next.invokeWrite (m, promise) callback method to call the write method of channeloutboundhandler encapsulated in the next node, so as to realize the transmission of write method event

private void invokeWrite(Object msg, ChannelPromise promise) {
        If (invokehandler()) {// judge whether the current channeloutboundhandler has been added to the pipeline (triggered by the handleradded event)
            invokeWrite0(msg, promise);
        } else {
            write(msg, promise);
        }
    }

    private boolean invokeHandler() {
        // Store in local variable to reduce volatile reads.
        int handlerState = this.handlerState;
        return handlerState == ADD_COMPLETE || (!ordered && handlerState == ADD_PENDING);
    }

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

By now, the whole outbound event propagation process has been basically clear. The wirte method itself is a process of finding and calling back the wirte method in the next node.

3. Write and writeandflush

In the above code, you can see that these two methods mainly depend on whether the flush method will be executed after the write method is executed.

private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
        If (invokehandler()) {// call callback method or not
            //Call the write and flush callback methods, and finally call the corresponding implementation of the custom handler
            invokeWrite0(msg, promise);
            invokeFlush0();
        } else {
            writeAndFlush(msg, promise);
        }
    }

It should be noted here that invokeflush0() is executed after invokewrite0, that is, flush is called to flush the data to the remote node after the outbound message event is delivered. Simple understanding is that whether you call writeAndFlush in OutHandlerA, OutHandlerB or OutHandlerC, you end up with flush data after the write event is completed.

At the same time, we need to note that when the write and flush events are passed up from outhandler a, the last node of outhandler A is the header node headcontext of pipeline. Let’s take a look at the implementation of write and flush methods of headcontext;

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

        @Override
        public void flush(ChannelHandlerContext ctx) {
            unsafe.flush();
        }

From here, we can see that the real queuing and sending of messages is ultimately realized through the write and flush methods of headcontext.

 

Through the above analysis, we can see the propagation process of pipeline outbound events. At the same time, we need to pay attention to ctx.write And ctx.channel (). The difference between write and message sending is realized by calling the unsafe write and flush methods by the header node. If there is any deficiency or incorrect, please point out that it is the same as Haihan.

 

WeChat official account for more technical articles.