This article mainly analyzes the implementation of Netty. Due to limited energy, I have not carefully studied its source code. If the following content is incorrect or not rigorous, please correct and understand. For Netty users, Netty provides several typical examples with detailed API doc and guide doc. Some of the content and illustration in this article also come from Netty's document.
1. Overall structure
First, put a beautiful overall structure of Netty. The following content mainly analyzes some core functions on the graph. However, this article does not analyze advanced optional functions such as Container Integration and Security Support.
2. Network model
Netty is a typical Reactor model structure. For details about Reactor, refer to POSA2. No conceptual explanation is provided here. Using Java NIO to build the Reactor mode, Doug Lea (the amazing guy) gave a good explanation in Scalable IO in Java. Here, we take a classic example in the PPT to illustrate the typical implementation of the Reactor mode:
1. This is the simplest single-threaded Reactor model. The Reactor thread is a multi-party operator responsible for multi-channel separation of sockets, Accept new connections, and dispatching requests to the processor chain. This model is suitable for scenarios where the service processing components in the processor chain can be quickly completed. However, this single-threaded model cannot fully utilize multi-core resources, so it is not used in practice.
2. Compared with the previous model, this model uses Multithreading (thread pool) in the processor chain and is also a common model for backend programs.
3. Compared with the second model, the third model divides the Reactor into two parts. The mainReactor is responsible for listening to server socket, accept new connections, and assigning the established socket to subReactor. SubReactor is responsible for separating connected sockets, reading and writing network data, and processing services. It is thrown to the worker thread pool. Generally, the number of subreactors can be the same as the number of CPUs.
After talking about the three forms of the reaco TR model, what is Netty? In fact, I still have a variant of the Reactor model, which is to remove the third form variant of the thread pool, which is also the default mode of Netty NIO. In implementation, the Boss class in Netty acts as mainReactor, and the NioWorker class acts as subReactor (the default number of NioWorker is Runtime. getRuntime (). availableProcessors ()). When processing new requests, NioWorker reads the received data to ChannelBuffer, and then triggers the ChannelHandler stream in ChannelPipeline.
Netty is event-driven and can control the execution flow through the ChannelHandler chain. Because the execution process of the ChannelHandler chain is synchronized in the subReactor, if the business processing handler takes a long time, it will seriously affect the number of supported concurrency. This model is suitable for application scenarios such as Memcache, but it is not suitable for systems that need to operate databases or block interaction with other modules. The scalability of Netty is very good. To achieve ChannelHandler thread pooling, you can add the built-in ChannelHandler class in ChannelPipeline? ExecutionHandler is implemented by adding a line of code to the user. For the thread pool model required by ExecutionHandler, Netty provides two options: 1) MemoryAwareThreadPoolExecutor can control the maximum number of tasks to be processed in Executor (beyond the upper limit, subsequent tasks will be blocked), and the maximum number of tasks to be processed in a single Channel can be controlled; 2) OrderedMemoryAwareThreadPoolExecutor is a subclass of MemoryAwareThreadPoolExecutor, it can also ensure the sequence of event streams processed in the same Channel, which mainly controls the sequence of events that may occur in asynchronous processing mode, however, it does not ensure that all events in the same Channel are executed in one thread (usually unnecessary ). Generally speaking, OrderedMemoryAwareThreadPoolExecutor is a good choice. Of course, you can also DIY it if necessary.
3. buffer
The structure of the org. jboss. netty. buffer package interface and class is as follows:
The core interfaces of this package are ChannelBuffer and ChannelBufferFactory. The following is a brief introduction.
Netty uses ChannelBuffer to store and operate read/write network data. In addition to methods similar to ByteBuffer, ChannelBuffer also provides some practical methods. For details, refer to its API documentation. There are multiple implementation classes of ChannelBuffer. Here are the main ones:
1) HeapChannelBuffer: this is the default ChannelBuffer used by Netty to read network data. Here, Heap is the meaning of Java Heap, because the data read from SocketChannel must pass through ByteBuffer, the actual operation of ByteBuffer is a byte array, so the ChannelBuffer contains a byte array, so that the conversion between ByteBuffer and ChannelBuffer is a zero copy method. HeapChannelBuffer is divided into BigEndianHeapChannelBuffer and LittleEndianHeapChannelBuffer based on the network word segments. BigEndianHeapChannelBuffer is used by default. Netty uses HeapChannelBuffer when reading network data. HeapChannelBuffer is a fixed-size buffer. In order not to assign a suitable Buffer size, netty will refer to the size required by the last request when allocating the Buffer.
2) DynamicChannelBuffer: compared with HeapChannelBuffer, DynamicChannelBuffer can be dynamically adaptive and small. DynamicChannelBuffer is usually used for data write operations in DecodeHandler when the data size is unknown.
3) ByteBufferBackedChannelBuffer: this is directBuffer, which directly encapsulates the directBuffer of ByteBuffer.
For the buffer for reading and writing network data, there are two allocation policies: 1) normally, a fixed size buffer is directly allocated for simple consideration. The disadvantage is that, for some applications, this size limit is sometimes not reasonable, and if the buffer limit is large, there will be a waste of memory. 2) for the disadvantage of fixed buffer size, dynamic buffer is introduced. Dynamic buffer is equivalent to List in Array.
There are also two common buffer storage policies (I only know this): 1) under the multi-thread (thread pool) model, each thread maintains its own read/write buffer, each time the buffer is cleared before a new request is processed (or cleared after processing), the read/write operations of the request must be completed in this thread. 2) binding the buffer and socket is not related to the thread. Both methods aim to reuse the buffer.
Netty's buffer processing policy is: When reading request data, Netty first reads the data to the newly created fixed-size HeapChannelBuffer. When the HeapChannelBuffer is full or there is no data readable, call handler to process data. This usually triggers the user-defined DecodeHandler first. Because the handler object is bound to the ChannelSocket, you can set the ChannelBuffer member in the DecodeHandler, when the parsed data packet finds that the data is incomplete, the processing process is terminated. When the next read event is triggered, the last data will continue to be parsed. In this process, the Buffer in the DecodeHandler bound to the ChannelSocket is usually a dynamic reusable Buffer (DynamicChannelBuffer ), in NioWorker, the buffer for reading data in ChannelSocket is a temporary allocated HeapChannelBuffer of a fixed size. This conversion process involves a byte copy behavior.
For the creation of ChannelBuffer, Netty uses the ChannelBufferFactory interface internally. The specific implementation includes DirectChannelBufferFactory and HeapChannelBufferFactory. For developers to create ChannelBuffer, you can use the factory method in the handler class ChannelBuffers.
4. Channel
The structure of the Channel-related interfaces and classes is as follows:
The structure diagram shows that the main functions of the Channel are as follows:
1) status information of the current Channel, such as whether to enable or disable the Channel.
2) Channel configuration information that can be obtained through ChannelConfig.
3) IO operations supported by the Channel, such as read, write, bind, and connect.
4) obtain the ChannelPipeline that processes the Channel. You can call the Channel to perform IO operations related to the request.
In terms of Channel implementation, NioServerSocketChannel and NioSocketChannel in Netty encapsulate the ServerSocketChannel and SocketChannel functions in java. nio respectively.
5. ChannelEvent
As mentioned above, Netty is event-driven and uses ChannelEvent to determine the direction of the event stream. A ChannelEvent is processed by the Channel's ChannelPipeline, and ChannelPipeline calls ChannelHandler for specific processing. The following figure shows the ChannelEvent-related interfaces and classes:
For the user, the ChannelHandler implementation class uses the MessageEvent inherited from ChannelEvent and calls its getMessage () method to obtain the read ChannelBuffer or converted object.
6. ChannelPipeline
In terms of event processing, Netty uses ChannelPipeline to control event streams and registers a series of ChannelHandler on it to process events. This is also a typical interception mode. The interfaces and class diagrams related to ChannelPipeline are as follows:
There are two types of event streams: upstream events and downstream events. In ChannelPipeline, the registered ChannelHandler can be either ChannelUpstreamHandler or ChannelDownstreamHandler. However, only ChannelHandler that matches the stream is called during ChannelPipeline transmission. In the filter chain of the event stream, ChannelUpstreamHandler or ChannelDownstreamHandler can terminate the process, or pass the event by calling ChannelHandlerContext. sendUpstream (ChannelEvent) or ChannelHandlerContext. sendDownstream (ChannelEvent. The following figure shows how to process event streams:
As shown in the preceding figure, upstream events are processed by Upstream Handler from bottom to top, and downstream events are processed by Downstream Handler one by one from top to bottom, the upper and lower relations here are the sequential relationships of Handler added to ChannelPipeline. Simple solution: upstream event is the process of processing external requests, while downstream event is the process of processing external requests.
The request processing process on the server is usually decoding the request, business logic processing, and encoding response. The constructed ChannelPipeline is similar to the following code snippet:
ChannelPipeline pipeline = Channels. pipeline (); pipeline. addLast ("decoder", new MyProtocolDecoder (); pipeline. addLast ("encoder", new MyProtocolEncoder (); pipeline. addLast ("handler", new MyBusinessLogicHandler ());
Specifically, MyProtocolDecoder is the ChannelUpstreamHandler type, and MyProtocolEncoder is the ChannelDownstreamHandler type. The handler can be both the ChannelUpstreamHandler type and the ChannelDownstreamHandler type, depending on whether it is a server program or a client program.
Additionally, Netty decouples abstraction and implementation. Like org. jboss. netty. channel. the socket package defines some interfaces related to socket processing, while org. jboss. netty. channel. socket. nio, org. jboss. netty. channel. socket. oio and other packages are Protocol-related implementations.
7. codec framework
For encoding and decoding of the request protocol, you can operate the byte data in ChannelBuffer according to the protocol format. On the other hand, Netty also made several practical codec helper, which is a brief introduction here.
1) FrameDecoder: FrameDecoder maintains a DynamicChannelBuffer member internally to store the received data. It is like an abstract template, which has completed the entire decoding process template, its subclass only needs to implement the decode function. There are two direct implementation classes of FrameDecoder: (1) DelimiterBasedFrameDecoder is a decoder based on a delimiter (such as \ r \ n). You can specify a delimiter in the constructor. (2) LengthFieldBasedFrameDecoder is a decoder based on the length field. This decoder can be used if the proposed format is similar to "content length" + content, "Fixed header" + "content length" + dynamic content, the usage is explained in detail in the api doc.
2) ReplayingDecoder: it is a variable subclass of FrameDecoder. It is non-blocking decoding relative to FrameDecoder. That is to say, when using FrameDecoder, you need to consider that the data read may be incomplete, and you can use ReplayingDecoder to assume that you have read all the data.
3) ObjectEncoder and ObjectDecoder: encode and decode the serialized Java object.
4) HttpRequestEncoder and HttpRequestDecoder: http protocol processing.
Here are two examples of using FrameDecoder and ReplayingDecoder:
Public class IntegerHeaderFrameDecoder extends FrameDecoder {protected Object decode (ChannelHandlerContext ctx, Channel channel, ChannelBuffer buf) throws Exception {if (buf. readableBytes () <4) {return null;} buf. markReaderIndex (); int length = buf. readInt (); if (buf. readableBytes () <length) {buf. resetReaderIndex (); return null;} return buf. readBytes (length );}}
The decoding fragments using ReplayingDecoder are similar to the following, which is much simpler.
Public class extends ReplayingDecoder {protected Object decode (ChannelHandlerContext ctx, Channel channel, ChannelBuffer buf, VoidEnum state) throws Exception {return buf. readBytes (buf. readInt ());}}
In implementation, when the ChannelBuffer is called in the decode function of the ReplayingDecoder subclass to read data, if the Read fails, the ReplayingDecoder will catch the Error thrown by it and then take control of the ReplayingDecoder, wait for the next subsequent data to be read.
8. Summary
Although this article is about to end, this article does not fully explain the Netty implementation principles. When I plan to write this article, I also read the Netty code while summing up some writeable east and west, but it was intermittent and I was not very interested at the end. I still love to do some source code analysis, but I have limited energy. If the source code analysis results cannot be organized, it cannot produce meaningful experiences, this analysis has no value or interest. According to the analysis of Netty code, Netty's code is beautiful and its structure is clear. However, this interface-oriented and abstract level is very problematic for code tracking, because tracing code often encounters interfaces and abstract classes, it can only use factory classes and api doc to repeatedly compare the ing between interfaces and implementation classes. Just like almost any good Java open-source project will use a series of excellent design patterns, you can also come up with an independent analysis chapter from the pattern, although I do not have this idea at the moment. After this article is completed, I am not very interested in looking at the Netty code.