Netty source code parsing (a) : start

Netty source code parsing (2) : Netty Channel

Netty source code parsing (three) : Netty Future and Promise

Netty source code analysis (four) : Netty’s ChannelPipeline

Netty source code parsing (five) : Netty thread pool analysis

Netty source code parsing (six) : Channel register operation

NioEventLoop (NioEventLoop

Return to Channel register operation

Netty source code parsing (nine) : Connect process and bind process analysis


Today! Mr. Lighthouse told us:


The Netty ChannelPipeline

ChannelPipeline and Inbound and Outbound

I think many readers have the concept of pipeline in Netty at some point. As mentioned earlier, with Netty, we usually just write custom handlers that form a pipeline for handling IO events. This means the same thing as an Interceptor or Filter.


A pipeline consists of multiple handlers. The order between handlers is important because IO events will pass through handlers on the pipeline in sequence. This allows each handler to focus on doing small things, and multiple handlers can be combined to do some complex logic.

We know from the diagram that this is a two-way linked list.
First, let’s look at two important concepts:
Inbound
Outbound. In Netty, I/O events are classified into Inbound events and Outbound events.
Outbound
outRefers to the
To go out, which IO events fall into this category? For example, I/O operations such as connect, write, and Flush are Outbound events.
Others, such as Accept and read, are Inbound events.
For example, when the client initiates a request, it needs 1️ connect to the server, then 2️ discount data transfer to the server, and then 3️ data returned by the server, the above connect and write are
outEvent, and then read
inEvents.
For example, many beginners will not understand the following code, which is used in the server childHandler:

1. pipeline.addLast(new StringDecoder());

2. pipeline.addLast(new StringEncoder());

3. pipeline.addLast(new BizHandler());

Beginners must wonder, think that this order is wrong, should be the first decode client over the data, and then BizHandler processing business logic, and finally encode data and then returned to the client, so the order should be added
1 -> 3 -> 2Just right.


The three handlers are grouped into Inbound (1 and 3) and Outbound (2) :

1. pipeline.addLast(new StringDecoder());
2. pipeline.addLast(new StringEncoder());
3. pipeline.addLast(new BizHandler());Copy the code
  • When the client connects in, the operation of reading (read) the data requested by the client is Inbound, and the operation of E is Outbound, and 2 is used.

  • After the data is processed, the write operation of the data returned to the client is Outbound, and 2 is used.

So while the order of addition is a bit strange, the order of execution is actually 1 -> 3 -> 2.
If we add the fourth line below to the top, this is an OutboundHandler:
4. pipeline.addLast(new OutboundHandlerA());Copy the code
Is the order of execution 1 -> 3 -> 2 -> 4? The answer is: no.
For Inbound operations, each Inbound type handler is executed in the order in which it was added. For Outbound operations, the Outbound handler is executed in reverse order from back to front.
Therefore, the order above should be first 1, then 3, they are Inbound, then 4, and finally 2, they are Outbound. To be honest, this kind of organization should be a headache for beginners.
So how do we write that in development? In fact, it is very simple to write from the outermost layer, step by step to the business processing layer, mixing Inbound and Outbound. For example, encode and decode are the outermost processing logic, so write them first. Assuming decode is going to be a string, there should be another layer of writing in and out logs. In the next layer, you can write interconversions of string <=> objects. Then it’s time to write the business layer.
At this point, I think we all know Inbound and Outbound, right? Let’s take a look at their interface usage.


The ChannelInboundHandler is required for defining Inbound events and the ChannelOutboundHandler for defining Outbound events. The bottom three classes are Netty provided adapters. In particular, if we want to define a handler that handles both Inbound and Outbound events, we can inherit the middle class
ChannelDuplexHandlerFor example
LoggingHandlerThis handler can be used to handle both Inbound and Outbound events.
With the concepts of Inbound and Outbound, we start to introduce the source code of Pipeline.
As we said, a Channel is associated with a pipeline, NioSocketChannel and NioServerSocketChannel both go to their parent AbstractChannel constructor when executing a constructor:
protected AbstractChannel(Channel parent) { this.parent = parent; // Assign a unique id to each channel id = newId(); // Within each channel, an instance Unsafe is required. Unsafe = newUnsafe(); Pipeline = newChannelPipeline(); }Copy the code

In the three lines above, id is not important. The Unsafe instance in Netty is, and here’s a quick look at it.

The sun.misc.Unsafe class, in JDK source code, provides low-level operations designed for use in JDK source code, such as AQS, ConcurrentHashMap, etc., The Unsafe class is not intended for our code, but for JDK source code (we can retrieve instances of it if we need to).
The Unsafe class constructor is private, but it provides the getUnsafe() static method:
Unsafe unsafe = Unsafe.getUnsafe();Copy the code

You can try it out. This line of code compiles fine, but it will throw when it executes
java.lang.SecurityExceptionException, because it’s not for our code.
However, if you’re looking for an instance of Unsafe, you can get it using the following code:
Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); Unsafe unsafe = (Unsafe) f.get(null);Copy the code

Broadening in Netty also means the same thing, encapsulating the NIO interface provided by the JDK that Netty uses, such as registering a channel with a selector, such as the bind operation, or the connect operation.
These operations are a little bit below the surface. Netty also didn’t want our business code to use Unsafe as an example, which was made available to source code in Netty.
However, for our source code analysis, we will still have a lot of time need to analyze the source of the Unsafe
Unsafe, as we’ll look at later, encapsulates most of the operations that need to access the JDK’s NIO interface. Here we continue to focus on the instantiation pipeline:
protected DefaultChannelPipeline newChannelPipeline() {    returnnew DefaultChannelPipeline(this); }Copy the code



The DefaultChannelPipeline constructor is called here, passing in a reference to the current channel:


protected DefaultChannelPipeline(Channel channel) {     this.channel = ObjectUtil.checkNotNull(channel, "channel");     succeededFuture = new SucceededChannelFuture(channel, null);     voidPromise =  new VoidChannelPromise(channel, true); tail = new TailContext(this); head = new HeadContext(this); head.next = tail; tail.prev = head; }Copy the code

The tail and head handlers are instantiated here. Tail implements ChannelInboundHandler, while head implements ChannelOutboundHandler and ChannelInboundHandler. And the last two lines join tail and head:Copy the code

Note that the source code varies slightly from version to version, and head is not necessarily in + out, so it’s good to know that.
As can be seen from head and tail above, every element in the pipeline is
ChannelHandlerContextContext wraps a handler, but we’ll use a handler to describe a pipeline node instead of a context.
The pipeline is constructed and two fixed handlers are added to it (head + tail). There is no custom handler code execution involved. Let’s go back to the following code:

We said that the handler specified in childHandler is not for NioServerSocketChannel, it’s for NioSocketChannel, so we’re not going to look at it here.
This calls handler(…) The LoggingHandler method specifies an instance of LoggingHandler, and then we go to bind(…) below. Method to see how this LoggingHandler instance entered the pipeline we constructed earlier.
Follow bind() all the way, bind() -> doBind() -> initAndRegister() :
final ChannelFuture initAndRegister() { Channel channel = null; Try {// 1. Create channel instance; / / now in the pipeline has a head and tail. Two handler channel = channelFactory newChannel (); Init (channel); } catch (Throwable t) { ...... }Copy the code

Init (channel, channel, channel, channel);
/ / ServerBootstrap:
@Override void init(Channel channel) throws Exception { ...... ChannelPipeline p = channel.pipeline(); . // Start adding a handler to the pipeline, This handler is an instance of ChannelInitializer. Addlast (new ChannelInitializer<Channel>() {// We'll see later, @override public void initChannel(final Channel CH) throws Exception {final ChannelPipeline pipeline = ch.pipeline(); ChannelHandler handler = config.handler(); // This method returns the LoggingHandler instance we originally specified.if(handler ! // Add LoggingHandler to pipeline.addLast(handler); EventLoop ch.eventloop ().execute(newRunnable() {                @Override                public void run() {// Add a handler to pipeline: ServerBootstrapAcceptor // As the name indicates, This handler is designed to receive client requests from pipeline.addLast(new ServerBootstrapAcceptor(ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); }}); }}); }Copy the code

The ChannelInitializer class is an Inbound handler, but it works a little differently than a normal handler. It is used to assist in adding other handlers to the pipeline.

Take a look at the initChannel method of ChannelInitializer to get a simple idea of what the pipeline should look like:

When the initChannel(channel) method of the ChannelInitializer is called, it adds the one we originally specified to the pipeline
LoggingHandlerAnd add a
ServerBootstrapAcceptor. But we do not yet know when the initChannel method will be called.
Bootstrap init(channel); Bootstrap init(channel); Bootstrap ();
void init(Channel channel) throws Exception { ChannelPipeline p = channel.pipeline(); p.addLast(config.handler()); . }Copy the code



Instead of adding ServerBootstrap to ServerBootstrapAcceptor, it simply adds the ChannelInitializer instance from the EchoClient class. Its ChannelInitializer added two handlers, LoggingHandler and EchoClientHandler:


Obviously, we need handlers like LoggingHandler and EchoClientHandler, but they’re not in pipelines yet, so when will they actually be? We’ll find out later.
Also, why do we specify a Handler instance on the Server side and a ChannelInitializer instance on the Client side? You can even add an instance of ChannelInitializer to the ChannelInitializer instance.
Sorry, I’m going to break again, so I’m going to start with thread pools, remember what Pipeline looks like now,
head + channelInitializer + tail.
This section does not describe the backward propagation of a handler. After a handler is processed, how does it pass to the next handler? For example, the familiar Filter in JavaEE is passed to the next Filter by calling chain.dofilter (Request, response) in one Filter instance.
We end this section with the following diagram. The following figure shows the propagation method, but I would like to show you which events are Inbound and which are Outbound:
Note that bind is also an Outbound type.