First of all, we know that in NIO network programming model, IO operations are directly related to channel, such as client request connection, or sending data to the server, the server must obtain this data from the client channel.
So what is Channel Pipeline?
In fact, this channel Pepiline is a component added by netty to the native channel. Annotations on the Channel Pipeline interface illustrate the role of channel Pipeline. This channel Pipeline is the implementation of advanced filters. netty directs the data in channel Pipeline and gives users 100% control over the data in channel. In addition, the channelPipeline data structure is a two-way linked list. Each node is ChannelContext. ChannelContext maintains the reference of the corresponding handler and pipeline. To sum up, through channelPipeline, users can easily write data to Channel and read data from Channel.
Create pipeline
Through the tracing of previous blogs, we know that whether we create the channel of the server through reflection or directly create the channel of the client by new, as the parent constructor is invoked layer by layer, we will eventually create a large component C of Channel in AbstractChannel, the top abstract class of Channel system. Hannel Pipeline
So at the entrance of our program, pipeline of AbstractChannel = new Channel Pipeline ();, follow up and see his source code as follows:
protected DefaultChannelPipeline newChannelPipeline() { // todo follow up return new DefaultChannelPipeline(this); }
As you can see, it creates a Default Channel Pipeline (this Channel)
Default Channel Pipeline is the default implementation of Channel Pipeline. It plays an important role. Let's take a look at the following inheritance system diagram of Channel ChannelContext Channel Pipeline. We can see two classes in the diagram, which are very important in fact.
What's the relationship between them?
When we have finished looking at what we have done in the Default Channel Pipeline () construct, it's natural to know.
// todo is here protected DefaultChannelPipeline(Channel channel) { this.channel = ObjectUtil.checkNotNull(channel, "channel"); // todo saves the current Channel succeededFuture = new SucceededChannelFuture(channel, null); voidPromise = new VoidChannelPromise(channel, true); // The two methods of todo are very important // todo setup tail tail = new TailContext(this); // todo setup header head = new HeadContext(this); // todo bidirectional linked list Association head.next = tail; tail.prev = head; }
The following main things have been done:
- Initialize succeeded future
- Initialize voidPromise
- Create tail nodes
- Create header nodes
- Associated Head and Tail Nodes
Actually, so far, the initialization of pipiline has been completed. Let's go on and look at it.
In addition, let's look at the internal classes and methods of Default Channel Pipeline, as shown below ()
We focus on the parts I circle.
- Two important internal classes
- Header Context of Header Node
- Tail Context
- Tasks processed after adding handler to Pending Handler AddedTask
- Pending Handler CallBack adds handler callbacks
- Tasks after PengdingHandler RemovedTask removes Handler
- A large number of addXXX methods,
final AbstractChannelHandlerContext head; final AbstractChannelHandlerContext tail;
Follow up its packaging methods:
TailContext(DefaultChannelPipeline pipeline) { super(pipeline, null, TAIL_NAME, true, false); setAddComplete(); } // todo is here AbstractChannelHandlerContext(DefaultChannelPipeline pipeline, EventExecutor executor, String name, boolean inbound, boolean outbound) { this.name = ObjectUtil.checkNotNull(name, "name"); // todo attaches a value to the pipeline of ChannelContext this.pipeline = pipeline; this.executor = executor; this.inbound = inbound; this.outbound = outbound; // Its ordered if its driven by the EventLoop or the given Executor is an instanceof OrderedEventExecutor. ordered = executor == null || executor instanceof OrderedEventExecutor; }
As we can see, this tail node is an inbound processor. At first, I was really puzzled. Shouldn't header be an inbound processor? I won't buy it anymore. Why?
Because, for netty, the data transmitted from the header node will start to propagate backwards, how to propagate it? Because it is a two-way linked list, directly find the latter node, what kind of node? Inbound type, so the data MSG propagates backwards from the first node after the header, if so, until the end, it is only propagating. Data will be propagated to tail node without any processing, because tail is also inbound type, tail node will release this msg for us to prevent memory leak. Of course, if we use MSG ourselves, and do not propagate back, nor release, memory leak is sooner or later, this is why tail is Inbound type, header node Contrary to it, here's how
ok, now you know the creation of Channel Pipeline
The relationship between Channel pipeline and Channel Handler and Channel Handler Context
In the process of pipeline creation, the head and tail nodes in Default Channel Pipeline are ChannelHandlerContext, which means that in the structure of pipeline bidirectional list, each node is a ChannelHandlerContext, and each ChannelHandlerContext maintains a han. If you don't believe it, you can see the figure above. The implementation class of ChannelHandlerContext, DefaultChannelHandlerContext, has the following source code:
final class DefaultChannelHandlerContext extends AbstractChannelHandlerContext { // There is a reference to handler in todo Context private final ChannelHandler handler; // todo creates the default ChannelHandlerContext, DefaultChannelHandlerContext( DefaultChannelPipeline pipeline, EventExecutor executor, String name, ChannelHandler handler) { super(pipeline, executor, name, isInbound(handler), isOutbound(handler)); if (handler == null) { throw new NullPointerException("handler"); } this.handler = handler;
ChannelHandlerContext interface inherits both ChannelOutBoundInvoker and ChannelInBoundInvoker so that it has both inbound and outbound events to propagate. After ChannelHandlerContext propagates events, who handles them? Of course, the inheritance system diagram of ChannelHandler is given below. You can see that it is for inbound events. Stand-out and outbound processing ChannelHandler have different inheritance branch responses
Add a new node:
Generally, we add multiple handler s dynamically at one time through Chanel Initialezer. Let's see the init() of ServerBootStrap in the process of server startup. The following source code: parse me and write it under the code.
// todo This is the implementation of ServerBootStrapt to initialize channel for his parent class to initialize NioServerSocket Channel @Override void init(Channel channel) throws Exception { // todo ChannelOption is the ChannelConfig information for configuring Channel final Map<ChannelOption<?>, Object> options = options0(); synchronized (options) { // todo passes NioserverSocketChannel and options Map in, assigning values to the properties in Channel // todo constants are all about information related to protocols such as TCP setChannelOptions(channel, options, logger); } // Another wave of todo assigning attrs0() to attributes in Channel is to get user-defined business logic attributes -- AttributeKey final Map<AttributeKey<?>, Object> attrs = attrs0(); // todo is a map that maintains dynamic business data while the program is running. It can realize that business data can be retrieved from the original stored data along with the operation of netty. synchronized (attrs) { for (Entry<AttributeKey<?>, Object> e : attrs.entrySet()) { @SuppressWarnings("unchecked") AttributeKey<Object> key = (AttributeKey<Object>) e.getKey(); channel.attr(key).set(e.getValue()); } } // Todo - --- options attrs: can be dynamically passed in when creating BootStrap // Todo Channel Pipeline is an important component in itself. It contains a processor one by one. It says that he is a high-level filter. Interactive data passes through it layer by layer. // p is called directly below todo, which means that pipeline has been created before channel calls pipeline method. // When exactly was todo created? In fact, when NioServer SocketChannel was created as a channel object, a default pipeline object was created in his top Abstract parent class (AbstractChannel). /// todo adds: ChannelHandlerContext is the bridge between ChannelHandler and Pipline ChannelPipeline p = channel.pipeline(); // Todo workerGroup handles IO threads final EventLoopGroup currentChildGroup = childGroup; // Initializer we added to todo final ChannelHandler currentChildHandler = childHandler; final Entry<ChannelOption<?>, Object>[] currentChildOptions; final Entry<AttributeKey<?>, Object>[] currentChildAttrs; // todo Here are some of the property settings we added to the Server class for the new connection channel, which are used by acceptor!!! synchronized (childOptions) { currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size())); } synchronized (childAttrs) { currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size())); } // todo adds a Channel Initializer to NioServer Socket Channel by default. // todo (the Channel Initializer that we later added ourselves inherited from the Channel Initializer, which we inherited from the Channel Initializer implements ChannelHandler) p.addLast(new ChannelInitializer<Channel>() { // todo enters addlast // todo is a Channel Initializer that allows us to add multiple processors to the pipeline at once. @Override public void initChannel(final Channel ch) throws Exception { final ChannelPipeline pipeline = ch.pipeline(); // todo gets the handler object of bootStrap without returning null // To do this handler for the Channel of the bossgroup, add the handler() we added to the server class to add the processor ChannelHandler handler = config.handler(); if (handler != null) { pipeline.addLast(handler); } // The todo Server Bootstrap Acceptor receiver is a special Chanel handler ch.eventLoop().execute(new Runnable() { @Override public void run() { // todo!!! - This is very important. In Server BootStrap, netty has generated receivers for us!!! // todo specializes in handling access to new connections, binding channel s of new connections to a thread in workerGroup // todo is used to process user requests, but it's not clear how it triggers execution pipeline.addLast(new ServerBootstrapAcceptor( // todo These parameters are user-defined parameters // Todo NioServer Socket Channel, Worker Thread Group Processor Relations Events ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); } }); } });
This function is really long, but our focus is on channel Initializer. At this stage, channel has not registered with Selector on EventLoop.
Is there any analysis of how to add handler? How to come here? In fact, the following Server Bootstrap Acceptor is a handler
Let's see what the above code does.
ch.eventLoop().execute(new Runnable() { @Override public void run() { // todo!!! - This is very important. In Server BootStrap, netty has generated receivers for us!!! // todo specializes in handling access to new connections, binding channel s of new connections to a thread in workerGroup // todo is used to process user requests, but it's not clear how it triggers execution pipeline.addLast(new ServerBootstrapAcceptor( // todo These parameters are user-defined parameters // Todo NioServer Socket Channel, Worker Thread Group Processor Relations Events ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); } });
No? It was really a cover-up for me at that time, but it was not related to EventLoop!!! Where did ch. EvetLoop come from?.
Later, it became clear that this was actually a callback. netty provided users with the means to add handler to pipeline at any time.
So where is the callback? Actually, it's called back immediately after the selection in EventLoop from jdk's native channel group. The source code is as follows
private void register0(ChannelPromise promise) { try { // check if the channel is still open as it could be closed in the mean time when the register // call was outside of the eventLoop if (!promise.setUncancellable() || !ensureOpen(promise)) { return; } boolean firstRegistration = neverRegistered; // todo enters the method doRegister() // todo registers Server Socket Channel created by the system into the selector doRegister(); neverRegistered = false; registered = true; // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the // user may already fire events through the pipeline in the ChannelFutureListener. // todo ensures that handler Added (...) is called before notify the promise // todo is necessary because the user may have triggered the event through a pipeline in Channel FutureListener. // todo executes the HandlerAdded() method if necessary // todo is just this method, which calls back the important way we added Accpter to Initializer pipeline.invokeHandlerAddedIfNeeded();
The callback function is pipeline. invokeHandler AddedIfNeeded ();, look at its name, if necessary, the execution handler has added the operation Haha, we certainly need now, just added a Server BootstrapAcceptor.
Between looking at the source code, notice that the method is called by pipeline. What pipeline is it? That's what we said above: Default Channel Pipeline, ok, follow up the source code and go to Default Channel Pipeline.
// todo performs the addition of handler, if necessary final void invokeHandlerAddedIfNeeded() { assert channel.eventLoop().inEventLoop(); if (firstRegistration) { firstRegistration = false; // todo Now that our channel is registered with EvetLoop in the bossGroup, it's time to call back the handler s that were added before registration. callHandlerAddedForAllHandlers(); } }
Call this class method callHandlerAddedForAllHandlers(); follow up
// todo callback handler added before registration was completed private void callHandlerAddedForAllHandlers() { final PendingHandlerCallback pendingHandlerCallbackHead; synchronized (this) { assert !registered; // This Channel itself was registered. registered = true; pendingHandlerCallbackHead = this.pendingHandlerCallbackHead; // Null out so it can be GC'ed. this.pendingHandlerCallbackHead = null; } PendingHandlerCallback task = pendingHandlerCallbackHead; while (task != null) { task.execute(); task = task.next; } }
Our action task.execute();
The pending handler CallbackHead is an internal class of Default Channel Pipeline, which assists in completing callbacks after adding handlers, and the source code is as follows:
private abstract static class PendingHandlerCallback implements Runnable { final AbstractChannelHandlerContext ctx; PendingHandlerCallback next; PendingHandlerCallback(AbstractChannelHandlerContext ctx) { this.ctx = ctx; } abstract void execute(); }
As we follow up on task.execute() in the previous step, we will see its compact approach, so who implemented it? The implementation class is PendingHandlerAddedTask, which is also the internal class of DefaultChannelPipeline. Since it is not an abstract class, we have to implement the abstract method of its parent class PendingHandlerCallback at the same time. In fact, there are two ways: excute(). The other is run() --Runable
Let's go in and see how it implements excute. The source code is as follows:
@Override void execute() { EventExecutor executor = ctx.executor(); if (executor.inEventLoop()) { callHandlerAdded0(ctx); } else { try { executor.execute(this); } catch (RejectedExecutionException e) { if (logger.isWarnEnabled()) { logger.warn( "Can't invoke handlerAdded() as the EventExecutor {} rejected it, removing handler {}.", executor, ctx.name(), e); } remove0(ctx); ctx.setRemoved(); } }
Callback timing of HandlerAdded()
We trace down and call the class method callHandlerAdded0(ctx); the source code is as follows:
// todo focuses on this method, and the entry is the Context that was just added. private void callHandlerAdded0(final AbstractChannelHandlerContext ctx) { try { // todo calls after handler is associated with channel and Context is added to Pipeline!!! ctx.handler().handlerAdded(ctx); // todo is the first of many callback methods to be called ctx.setAddComplete(); // todo modification status } ...
Keep tracking down
- ctx.handler() -- Gets the current channel
- Call channel's. handler Added (ctx);
This handler Added () is a callback method defined in ChannelHandler. When is the callback? When handler is added, callback is made because we know that when the server-side channel is started, the Server Bootstrap Acceptor is added through the channel Initializer, so when is the handler Added () callback of the Server Bootstrap Acceptor? The machine is ctx. handler (). handler Added (ctx) in the code above.
If you click on this function directly, it must be in the ChannelHandler interface; then the new question comes, who is the implementation class? The answer is the abstract class ChannelInitializer `, on which we add the ServerBootstrapAcceptor and create an anonymous object of ChannelInitializer `.
Its inheritance system chart is as follows:
Introduce this Channel Initializer, which is an auxiliary class provided by Netty to provide initialization work for channel. What work? Batch initialization of channel?
There are three important ways to do this, as follows
- Handler Added () of the rewritten channel
- My own initChannel()
- Own remove()
Follow up on our handler Added (Channel Handler Context ctx) source code as follows:
@Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { initChannel(ctx); // The todo method is above, and the logic to remove Initializer can be found in final. } }
Call initChannel(ctx) of this class; the source code is as follows:
private boolean initChannel(ChannelHandlerContext ctx) throws Exception { if (initMap.putIfAbsent(ctx, Boolean.TRUE) == null) { // Guard against re-entrance. try { initChannel((C) ctx.channel()); } catch (Throwable cause) { // Explicitly call exceptionCaught(...) as we removed the handler before calling initChannel(...). // We do so to prevent multiple calls to initChannel(...). exceptionCaught(ctx, cause); } finally { // Todo remove (ctx); delete Channel Initializer remove(ctx); } return true; } return false; }
Two points
- First, continue to call the abstract method initChannel((C) ctx.channel()) of this class.
- Second, remove(ctx);
Take the first step separately
InitChannel ((C) ctx. channel (); Initialization channel, this function is designed to be abstract, the question arises, who is the implementation class? In fact, the implementation class just said that netty created the anonymous internal class when adding ServerBootStrapAcceptor. Let's follow up to see its implementation: The source code is as follows:
@Override public void initChannel(final Channel ch) throws Exception { final ChannelPipeline pipeline = ch.pipeline(); // todo gets the handler object of bootStrap without returning null // To do this handler for the Channel of the bossgroup, add the handler() we added to the server class to add the processor ChannelHandler handler = config.handler(); if (handler != null) { pipeline.addLast(handler); } // The todo Server Bootstrap Acceptor receiver is a special Chanel handler ch.eventLoop().execute(new Runnable() { @Override public void run() { // todo!!! - This is very important. In Server BootStrap, netty has generated receivers for us!!! // todo specializes in handling access to new connections, binding channel s of new connections to a thread in workerGroup // todo is used to process user requests, but it's not clear how it triggers execution pipeline.addLast(new ServerBootstrapAcceptor( // todo These parameters are user-defined parameters // Todo NioServer Socket Channel, Worker Thread Group Processor Relations Events ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); } }); }
In fact, it completes a method callback and successfully adds a Server Bootstrap Acceptor processor.
Delete a node
Come back to step two
remove(ctx); delete a node, delete Initializer? Yes, delete this initializer, why delete it, said many times, in fact, he is an auxiliary class, the purpose is to add multiple handlers to the channel at one time through him, and now the handler has been added, leaving him useless, move directly. except
Let's look at the source code.
// todo deletes the current ctx node private void remove(ChannelHandlerContext ctx) { try { ChannelPipeline pipeline = ctx.pipeline(); if (pipeline.context(this) != null) { pipeline.remove(this); } } finally { initMap.remove(ctx); } }
Remove from pipeline. Looking all the way through, you will find that the dungeon deletes linked list nodes.
private static void remove0(AbstractChannelHandlerContext ctx) { AbstractChannelHandlerContext prev = ctx.prev; AbstractChannelHandlerContext next = ctx.next; prev.next = next; next.prev = prev; }
Dissemination of inbound events
What is an inbound event
Inbound event is actually an event initiated by the client actively, such as client requesting connection, client sending valid data to server actively after connection, and so on. As long as the event initiated by client actively is an Inbound event, it is characterized by event triggering type. When channel is in a node, it triggers service. What actions do end-to-end propagate?
How netty Treats inbound
Netty adds pipeline components to jdk's native channel in order to better handle the data in channel. netty will direct the data in the channel of jdk's native channel to the pipeline, and start to spread downward from the head of the pipeline. Users have 100% control over the process. They can take the data out for processing or download it. Broadcast, spread to the tail node, tail node will be recycled, if in the process of transmission, the end node did not reach, and they did not recycle, they will face the problem of memory leak.
In conclusion, in the face of Inbound data, passive communication
Does netty know what type of data the client sends?
For example, a chat program, the client may send a heartbeat package, or may send chat content, netty is not human, he does not know what the data is, he only knows the data, need to be further processed, how to deal with it? Directing the data to the user-specified handler chain.
Start reading source code
Here the book is followed by the end of a blog, the dissemination of events.
The key steps are as follows
Step 1: Wait for the server to start and finish
Step 2: Use telnet to simulate the access logic for sending requests - > new connections
Step 3: The channel Promise Promise method propagates the channel Activation Event - > for the purpose of re-registering the port.
The third is the starting point of our program: fireChannelActive() source code is as follows:
@Override public final ChannelPipeline fireChannelActive() { // Todo Channel Active spreads from head AbstractChannelHandlerContext.invokeChannelActive(head); return this; }
Called the invokeChannelActive method of AbstractChannelHandlerContext
Here, I think it's especially necessary to tell myself the importance of AbstractChannelHandlerContext. Every node in Default Channel Pipeline, including header,tail and our own additions, is of the AbstractChannelHandlerContext type. Events are propagated around AbstractChannelHandlerContext. To begin with, review its inheritance system as follows
Then go back to AbstractChannelHandlerContext.invokeChannelActive(head); obviously, this is a static method, follow up, the source code is as follows:
// todo is here static void invokeChannelActive(final AbstractChannelHandlerContext next) { EventExecutor executor = next.executor(); if (executor.inEventLoop()) { next.invokeChannelActive(); ... }
- First point: events of inbound type are propagated from header, next - > HeaderContext
- Second point: HeaderContext is actually the AbstractChannelHandlerContext type, so invokeChannelActive() is actually the method of the current class.
ok, follow up and see what he did, source code:
// todo makes Channel active private void invokeChannelActive() { // todo keeps going in ((ChannelInboundHandler) handler()).channelActive(this); }
Let's see, the above code does the following things
- handler() -- Return to the current handler, which is to take handler out of the Handler Context
- Strongly converted to ChannelInboundHandler type because he is an InBound type processor
If we click on Channel Active (this) with our mouse, we will undoubtedly enter Channel InboundHandler and see the abstract method.
So the question arises, who achieved it?
Actually, the headerContext header node did it. As I said before, the Inbound event started to spread from the header. Follow it up and see the source code:
// todo comes here in two steps. 1. Continue to spread Channel Active. After the dissemination is over, do the second thing. // todo 2. readIfIsAutoRead(); @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { // Todo FireChannel Active triggers callbacks only after the actual port bindings have been made ctx.fireChannelActive(); // todo registers a read event by default // To do follow up, readIfIsAutoRead is used to register events that have been registered on selector, and to re-register the Accept events added to the binding when initializing NioServer Socket Channel // The goal of todo is to poll for accept events when new connections come in, so that netty can further handle this event. readIfIsAutoRead(); }
In fact, there are two important things, as we have seen above:
- The purpose of propagating channelActive() downward is for the channelActive() in the handler added by the user after the header to be called back.
- readIfIsAutoRead(); that is, to test two interesting events Netty can understand.
Now let's see how its events spread down, and we're back to AbstractChannelHandlerContext, with the following source code:
public ChannelHandlerContext fireChannelActive() { invokeChannelActive(findContextInbound()); return this; }
- findContextInbound() finds the next processor of Inbound type. Let's see its implementation. The source code is as follows:
private AbstractChannelHandlerContext findContextInbound() { AbstractChannelHandlerContext ctx = this; do { ctx = ctx.next; } while (!ctx.inbound); return ctx; }
Is it clear? From the current node, the whole list of variables will be changed later, and who will be the next node? In the logic of new link access, I manually added three InboundHandler s in batch. In the order I added, they will be found at one time.
Continue to follow up the invokeChannelActive(findContextInbound()) method with the following source code
// todo is here static void invokeChannelActive(final AbstractChannelHandlerContext next) { EventExecutor executor = next.executor(); if (executor.inEventLoop()) { next.invokeChannelActive(); ...
Beginning next - > HeaderContext
Now next is the handler of Inbound that I added manually after header.
Similarly, invokeChannelActive() is called with the following source code:
// todo makes Channel active private void invokeChannelActive() { // todo keeps going in ((ChannelInboundHandler) handler()).channelActive(this);
See again, callback, handler.channelActive(this);, go to view
public class MyServerHandlerA extends ChannelInboundHandlerAdapter { // When the channel on the server is bound to the upper port, todo propagates the channelActive event. // After the todo event is propagated below, we manually propagate a channelRead event @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.channel().pipeline().fireChannelRead("hello MyServerHandlerA"); }
In my processor, continue to propagate the manually added data "hello MyServer Handler A"
Similarly, she will spread it in the order in which she finds it.
Eventually she will come to tail and do the following work in tail. The source code is as follows
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // todo channelRead onUnhandledInboundMessage(msg); } protected void onUnhandledInboundException(Throwable cause) { try { logger.warn( "An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " + "It usually means the last handler in the pipeline did not handle the exception.", cause); } finally { ReferenceCountUtil.release(cause); } }
Why is the Tail node an Inbound processor?
The last step explains why tail is designed as Inbound. The data in channel, whether used or not by the server, will eventually be released. tail can finish the work and clean up the memory.
Dissemination of outbound events
What is an outBound event
outbound events created such as: connect,disconnect,bind,write,flush,read,close,register,deregister, out type events are more initiative events initiated by the server, such as binding the upper port to the active channel, initiatively writing data to the channel, initiatively closing the user's connection.
Start reading source code
The most typical outbound event is when the server writes data to the client and prepares for testing, such as the following:
public class OutBoundHandlerB extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { System.out.println( "hello OutBoundHandlerB"); ctx.write(ctx, promise); } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { ctx.executor().schedule(()->{ // todo simulates a response to the client ctx.channel().write("Hello World"); // Writing 2: ctx.write("Hello World"); },3, TimeUnit.SECONDS); } } public class OutBoundHandlerA extends ChannelOutboundHandlerAdapter { // When the channel on the server is bound to the upper port, todo propagates the channelActive event. // After the todo event is propagated below, we manually propagate a channelRead event @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { System.out.println( "hello OutBoundHandlerA"); ctx.write(ctx, promise); } } public class OutBoundHandlerC extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { System.out.println( "hello OutBoundHandlerC"); ctx.write(ctx, promise); } }
Next, we debug the breakpoint, hit the breakpoint on the handler Added of OutBoundHandlerB, simulate sending data to the client, start the program, the process is as follows.
- Waiting for the startup of the server
- Server channel Polls Possible Interesting Events on Server channel
- Sending requests to the server using tennet
- The server creates a channel for the client and registers the native chanenl for the client on the Selector
- Add the handler we added to the Initializer to the pipeline by invokeChannelAddedIfNeeded()
-
Callback the channelAdded() method in these handler s one by one
- Contrary to the order we added in
- C --> B --->A
- These child handlers are added to the pipeline of channel on each client
-
Callback the channelAdded() method in these handler s one by one
- Propagating channel Registration Completion Events
- Propagating channelActive events
- readIfAutoRead() completes the second registration of events of interest that netty can handle
In addition, our above writes are submitted in the form of timed tasks. When the only thread executor in CTX is used to execute tasks in three seconds, the program will continue to bind the ports, and after three seconds, the timed tasks will be aggregated into the common task queue, then the ctx. channel (). write ("Hello Wo") in our Outbound Handler B will be executed. Rld ";
What does the order of handler addition and execution of outBound type have to do with it?
Because events of the Outbound type are propagated from tail of the linked list, the order of execution is contrary to the order in which we add them.
It's too long. Rewrite and fill in a picture.
From ctx.channel().write("Hello World"); start with the source code, the mouse directly follow up, enter Channel Outbound Invoker, write to channel, we enter the implementation of Default Channel Pipeline, the source code is as follows
@Override public final ChannelFuture write(Object msg) { return tail.write(msg); }
Once again, the outbound event is passed forward from the tail. We know that the tail node is of Default Channel Handler Context type, so let's see how its write() method is implemented.
@Override public ChannelFuture write(Object msg) { return write(msg, newPromise()); }
Among them, MSG - > we want to write the content of the client, the default promise() of newPromise().
Continue to follow up on this method write (msg, new Promise ()), the source code is as follows:
@Override public ChannelFuture write(final Object msg, final ChannelPromise promise) { if (msg == null) { throw new NullPointerException("msg"); } try { if (isNotValidPromise(promise, true)) { ReferenceCountUtil.release(msg); // cancelled return promise; } } catch (RuntimeException e) { ReferenceCountUtil.release(msg); throw e; } write(msg, false, promise); return promise; }
A lot of judgments have been made, of which we only care about write(msg, false, promise); the source code is as follows:
private void write(Object msg, boolean flush, ChannelPromise promise) { AbstractChannelHandlerContext next = findContextOutbound(); final Object m = pipeline.touch(msg, next); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { if (flush) { next.invokeWriteAndFlush(m, promise); } else { next.invokeWrite(m, promise); }
As you can see, the important logic findContextOutbound(); its source code is as follows, traversing the list from the tail node to find the handler of the previous outbound type
private AbstractChannelHandlerContext findContextOutbound() { AbstractChannelHandlerContext ctx = this; do { ctx = ctx.prev; } while (!ctx.outbound); return ctx; }
When we find it, because we use the function write instead of write AndFlush, we go to the above else block invokeWrite.
private void invokeWrite(Object msg, ChannelPromise promise) { if (invokeHandler()) { invokeWrite0(msg, promise); } else { write(msg, promise); } }
Follow up on invokeWrite0(msg, promise); finally see handler's write logic
private void invokeWrite0(Object msg, ChannelPromise promise) { try { ((ChannelOutboundHandler) handler()).write(this, msg, promise); } catch (Throwable t) { notifyOutboundHandlerException(t, promise); } }
Among them:
- (Channel Outbound Handler) Handler () -- The node in front of tail
- Call the write function of the current node
In fact, we call back our own add handler write function, we follow up, the source code is as follows:
public class OutBoundHandlerC extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { System.out.println( "hello OutBoundHandlerC"); ctx.write(msg, promise); } }
We continue to call write, and msg will continue to pass forward with the same logic
Pass it all the way to the HeadContext node, because this node is also of Outbound type, which is the propagation of Outbound events. Let's see how the HeaderContext ends. The source code is as follows:
@Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { unsafe.write(msg, promise); }
Header uses unsafe class, which is not a problem, and the operation related to data reading and writing is ultimately inseparable from unsafe.
Why is the Header node an outBound processor?
Take the above write event for example, msg is processed by so many handler s that the ultimate goal is to pass it to the client, so netty designs the header as an outBound type node, which completes the writing to the client.
The difference between context.write() and context.channel().write()
- context.write(), which propagates forward from the current node
- context.channel().write() propagates forward in turn from the tail node
Abnormal propagation
If an exception occurs in netty, the propagation of the exception event has nothing to do with whether the current node is an inbound or outbound processor. It propagates all the way to the next node. If there is no handler handling the exception, it is finally handled by the tail node.
The Best Solution to Exception Handling
Since the propagation of exceptions has nothing to do with the inbound and outbound processors, we can add our Unified Exception Processor at the end of the pipeline, that is, tail, just like the following:
public class MyServerInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { // Best practices for todo exception handling, with exception handler added at the end of the pipeline channelPipeline.addLast(new myExceptionCaughtHandler()); } } public class myExceptionCaughtHandler extends ChannelInboundHandlerAdapter { // Eventually all the anomalies will come here. @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (cause instanceof Custom exception 1){ }else if(cause instanceof Custom exception 2){ } // Don't propagate down below todo // super.exceptionCaught(ctx, cause); } }
Characteristics of SimpleChannel InboundHandler
Through the previous analysis, we know that if the msg of the client blindly propagates back, it will eventually propagate to the tail node, which will be released by the tail node, thus avoiding memory leak.
If our handler uses msg and does not pass it back, it will be unlucky. Memory leaks will occur over time.
The generic SimpleChannelInboundHandler <T> provided by netty's wayward words can automatically release memory for us. Let's see how he can do it.
/ todo Direct inheritance ChanelInboundHandlerAdapter Implementing abstract classes // todo's own processor can also inherit the SimpleChannel InboundHandler adapter to achieve the same effect public abstract class SimpleChannelInboundHandler<I> extends ChannelInboundHandlerAdapter { private final TypeParameterMatcher matcher; private final boolean autoRelease; protected SimpleChannelInboundHandler() { this(true); } protected SimpleChannelInboundHandler(boolean autoRelease) { matcher = TypeParameterMatcher.find(this, SimpleChannelInboundHandler.class, "I"); this.autoRelease = autoRelease; } protected SimpleChannelInboundHandler(Class<? extends I> inboundMessageType) { this(inboundMessageType, true); } protected SimpleChannelInboundHandler(Class<? extends I> inboundMessageType, boolean autoRelease) { matcher = TypeParameterMatcher.get(inboundMessageType); this.autoRelease = autoRelease; } public boolean acceptInboundMessage(Object msg) throws Exception { return matcher.match(msg); } // Todo ChannelRead has been completely rewritten // todo is actually a design pattern, template method design pattern. @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { boolean release = true; try { if (acceptInboundMessage(msg)) { @SuppressWarnings("unchecked") // todo turned the message around I imsg = (I) msg; // todo channelRead0() is abstract in its parent class, so when we write handler ourselves, we need to rewrite this abstract method, as follows // todo is actually a design pattern, template method design pattern. channelRead0(ctx, imsg); } else { release = false; ctx.fireChannelRead(msg); } } finally {// todo decreases the msg count by one, which means that the reference to the message is decreases by one. That means we don't want to do anything about it. if (autoRelease && release) { ReferenceCountUtil.release(msg); } } } protected abstract void channelRead0(ChannelHandlerContext ctx, I msg) throws Exception; }
- It's an abstract class itself, and the abstract method is channelRead0, which means we need to rewrite this method.
- He inherited ChannelInboundHandlerAdapter, an adapter class that allows him to implement only some of the methods he needs.
Let's look at the channelRead implementation. The template method design pattern mainly does the following three things.
- Converting msg strongly to data of a specific generic type
- Pass CTX and msg to your chanenlRead0 using MSG and ctx(ctx,msg)
- chanenlRead0 uses msg and ctx
- In the final code block, release msg