netty source code-server initialization

Creation of NioServerSocketChannel

Looking at the server code again, we call the channel(NioServerSocketChannel.class) method of ServerBootstarap, and the passed parameter is the NioServerSocketChannel.class object. In this way, following the same process of the client code, we can determine that the instantiation of NioServerSocketChannel is also done through the ReflectiveChannelFactory factory class, and the clazz field in ReflectiveChannelFactory is assigned the value of NioServerSocketChannel.class, so when the newChannel() method of ReflectiveChannelFactory is called, An instance of NioServerSocketChannel can be obtained. The source code of the newChannel() method is as follows:

public ReflectiveChannelFactory(Class<? extends T> clazz) {
        ObjectUtil.checkNotNull(clazz, "clazz");
        try {
          
            this.constructor = clazz.getConstructor();
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException("Class " + StringUtil.simpleClassName(clazz) +
                    " does not have a public non-arg constructor", e);
        }
    }
​
    @Override
    public T newChannel() {
        try {
            //通过反射调用下面代码
            return constructor.newInstance();
        } catch (Throwable t) {
            throw new ChannelException("Unable to create Channel from class " + constructor.getDeclaringClass(), t);
        }
    }

Finally, we also summarize:

1. The implementation class of ChannelFactory in ServerBootstrap is ReflectiveChannelFactory.

2. The specific type of the created Channel is NioServerSocketChannel.

The instantiation process of Channel is actually to call the newChannel() method of ChannelFactory, and the concrete type of the instantiated Channel is the actual parameter passed to the channel() method when the ServerBootstrap is initialized. Therefore, the server-side ServerBootstrap in the above code case creates the Channel. An instance is an instance of NioServerSocketChannel.

Server Channel initialization

Let's analyze the instantiation process of NioServerSocketChannel, first look at the class hierarchy diagram of NioServerSocketChannel:

First, let's trace the default structure of NioServerSocketChannel. Similar to NioSocketChannel, the constructor calls newSocket() to open a Java NIO Socket. However, it should be noted that the client's newSocket() method calls openSocketChannel(), while the server's newSocket() calls openServerSocketChannel(). As the name implies, one is the client-side Java SocketChannel, and the other is the server-side Java ServerSocketChannel. Look at the code:

private static ServerSocketChannel newSocket(SelectorProvider provider) {
        try {
            /**
             *  Use the {@link SelectorProvider} to open {@link SocketChannel} and so remove condition in
             *  {@link SelectorProvider#provider()} which is called by each ServerSocketChannel.open() otherwise.
             *
             *  See <a href="https://github.com/netty/netty/issues/2308">#2308</a>.
             */
            
            return provider.openServerSocketChannel();
        } catch (IOException e) {
            throw new ChannelException(
                    "Failed to open a server socket.", e);
        }
    }
​
    private final ServerSocketChannelConfig config;
​
    /**
     * Create a new instance,通过反射创建,调用构造
     */
    public NioServerSocketChannel() {
        this(newSocket(DEFAULT_SELECTOR_PROVIDER));
    }

Next, the overloaded construction method will be called:

 public NioServerSocketChannel(ServerSocketChannel channel) {
        super(null, channel, SelectionKey.OP_ACCEPT);
        config = new NioServerSocketChannelConfig(this, javaChannel().socket());
 }

In this construction method, the parameter passed in when calling the parent class construction method is SelectionKey.OP_ACCEPT. For comparison, let's review that when the client's Channel is initialized, the parameter passed in is SelectionKey.OP_READ. After the service starts, you need to listen to the client's connection request, so here we set SelectionKey.OP_ACCEPT, which is to notify the selector that we are interested in the client's connection request

Then compare and analyze with the client, the parent class constructor NioServerSocketChannel -> AbstractNioMessageChannel -> AbstractNioChannel -> AbstractChannel will be called step by step. Similarly, instantiate an unsafe and pipeline in AbstractChannel:

protected AbstractChannel(Channel parent) {
        this.parent = parent;
        id = newId();
        unsafe = newUnsafe();
        pipeline = newChannelPipeline();
 }

However, it should be noted here that unsafe on the client side is an instance of AbstractNioByteChannel#NioByteUnsafe, and unsafe on the server side is an instance of AbstractNioMessageChannel.AbstractNioUnsafe. Because AbstractNioMessageChannel rewrites the newUnsafe() method, its source code is as follows:

@Override
protected AbstractNioUnsafe newUnsafe() {
    return new NioMessageUnsafe();
}

Finally, to summarize, the execution logic during the instantiation of NioServerSocketChannel:

1. Call the NioServerSocketChannel.newSocket(DEFAULT_SELECTOR_PROVIDER) method to open a new Java NIO

ServerSocketChannel

2. AbstractChannel is initialized and assigned an attribute:

parent: set to null

unsafe: instantiate an unsafe object through newUnsafe(), the type is AbstractNioMessageChannel#AbstractNioUnsafe

pipeline: Create an instance of DefaultChannelPipeline

3. The assigned attributes in AbstractNioChannel:

ch: Assigned to the ServerSocketChannel of Java NIO, call the newSocket() method of NioServerSocketChannel to obtain it.

readInterestOp: The default assignment is SelectionKey.OP_ACCEPT.

ch is set to non-blocking, call ch.configureBlocking(false) method.

4. The assigned attributes in NioServerSocketChannel:

config = new NioServerSocketChannelConfig(this, javaChannel().socket())

ChannelPipeline initialization

Above we analyzed the general initialization process of NioServerSocketChannel, but we missed a key part, namely the initialization of ChannelPipeline. In the notes of the Pipeline, it says "Each channel has its own pipeline and it is created automatically when a new channel is created.", we know that when a Channel is instantiated, a ChannelPipeline must be instantiated. And we did see in the constructor of AbstractChannel that the pipeline field was initialized to an instance of DefaultChannelPipeline. So let’s take a look at what the DefaultChannelPipeline constructor does

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

The constructor of DefaultChannelPipeline needs to pass in a channel, and this channel is actually the NioServerSocketChannel we instantiated, and DefaultChannelPipeline will store this NioServerSocketChannel object in the channel field. There are also two special fields in DefaultChannelPipeline, namely head and tail. These two fields are the head and tail of the doubly linked list. In fact, in DefaultChannelPipeline, a doubly linked list with AbstractChannelHandlerContext as the node element is maintained. This linked list is the key to Netty's implementation of the Pipeline mechanism.

In the linked list, the head is a ChannelOutboundHandler, and the tail is a ChannelInboundHandler. Then look at the HeadContext constructor:

HeadContext(DefaultChannelPipeline pipeline) {
            super(pipeline, null, HEAD_NAME, HeadContext.class);
            unsafe = pipeline.channel().unsafe();
            setAddComplete();
 }

Server Channel registered to Selector

Let's summarize the registration process of Channel:

1. First, in the initAndRegister() method of AbstractBootstrap, call group().register(channel)

The register() method of MultithreadEventLoopGroup.

2. In the register() of MultithreadEventLoopGroup, call the next() method to obtain an available SingleThreadEventLoop, and then call its register() method.

3. In the register() method of SingleThreadEventLoop, call channel.unsafe().register(this, promise) method to get

The channel's unsafe() underlying operation object, and then unsafe's register() is called.

4. In the register() method of AbstractUnsafe, call the register0() method to register the Channel object.

5. In the register0() method of AbstractUnsafe, call the doRegister() method of AbstractNioChannel.

6. The doRegister() method of AbstractNioChannel registers the Java NIO SocketChannel corresponding to the Channel to an eventLoop selector through javaChannel().register(eventLoop().selector, 0, this), and associates the current Channel as an attachment with the ServerSocketChannel .

In general, the work done by the Channel registration process is to associate the Channel with the corresponding EventLoop, so this is also reflected in the Netty

Each Channel is associated with a specific EventLoop, and all IO operations in this Channel are performed in this EventLoop; when the Channel and EventLoop are associated, the register() of the ServerSocketChannel object of the underlying Java NIO will continue to be called Method, register the ServerSocketChannel of the underlying Java NIO to the specified selector. Through these two steps, the registration process of Netty to Channel is completed.

bossGroup 与 workerGroup

On the client side, we initialized an EventLoopGroup object, and on the server side, we set two

EventLoopGroup, one is bossGroup and the other is workerGroup. So what are these two EventLoopGroup used for? Let's explore in detail next. In fact, bossGroup is only used for accept on the server side, that is, it is used to process the client's new connection request. We can compare Netty to a restaurant. BossGroup is like a lobby manager. When customers come to eat in the restaurant, the lobby manager will guide the customers to sit and serve them with tea and water. The workerGroup is the chef who actually works. They are responsible for the IO operation of the client connection channel: when the lobby experiences receiving customers, the customers can take a break, and the chefs in the back kitchen (workerGroup) start busy preparing The meal is out. Regarding the relationship between bossGroup and workerGroup, we can use the following figure to show. We have also analyzed the previous chapters. Here we will consolidate it:

 

First, the bossGroup of the server constantly monitors whether there is a client connection. When a new client connection is found, the bossGroup will initialize various resources for this connection, and then select an EventLoop from the workerGroup to bind to this client End connecting. Then the next server-client interaction process is all completed in the EventLoop allocated here. There is no proof in the mouth, let's speak with the source code. First, when ServerBootstrap is initialized, b.group(bossGroup, workerGroup) is called to set up two EventLoopGroups. After we trace it in, we will see:

public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
        super.group(parentGroup);
        if (this.childGroup != null) {
            throw new IllegalStateException("childGroup set already");
        }
        this.childGroup = ObjectUtil.checkNotNull(childGroup, "childGroup");
        return this;
    }

The bind() method will trigger the following call chain:

AbstractBootstrap.bind() -> AbstractBootstrap.doBind() -> AbstractBootstrap.initAndRegister()

So far from the source code, we found that the initAndRegister() method of AbstractBootstrap is already our old friend. We have dealt with it a lot when analyzing the client program. Now let’s review this method:

final ChannelFuture initAndRegister() {
        Channel channel = null;
        try {
            channel = channelFactory.newChannel();
            init(channel);
        } catch (Throwable t) {
            if (channel != null) {
                // channel can be null if newChannel crashed (eg SocketException("too many open files"))
                channel.unsafe().closeForcibly();
                // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
                return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
            }
            // as the Channel is not registered yet we need to force the usage of the GlobalEventExecutor
            return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
        }
        //*
        ChannelFuture regFuture = config().group().register(channel);
        if (regFuture.cause() != null) {
            if (channel.isRegistered()) {
                channel.close();
            } else {
                channel.unsafe().closeForcibly();
            }
        }
        return regFuture;
    }

Here the group() method returns the bossGroup we mentioned above, and the channel here is actually an instance of NioServerSocketChannel, so we can guess that group().register(channel) should associate the bossGroup and NioServerSocketChannel. So where exactly is workerGroup related to NioServerSocketChannel? We continue to look at the init(channel) method:

@Override
    void init(Channel channel) {
        setChannelOptions(channel, newOptionsArray(), logger);
        setAttributes(channel, attrs0().entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY));

        ChannelPipeline p = channel.pipeline();

        final EventLoopGroup currentChildGroup = childGroup;
        final ChannelHandler currentChildHandler = childHandler;
        final Entry<ChannelOption<?>, Object>[] currentChildOptions;
        synchronized (childOptions) {
            currentChildOptions = childOptions.entrySet().toArray(EMPTY_OPTION_ARRAY);
        }
        final Entry<AttributeKey<?>, Object>[] currentChildAttrs = childAttrs.entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY);

        p.addLast(new ChannelInitializer<Channel>() {
            @Override
            public void initChannel(final Channel ch) {
                final ChannelPipeline pipeline = ch.pipeline();
                ChannelHandler handler = config.handler();
                if (handler != null) {
                    pipeline.addLast(handler);
                }

                ch.eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.addLast(new ServerBootstrapAcceptor(
                                ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });
    }

In fact, the init() method has been rewritten in ServerBootstrap. From the code snippet above, we can see that it adds a

ChannelInitializer, and this ChannelInitializer adds a very critical ServerBootstrapAcceptor handler. Regarding the process of adding and initializing the handler, we leave it to the following chapters for detailed analysis. Now, let's focus on the ServerBootstrapAcceptor class. The channelRead() method is rewritten in ServerBootstrapAcceptor, and its main code is as follows:

@Override
        @SuppressWarnings("unchecked")
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            final Channel child = (Channel) msg;

            child.pipeline().addLast(childHandler);

            setChannelOptions(child, childOptions, logger);
            setAttributes(child, childAttrs);

            try {
                childGroup.register(child).addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        if (!future.isSuccess()) {
                            forceClose(child, future.cause());
                        }
                    }
                });
            } catch (Throwable t) {
                forceClose(child, t);
            }
        }

The childGroup in ServerBootstrapAcceptor is the currentChildGroup passed in to construct this object, which is the workerGroup object. The Channel here is an instance of NioSocketChannel, so the register() method of childGroup here is to associate a certain EventLoop in the workerGroup with the NioSocketChannel. That being the case, the question now is where is the channelRead() method of ServerBootstrapAcceptor called? In fact, when a client connects to the server, the ServerSocketChannel of the Java underlying NIO will have a SelectionKey.OP_ACCEPT event ready, and then it will Call the doReadMessages() method of NioServerSocketChannel:

protected AbstractChannel(Channel parent) {
        this.parent = parent;
        id = newId();
        //创建java的socket
        unsafe = newUnsafe();
        pipeline = newChannelPipeline();
 }

/**
     * @param
     * @return
     * @description: 创建的netty的服务端的socket
     * @author madongyu
     * @date 2020/10/29 15:36
     */
    @Override
    protected AbstractNioUnsafe newUnsafe() {
        return new NioMessageUnsafe();
    }

private final class NioMessageUnsafe extends AbstractNioUnsafe {

        private final List<Object> readBuf = new ArrayList<Object>();

        @Override
        public void read() {
            assert eventLoop().inEventLoop();
            final ChannelConfig config = config();
            final ChannelPipeline pipeline = pipeline();
            final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
            allocHandle.reset(config);

            boolean closed = false;
            Throwable exception = null;
            try {
                try {
                    do {
                        //*
                        int localRead = doReadMessages(readBuf);
                        if (localRead == 0) {
                            break;
                        }
                        if (localRead < 0) {
                            closed = true;
                            break;
                        }

                        allocHandle.incMessagesRead(localRead);
                    } while (allocHandle.continueReading());
                } catch (Throwable t) {
                    exception = t;
                }

                int size = readBuf.size();
                for (int i = 0; i < size; i++) {
                    readPending = false;
                    pipeline.fireChannelRead(readBuf.get(i));
                }
                readBuf.clear();
                allocHandle.readComplete();
                pipeline.fireChannelReadComplete();

                if (exception != null) {
                    closed = closeOnReadError(exception);

                    pipeline.fireExceptionCaught(exception);
                }

                if (closed) {
                    inputShutdown = true;
                    if (isOpen()) {
                        close(voidPromise());
                    }
                }
            } finally {
                // Check if there is a readPending which was not processed yet.
                // This could be for two reasons:
                // * The user called Channel.read() or ChannelHandlerContext.read() in channelRead(...) method
                // * The user called Channel.read() or ChannelHandlerContext.read() in channelReadComplete(...) method
                //
                // See https://github.com/netty/netty/issues/2254
                if (!readPending && !config.isAutoRead()) {
                    removeReadOp();
                }
            }
        }
    }


@Override
    protected int doReadMessages(List<Object> buf) throws Exception {
        SocketChannel ch = SocketUtils.accept(javaChannel());

        try {
            if (ch != null) {
                buf.add(new NioSocketChannel(this, ch));
                return 1;
            }
        } catch (Throwable t) {
            logger.warn("Failed to create a new channel from an accepted socket.", t);

            try {
                ch.close();
            } catch (Throwable t2) {
                logger.warn("Failed to close a socket.", t2);
            }
        }

        return 0;
    }

In the doReadMessages() method, the newly connected SocketChannel object of the client is obtained by calling the javaChannel().accept() method, and then a NioSocketChannel is instantiated and the NioServerSocketChannel object (ie this) is passed in. From this, we can see that the parent class Channel of the NioSocketChannel we created is an instance of NioServerSocketChannel. Next, through Netty's ChannelPipeline mechanism, the read event is sent to each handler step by step, and then the channelRead() method of the ServerBootstrapAcceptor we mentioned earlier will be triggered.

Server Selector event polling

Back to the startup code of ServerBootStrap on the server side, it starts from the bind() method. The bind() method of ServerBootStrapt is actually the bind() method of its parent class AbstractBootstrap. Look at the code:

private static void doBind0(
            final ChannelFuture regFuture, final Channel channel,
            final SocketAddress localAddress, final ChannelPromise promise) {

        // This method is invoked before channelRegistered() is triggered.  Give user handlers a chance to set up
        // the pipeline in its channelRegistered() implementation.
        channel.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                if (regFuture.isSuccess()) {
                    channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
                } else {
                    promise.setFailure(regFuture.cause());
                }
            }
        });
    }

In the doBind0() method, the execute() method of EventLoop is called, and we continue to follow up:

SingleThreadEventExecutor

private void execute(Runnable task, boolean immediate) {
    boolean inEventLoop = inEventLoop();
    addTask(task);
    if (!inEventLoop) {
        startThread();
        if (isShutdown()) {
            boolean reject = false;
            try {
                if (removeTask(task)) {
                    reject = true;
                }
            } catch (UnsupportedOperationException e) {
                // The task queue does not support removal so the best thing we can do is to just move on and
                // hope we will be able to pick-up the task before its completely terminated.
                // In worst case we will log on termination.
            }
            if (reject) {
                reject();
            }
        }
    }

    if (!addTaskWakesUp && immediate) {
        wakeup(inEventLoop);
    }
}

In execute(), thread is created and added to the lock-free serial task queue of EventLoop. We focus on the startThread() method and continue to look at the source code:

private void startThread() {
        if (state == ST_NOT_STARTED) {
            if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
                boolean success = false;
                try {
                    doStartThread();
                    success = true;
                } finally {
                    if (!success) {
                        STATE_UPDATER.compareAndSet(this, ST_STARTED, ST_NOT_STARTED);
                    }
                }
            }
        }
    }

private void doStartThread() {
        assert thread == null;
        executor.execute(new Runnable() {
            @Override
            public void run() {
                thread = Thread.currentThread();
                if (interrupted) {
                    thread.interrupt();
                }

                boolean success = false;
                updateLastExecutionTime();
                try {
                    //*
                    SingleThreadEventExecutor.this.run();
                    success = true;
                } catch (Throwable t) {
                    logger.warn("Unexpected exception from an event executor: ", t);
                } finally {
                    for (;;) {
                        int oldState = state;
                        if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet(
                                SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) {
                            break;
                        }
                    }

                    // Check if confirmShutdown() was called at the end of the loop.
                    if (success && gracefulShutdownStartTime == 0) {
                        if (logger.isErrorEnabled()) {
                            logger.error("Buggy " + EventExecutor.class.getSimpleName() + " implementation; " +
                                    SingleThreadEventExecutor.class.getSimpleName() + ".confirmShutdown() must " +
                                    "be called before run() implementation terminates.");
                        }
                    }

                    try {
                        // Run all remaining tasks and shutdown hooks. At this point the event loop
                        // is in ST_SHUTTING_DOWN state still accepting tasks which is needed for
                        // graceful shutdown with quietPeriod.
                        for (;;) {
                            if (confirmShutdown()) {
                                break;
                            }
                        }

                        // Now we want to make sure no more tasks can be added from this point. This is
                        // achieved by switching the state. Any new tasks beyond this point will be rejected.
                        for (;;) {
                            int oldState = state;
                            if (oldState >= ST_SHUTDOWN || STATE_UPDATER.compareAndSet(
                                    SingleThreadEventExecutor.this, oldState, ST_SHUTDOWN)) {
                                break;
                            }
                        }

                        // We have the final set of tasks in the queue now, no more can be added, run all remaining.
                        // No need to loop here, this is the final pass.
                        confirmShutdown();
                    } finally {
                        try {
                            cleanup();
                        } finally {
                            // Lets remove all FastThreadLocals for the Thread as we are about to terminate and notify
                            // the future. The user may block on the future and once it unblocks the JVM may terminate
                            // and start unloading classes.
                            // See https://github.com/netty/netty/issues/6596.
                            FastThreadLocal.removeAll();

                            STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);
                            threadLock.countDown();
                            int numUserTasks = drainTasks();
                            if (numUserTasks > 0 && logger.isWarnEnabled()) {
                                logger.warn("An event executor terminated with " +
                                        "non-empty task queue (" + numUserTasks + ')');
                            }
                            terminationFuture.setSuccess(null);
                        }
                    }
                }
            }
        });
    }

We find that startThread() finally calls the SingleThreadEventExecutor.this.run() method, this this is the NioEventLoop object:

@Override
    protected void run() {
        int selectCnt = 0;
        for (;;) {
            try {
                int strategy;
                try {
                    strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
                    switch (strategy) {
                    case SelectStrategy.CONTINUE:
                        continue;

                    case SelectStrategy.BUSY_WAIT:
                        // fall-through to SELECT since the busy-wait is not supported with NIO

                    case SelectStrategy.SELECT:
                        long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
                        if (curDeadlineNanos == -1L) {
                            curDeadlineNanos = NONE; // nothing on the calendar
                        }
                        nextWakeupNanos.set(curDeadlineNanos);
                        try {
                            if (!hasTasks()) {
                                //*解决JDK空轮训的bug
                                strategy = select(curDeadlineNanos);
                            }
                        } finally {
                            // This update is just to help block unnecessary selector wakeups
                            // so use of lazySet is ok (no race condition)
                            nextWakeupNanos.lazySet(AWAKE);
                        }
                        // fall through
                    default:
                    }
                } catch (IOException e) {
                    // If we receive an IOException here its because the Selector is messed up. Let's rebuild
                    // the selector and retry. https://github.com/netty/netty/issues/8566
                    rebuildSelector0();
                    selectCnt = 0;
                    handleLoopException(e);
                    continue;
                }

                selectCnt++;
                cancelledKeys = 0;
                needsToSelectAgain = false;
                final int ioRatio = this.ioRatio;
                boolean ranTasks;
                if (ioRatio == 100) {
                    try {
                        if (strategy > 0) {
                            //针对不同的轮训事件进行处理
                            processSelectedKeys();
                        }
                    } finally {
                        // Ensure we always run tasks.
                        ranTasks = runAllTasks();
                    }
                } else if (strategy > 0) {
                    final long ioStartTime = System.nanoTime();
                    try {
                        processSelectedKeys();
                    } finally {
                        // Ensure we always run tasks.
                        final long ioTime = System.nanoTime() - ioStartTime;
                        ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                    }
                } else {
                    ranTasks = runAllTasks(0); // This will run the minimum number of tasks
                }

                if (ranTasks || strategy > 0) {
                    if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
                        logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
                                selectCnt - 1, selector);
                    }
                    selectCnt = 0;
                } else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
                    selectCnt = 0;
                }
            } catch (CancelledKeyException e) {
                // Harmless exception - log anyway
                if (logger.isDebugEnabled()) {
                    logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
                            selector, e);
                }
            } catch (Error e) {
                throw (Error) e;
            } catch (Throwable t) {
                handleLoopException(t);
            } finally {
                // Always handle shutdown even if the loop processing threw an exception.
                try {
                    if (isShuttingDown()) {
                        closeAll();
                        if (confirmShutdown()) {
                            return;
                        }
                    }
                } catch (Error e) {
                    throw (Error) e;
                } catch (Throwable t) {
                    handleLoopException(t);
                }
            }
        }
    }

Finally saw the familiar code. The above code mainly uses an infinite loop to continuously poll the SelectionKey. The select() method is mainly used to solve the JDK empty polling bug, and processSelectedKeys() is to deal with different polling events. If the client has data to write, it will eventually call the doReadMessages() method of AbstractNioMessageChannel. in conclusion:

AbstractBootstrap.doBind0->SingleThreadEventExecutor.execute->SingleThreadEventExecutor.run->

NioEventLoop.run(不断轮训selectKeys)->AbstractNioChannel.read->AbstractNioMessageChannel.read->NioServerSocketChannel.doReadMessages

1. The Selector event polling in Netty starts from the execute() method of EventLoop.

2. In the execute() method of EventLoop, an independent thread will be created for each task and saved to the unlocked serial task queue.

3. Each task of the thread task queue actually calls the run() method of NioEventLoop.

4. Call processSelectedKeys() in the run method to handle polling events.

The process of adding Handler

The process of adding a server-side handler is somewhat different from that of the client-side. Like EventLoopGroup, there are two server-side handlers: one is the handler set by the handler() method, and the other is the childHandler set by the childHandler() method. Through the previous analysis of bossGroup and workerGroup, we can boldly guess here: the handler is related to the accept process. That is, the handler is responsible for processing the client's new connection access request; and the childHandler is responsible for the IO interaction with the client connection. So is this actually the case? We continue to use code to prove. In the previous chapters, we have learned that ServerBootstrap rewrites the init() method, and the handler is also added to this method:

@Override
    void init(Channel channel) {
        setChannelOptions(channel, newOptionsArray(), logger);
        setAttributes(channel, attrs0().entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY));

        ChannelPipeline p = channel.pipeline();

        final EventLoopGroup currentChildGroup = childGroup;
        final ChannelHandler currentChildHandler = childHandler;
        final Entry<ChannelOption<?>, Object>[] currentChildOptions;
        synchronized (childOptions) {
            currentChildOptions = childOptions.entrySet().toArray(EMPTY_OPTION_ARRAY);
        }
        final Entry<AttributeKey<?>, Object>[] currentChildAttrs = childAttrs.entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY);

        p.addLast(new ChannelInitializer<Channel>() {
            @Override
            public void initChannel(final Channel ch) {
                final ChannelPipeline pipeline = ch.pipeline();
                ChannelHandler handler = config.handler();
                if (handler != null) {
                    pipeline.addLast(handler);
                }

                ch.eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.addLast(new ServerBootstrapAcceptor(
                                ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });
    }

In the initChannel() method of the above code, first obtain a handler through the handler() method. If the obtained handler is not empty, add it to the pipeline. Then, add an instance of ServerBootstrapAcceptor. So which object is returned by the handler() method here? In fact, it returns the handler field, and this field is what we set in the server-side startup code:

ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)

At this time, the handler in the pipeline is as follows:

According to the analysis of our original client code, we specify that the channel is bound to eventLoop (in this case, NioServerSocketChannel is bound to bossGroup), the fireChannelRegistered event will be triggered in the pipeline, and then the initChannel() method of ChannelInitializer will be triggered transfer. Therefore, after the binding is completed, the pipeline at this time is as follows:

When we analyzed bossGroup and workerGroup earlier, we already knew that the channelRead() method of ServerBootstrapAcceptor would set the handler for the newly created Channel and register it in an eventLoop, namely:

@Override
        @SuppressWarnings("unchecked")
        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            final Channel child = (Channel) msg;

            child.pipeline().addLast(childHandler);

            setChannelOptions(child, childOptions, logger);
            setAttributes(child, childAttrs);

            try {
                childGroup.register(child).addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        if (!future.isSuccess()) {
                            forceClose(child, future.cause());
                        }
                    }
                });
            } catch (Throwable t) {
                forceClose(child, t);
            }
        }

And the childHandler here is the handler we set in the server-side startup code:

ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {

                }
            }).option(ChannelOption.SO_BACKLOG, 128).childOption(ChannelOption.SO_KEEPALIVE, true);

The following steps are basically clear to us. When the client connects to the Channel registration, it will trigger the initChannel() method of the ChannelInitializer. Finally, let's summarize the difference and connection between server handler and childHandler:

1. In the pipeline of the server NioServerSocketChannel, handler and ServerBootstrapAcceptor are added.

2. When there is a new client connection request, call the channelRead() method of ServerBootstrapAcceptor to create this connection

NioSocketChannel and add childHandler to the pipeline corresponding to NioSocketChannel, and bind this channel to

In an eventLoop in workerGroup.

3. The handler works in the accept phase, and it processes the client's connection request.

4. The childHandler takes effect after the client connection is established, and it is responsible for the IO interaction of the client connection.

Finally, look at a picture to deepen your understanding. The following figure describes the change process from the server initialization to the new connection access:

Guess you like

Origin blog.csdn.net/madongyu1259892936/article/details/111322258