This is the seventh article in the Netty series

In the last article, we studied EventLoop and EventLoopGroup, the core components of Netty logic architecture, grasped Netty threading model, and introduced the design of lockless serialization in Netty4 threading model.

Today, we continue with ChannelHandler and ChannelPipeline, another core component of Netty’s logical architecture.

If the threading model is Netty’s “core internal strength”, then ChannelHandler is Netty’s most famous “martial art”, is our daily use of Netty in contact with the most components.

To quote a line from Netty in Action

From the appliaction developer’s standpoint, the primary component of Netty is the ChannelHandler.

So, Aru as far as possible through the diagram and code demo, to let everyone get the most intuitive experience.

This article will take about 10 minutes to read and will focus on the following questions:

  • What are Channel Handlers and Channel Pipelines?
  • ChannelHandler event propagation mechanism
  • Exception handling mechanism of ChannelHandler
  • ChannelHandler best practices

1. What is ChannelHandler and ChannelPipeline?

ChannelHandler is a container that contains all application processing logic and is used to process Netty input and output data.

Such as data format conversion, exception handling and so on

ChannelPipeline is the container carrier of ChannelHandler and is responsible for scheduling each registered ChannelHandler in the form of chain.

Let’s review the logic architecture of Netty and look at the location of ChannelPipeline and ChannelHandler.

From the local zoom in, you can see the ChannelPipeline and ChannelHandler more clearly.

As shown in the figure above, when an event is heard in an EventLoop, the I/O event is processed. This processing is handed over to ChannelPipeline, or, more strictly, to each ChannelHandler in ChannelPipeline in a certain order.

Netty divides channelHandlers into two classes, InboundHandler and OutboundHandler, based on the direction of the data.

As shown in the figure above, Netty receives data successfully after several InboundHandler processes. If you want to output data, you need to send it after several OutboundHandler processes.

For example, we often need to decode incoming data in an InboundHandler specialized to decode. If you want to send data, you usually need to encode it, which is handled in an OutBoundHandler that is specialized for encode.

It is worth mentioning that although we use Netty to deal with ChannelPipeline and ChannelHandler directly, there is an “invisible” bridge between them called ChannelHandlerContext.

As the name implies, ChannelHanderContext is the ChannelHandler context, and each ChannelHandler corresponds to a ChannelHandlerContext.

Each ChannelPipeline contains multiple ChannelHandlerContexts, all of which form a bidirectional linked list. As shown in the figure below.

There are two special ChannelHandlerContext, HeadContext and TailContext, representing the head and tail nodes of the bidirectional list.

As you can see from the class diagram, HeadContext implements both ChannelInboundHandler and ChannelOutboundHandler. Therefore, HeadContext acts as the head node when reading data, passing InBound events backwards, and acts as the tail node when writing data, handling the final OutBound events.

TailContext only implements ChannelInboundHandler. It handles some resource releases at the end of InBound event delivery. On the first node where the OutBound event is transmitted, no processing is done and only the OutBound event is transmitted to the PREV node.

Our custom ChannelHandler is inserted between these two nodes.

At this point, we have a basic understanding of ChannelHandler and ChannelPipeline. In practice, how do we use ChannelHandler correctly?

To use ChannelHandler, you must first understand the event propagation mechanism and exception handling mechanism of ChannelHandler.

2. Event propagation mechanism of ChannelHandler

The two types of events in Netty, Inbound and Outbound, are handled by InboundHandler and OutbountHandler, respectively.

When developing with Netty, we must understand how Inbound and Outbound events are “event propagated” in ChannelPipeline, and the order in which InboundHandler and OutboundHandler are registered.

So without further ado, let’s do a demo to get a feel for it.

Create a custom ChannelInboundHandler

Create a custom ChannelOutboundHandler

Let’s briefly assemble the EchoPipelineServer, paying particular attention to the order in which the six handlers are registered.

Then we can access the Netty Server briefly from the command line

curl localhost:8081
Copy the code

You can see the following output from the console

– InboundHandler handles Inbound events in the same order as the registration order – OutboundHandler handles Outbound events in the opposite order as the registration order

In combination with the HeadContext and TailContext from the previous section, let’s draw a more intuitive picture of the build order of the handlers in the ChannelPipeline.

In the ChannelInitializer above, we added three InboundHandlers and three OutboundHandlers as needed. So, between the head nodes HeadContext and TailContext, there is an ordered bidirectional linked list.

InboundHandler3, by calling the CTX. Channel. WriteAndFlush (MSG) method, the message from the beginning of the TailContext, according to the path of OutboundHandler HeadContext direction spread out. See the implementation of DefaultChannelPipeline class

Although this is a bidirectional list, both Inbound and Outbound events are filtered by event type when accessing the list nodes sequentially.

3. Exception propagation mechanism of ChannelHandler

Now that we know about ChannelPipeline’s chain passing rules, what happens if any of the handlers in the two-way list throw an exception?

3.1 Exception Handling of InboundHandler

Let’s modify the TestInboudHandler in the example to simulate.

  • An exception is thrown in the channelRead method
  • Rewrite the exceptionCaught method to print the exceptionCaught by the current node

The output is as follows

As you can see, although an exception is thrown in InboundHander1, each of the three InboundHandlers catches it once, passes it in the direction of the tail node, and then throws the exception.

As we can see, Netty gave a warning that no exception handling was done on the last node.

An exceptionCaught() event was fired, and it reached at the tail of the pipeline. 
It usually means the last handler in the pipeline did not handle the exception.
Copy the code

3.2 Exception handling of OutboundHandler

Does OutboundHandler do the same thing? Let’s do an experiment.

  • Throw an exception in the write operation
  • Override the exceptionCaught method (which is marked deprecated in OutboundHandler)

Rewrite assembles the next channelPipeline, and throws an exception in the second OutboundHandler

The resulting output is as follows

Yi? The exception is eaten!! Not only is the exceptionCaught method not walked into, no other exception is thrown. However, the subsequent handler write method is not executed, and the flush method is executed again.

Let’s find the reason from the source code. Follow the breakpoint and immediately find the reason:

In AbstractChannelHandlerContext, methods to write OutboundHandler exception handling, and then to inform ChannelPromise. Follow-up source code will not be expanded, interested in their own interrupt point with, more clear.

So how do you catch exceptions in OutboundHandler? The obvious thing is to just add the ChannelPromise callback. The code:

In the ExceptionHandler mentioned earlier, simply replicate the write method and register a ChannelPromise Listener. Of course, this ExceptionHandler is also registered with the ChannelPipeline.

Be careful!! Here again ExceptionHandler is added to the end of the tail direction of the ChannelPipeline instead of the head direction. Exceptions, whether inboundHandler or outboundHandler, are passed in the tail direction in sequence.

The exception is caught.

4. ChannelHandler best practices

Now that we’ve covered ChannelHandler’s common mechanisms, here are a couple of best practices.

4.1 Time Processing is Not Performed in ChannelHandler

This was mentioned in the previous article, “Get into Netty logic and start with the Reactor Thread Model”, but again, as a best practice for customizing ChannelHandler, do not do time processing in ChannelHandler.

There are two points here.

One is that time-consuming operations are not handled directly in the I/O thread.

Second, it also does not put time-consuming operations into the EventLoop task queue.

Due to the lockless serialization design of Netty4, if any time-consuming operation blocks an EventLoop, all channels on that EventLoop will be blocked. For more details, see the Reactor Thread Model in the previous article, Diving into Netty’s logical architecture.

Therefore, for time-consuming operations, we need to put them in our own business thread pool for processing. If we need to send response, we need to submit the task to the task queue of EventLoop for execution.

I’ll give you a simple demo.

4.2 Unified Exception Handling

In the third section of this article, I explained the exception propagation mechanism of ChannelHandler.

For The InboundHandler, you can do exceptionCaught directly in the Handler if you have an exception that is specific to the handler. If the exception is generic, you can register the custom ExceptionHandler to the end of the ChannelPipeline for uniform interception.

In the case of OutboudHandler, this is done by customizing the ExceptionHandler, overriding the corresponding method, and registering the ChannelPromise Listener. Again, ExceptionHandler registers at the end of the ChannelPipeline for uniform interception.

So, how do you add a “unified” exception interceptor?

  • Custom ExceptionHandler inherits ChannelDuplexHandler and registers it before the tail node.
  • For Inbound events, we need to handle exceptionCaught()
  • For Outbound events, we need a different ChannelFutureListener

The exception interceptor should be registered with the last Handler in the tail direction.

Note that in addition to making common exceptions more elegant, unified exception handling is also a good aid in troubleshooting. For example, sometimes codec exceptions can be captured in the unified exception processing place to quickly locate the problem.

5. Summary

Just a quick review.

This article introduces what ChannelHandler and ChannelPipeline are. InboundChannelHandler, OutboundChannelHandler, ChannelHandlerContext.

Then the event propagation mechanism and exception handling mechanism of ChannelHandler are introduced in detail.

Finally, the best practices for ChannelHandler in daily development are explained.

Hope to help you.

Bibliography: Netty in Action

See the end, the original is not easy, point a concern, point a like it ~

Reorganize the knowledge fragments and construct the Java knowledge graph: github.com/saigu/JavaK… (Easy access to historical articles)