Detailed explanation of the execution order case of Pipeline's ChannelHandler in netty

Posted by alexdoug on Fri, 25 Oct 2019 12:43:24 +0200

I. Pipeline model of netty

Netty's Pipeline model uses the responsibility chain design pattern. When the boss thread monitors that there is an accept event on the binding port, it instantiates the Pipeline for the socket connection, loads the InboundHandler and OutboundHandler into the Pipeline in order, and then mounts the socket connection (that is, the Channel object) to the selector. A selector corresponds to a thread. The thread will poll all socket connections attached to it for read or write events, and then execute the Pipeline business flow through the thread pool. How the selector queries which socket connections have read or write events depends on which IO multiplexing kernel of the operating system is called. If it is select (note that select here refers to the select IO multiplexing of the operating system kernel, not the netlity seletor object), then it will traverse all socket connections, ask whether there are read or write events in turn, and finally the operating system. The unified kernel returns socket connections of all IO events to the netty process. When there are many socket connections, this method will greatly reduce performance, because there are a lot of traversal of socket connections and copies of kernel memory. If it is epoll, the performance will be greatly improved, because it has maintained the socket connection list with IO events based on the completion of port events, and the selector can take it directly without traversal, and also reduce the performance loss caused by kernel memory copy.

The Pipeline's responsibility chain is connected in series through the ChannelHandlerContext object. The ChannelHandlerContext object encapsulates the ChannelHandler object. The two-way linked list is realized through prev and next nodes. The first and last nodes of Pipeline are head and tail. When the selector polls the socket for read event, the Pipeline responsibility chain will be triggered. The ChannelRead event of the first InboundHandler will be called from head, and then the next ChannelHandler on the Pipeline will be triggered successively through fire method, as shown in the following figure:

ChannelHandler is divided into InbounHandler and OutboundHandler. InboundHandler is used to process received messages and OutboundHandler is used to process sent messages. Head's ChannelHandler is both an InboundHandler and an OutboundHandler. Both read and write will pass through head. Therefore, head encapsulates the unsafe method to operate the read and write of socket. The channel handler of tail is just the InboundHandler, and the pipeline processing of read will finally arrive at tail.

2. Verify the execution order of InboundHandler and OutboundHandler through six groups of experiments

Before doing the experiment, paste out the experiment code.

EchoServer class:

 1 package com.wisdlab.nettylab;
 2 
 3 import io.netty.bootstrap.ServerBootstrap;
 4 import io.netty.channel.ChannelFuture;
 5 import io.netty.channel.ChannelInitializer;
 6 import io.netty.channel.ChannelOption;
 7 import io.netty.channel.EventLoopGroup;
 8 import io.netty.channel.nio.NioEventLoopGroup;
 9 import io.netty.channel.socket.SocketChannel;
10 import io.netty.channel.socket.nio.NioServerSocketChannel;
11 
12 /**
13  * @ClassName EchoServer
14  * @Description TODO
15  * @Author felix
16  * @Date 2019/9/26 10:37
17  * @Version 1.0
18  **/
19 public class EchoServer {
20     private int port;
21 
22     public EchoServer(int port) {
23         this.port = port;
24     }
25 
26     private void run() {
27         EventLoopGroup bossGroup = new NioEventLoopGroup();
28         EventLoopGroup workGroup = new NioEventLoopGroup();
29 
30         try {
31             ServerBootstrap serverBootstrap = new ServerBootstrap();
32             serverBootstrap.group(bossGroup, workGroup)
33                     .channel(NioServerSocketChannel.class)
34                     .childHandler(new ChannelInitializer<SocketChannel>() {
35                         @Override
36                         protected void initChannel(SocketChannel socketChannel) throws Exception {
37                             //outboundhandler Make sure it's the last one inboundhandler before
38                             //otherwise outboundhandler Will not be executed to
39                             socketChannel.pipeline().addLast(new EchoOutboundHandler3());
40                             socketChannel.pipeline().addLast(new EchoOutboundHandler2());
41                             socketChannel.pipeline().addLast(new EchoOutboundHandler1());
42 
43                             socketChannel.pipeline().addLast(new EchoInboundHandler1());
44                             socketChannel.pipeline().addLast(new EchoInboundHandler2());
45                             socketChannel.pipeline().addLast(new EchoInboundHandler3());
46                         }
47                     })
48                     .option(ChannelOption.SO_BACKLOG, 10000)
49                     .childOption(ChannelOption.SO_KEEPALIVE, true);
50             System.out.println("EchoServer Starting up.");
51 
52             ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
53             System.out.println("EchoServer Binding port" + port);
54 
55             channelFuture.channel().closeFuture().sync();
56             System.out.println("EchoServer Closed.");
57         } catch (Exception e) {
58             e.printStackTrace();
59         } finally {
60             bossGroup.shutdownGracefully();
61             workGroup.shutdownGracefully();
62         }
63     }
64 
65     public static void main(String[] args) {
66         int port = 8080;
67         if (args != null && args.length > 0) {
68             try {
69                 port = Integer.parseInt(args[0]);
70             } catch (Exception e) {
71                 e.printStackTrace();
72             }
73         }
74 
75         EchoServer server = new EchoServer(port);
76         server.run();
77     }
78 }

EchoInboundHandler1 class:

 1 package com.wisdlab.nettylab;
 2 
 3 import io.netty.buffer.ByteBuf;
 4 import io.netty.buffer.Unpooled;
 5 import io.netty.channel.ChannelHandlerContext;
 6 import io.netty.channel.ChannelInboundHandlerAdapter;
 7 import io.netty.util.CharsetUtil;
 8 
 9 /**
10  * @ClassName EchoInboundHandler1
11  * @Description TODO
12  * @Author felix
13  * @Date 2019/9/26 11:15
14  * @Version 1.0
15  **/
16 public class EchoInboundHandler1 extends ChannelInboundHandlerAdapter {
17     @Override
18     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
19         System.out.println("Get into EchoInboundHandler1.channelRead");
20 
21         String data = ((ByteBuf)msg).toString(CharsetUtil.UTF_8);
22         System.out.println("EchoInboundHandler1.channelRead Data received:" + data);
23         ctx.fireChannelRead(Unpooled.copiedBuffer("[EchoInboundHandler1] " + data, CharsetUtil.UTF_8));
24 
25         System.out.println("Sign out EchoInboundHandler1 channelRead");
26     }
27 
28     @Override
29     public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
30         System.out.println("[EchoInboundHandler1.channelReadComplete]");
31     }
32 
33     @Override
34     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
35         System.out.println("[EchoInboundHandler1.exceptionCaught]" + cause.toString());
36     }
37 }

Echoinboundhandler class 2:

 1 package com.wisdlab.nettylab;
 2 
 3 import io.netty.buffer.ByteBuf;
 4 import io.netty.buffer.Unpooled;
 5 import io.netty.channel.ChannelHandlerContext;
 6 import io.netty.channel.ChannelInboundHandlerAdapter;
 7 import io.netty.util.CharsetUtil;
 8 
 9 /**
10  * @ClassName EchoInboundHandler2
11  * @Description TODO
12  * @Author felix
13  * @Date 2019/9/27 15:35
14  * @Version 1.0
15  **/
16 public class EchoInboundHandler2 extends ChannelInboundHandlerAdapter {
17     @Override
18     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
19         System.out.println("Get into EchoInboundHandler2.channelRead");
20 
21         String data = ((ByteBuf) msg).toString(CharsetUtil.UTF_8);
22         System.out.println("EchoInboundHandler2.channelRead Data received:" + data);
23         //ctx.writeAndFlush(Unpooled.copiedBuffer("[For the first time write] [EchoInboundHandler2] " + data, CharsetUtil.UTF_8));
24         ctx.channel().writeAndFlush(Unpooled.copiedBuffer("Test it. channel().writeAndFlush", CharsetUtil.UTF_8));
25         ctx.fireChannelRead(Unpooled.copiedBuffer("[EchoInboundHandler2] " + data, CharsetUtil.UTF_8));
26 
27         System.out.println("Sign out EchoInboundHandler2 channelRead");
28     }
29 
30     @Override
31     public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
32         System.out.println("[EchoInboundHandler2.channelReadComplete]Read data complete");
33     }
34 
35     @Override
36     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
37         System.out.println("[EchoInboundHandler2.exceptionCaught]");
38     }
39 }

Echoinboundhandler class 3:

 1 package com.wisdlab.nettylab;
 2 
 3 import io.netty.buffer.ByteBuf;
 4 import io.netty.buffer.Unpooled;
 5 import io.netty.channel.ChannelHandlerContext;
 6 import io.netty.channel.ChannelInboundHandlerAdapter;
 7 import io.netty.util.CharsetUtil;
 8 
 9 /**
10  * @ClassName EchoInboundHandler3
11  * @Description TODO
12  * @Author felix
13  * @Date 2019/10/23 13:43
14  * @Version 1.0
15  **/
16 public class EchoInboundHandler3 extends ChannelInboundHandlerAdapter {
17     @Override
18     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
19         System.out.println("Get into EchoInboundHandler3.channelRead");
20 
21         String data = ((ByteBuf)msg).toString(CharsetUtil.UTF_8);
22         System.out.println("EchoInboundHandler3.channelRead Data received:" + data);
23         //ctx.writeAndFlush(Unpooled.copiedBuffer("[The second time write] [EchoInboundHandler3] " + data, CharsetUtil.UTF_8));
24         ctx.fireChannelRead(msg);
25 
26         System.out.println("Sign out EchoInboundHandler3 channelRead");
27     }
28 
29     @Override
30     public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
31         System.out.println("[EchoInboundHandler3.channelReadComplete]Read data complete");
32     }
33 
34     @Override
35     public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
36         System.out.println("[EchoInboundHandler3.exceptionCaught]");
37     }
38 
39 
40 }

EchoOutboundHandler1 class:

 1 package com.wisdlab.nettylab;
 2 
 3 import io.netty.buffer.Unpooled;
 4 import io.netty.channel.ChannelHandlerContext;
 5 import io.netty.channel.ChannelOutboundHandlerAdapter;
 6 import io.netty.channel.ChannelPromise;
 7 import io.netty.util.CharsetUtil;
 8 
 9 /**
10  * @ClassName EchoOutboundHandler1
11  * @Description TODO
12  * @Author felix
13  * @Date 2019/9/27 15:36
14  * @Version 1.0
15  **/
16 public class EchoOutboundHandler1 extends ChannelOutboundHandlerAdapter {
17     @Override
18     public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
19         System.out.println("Get into EchoOutboundHandler1.write");
20 
21         //ctx.writeAndFlush(Unpooled.copiedBuffer("[For the first time write Medium write]", CharsetUtil.UTF_8));
22         ctx.channel().writeAndFlush(Unpooled.copiedBuffer("stay OutboundHandler Let's test it channel().writeAndFlush", CharsetUtil.UTF_8));
23         ctx.write(msg);
24 
25         System.out.println("Sign out EchoOutboundHandler1.write");
26     }
27 }

EchoOutboundHandler2 class:

 1 package com.wisdlab.nettylab;
 2 
 3 import io.netty.buffer.Unpooled;
 4 import io.netty.channel.ChannelHandlerContext;
 5 import io.netty.channel.ChannelOutboundHandlerAdapter;
 6 import io.netty.channel.ChannelPromise;
 7 import io.netty.util.CharsetUtil;
 8 
 9 /**
10  * @ClassName EchoOutboundHandler2
11  * @Description TODO
12  * @Author felix
13  * @Date 2019/9/27 15:36
14  * @Version 1.0
15  **/
16 public class EchoOutboundHandler2 extends ChannelOutboundHandlerAdapter {
17 
18     @Override
19     public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
20         System.out.println("Get into EchoOutboundHandler2.write");
21 
22         //ctx.writeAndFlush(Unpooled.copiedBuffer("[The second time write Medium write]", CharsetUtil.UTF_8));
23         ctx.write(msg);
24 
25         System.out.println("Sign out EchoOutboundHandler2.write");
26     }
27 }

EchoOutboundHandler3 class:

 1 package com.wisdlab.nettylab;
 2 
 3 import io.netty.channel.ChannelHandlerContext;
 4 import io.netty.channel.ChannelOutboundHandlerAdapter;
 5 import io.netty.channel.ChannelPromise;
 6 
 7 /**
 8  * @ClassName EchoOutboundHandler3
 9  * @Description TODO
10  * @Author felix
11  * @Date 2019/10/23 23:23
12  * @Version 1.0
13  **/
14 public class EchoOutboundHandler3 extends ChannelOutboundHandlerAdapter {
15     @Override
16     public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
17         System.out.println("Get into EchoOutboundHandler3.write");
18 
19         ctx.write(msg);
20 
21         System.out.println("Sign out EchoOutboundHandler3.write");
22     }
23 
24 }

Experiment 1: can subsequent inboundhandlers execute in sequence without triggering the fire method in InboundHandler?

As shown in the figure above, InboundHandler2 did not call the fire method:

1     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
2         System.out.println("Get into EchoInboundHandler1.channelRead");
3 
4         String data = ((ByteBuf)msg).toString(CharsetUtil.UTF_8);
5         System.out.println("EchoInboundHandler1.channelRead Data received:" + data);
6         //ctx.fireChannelRead(Unpooled.copiedBuffer("[EchoInboundHandler1] " + data, CharsetUtil.UTF_8));
7 
8         System.out.println("Sign out EchoInboundHandler1 channelRead");
9     }

Will the code in InboundHandler still be executed? Take a look at the execution results:

As can be seen from the above figure, InboundHandler2 did not call the fire event and InboundHandler3 was not executed.

Conclusion: the InboundHandler determines whether to execute the next InboundHandler through the fire event. If any InboundHandler does not call the fire event, the Pipeline will be broken later.

Experiment 2: what is the execution order of InboundHandler and OutboundHandler?

The order of adding Pipeline's ChannelHandler is shown in the figure above. What's the final execution order? The results are as follows:

As can be seen from the above figure, the execution sequence is as follows:

InboundHandler1 => InboundHandler2 => OutboundHandler1 => OutboundHander2 => OutboundHandler3 => InboundHandler3

Therefore, we have the following conclusions:

1. The InboundHandler is executed according to the loading order of pipeline.

2. The OutboundHandler is executed in reverse order according to the loading order of Pipeline.

Experiment 3: if you put the OutboundHandler after the InboundHandler, will the OutboundHandler execute?

The results are as follows:

Thus, the OutboundHandler is not executed. Why? Because pipeline is to execute all valid inboundhandlers, and then return the OutboundHandler executed before the last InboundHandler. Note that valid InboundHandler refers to the InboundHandler reached by the fire event. If an InboundHandler does not call the fire event, the following inboundhandlers are all invalid inboundhandlers. To prove this, we continue to do an experiment. We put one of the outboundhandlers before the last valid InboundHandler to see whether the only OutboundHandler will execute and other outboundhandlers will not.

The results are as follows:

It can be seen that only OutboundHandler1 is executed, and other outboundhandlers are not executed.

Therefore, we have the following conclusions:

1. Valid InboundHandler refers to the last InboundHandler that can be reached through fire event.

2. If you want all the outboundhandlers to be executed, you must put the OutboundHandler before the last valid InboundHandler.

3. The recommended method is to load all outboundhandlers through addFirst, and then load all inboundhandlers through addLast.

Experiment 4: if one of the OutboundHandler does not execute the write method, will the message be sent?

Let's note out the write method of OutboundHandler2.

1     public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
2         System.out.println("Get into EchoOutboundHandler3.write");
3 
4         //ctx.write(msg);
5 
6         System.out.println("Sign out EchoOutboundHandler3.write");
7     }

The results are as follows:

As you can see, OutboundHandler3 has not been executed to, and the client has not received the sent message.

Therefore, we have the following conclusions:

1. The OutboundHandler implements the concatenation of Pipeline through the write method.

2. If the OutboundHandler is on the Pipeline processing chain and one of the outboundhandlers does not call the write method, the final message will not be sent out.

Experiment 5: what is the execution order of the outbound handler of ctx.writeAndFlush?

We set the loading order of ChannelHandler in Pipeline as follows:

OutboundHandler3 => InboundHandler1 => OutboundHandler2 => InboundHandler2 => OutboundHandler1 => InboundHandler3

Call ctx.writeAndFlush in InboundHander2:

 1     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
 2         System.out.println("Get into EchoInboundHandler2.channelRead");
 3 
 4         String data = ((ByteBuf) msg).toString(CharsetUtil.UTF_8);
 5         System.out.println("EchoInboundHandler2.channelRead Data received:" + data);
 6         ctx.writeAndFlush(Unpooled.copiedBuffer("[For the first time write] [EchoInboundHandler2] " + data, CharsetUtil.UTF_8));
 7         //ctx.channel().writeAndFlush(Unpooled.copiedBuffer("Test it. channel().writeAndFlush", CharsetUtil.UTF_8));
 8         ctx.fireChannelRead(Unpooled.copiedBuffer("[EchoInboundHandler2] " + data, CharsetUtil.UTF_8));
 9 
10         System.out.println("Sign out EchoInboundHandler2 channelRead");
11     }

The results are as follows:

From the above figure, we can see that OutboundHandler2 and OutboundHandler3 are executed successively. Why is this? Because ctx.writeAndFlush starts from the current ChannelHandler and executes the write method of OutboundHandler in turn, it executes OutboundHandler2 and OutboundHandler3 respectively.

OutboundHandler3 => InboundHandler1 => OutboundHandler2 => InboundHandler2 => OutboundHandler1 => InboundHandler3

Therefore, we come to the following conclusion:

1. ctx.writeAndFlush starts from the current ChannelHandler and executes the OutboundHandler in reverse order.

2. The OutboundHandler behind the ChannelHandler where ctx.writeAndFlush is located will not be executed.

Experiment 6: what is the execution order of the outbound handler of ctx.channel().writeAndFlush?

The difference is that ctx.writeAndFlush is changed to ctx.channel().writeAndFlush.

 1     public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
 2         System.out.println("Get into EchoInboundHandler2.channelRead");
 3 
 4         String data = ((ByteBuf) msg).toString(CharsetUtil.UTF_8);
 5         System.out.println("EchoInboundHandler2.channelRead Data received:" + data);
 6         //ctx.writeAndFlush(Unpooled.copiedBuffer("[For the first time write] [EchoInboundHandler2] " + data, CharsetUtil.UTF_8));
 7         ctx.channel().writeAndFlush(Unpooled.copiedBuffer("Test it. channel().writeAndFlush", CharsetUtil.UTF_8));
 8         ctx.fireChannelRead(Unpooled.copiedBuffer("[EchoInboundHandler2] " + data, CharsetUtil.UTF_8));
 9 
10         System.out.println("Sign out EchoInboundHandler2 channelRead");
11     }

The results are as follows:

From the above figure, we can see that all outbound handlers have been executed, so we get the conclusion:

1. ctx.channel().writeAndFlush starts from the last OutboundHandler and executes other outboundhandlers in reverse order. Even if the last ChannelHandler is an OutboundHandler, it will execute the OutboundHandler before the InboundHandler.

2. Do not execute ctx.channel().writeAndFlush in the write method of OutboundHandler, otherwise it will be in a dead cycle.

III. summary

1. The InboundHandler determines whether to execute the next InboundHandler through the fire event. If any InboundHandler does not call the fire event, the Pipeline in the future will be broken.
2. The InboundHandler is executed according to the loading order of pipeline.
3. The OutboundHandler is executed in reverse order according to the loading order of Pipeline.
4. Valid InboundHandler refers to the last InboundHandler that can be reached through fire event.
5. If you want all the outboundhandlers to be executed, you must put the OutboundHandler before the last valid InboundHandler.
6. The recommended method is to load all outboundhandlers through addFirst, and then load all inboundhandlers through addLast.
7. The OutboundHandler implements the concatenation of Pipeline through the write method.
8. If the OutboundHandler is on the Pipeline processing chain, and one of the outboundhandlers does not call the write method, the final message will not be sent out.
9. ctx.writeAndFlush starts from the current ChannelHandler and executes the OutboundHandler in reverse order.
10. The OutboundHandler after the ChannelHandler where ctx.writeAndFlush is located will not be executed.
11. ctx.channel().writeAndFlush starts from the last OutboundHandler and executes other outboundhandlers in reverse order. Even if the last ChannelHandler is an OutboundHandler, it will execute the OutboundHandler before the InboundHandler.
12. Do not execute ctx.channel().writeAndFlush in the write method of OutboundHandler, otherwise it will be in a dead cycle.

Topics: Java Netty socket