Chapter 3 of Netty In Action: core concepts of Netty, nettyinaction
Note: This article is from Netty In Action;
NOTE: If your original translation is reprinted, please indicate the source!
In this chapter, we will discuss the 10 core classes of Netty. It is helpful to understand their structure. Some may not be used at work, but some may be very common and core, and you will encounter it.
- Bootstrap or ServerBootstrap
- EventLoop
- EventLoopGroup
- ChannelPipeline
- Channel
- Future or ChannelFuture
- ChannelInitializer
- ChannelHandler
The purpose of this section is to introduce these concepts and help you understand their usage.
3.1 Netty Crash Course
Before we start, it would be better if you understand the general structure and general usage of the Netty Program (both the client and server have a similar structure. A Netty program starts with the Bootstrap class. The Bootstrap class is a very important class provided by Netty that can be set or "Guided" by simple configuration. Netty designed Handlers to process specific "events" and set events in Netty to process multiple comments and data. Events can be described as a very common method, because you can customize a handler to convert an Object into a byte [] or convert a byte [] to an Object; you can also define a handler to handle thrown exceptions. You will often write a class that implements ChannelInboundHandler. ChannelInboundHandler is used to receive messages. When there is a message, you can decide how to handle it. When the program needs to return a message, it can write/flush data in ChannelInboundHandler. It can be considered that the business logic of the application is handled by ChannelInboundHandler, And the lifecycle of the business is in ChannelInboundHandler. Netty needs to know how to send or receive messages when connecting to the client or binding the server. This is done through different types of handlers. How do I configure multiple Handlers? Netty provides the ChannelInitializer class for configuring Handlers. ChannelInitializer adds ChannelHandler through ChannelPipeline, such as sending and receiving messages. These Handlers will determine what messages are sent. ChannelInitializer itself is also a ChannelHandler. After adding other handlers, it will automatically delete itself from ChannelPipeline. All Netty programs are based on ChannelPipeline. ChannelPipeline and EventLoop are closely related to EventLoopGroup, because the three of them are related to event processing, which is why they manage IO processing by EventLoop. All the I/O operations in Netty are executed asynchronously. For example, when you connect to a host, they are completed asynchronously by default, and when you write or send messages, they are also asynchronous. That is to say, the operation will not be executed directly, but will wait for a while, because you do not know whether the returned operation result is successful or failed, but you need to check whether the method is successful or register the listener for notification; netty uses Futures and ChannelFutures for this purpose. Future registers a listener, which will be notified when the operation is successful or fails. ChannelFuture encapsulates information about an operation. When an operation is executed, ChannelFuture is immediately returned. 3.2 Channels, Events and Input/Output (IO)
Netty is a non-blocking, event-driven network framework. Netty actually uses multithreading to process IO events. Readers familiar with multithreaded programming may need to synchronize code. This method is not good, because synchronization will affect program performance. Netty's design ensures that program processing events will not be synchronized. Display An EventLoopGroup and a Channel associated with a single EventLoop. The EventLoopGroup in Netty contains one or more eventloops, and EventLoop is the thread in which a Channel executes the actual work. EventLoop is always bound to a single thread and will not change during its lifecycle.
After registering a Channel, Netty binds the Channel to an EventLoop, which is always bound to an EventLoop during the Channel lifecycle. In Netty IO operations, your program does not need to be synchronized, because all IO operations of a specified channel are always executed by the same thread.
To help you understand the relationship between EventLoop and EventLoopGroup:
The association between EventLoop and EventLoopGroup is not intuitive, because we have said that EventLoopGroup contains one or more eventloops, but the figure above shows that EventLoop is an EventLoopGroup, this means that you can only use a specific EventLoop.
3.3 What is Bootstrap? Why use it?
"Bootstrap" is the process of configuring programs in Netty. bootstrap is required when you need to connect to a client or server to bind a specified port. As described above, "Bootstrap" has two types: Bootstrap for the client (also applicable to the mongoramchannel) and ServerBootstrap for the server. Regardless of the protocol used by the program, "Boot" is required for creating a client or server ". There are some similarities between the two bootsstraps. In fact, they have many similarities and differences. Differences between Bootstrap and ServerBootstrap:
- Bootstrap is used to connect to a remote host with one EventLoopGroup
- ServerBootstrap is used to bind a local port with two EventLoopGroup
The events group, transports, and handlers are described later in this chapter. Here we will only discuss the differences between the two "Bootstrap" types (Bootstrap and ServerBootstrap ). The first difference is obvious. The "ServerBootstrap" listener listens to a port on the server to poll whether the "Bootstrap" or "initramchannel" of the client is connected to the server. You usually need to call the connect () method of the "Bootstrap" class, but you can also call bind () before connect () for connection. The Channel used later is included in bind () returned ChannelFuture. The second difference may be the most important. The client bootstraps/applications uses a singleton EventLoopGroup, while ServerBootstrap uses two eventloopgroups (actually using the same instance). It may not be obvious, but it is a good solution. A ServerBootstrap can be considered as having two channels groups. The first group contains a singleton ServerChannel, which indicates holding a socket bound to the local port. The second group contains all the channels, indicates that the server has accepted the connection. The image describes this situation:
The only purpose of EventLoopGroup A is to accept the connection and hand it over to EventLoopGroup B. Netty can use two different groups, because the server program needs to accept many client connections, an EventLoopGroup will be the bottleneck of program performance, because the event loop is busy processing connection requests, there are no redundant resources or idle resources to process the business logic. The final result is that many connection requests have timed out. If two EventLoops instances exist, all connections are accepted even under high loads, because EventLoops accepts connections and does not process shared resources that are already connected.
What is the relationship between EventLoopGroup and EventLoop? EventLoopGroup can contain multiple eventloops. An EventLoop bound to each Channel is not changed because EventLoopGroup contains a small number of EventLoop Channels, and many Channels share the same EventLoop. This means that when EventLoop is busy in a Channel, other channels are prohibited from binding to the same EventLoop. We can understand that EventLoop is an event loop thread, while EventLoopGroup is an event loop set. If you decide to use the same EventLoopGroup instance to configure the Netty server twice, it shows how it changed:
Netty allows the same EventLoopGroup to process IO and accept connections, which is applicable to multiple applications. An EventLoopGroup is used to process connection requests and IO operations.
In the next section, we will introduce how Netty performs IO operations and when to execute them. 3.4 Channel Handlers and Data Flow (Channel Processing and Data Stream)
In this section, let's take a look at what happens when you send or receive data? Recall the handler concept mentioned in this chapter. To understand what happens when Netty program wirte or read occurs, you must first understand what Handler is. Handlers themselves rely on ChannelPipeline to determine the sequence of their execution. Therefore, it is impossible to define some aspects of the processing program through ChannelPipeline. In turn, it is impossible to define or define some aspects of ChannelPipeline through ChannelHandler. There is no need to say that we must define ourselves and other rules. This section describes the slight dependencies between ChannelHandler and ChannelPipeline. In many places, Netty's ChannelHandler is the most processed in your application. Even if you do not mean this, if you use a Netty application, at least one ChannelHandler will be involved. In other words, ChannelHandler is critical to many things. So what is ChannelHandler? It is not easy to define ChannelHandler. We can understand that ChannelHandler is a piece of code that executes business logic to process data. They often come and go through ChannelPipeline. In fact, ChannelHandler defines a handler's parent interface. Both ChannelInboundHandler and ChannelOutboundHandler implement the ChannelHandler interface, for example:
It is easier to display, and more importantly, the application of ChannelHandler in data stream. The example discussed here is just a simple example. ChannelHandler is applied in many aspects and will be learned in this book.
Netty has two data streams, showing a significant difference between the ChannelInboundHandler and the ChannelOutboundHandler: if the data is from the user application to the remote host, it is "outbound". If the data is from the remote host to the user application, it is "inbound) ". One or more ChannelHandler will operate the data in some way to make the data arrive at the other end from one end. These ChannelHandler will be added to ChannelPipeline in the program's "Pilot" phase, and the order of addition will determine the order of processing data. The role of ChannelPipeline can be understood as a container used to manage ChannelHandler. Each ChannelHandler processes its own data (for example, the inbound data can only be processed by ChannelInboundHandler ), after processing, place the converted data in ChannelPipeline and hand it to the next ChannelHandler for further processing until the last ChannelHandler completes processing. Shows the ChannelPipeline processing process:
Shows that both ChannelInboundHandler and ChannelOutboundHandler must go through the same ChannelPipeline.
In ChannelPipeline, if a message is read or has any other inbound events, the message will be passed to the first ChannelInboundHandler starting from the header of ChannelPipeline, this ChannelInboundHandler can process the message or transmit the message to the next ChannelInboundHandler. Once there is no remaining ChannelInboundHandler in ChannelPipeline, ChannelPipeline will know that the message has been processed by all the hungry Handler. In turn, any outbound event or write will start at the end of ChannelPipeline and be passed to the last ChannelOutboundHandler. ChannelOutboundHandler is the same as ChannelInboundHandler. It can transmit event messages to the next Handler or process messages by itself. The difference is that ChannelOutboundHandler starts from the tail of ChannelPipeline, and ChannelInboundHandler starts from the header of ChannelPipeline. After the first ChannelOutboundHandler is processed, some operations are performed, such as a write operation. An event can be passed to the next ChannelInboundHandler or the last ChannelOutboundHandler. In ChannelPipeline, each method is called by using ChannelHandlerContext. Netty Provides abstract event base classes called ChannelInboundHandlerAdapter and ChannelOutboundHandlerAdapter. Each provides the implementation of passing an event to the next Handler by calling the corresponding method in ChannelPipeline. The method we can cover is what we need to do. Some readers may wonder that the operations on the outbound and inbound sites are different. Can they work in the same ChannelPipeline? Netty's design is clever. the inbound and outbound Handler have different implementations. Netty can skip an unhandable operation, so when an outbound event occurs, channelInboundHandler will be skipped. Netty knows that each handler must implement ChannelInboundHandler or ChannelOutboundHandler. When a ChannelHandler is added to ChannelPipeline, A ChannelHandlerContext is obtained. It is usually safe to get the reference of this object, but this is incorrect when a datagram protocol such as UDP, this object can be used later to get the underlying channel, because it is used to read/write messages, the channel will retain them. In other words, there are two methods to send messages in Netty: directly writing to the channel or writing to the ChannelHandlerContext object. The main differences between the two methods are as follows:
- Direct Writing to the channel causes message processing to start from the tail of ChannelPipeline
- Write the ChannelHandlerContext object to process the message starting from the next handler of ChannelPipeline.
3.5 encoder, decoder, and business logic: detailed view of Handlers
As mentioned above, there are many different types of handlers, and each handler depends on their base classes. Netty provides a series of "Adapter" classes, which makes things simple. Each handler forwards the time to the next handler of ChannelPipeline. The * Adapter class (and subclass) is automatically completed, so we only need to rewrite the method in the * Adapter class of interest. These functions can help us easily encode/decode messages. There are several adapters that allow custom ChannelHandler. Generally, custom ChannelHandler must inherit one of the encoding/decoding adapter classes. Netty has an adapter:
- ChannelHandlerAdapter
- ChannelInboundHandlerAdapter
- ChannelOutboundHandlerAdapter
The three ChannelHandler went up. Let's take a look at ecoders, decoders and SimpleChannelInboundHandler <I> to inherit ChannelInboundHandlerAdapter.
3.5.1 Encoders (Encoders) and decoders (decoder)
After a message is sent or received, Netty must convert the message data from one form to another. After receiving a message, you need to convert the message from bytecode to a Java object (decoded by a decoder). Before sending the message, you need to convert the Java object to a byte (encoded by certain types of encoders ). This type of conversion generally occurs in network programs, because only byte data can be transmitted on the network. There are multiple basic types of encoders and decoders. Which one to use depends on the functions you want to implement. To find out a certain type of decoder, we can see from the class name, such as "ByteToMessageDecoder", "MessageToByteEncoder", and Google's protocols "ProtobufEncoder" and "ProtobufDecoder ". Strictly speaking, other handlers can be used as encoders and adapters. Different Adapter classes depend on what you want to do. If it is a decoder, there is a ChannelInboundHandlerAdapter or ChannelInboundHandler. All the decoders inherit or implement them. The "channelRead" method/event is overwritten. This method reads each message from the inbound channel. The rewritten channelRead method calls the "decode" method of each decoder and passes it to the next ChannelInboundHandler in ChannelPipeline through ChannelHandlerContext. fireChannelRead (Object msg. Similar to an inbound message, when you send a message (outbound), the encoder forwards the message to the next ChannelOutboundHandler in addition to converting it into a bytecode. 3.5.2 Domain logic)
Perhaps the most common thing is that an application decodes a received message and uses it for the business logic module. Therefore, the application only needs to extend SimpleChannelInboundHandler <I>, that is, we define a handler class that inherits SimpleChannelInboundHandler <I>, where <I> is the type of message that handler can process. You can obtain a ChannelHandlerContext reference by overwriting the parent class. They accept a ChannelHandlerContext parameter and you can store it as an attribute in the class. The main method that the handler pays attention to is "channelRead0 (ChannelHandlerContext ctx, I msg)". Whenever Netty calls this method, the object "I" is a message, and Java generic design is used here, the program can process I. How messages are processed depends entirely on the needs of the program. When processing messages, you must note that in Netty, event IO processing is usually multithreading. Do not block the IO thread in the program as much as possible, because blocking will reduce program performance. The IO thread must not be blocked, which means that blocking operations in ChannelHandler may be faulty. Fortunately, Netty provides a solution. We can specify an EventExecutorGroup when adding ChannelHandler to ChannelPipeline. EventExecutorGroup will get an EventExecutor and EventExecutor will execute all ChannelHandler methods. EventExecutor uses different threads to execute and release EventLoop.