Chapter 6. ChannelHandler and ChannelPipeline

This chapter covers

  • The ChannelHandler and ChannelPipeline APIs
  • Detecting resource leaks
  • Exception handling

In the previous chapter you studied ByteBuf, Netty’s data container. As we explore Netty’s dataflow and processing components in this chapter, we’ll build on what you’ve learned and you’ll begin to see important elements of the framework coming together.

You already know that ChannelHandlers can be chained together in a ChannelPipeline to organize processing logic. We’ll examine a variety of use cases involving these classes and an important relation, ChannelHandlerContext.

Understanding the interactions among all of these components is essential to building modular, reusable implementations with Netty.

6.1. The ChannelHandler family

To prepare for our detailed study of ChannelHandler, we’ll spend time on some of the underpinnings of this part of Netty’s component model.

6.1.1. The Channel lifecycle

Interface Channel defines a simple but powerful state model that’s closely related to the ChannelInboundHandler API. The four Channel states are listed in table 6.1.

Table 6.1. Channel lifecycle states

State

Description

ChannelUnregistered The Channel was created, but isn’t registered to an EventLoop.
ChannelRegistered The Channel is registered to an EventLoop.
ChannelActive The Channel is active (connected to its remote peer). It’s now possible to receive and send data.
ChannelInactive The Channel isn’t connected to the remote peer.

The normal lifecycle of a Channel is shown in figure 6.1. As these state changes occur, corresponding events are generated. These are forwarded to ChannelHandlers in the ChannelPipeline, which can then act on them.

Figure 6.1. Channel state model

6.1.2. The ChannelHandler lifecycle

The lifecycle operations defined by interface ChannelHandler, listed in table 6.2, are called after a ChannelHandler has been added to, or removed from, a ChannelPipeline. Each method accepts a ChannelHandlerContext argument.

Table 6.2. ChannelHandler lifecycle methods

Type

Description

handlerAdded Called when a ChannelHandler is added to a ChannelPipeline
handlerRemoved Called when a ChannelHandler is removed from a ChannelPipeline
exceptionCaught Called if an error occurs in the ChannelPipeline during processing

Netty defines the following two important subinterfaces of ChannelHandler:

  • ChannelInboundHandler —Processes inbound data and state changes of all kinds
  • ChannelOutboundHandler —Processes outbound data and allows interception of all operations

In the next sections, we’ll discuss these interfaces in detail.

6.1.3. Interface ChannelInboundHandler

Table 6.3 lists the lifecycle methods of interface ChannelInboundHandler. These are called when data is received or when the state of the associated Channel changes. As we mentioned earlier, these methods map closely to the Channel lifecycle.

Table 6.3. ChannelInboundHandler methods

Type

Description

channelRegistered Invoked when a Channel is registered to its EventLoop and is able to handle I/O.
channelUnregistered Invoked when a Channel is deregistered from its EventLoop and can’t handle any I/O.
channelActive Invoked when a Channel is active; the Channel is connected/bound and ready.
channelInactive Invoked when a Channel leaves active state and is no longer connected to its remote peer.
channelReadComplete Invoked when a read operation on the Channel has completed.
channelRead Invoked if data is read from the Channel.
channelWritabilityChanged Invoked when the writability state of the Channel changes. The user can ensure writes are not done too quickly (to avoid an OutOfMemoryError) or can resume writes when the Channel becomes writable again. The Channel method isWritable() can be called to detect the writability of the channel. The threshold for writability can be set via Channel.config().setWriteHighWaterMark() and Channel.config().setWriteLowWaterMark().
userEventTriggered Invoked when ChannelnboundHandler.fireUserEventTriggered() is called because a POJO was passed through the ChannelPipeline.

When a ChannelInboundHandler implementation overrides channelRead(), it is responsible for explicitly releasing the memory associated with pooled ByteBuf instances. Netty provides a utility method for this purpose, ReferenceCountUtil.release(), as shown next.

Listing 6.1. Releasing message resources

Netty logs unreleased resources with a WARN-level log message, making it fairly simple to find offending instances in the code. But managing resources in this way can be cumbersome. A simpler alternative is to use SimpleChannelInboundHandler. The next listing is a variation of listing 6.1 that illustrates this.

Listing 6.2. Using SimpleChannelInboundHandler

Because SimpleChannelInboundHandler releases resources automatically, you shouldn’t store references to any messages for later use, as these will become invalid.

Section 6.1.6 provides a more detailed discussion of reference handling.

6.1.4. Interface ChannelOutboundHandler

Outbound operations and data are processed by ChannelOutboundHandler. Its methods are invoked by Channel, ChannelPipeline, and ChannelHandlerContext.

A powerful capability of ChannelOutboundHandler is to defer an operation or event on demand, which allows for sophisticated approaches to request handling. If writing to the remote peer is suspended, for example, you can defer flush operations and resume them later.

Table 6.4 shows all of the methods defined locally by ChannelOutboundHandler (leaving out those inherited from ChannelHandler).

Table 6.4. ChannelOutboundHandler methods

Type

Description

bind(ChannelHandlerContext, SocketAddress,ChannelPromise) Invoked on request to bind the Channel to a local address
connect(ChannelHandlerContext, SocketAddress,SocketAddress,ChannelPromise) Invoked on request to connect the Channel to the remote peer
disconnect(ChannelHandlerContext, ChannelPromise) Invoked on request to disconnect the Channel from the remote peer
close(ChannelHandlerContext,ChannelPromise) Invoked on request to close the Channel
deregister(ChannelHandlerContext, ChannelPromise) Invoked on request to deregister the Channel from its EventLoop
read(ChannelHandlerContext) Invoked on request to read more data from the Channel
flush(ChannelHandlerContext) Invoked on request to flush queued data to the remote peer through the Channel
write(ChannelHandlerContext,Object, ChannelPromise) Invoked on request to write data through the Channel to the remote peer
ChannelPromise vs. ChannelFuture

Most of the methods in ChannelOutboundHandler take a ChannelPromise argument to be notified when the operation completes. ChannelPromise is a subinterface of ChannelFuture that defines the writable methods, such as setSuccess() or setFailure(), thus making ChannelFuture immutable.

Next we’ll look at classes that simplify the task of writing ChannelHandlers.

6.1.5. ChannelHandler adapters

You can use the classes ChannelInboundHandlerAdapter and ChannelOutboundHandlerAdapter as starting points for your own ChannelHandlers. These adapters provide basic implementations of ChannelInboundHandler and ChannelOutboundHandler respectively. They acquire the methods of their common superinterface, ChannelHandler, by extending the abstract class ChannelHandlerAdapter. The resulting class hierarchy is shown in figure 6.2.

Figure 6.2. ChannelHandlerAdapter class hierarchy

ChannelHandlerAdapter also provides the utility method isSharable(). This method returns true if the implementation is annotated as Sharable, indicating that it can be added to multiple ChannelPipelines (as discussed in section 2.3.1).

The method bodies provided in ChannelInboundHandlerAdapter and ChannelOutboundHandlerAdapter call the equivalent methods on the associated ChannelHandlerContext, thereby forwarding events to the next ChannelHandler in the pipeline.

To use these adapter classes in your own handlers, simply extend them and override the methods you want to customize.

6.1.6. Resource management

Whenever you act on data by calling ChannelInboundHandler.channelRead() or ChannelOutboundHandler.write(), you need to ensure that there are no resource leaks. As you may remember from the previous chapter, Netty uses reference counting to handle pooled ByteBufs. So it’s important to adjust the reference count after you have finished using a ByteBuf.

To assist you in diagnosing potential problems, Netty provides class ResourceLeakDetector, which will sample about 1% of your application’s buffer allocations to check for memory leaks. The overhead involved is very small.

If a leak is detected, a log message similar to the following will be produced:

LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable
advanced leak reporting to find out where the leak occurred. To enable
advanced leak reporting, specify the JVM option
'-Dio.netty.leakDetectionLevel=ADVANCED' or call
ResourceLeakDetector.setLevel().

Netty currently defines the four leak detection levels, as listed in table 6.5.

Table 6.5. Leak-detection levels

Level

Description

DISABLED Disables leak detection. Use this only after extensive testing.
SIMPLE Reports any leaks found using the default sampling rate of 1%. This is the default level and is a good fit for most cases.
ADVANCED Reports leaks found and where the message was accessed. Uses the default sampling rate.
PARANOID Like ADVANCED except that every access is sampled. This has a heavy impact on performance and should be used only in the debugging phase.

The leak-detection level is defined by setting the following Java system property to one of the values in the table:

java -Dio.netty.leakDetectionLevel=ADVANCED

If you relaunch your application with the JVM option you’ll see the recent locations of your application where the leaked buffer was accessed. The following is a typical leak report generated by a unit test:

Running io.netty.handler.codec.xml.XmlFrameDecoderTest
15:03:36.886 [main] ERROR io.netty.util.ResourceLeakDetector - LEAK:
     ByteBuf.release() was not called before it's garbage-collected.
Recent access records: 1
#1: io.netty.buffer.AdvancedLeakAwareByteBuf.toString(
    AdvancedLeakAwareByteBuf.java:697)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithXml(
    XmlFrameDecoderTest.java:157)
io.netty.handler.codec.xml.XmlFrameDecoderTest.testDecodeWithTwoMessages(
    XmlFrameDecoderTest.java:133)
...

How do you use this diagnostic tool to prevent leaks when you implement ChannelInboundHandler.channelRead() and ChannelOutboundHandler.write()? Let’s examine the case where your channelRead() operation consumes an inbound message; that is, without passing it on to the next ChannelInboundHandler by calling ChannelHandlerContext.fireChannelRead(). This listing shows how to release the message.

Listing 6.3. Consuming and releasing an inbound message

Consuming inbound messages the easy way

Because consuming inbound data and releasing it is such a common task, Netty provides a special ChannelInboundHandler implementation called SimpleChannelInboundHandler. This implementation will automatically release a message once it’s consumed by channelRead0().

On the outbound side, if you handle a write() operation and discard a message, you’re responsible for releasing it. The next listing shows an implementation that discards all written data.

Listing 6.4. Discarding and releasing outbound data

It’s important not only to release resources but also to notify the ChannelPromise. Otherwise a situation might arise where a ChannelFutureListener has not been notified about a message that has been handled.

In sum, it is the responsibility of the user to call ReferenceCountUtil.release() if a message is consumed or discarded and not passed to the next ChannelOutboundHandler in the ChannelPipeline. If the message reaches the actual transport layer, it will be released automatically when it’s written or the Channel is closed.

6.2. Interface ChannelPipeline

If you think of a ChannelPipeline as a chain of ChannelHandler instances that intercept the inbound and outbound events that flow through a Channel, it’s easy to see how the interaction of these ChannelHandlers can make up the core of an application’s data and event-processing logic.

Every new Channel that’s created is assigned a new ChannelPipeline. This association is permanent; the Channel can neither attach another ChannelPipeline nor detach the current one. This is a fixed operation in Netty’s component lifecycle and requires no action on the part of the developer.

Depending on its origin, an event will be handled by either a ChannelInboundHandler or a ChannelOutboundHandler. Subsequently it will be forwarded to the next handler of the same supertype by a call to a ChannelHandlerContext implementation.

ChannelHandlerContext

A ChannelHandlerContext enables a ChannelHandler to interact with its ChannelPipeline and with other handlers. A handler can notify the next ChannelHandler in the ChannelPipeline and even dynamically modify the ChannelPipeline it belongs to.

ChannelHandlerContext has a rich API for handling events and performing I/O operations. Section 6.3 will provide more information on ChannelHandlerContext.

Figure 6.3 illustrates a typical ChannelPipeline layout with both inbound and outbound ChannelHandlers and illustrates our earlier statement that a ChannelPipeline is primarily a series of ChannelHandlers. ChannelPipeline also provides methods for propagating events through the ChannelPipeline itself. If an inbound event is triggered, it’s passed from the beginning to the end of the ChannelPipeline. In figure 6.3, an outbound I/O event will start at the right end of the ChannelPipeline and proceed to the left.

Figure 6.3. ChannelPipeline and ChannelHandlers

ChannelPipeline relativity

You might say that from the point of view of an event traveling through the ChannelPipeline, the starting end depends on whether the event is inbound or outbound. But Netty always identifies the inbound entry to the ChannelPipeline (the left side in figure 6.3) as the beginning and the outbound entry (the right side) as the end.

When you’ve finished adding your mix of inbound and outbound handlers to a ChannelPipeline using the ChannelPipeline.add*() methods, the ordinal of each ChannelHandler is its position from beginning to end as we just defined them. Thus, if you number the handlers in figure 6.3 from left to right, the first ChannelHandler seen by an inbound event will be 1; the first handler seen by an outbound event will be 5.

As the pipeline propagates an event, it determines whether the type of the next ChannelHandler in the pipeline matches the direction of movement. If not, the ChannelPipeline skips that ChannelHandler and proceeds to the next one, until it finds one that matches the desired direction. (Of course, a handler might implement both ChannelInboundHandler and ChannelOutboundHandler interfaces.)

6.2.1. Modifying a ChannelPipeline

A ChannelHandler can modify the layout of a ChannelPipeline in real time by adding, removing, or replacing other ChannelHandlers. (It can remove itself from the ChannelPipeline as well.) This is one of the most important capabilities of the ChannelHandler, so we’ll take a close look at how it’s done. The relevant methods are listed in table 6.6.

Table 6.6. ChannelHandler methods for modifying a ChannelPipeline

Name

Description

addFirst addBefore addAfter addLast Adds a ChannelHandler to the ChannelPipeline
remove Removes a ChannelHandler from the ChannelPipeline
replace Replaces a ChannelHandler in the ChannelPipeline with another ChannelHandler

This listing shows these methods in use.

Listing 6.5. Modify the ChannelPipeline

You’ll see later on that this ability to reorganize ChannelHandlers with ease lends itself to the implementation of extremely flexible logic.

ChannelHandler execution and blocking

Normally each ChannelHandler in the ChannelPipeline processes events that are passed to it by its EventLoop (the I/O thread). It’s critically important not to block this thread as it would have a negative effect on the overall handling of I/O.

Sometimes it may be necessary to interface with legacy code that uses blocking APIs. For this case, the ChannelPipeline has add() methods that accept an EventExecutorGroup. If an event is passed to a custom EventExecutorGroup, it will be handled by one of the EventExecutors contained in this EventExecutorGroup and thus be removed from the EventLoop of the Channel itself. For this use case Netty provides an implementation called DefaultEventExecutorGroup.

In addition to these operations, there are others for accessing ChannelHandlers either by type or by name. These are listed in table 6.7.

Table 6.7. ChannelPipeline operations for accessing ChannelHandlers

Name

Description

get Returns a ChannelHandler by type or name
context Returns the ChannelHandlerContext bound to a ChannelHandler
names Returns the names of all the ChannelHandlers in the ChannelPipeline

6.2.2. Firing events

The ChannelPipeline API exposes additional methods for invoking inbound and outbound operations. Table 6.8 lists the inbound operations, which notify ChannelInboundHandlers of events occurring in the ChannelPipeline.

Table 6.8. ChannelPipeline inbound operations

Method name

Description

fireChannelRegistered Calls channelRegistered(ChannelHandlerContext) on the next ChannelInboundHandler in the ChannelPipeline
fireChannelUnregistered Calls channelUnregistered(ChannelHandlerContext) on the next ChannelInboundHandler in the ChannelPipeline
fireChannelActive Calls channelActive(ChannelHandlerContext) on the next ChannelInboundHandler in the ChannelPipeline
fireChannelInactive Calls channelInactive(ChannelHandlerContext) on the next ChannelInboundHandler in the ChannelPipeline
fireExceptionCaught Calls exceptionCaught(ChannelHandlerContext, Throwable) on the next ChannelHandler in the ChannelPipeline
fireUserEventTriggered Calls userEventTriggered(ChannelHandlerContext, Object) on the next ChannelInboundHandler in the ChannelPipeline
fireChannelRead Calls channelRead(ChannelHandlerContext, Object msg) on the next ChannelInboundHandler in the ChannelPipeline
fireChannelReadComplete Calls channelReadComplete(ChannelHandlerContext) on the next ChannelStateHandler in the ChannelPipeline

On the outbound side, handling an event will cause some action to be taken on the underlying socket. Table 6.9 lists the outbound operations of the ChannelPipeline API.

Table 6.9. ChannelPipeline outbound operations

Method name

Description

bind Binds the Channel to a local address. This will call bind(ChannelHandlerContext, SocketAddress, ChannelPromise) on the next ChannelOutboundHandler in the ChannelPipeline.
connect Connects the Channel to a remote address. This will call connect(ChannelHandlerContext, SocketAddress, ChannelPromise) on the next ChannelOutboundHandler in the ChannelPipeline.
disconnect Disconnects the Channel. This will call disconnect(ChannelHandlerContext, ChannelPromise) on the next ChannelOutboundHandler in the ChannelPipeline.
close Closes the Channel. This will call close(ChannelHandlerContext, ChannelPromise) on the next ChannelOutboundHandler in the ChannelPipeline.
deregister Deregisters the Channel from the previously assigned EventExecutor (the EventLoop). This will call deregister(ChannelHandlerContext, ChannelPromise) on the next ChannelOutboundHandler in the ChannelPipeline.
flush Flushes all pending writes of the Channel. This will call flush(ChannelHandlerContext) on the next ChannelOutboundHandler in the ChannelPipeline.
write Writes a message to the Channel. This will call write(ChannelHandlerContext, Object msg, ChannelPromise) on the next ChannelOutboundHandler in the ChannelPipeline. Note: this does not write the message to the underlying Socket, but only queues it. To write it to the Socket, call flush() or writeAndFlush().
writeAndFlush This is a convenience method for calling write() then flush().
read Requests to read more data from the Channel. This will call read(ChannelHandlerContext) on the next ChannelOutboundHandler in the ChannelPipeline.

In summary,

  • A ChannelPipeline holds the ChannelHandlers associated with a Channel.
  • A ChannelPipeline can be modified dynamically by adding and removing ChannelHandlers as needed.
  • ChannelPipeline has a rich API for invoking actions in response to inbound and outbound events.

6.3. Interface ChannelHandlerContext

A ChannelHandlerContext represents an association between a ChannelHandler and a ChannelPipeline and is created whenever a ChannelHandler is added to a ChannelPipeline. The primary function of a ChannelHandlerContext is to manage the interaction of its associated ChannelHandler with others in the same ChannelPipeline.

ChannelHandlerContext has numerous methods, some of which are also present on Channel and on ChannelPipeline itself, but there is an important difference. If you invoke these methods on a Channel or ChannelPipeline instance, they propagate through the entire pipeline. The same methods called on a ChannelHandlerContext will start at the current associated ChannelHandler and propagate only to the next ChannelHandler in the pipeline that is capable of handling the event.

Table 6.10 summarizes the ChannelHandlerContext API.

Table 6.10. The ChannelHandlerContext API

Method name

Description

bind Binds to the given SocketAddress and returns a ChannelFuture
channel Returns the Channel that is bound to this instance
close Closes the Channel and returns a ChannelFuture
connect Connects to the given SocketAddress and returns a ChannelFuture
deregister Deregisters from the previously assigned EventExecutor and returns a ChannelFuture
disconnect Disconnects from the remote peer and returns a ChannelFuture
executor Returns the EventExecutor that dispatches events
fireChannelActive Triggers a call to channelActive() (connected) on the next ChannelInboundHandler
fireChannelInactive Triggers a call to channelInactive() (closed) on the next ChannelInboundHandler
fireChannelRead Triggers a call to channelRead() (message received) on the next ChannelInboundHandler
fireChannelReadComplete Triggers a channelWritabilityChanged event to the next ChannelInboundHandler
handler Returns the ChannelHandler bound to this instance
isRemoved Returns true if the associated ChannelHandler was removed from the ChannelPipeline
name Returns the unique name of this instance
pipeline Returns the associated ChannelPipeline
read Reads data from the Channel into the first inbound buffer; triggers a channelRead event if successful and notifies the handler of channelReadComplete
write Writes a message via this instance through the pipeline

When using the ChannelHandlerContext API, please keep the following points in mind:

  • The ChannelHandlerContext associated with a ChannelHandler never changes, so it’s safe to cache a reference to it.
  • ChannelHandlerContext methods, as we explained at the start of this section, involve a shorter event flow than do the identically named methods available on other classes. This should be exploited where possible to provide maximum performance.

6.3.1. Using ChannelHandlerContext

In this section we’ll discuss the use of ChannelHandlerContext and the behaviors of methods available on ChannelHandlerContext, Channel, and ChannelPipeline. Figure 6.4 shows the relationships among them.

Figure 6.4. The relationships among Channel, ChannelPipeline, ChannelHandler, and ChannelHandlerContext

In the following listing you acquire a reference to the Channel from a ChannelHandlerContext. Calling write() on the Channel causes a write event to flow all the way through the pipeline.

Listing 6.6. Accessing the Channel from a ChannelHandlerContext

The next listing shows a similar example, but writing this time to a ChannelPipeline. Again, the reference is retrieved from the ChannelHandlerContext.

Listing 6.7. Accessing the ChannelPipeline from a ChannelHandlerContext

As you can see in figure 6.5, the flows in listings 6.6 and 6.7 are identical. It’s important to note that although the write() invoked on either the Channel or the ChannelPipeline operation propagates the event all the way through the pipeline, the movement from one handler to the next at the ChannelHandler level is invoked on the ChannelHandlerContext.

Figure 6.5. Event propagation via the Channel or the ChannelPipeline

Why would you want to propagate an event starting at a specific point in the ChannelPipeline?

  • To reduce the overhead of passing the event through ChannelHandlers that are not interested in it
  • To prevent processing of the event by handlers that would be interested in the event

To invoke processing starting with a specific ChannelHandler, you must refer to the ChannelHandlerContext that’s associated with the ChannelHandler before that one. This ChannelHandlerContext will invoke the ChannelHandler that follows the one with which it’s associated.

The following listing and figure 6.6 illustrate this use.

Figure 6.6. Event flow for operations triggered via the ChannelHandlerContext

Listing 6.8. Calling ChannelHandlerContext write()

As shown in figure 6.6, the message flows through the ChannelPipeline starting at the next ChannelHandler, bypassing all the preceding ones.

The use case we just described is a common one, and it’s especially useful for calling operations on a specific ChannelHandler instance.

6.3.2. Advanced uses of ChannelHandler and ChannelHandlerContext

As you saw in listing 6.6, you can acquire a reference to the enclosing ChannelPipeline by calling the pipeline() method of a ChannelHandlerContext. This enables runtime manipulation of the pipeline’s ChannelHandlers, which can be exploited to implement sophisticated designs. For example, you could add a ChannelHandler to a pipeline to support a dynamic protocol change.

Other advanced uses can be supported by caching a reference to a ChannelHandlerContext for later use, which might take place outside any ChannelHandler methods and could even originate from a different thread. This listing shows this pattern being used to trigger an event.

Listing 6.9. Caching a ChannelHandlerContext

Because a ChannelHandler can belong to more than one ChannelPipeline, it can be bound to multiple ChannelHandlerContext instances. A ChannelHandler intended for this use must be annotated with @Sharable; otherwise, attempting to add it to more than one ChannelPipeline will trigger an exception. Clearly, to be safe for use with multiple concurrent channels (that is, connections), such a ChannelHandler must be thread-safe.

This listing shows a correct implementation of this pattern.

Listing 6.10. A sharable ChannelHandler

The preceding ChannelHandler implementation meets all the requirements for inclusion in multiple pipelines; namely, it’s annotated with @Sharable and doesn’t hold any state. Conversely, the code in listing 6.11 will cause problems.

Listing 6.11. Invalid usage of @Sharable

The problem with this code is that it has state; namely the instance variable count, which tracks the number of method invocations. Adding an instance of this class to the ChannelPipeline will very likely produce errors when it’s accessed by concurrent channels. (Of course, this simple case could be corrected by making channelRead() synchronized.)

In summary, use @Sharable only if you’re certain that your ChannelHandler is thread-safe.

Why share a ChannelHandler?

A common reason for installing a single ChannelHandler in multiple ChannelPipelines is to gather statistics across multiple Channels.

This concludes our discussion of ChannelHandlerContext and its relationship to other framework components. Next we’ll look at exception handling.

6.4. Exception handling

Exception handling is an important part of any substantial application, and it can be approached in a variety of ways. Accordingly, Netty provides several options for handling exceptions thrown during inbound or outbound processing. This section will help you understand how to design the approach that best suits your needs.

6.4.1. Handling inbound exceptions

If an exception is thrown during processing of an inbound event, it will start to flow through the ChannelPipeline starting at the point in the ChannelInboundHandler where it was triggered. To handle such an inbound exception, you need to override the following method in your ChannelInboundHandler implementation.

public void exceptionCaught(
    ChannelHandlerContext ctx, Throwable cause) throws Exception

The following listing shows a simple example that closes the Channel and prints the exception’s stack trace.

Listing 6.12. Basic inbound exception handling
public class InboundExceptionHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,
        Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

Because the exception will continue to flow in the inbound direction (just as with all inbound events), the ChannelInboundHandler that implements the preceding logic is usually placed last in the ChannelPipeline. This ensures that all inbound exceptions are always handled, wherever in the ChannelPipeline they may occur.

How you should react to an exception is likely to be quite specific to your application. You may want to close the Channel (and connections) or you may attempt to recover. If you don’t implement any handling for inbound exceptions (or don’t consume the exception), Netty will log the fact that the exception wasn’t handled.

To summarize,

  • The default implementation of ChannelHandler.exceptionCaught() forwards the current exception to the next handler in the pipeline.
  • If an exception reaches the end of the pipeline, it’s logged as unhandled.
  • To define custom handling, you override exceptionCaught(). It’s then your decision whether to propagate the exception beyond that point.

6.4.2. Handling outbound exceptions

The options for handling normal completion and exceptions in outbound operations are based on the following notification mechanisms:

  • Every outbound operation returns a ChannelFuture. The ChannelFutureListeners registered with a ChannelFuture are notified of success or error when the operation completes.
  • Almost all methods of ChannelOutboundHandler are passed an instance of ChannelPromise. As a subclass of ChannelFuture, ChannelPromise can also be assigned listeners for asynchronous notification. But ChannelPromise also has writable methods that provide for immediate notification:
    ChannelPromise setSuccess();
    ChannelPromise setFailure(Throwable cause);

Adding a ChannelFutureListener is a matter of calling addListener(ChannelFutureListener) on a ChannelFuture instance, and there are two ways to do this. The one most commonly used is to invoke addListener() on the ChannelFuture that is returned by an outbound operation (for example write()).

The following listing uses this approach to add a ChannelFutureListener that will print the stack trace and then close the Channel.

Listing 6.13. Adding a ChannelFutureListener to a ChannelFuture
ChannelFuture future = channel.write(someMessage);
future.addListener(new ChannelFutureListener() {
    @Override
    public void operationComplete(ChannelFuture f) {
        if (!f.isSuccess()) {
            f.cause().printStackTrace();
            f.channel().close();
        }
    }
});

The second option is to add a ChannelFutureListener to the ChannelPromise that is passed as an argument to the ChannelOutboundHandler methods. The code shown next will have the same effect as the previous listing.

Listing 6.14. Adding a ChannelFutureListener to a ChannelPromise
public class OutboundExceptionHandler extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg,
        ChannelPromise promise) {
        promise.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture f) {
                if (!f.isSuccess()) {
                    f.cause().printStackTrace();
                    f.channel().close();
                }
            }
        });
    }
}
ChannelPromise writable methods

By calling setSuccess() and setFailure() on ChannelPromise, you can make the status of an operation known as soon as the ChannelHandler method returns to the caller.

Why choose one approach over the other? For detailed handling of an exception, you’ll probably find it more appropriate to add the ChannelFutureListener when calling the outbound operation, as shown in listing 6.13. For a less specialized approach to handling exceptions, you might find the custom ChannelOutboundHandler implementation shown in listing 6.14 to be simpler.

What happens if your ChannelOutboundHandler itself throws an exception? In this case, Netty itself will notify any listeners that have been registered with the corresponding ChannelPromise.

6.5. Summary

In this chapter we took a close look at Netty’s data processing component, ChannelHandler. We discussed how ChannelHandlers are chained together and how they interact with the ChannelPipeline as ChannelInboundHandlers and ChannelOutboundHandlers.

The next chapter will focus on Netty’s codec abstraction, which makes writing protocol encoders and decoders much easier than using the underlying ChannelHandler implementations directly.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset