Chapter 3. Netty components and design

This chapter covers

  • Technical and architectural aspects of Netty
  • Channel, EventLoop, and ChannelFuture
  • ChannelHandler and ChannelPipeline
  • Bootstrapping

In chapter 1 we presented a summary of the history and technical foundations of high-performance network programming in Java. This provided the background for an overview of Netty’s core concepts and building blocks.

In chapter 2 we expanded the scope of our discussion to application development. By building a simple client and server you learned about bootstrapping and gained hands-on experience with the all-important ChannelHandler API. Along the way, you also verified that your development tools were functioning properly.

As we build on this material in the rest of the book, we’ll explore Netty from two distinct but closely related points of view: as a class library and as a framework. Both are essential to writing efficient, reusable, and maintainable code with Netty.

From a high-level perspective, Netty addresses two corresponding areas of concern, which we might label broadly as technical and architectural. First, its asynchronous and event-driven implementation, built on Java NIO, guarantees maximum application performance and scalability under heavy load. Second, Netty embodies a set of design patterns that decouple application logic from the network layer, simplifying development while maximizing the testability, modularity, and reusability of code.

As we study Netty’s individual components in greater detail, we’ll pay close attention to how they collaborate to support these architectural best practices. By following the same principles, we can reap all the benefits Netty can provide. With this goal in mind, in this chapter we’ll review the main concepts and components we’ve introduced up to now.

3.1. Channel, EventLoop, and ChannelFuture

The following sections will add detail to our discussion of the Channel, EventLoop, and ChannelFuture classes which, taken together, can be thought of as representing Netty’s networking abstraction:

  • Channel Sockets
  • EventLoop —Control flow, multithreading, concurrency
  • ChannelFuture —Asynchronous notification

3.1.1. Interface Channel

Basic I/O operations (bind(), connect(), read(), and write()) depend on primitives supplied by the underlying network transport. In Java-based networking, the fundamental construct is class Socket. Netty’s Channel interface provides an API that greatly reduces the complexity of working directly with Sockets. Additionally, Channel is the root of an extensive class hierarchy having many predefined, specialized implementations, of which the following is a short list:

  • EmbeddedChannel
  • LocalServerChannel
  • NioDatagramChannel
  • NioSctpChannel
  • NioSocketChannel

3.1.2. Interface EventLoop

The EventLoop defines Netty’s core abstraction for handling events that occur during the lifetime of a connection. We’ll discuss EventLoop in detail in chapter 7 in the context of Netty’s thread-handling model. For now, figure 3.1 illustrates at a high level the relationships among Channels, EventLoops, Threads, and EventLoopGroups.

Figure 3.1. Channels, EventLoops, and EventLoopGroups

These relationships are:

  • An EventLoopGroup contains one or more EventLoops.
  • An EventLoop is bound to a single Thread for its lifetime.
  • All I/O events processed by an EventLoop are handled on its dedicated Thread.
  • A Channel is registered for its lifetime with a single EventLoop.
  • A single EventLoop may be assigned to one or more Channels.

Note that this design, in which the I/O for a given Channel is executed by the same Thread, virtually eliminates the need for synchronization.

3.1.3. Interface ChannelFuture

As we’ve explained, all I/O operations in Netty are asynchronous. Because an operation may not return immediately, we need a way to determine its result at a later time. For this purpose, Netty provides ChannelFuture, whose addListener() method registers a ChannelFutureListener to be notified when an operation has completed (whether or not successfully).

More on ChannelFuture

Think of a ChannelFuture as a placeholder for the result of an operation that’s to be executed in the future. When exactly it will be executed may depend on several factors and thus be impossible to predict with precision, but it is certain that it will be executed. Furthermore, all operations belonging to the same Channel are guaranteed to be executed in the order in which they were invoked.

We’ll discuss EventLoop and EventLoopGroup in depth in chapter 7.

3.2. ChannelHandler and ChannelPipeline

Now we’ll take a more detailed look at the components that manage the flow of data and execute an application’s processing logic.

3.2.1. Interface ChannelHandler

From the application developer’s standpoint, the primary component of Netty is the ChannelHandler, which serves as the container for all application logic that applies to handling inbound and outbound data. This is possible because ChannelHandler methods are triggered by network events (where the term “event” is used very broadly). In fact, a ChannelHandler can be dedicated to almost any kind of action, such as converting data from one format to another or handling exceptions thrown during processing.

As an example, ChannelInboundHandler is a subinterface you’ll implement frequently. This type receives inbound events and data to be handled by your application’s business logic. You can also flush data from a ChannelInboundHandler when you’re sending a response to a connected client. The business logic of your application will often reside in one or more ChannelInboundHandlers.

3.2.2. Interface ChannelPipeline

A ChannelPipeline provides a container for a chain of ChannelHandlers and defines an API for propagating the flow of inbound and outbound events along the chain. When a Channel is created, it is automatically assigned its own ChannelPipeline.

ChannelHandlers are installed in the ChannelPipeline as follows:

  • A ChannelInitializer implementation is registered with a ServerBootstrap.
  • When ChannelInitializer.initChannel() is called, the ChannelInitializer installs a custom set of ChannelHandlers in the pipeline.
  • The ChannelInitializer removes itself from the ChannelPipeline.

Let’s go a bit deeper into the symbiotic relationship between ChannelPipeline and ChannelHandler to examine what happens to data when you send or receive it.

ChannelHandler has been designed specifically to support a broad range of uses, and you can think of it as a generic container for any code that processes events (including data) coming and going through the ChannelPipeline. This is illustrated in figure 3.2, which shows the derivation of ChannelInboundHandler and ChannelOutboundHandler from ChannelHandler.

Figure 3.2. ChannelHandler class hierarchy

The movement of an event through the pipeline is the work of the ChannelHandlers that have been installed during the initialization, or bootstrapping phase of the application. These objects receive events, execute the processing logic for which they have been implemented, and pass the data to the next handler in the chain. The order in which they are executed is determined by the order in which they were added. For all practical purposes, it’s this ordered arrangement of ChannelHandlers that we refer to as the ChannelPipeline.

Figure 3.3 illustrates the distinction between inbound and outbound data flow in a Netty application. From the point of view of a client application, events are said to be outbound if the movement is from the client to the server and inbound in the opposite case.

Figure 3.3. ChannelPipeline with inbound and outbound ChannelHandlers

Figure 3.3 also shows that both inbound and outbound handlers can be installed in the same pipeline. If a message or any other inbound event is read, it will start from the head of the pipeline and be passed to the first ChannelInboundHandler. This handler may or may not actually modify the data, depending on its specific function, after which the data will be passed to the next ChannelInboundHandler in the chain. Finally, the data will reach the tail of the pipeline, at which point all processing is terminated.

The outbound movement of data (that is, data being written) is identical in concept. In this case, data flows from the tail through the chain of ChannelOutboundHandlers until it reaches the head. Beyond this point, outbound data will reach the network transport, shown here as a Socket. Typically, this will trigger a write operation.

More on inbound and outbound handlers

An event can be forwarded to the next handler in the current chain by using the ChannelHandlerContext that’s supplied as an argument to each method. Because you’ll sometimes ignore uninteresting events, Netty provides the abstract base classes ChannelInboundHandlerAdapter and ChannelOutboundHandlerAdapter. Each provides method implementations that simply pass the event to the next handler by calling the corresponding method on the ChannelHandlerContext. You can then extend the class by overriding the methods that interest you.

Given that outbound and inbound operations are distinct, you might wonder what happens when the two categories of handlers are mixed in the same ChannelPipeline. Although both inbound and outbound handlers extend ChannelHandler, Netty distinguishes implementations of ChannelInboundHandler and ChannelOutboundHandler and ensures that data is passed only between handlers of the same directional type.

When a ChannelHandler is added to a ChannelPipeline, it’s assigned a ChannelHandlerContext, which represents the binding between a ChannelHandler and the ChannelPipeline. Although this object can be used to obtain the underlying Channel, it’s mostly utilized to write outbound data.

There are two ways of sending messages in Netty. You can write directly to the Channel or write to a ChannelHandlerContext object associated with a ChannelHandler. The former approach causes the message to start from the tail of the ChannelPipeline, the latter causes the message to start from the next handler in the ChannelPipeline.

3.2.3. A closer look at ChannelHandlers

As we said earlier, there are many different types of ChannelHandlers, and the functionality of each is largely determined by its superclass. Netty provides a number of default handler implementations in the form of adapter classes, which are intended to simplify the development of an application’s processing logic. You’ve seen that each ChannelHandler in a pipeline is responsible for forwarding events to the next handler in the chain. These adapter classes (and their subclasses) do this automatically, so you can override only the methods and events you want to specialize.

Why adapters?

There are a few adapter classes that reduce the effort of writing custom ChannelHandlers to a bare minimum, because they provide default implementations of all the methods defined in the corresponding interface.

These are the adapters you’ll call most often when creating your custom handlers:

  • ChannelHandlerAdapter
  • ChannelInboundHandlerAdapter
  • ChannelOutboundHandlerAdapter
  • ChannelDuplexHandlerAdapter

Next we’ll examine three ChannelHandler subtypes: encoders, decoders, and SimpleChannelInboundHandler<T>, a subclass of ChannelInboundHandlerAdapter.

3.2.4. Encoders and decoders

When you send or receive a message with Netty, a data conversion takes place. An inbound message will be decoded; that is, converted from bytes to another format, typically a Java object. If the message is outbound, the reverse will happen: it will be encoded to bytes from its current format. The reason for both conversions is simple: network data is always a series of bytes.

Various types of abstract classes are provided for encoders and decoders, corresponding to specific needs. For example, your application may use an intermediate format that doesn’t require the message to be converted to bytes immediately. You’ll still need an encoder, but it will derive from a different superclass. To determine the appropriate one, you can apply a simple naming convention.

In general, base classes will have a name resembling ByteToMessageDecoder or MessageToByteEncoder. In the case of a specialized type, you may find something like ProtobufEncoder and ProtobufDecoder, provided to support Google’s protocol buffers.

Strictly speaking, other handlers could do what encoders and decoders do. But just as there are adapter classes to simplify the creation of channel handlers, all of the encoder/decoder adapter classes provided by Netty implement either ChannelInboundHandler or ChannelOutboundHandler.

You’ll find that for inbound data the channelRead method/event is overridden. This method is called for each message that’s read from the inbound Channel. It will then call the decode() method of the provided decoder and forward the decoded bytes to the next ChannelInboundHandler in the pipeline.

The pattern for outbound messages is the reverse: an encoder converts the message to bytes and forwards them to the next ChannelOutboundHandler.

3.2.5. Abstract class SimpleChannelInboundHandler

Most frequently your application will employ a handler that receives a decoded message and applies business logic to the data. To create such a ChannelHandler, you need only extend the base class SimpleChannelInboundHandler<T>, where T is the Java type of the message you want to process. In this handler you’ll override one or more methods of the base class and obtain a reference to the ChannelHandlerContext, which is passed as an input argument to all the handler methods.

The most important method in a handler of this type is channelRead0(ChannelHandlerContext,T). The implementation is entirely up to you, except for the requirement that the current I/O thread not be blocked. We’ll have much more to say on this topic later.

3.3. Bootstrapping

Netty’s bootstrap classes provide containers for the configuration of an application’s network layer, which involves either binding a process to a given port or connecting one process to another one running on a specified host at a specified port.

In general, we refer to the former use case as bootstrapping a server and the latter as bootstrapping a client. This terminology is simple and convenient, but it slightly obscures the important fact that the terms “server” and “client” denote different network behaviors; namely, listening for incoming connections versus establishing connections with one or more processes.

Connection-oriented protocols

Please keep in mind that strictly speaking the term “connection” applies only to connection-oriented protocols such as TCP, which guarantee ordered delivery of messages between the connected endpoints.

Accordingly, there are two types of bootstraps: one intended for clients (called simply Bootstrap), and the other for servers (ServerBootstrap). Regardless of which protocol your application uses or the type of data processing it performs, the only thing that determines which bootstrap class it uses is its function as a client or server. Table 3.1 compares the two types of bootstraps.

Table 3.1. Comparison of Bootstrap classes

Category

Bootstrap

ServerBootstrap

Networking function Connects to a remote host and port Binds to a local port
Number of EventLoopGroups 1 2

The first difference between the two types of bootstraps has been discussed: a ServerBootstrap binds to a port, because servers must listen for connections, while a Bootstrap is used by client applications that want to connect to a remote peer.

The second difference is perhaps more significant. Bootstrapping a client requires only a single EventLoopGroup, but a ServerBootstrap requires two (which can be the same instance). Why?

A server needs two distinct sets of Channels. The first set will contain a single ServerChannel representing the server’s own listening socket, bound to a local port. The second set will contain all of the Channels that have been created to handle incoming client connections—one for each connection the server has accepted. Figure 3.4 illustrates this model, and shows why two distinct EventLoopGroups are required.

Figure 3.4. Server with two EventLoopGroups

The EventLoopGroup associated with the ServerChannel assigns an EventLoop that is responsible for creating Channels for incoming connection requests. Once a connection has been accepted, the second EventLoopGroup assigns an EventLoop to its Channel.

3.4. Summary

In this chapter we discussed the importance of understanding Netty from both technical and architectural standpoints. We revisited in greater detail some of the concepts and components previously introduced, especially ChannelHandler, ChannelPipeline, and bootstrapping.

In particular, we discussed the hierarchy of ChannelHandlers and introduced encoders and decoders, describing their complementary functions in converting data to and from network byte format.

Many of the following chapters are devoted to in-depth study of these components, and the overview presented here should help you keep the big picture in focus.

The next chapter will explore the network transports provided by Netty and how to choose the one best suited to your application.

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

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