Chapter 10 Netty core source code analysis ②

Pipeline Handler HandlerContext to create source code analysis
ChannelPipeline scheduling handler source code analysis

The source code analysis purpose
Netty ChannelPipeline, ChannelHandler and ChannelHandlerContext are very core components. From the source code, we analyze how Netty designed these three core components, and analyze how they are created and coordinated.

ChannelPipeline| ChannelHandler| ChannelHandlerContextIntroduction

1.1 The relationship between the three

  1. Whenever ServerSocket creates a new connection, a Socket is created, corresponding to the target client.

  2. Each newly created Socket will be assigned a brand new ChannelPipeline (hereinafter referred to as pipeline)

  3. Each ChannelPipeline contains multiple ChannelHandlerContext (hereinafter referred to as Context)

  4. Together they form a doubly linked list. These Contexts are used to wrap the ChannelHandler (hereinafter referred to as handler) that we add when we call the addLast method.
    Insert picture description here

  • In the figure above: ChannelSocket and ChannelPipeline are one-to-one associations, and multiple Contexts inside the pipeline form a linked list, and Context is just an encapsulation of the Handler.
  • When a request comes in, it will enter the pipeline corresponding to the Socket and pass all the handlers of the pipeline. Yes, it is the filter mode in the design mode.

1.2 ChannelPipeline function and design

1)
Insert picture description here
Part of the source code of the interface design of the pipeline
Insert picture description here

It can be seen that the interface inherits the inBound, outBound, Iterable interfaces, which means that he can call the data outbound method and inbound method , and can also traverse the internal linked list . Look at several of his representative methods. All are for the insertion, addition, deletion, and replacement operations of the handler linked list, similar to a LinkedList. Meanwhile, it can return Channel (i.e. Socket)
Insert picture description here
. 1) in the pipeline interface document, there is provided a map of
the data flow pipeline is pushed onto the stack, the stack is flowing pipeline
Insert picture description here

Explanation of the above picture:

  • This is a list of handlers. The handler is used to process or intercept inbound and outbound events. The pipeline implements an advanced form of filters so that users can control how events are handled and how the handlers interact in the pipeline.

  • The figure above describes a typical handler's way of handling I / O events in the pipeline. IO events are handled by inboundHandler or outBoundHandler and ChannelHandlerContext.fireChannelReadforwarded to its nearest handler by calling the method.
    Insert picture description here
    Insert picture description here
    Insert picture description here

  • Inbound events are handled by the inbound handler in a bottom-up direction, as shown in the figure. The inbound handler usually processes the inbound data generated by the I / O thread at the bottom of the figure. Inbound data is usually obtained from eg SocketChannel.read (ByteBuffer).

  • Usually a pipeline has multiple handlers. For example, a typical server will have the following handlers in the pipeline of each channel.
    Protocol decoder-convert binary data into Java objects.
    Protocol encoder-convert Java objects to binary data.
    Business logic handlers-execute actual business logic (eg database access)

  • Your business program can't block the thread, it will affect the speed of IO, and then affect the performance of the entire Netty program. If your business program is fast, you can put it in the IO thread, otherwise, you need to execute it asynchronously. Or add a thread pool when adding the handler,
    for example:
    // When the following task is executed, it will not block the IO thread. The executed thread comes from the group thread pool
    pipeline.addLast (group, "handler", new MyBusinessLogicHandler () );
    Or put in taskQueue or scheduleTaskQueue

1.3 Function and design of ChannelHandler

  1. Source code
public interface ChannelHandler {

 //当把 ChannelHandler 添加到 pipeline 时被调用
 void handlerAdded(ChannelHandlerContext ctx) throws Exception;
 
//当从 pipeline 中移除时调用
 void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
 
// 当处理过程中在 pipeline 发生异常时调用
@Deprecated
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}

The role of ChannelHandler is to handle IO events or intercept IO events, and forward it to the next handler ChannelHandler.
When the Handler processes events, it is divided into inbound and outbound. The operations in both directions are different. Therefore, Netty defines two sub-interfaces to inherit ChannelHandler

2) ChannelInboundHandlerInbound event interface
Insert picture description here

  • channelActive is used when Channel is active;

  • channelRead is called when reading data from Channel.

  • Programmers need to rewrite some methods. When an event of interest occurs, we need to implement our business logic in the method, because when the event occurs, Netty will call back the corresponding method.

3) ChannelOutboundHandlerOutbound event interface
Insert picture description here

  • bind method, called when the channel is requested to be bound to a local address
  • close method, called when the channel is closed
  • Outbound operations are all similar methods of connecting and writing out data.

4) ChannelDuplexHandlerHandle outbound and inbound events
Insert picture description here

  • ChannelDuplexHandler indirectly implements the inbound interface and directly implements the outbound interface.
  • It is a general class that can handle both inbound and outbound events.

1.4 The role and design of ChannelHandlerContext

  1. ChannelHandlerContext UML diagram
    Insert picture description here
    ChannelHandlerContext inherits the outbound method invocation interface and inbound method invocation interface

1) ChannelOutboundInvokerand a ChannelInboundInvokerportion of the source
Insert picture description here
Insert picture description here

  • These two invokers are for the inbound or outbound method, which is to wrap a layer on the outer layer of the inbound or outbound handler to achieve the purpose of intercepting and doing some specific operations before and after the method

2) ChannelHandlerContextPart of the source code
Insert picture description here

  • ChannelHandlerContext not only inherits their two methods, but also defines some of its own methods
  • These methods can obtain whether the corresponding channel, executor, handler, pipeline, memory allocator, and associated handler in the Context context are deleted.
  • Context is to wrap everything related to the handler, so that the Context can easily operate the handler in the pipeline

ChannelPipeline| ChannelHandler| ChannelHandlerContextCreation Process

There are 3 steps to see the creation process:

  • When any ChannelSocket is created, a pipeline will be created at the same time.

  • When the user or the system internally calls the pipeline's add *** method to add a handler, a Context that wraps this handler is created.

  • These Contexts form a doubly linked list in the pipeline.

2.1 Socket is created when Socket is created; in the constructor of AbstractChannel, the abstract parent class of SocketChannel

 protected AbstractChannel(Channel parent) {
        this.parent = parent; //断点测试
        id = newId();
        unsafe = newUnsafe();
        pipeline = newChannelPipeline(); 
    }

Debug, you can see the code will be executed here, and then continue to trace

 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;
    }

Explanation:
1) Assign the channel to the channel field, used for pipeline operation channel.
2) Create a future and promise for asynchronous callback.
3) Create an inbound tailContext, create an inbound type and an outbound type headContext. Insert picture description here
Insert picture description here
4) Finally, connect the two Context to each other to form a doubly linked list.
5) tailContext and HeadContext are very important, all events in the pipeline will flow through them,

2.2 Create Context when adding Handler processor in add ** Look at the Context created by the addLast method of DefaultChannelPipeline, the code is as follows

@Override
    public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
        if (handlers == null) { //断点
            throw new NullPointerException("handlers");
        }

        for (ChannelHandler h: handlers) {
            if (h == null) {
                break;
            }
            addLast(executor, null, h);
        }

        return this;
    }

Continue Debug

public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
        final AbstractChannelHandlerContext newCtx;
        synchronized (this) {
            checkMultiplicity(handler);

            newCtx = newContext(group, filterName(name, handler), handler);//

            addLast0(newCtx);
            
            if (!registered) {
                newCtx.setAddPending();
                callHandlerCallbackLater(newCtx, true);
                return this;
            }

            EventExecutor executor = newCtx.executor();
            if (!executor.inEventLoop()) {
                newCtx.setAddPending();
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        callHandlerAdded0(newCtx);
                    }
                });
                return this;
            }
        }
        callHandlerAdded0(newCtx);
        return this;
    }

Explanation

  1. Add a handler to the pipeline, the parameter is the thread pool, the name is null, and the handler is the handler passed in by us or the system. Netty synchronized this code in order to prevent multiple threads from causing security problems. The steps are as follows:
  2. Check if the handler instance is shared, if not, and it is already used by another pipeline, then throw an exception.
  3. Call the newContext(group, filterName(name, handler), handler)method, create a Context . It can be seen from this that every time you add a handler, an associated Context is created.
  4. Call the addLast method to append the Context to the linked list.
  5. If the channel has not been registered with selecor, add this Context to the pending tasks of this pipeline. When the registration is completed, the callHandlerAdded0 method will be called (the default is to do nothing, the user can implement this method).
  6. At this point, for the three-object creation process, you know almost the same. As I said initially, each time a ChannelSocket is created, a bound pipeline is created, a one-to-one relationship, and a tail node and a tail node are created when the pipeline is created. The head node forms the initial linked list. tail is an inbound inbound type handler, and head is both an inbound and outbound type handler. When the addLast method of the pipeline is called, a Context is created according to the given handler, and then the Context is inserted into the end of the linked list (before the tail).

Insert picture description here

ChannelPipeline The source code analysis of how to schedule the handler

Insert picture description here
Insert picture description here
How DefaultChannelPipeline implements these fire methods

1.1 DefaultChannelPipelinesource code

public class DefaultChannelPipeline implements ChannelPipeline {
@Override
    public final ChannelPipeline fireChannelActive() {
        AbstractChannelHandlerContext.invokeChannelActive(head);
        return this;
    }

    @Override
    public final ChannelPipeline fireChannelInactive() {
        AbstractChannelHandlerContext.invokeChannelInactive(head);
        return this;
    }

    @Override
    public final ChannelPipeline fireExceptionCaught(Throwable cause) {
        AbstractChannelHandlerContext.invokeExceptionCaught(head, cause);
        return this;
    }

    @Override
    public final ChannelPipeline fireUserEventTriggered(Object event) {
        AbstractChannelHandlerContext.invokeUserEventTriggered(head, event);
        return this;
    }

    @Override
    public final ChannelPipeline fireChannelRead(Object msg) {
        AbstractChannelHandlerContext.invokeChannelRead(head, msg);
        return this;
    }

    @Override
    public final ChannelPipeline fireChannelReadComplete() {
        AbstractChannelHandlerContext.invokeChannelReadComplete(head);
        return this;
    }

    @Override
    public final ChannelPipeline fireChannelWritabilityChanged() {
        AbstractChannelHandlerContext.invokeChannelWritabilityChanged(head);
        return this;
    }
}

Explanation: It
can be seen that these methods are all inbound methods, that is, inbound events, and the static method is also called the inbound type head handler. These static methods will call the method of the ChannelInboundInvoker interface of the head, and then call the real method of the handler
Insert picture description here
Insert picture description here
Insert picture description here

1.2 Look at piepline's outbound fire method implementation source code

public class DefaultChannelPipeline implements ChannelPipeline {
 @Override
    public final ChannelFuture bind(SocketAddress localAddress) {
        return tail.bind(localAddress);
    }

    @Override
    public final ChannelFuture connect(SocketAddress remoteAddress) {
        return tail.connect(remoteAddress);
    }

    @Override
    public final ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
        return tail.connect(remoteAddress, localAddress);
    }

    @Override
    public final ChannelFuture disconnect() {
        return tail.disconnect();
    }

    @Override
    public final ChannelFuture close() {
        return tail.close();
    }

    @Override
    public final ChannelFuture deregister() {
        return tail.deregister();
    }

    @Override
    public final ChannelPipeline flush() {
        tail.flush();
        return this;
    }

    @Override
    public final ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
        return tail.bind(localAddress, promise);
    }

    @Override
    public final ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
        return tail.connect(remoteAddress, promise);
    }

    @Override
    public final ChannelFuture connect(
            SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
        return tail.connect(remoteAddress, localAddress, promise);
    }

    @Override
    public final ChannelFuture disconnect(ChannelPromise promise) {
        return tail.disconnect(promise);
    }
}

Description:

  1. These are all outbound implementations, but the outbound type tail handler is called for processing because these are outbound events.
  2. Outbound starts with tail, and inbound starts with head. Because the outbound is written from the inside out, starting from tail, the previous handler can be processed to prevent the handler from being missed, such as encoding. On the contrary, inbound is of course input from the head to the inside, so that subsequent handlers can process the data of these inputs. Such as decoding. So although head also implements the outbound interface, it does not start outbound tasks from head
    Insert picture description here

2. About how to schedule, use a picture to represent:
Insert picture description here

Description:

  1. The pipeline will first call the static method fireXXX of Context, and pass in the Context
  2. Then, the static method calls the Context's invoker method, and the invoker method internally calls the real XXX method of the Handler contained in the Context. After the call is over, if you need to continue to pass it back, call the Context's fireXXX2 method, looping back and forth. Chain of responsibility model

Insert picture description here

Published 138 original articles · won praise 3 · Views 7218

Guess you like

Origin blog.csdn.net/weixin_43719015/article/details/105398597