Let’s use a typical netty server code to illustrate:

EventLoopGroup parentGroup = new NioEventLoopGroup(1);
EventLoopGroup childGroup = new NioEventLoopGroup(3);
ServerBootstrap b = new ServerBootstrap(); 
b.group(parentGroup, childGroup)
        .channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
        .option(ChannelOption.SO_BACKLOG, 128)
      .attr(AttributeKey.valueOf("ssc.key"),"scc.value")
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            public void initChannel(SocketChannel ch) throws Exception {
                ch.pipeline().addLast(new DiscardServerHandler());
            }
        }) 
            .childOption(ChannelOption.SO_KEEPALIVE, true); 
        .childAttr(AttributeKey.valueOf("sc.key"),"sc.value")
        .bind(port);
Copy the code

The reactor thread model is as follows

It basically consists of the following five steps:

  1. Setting the ServerServerBootStrapLaunch parameters
  2. throughServerBootStrapThe bind method starts the serverparentGroupRegistered inNioServerScoketChannelTo listen for connection requests from clients
  3. The Client initiates a CONNECT request,parentGroupIn theNioEventLoopThe ACCEPT event is emitted if there is a new client request
  4. After the ACCEPT event is triggered,parentGroupIn theNioEventLoopthroughNioServerSocketChannelThe corresponding representative client is obtainedNioSocketChannelAnd register it tochildGroupIn the
  5. childGroupIn theNioEventLoopConstantly test your own managementNioSocketChannelAre read and write events ready, and if so, call the correspondingChannelHandlerFor processing

Here’s a breakdown of the steps:

1. Set startup parameters of the server ServerBootStrap

In general, ServerBootStrap is derived from AbstractBootstrap. It represents the startup class of the server, and when its bind method is called, it means that the server is started. Before starting, we call the methods group, Channel, Handler, option, attr, childHandler, childOption, childAttr, etc to set some startup parameters.

Group method:

In Netty, an EventLoopGroup functions like a thread pool. Each EventLoopGroup contains multiple EventLoop objects representing different threads.

As shown in the example above, we pass in construction parameters 1 and 3 when creating parentGroup and childGroup respectively. This corresponds to the parentGroup in red in the figure above, there is only one NioEventLoop. There are three NioEventloops in the green childGroup. If no parameter is specified or 0 is passed, the number of NioEventLoopGroup will be: number of CPU cores *2.

After that, we can call the ServerBootStrap group() method to get a reference to parentGroup, which AbstractBootstrap inherits. A reference to childGroup can also be obtained by calling ServerBootStrap’s own childGroup() method.

The relevant codes are as follows:

io.netty.bootstrap.ServerBootstrap

public final class ServerBootstrap extends AbstractBootstrap<ServerBootstrap.ServerChannel> {...private volatile EventLoopGroup childGroup;ServerBootStrap Maintain childGroup.public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
    super.group(parentGroup);// Pass parentGroup to the parent class AbstractBootstrap processing
    if (childGroup == null) {
        throw new NullPointerException("childGroup");
    }
    if (this.childGroup ! =null) {
        throw new IllegalStateException("childGroup set already");
    }
    this.childGroup = childGroup;// Assign a value to childGroup
    return this; }...public EventLoopGroup childGroup(a) {/ / get childGroup
    returnchildGroup; }}Copy the code

io.netty.bootstrap.AbstractBootstrap

public abstract class AbstractBootstrap<B extends AbstractBootstrap<B.C>, C extends Channel> implements Cloneable {...private volatile EventLoopGroup group;// This field will be set to parentGroup.public B group(EventLoopGroup group) {
    if (group == null) {
        throw new NullPointerException("group");
    }
    if (this.group ! =null) {
        throw new IllegalStateException("group set already");
    }
    this.group = group;
    return (B) this; }...public final EventLoopGroup group(a) {/ / get parentGroup
    returngroup; }}Copy the code

Methods the channel:

The channel method, derived from AbstractBootstrap, is used to construct an instance of the channel’s factory class ChannelFactory. Later, when you need to create a channel instance, such as NioServerSocketChannel, By calling the ChannelFactory. NewChannel () method to create.

public abstract class AbstractBootstrap<B extends AbstractBootstrap<B.C>, C extends Channel> implements Cloneable {...// the final channel instance created by the factory class is the NioServerSocketChannel specified by the channel method
private volatileChannelFactory<? extends C> channelFactory; .public B channel(Class<? extends C> channelClass) {...return channelFactory(new BootstrapChannelFactory<C>(channelClass));
}
public B channelFactory(ChannelFactory<? extends C> channelFactory) {...this.channelFactory = channelFactory;
    return (B) this; }...final ChannelFactory<? extends C> channelFactory() {
    returnchannelFactory; }}Copy the code

It can be found that except for the above two methods, the other three methods are one-to-one corresponding:

  • handler-->childHandler: Used for settingNioServerSocketChannelandNioSocketChannelThat is, when there is a NIO event, what steps should be taken to process it.
  • option-->childOption: Used for settingNioServerSocketChannelandNioSocketChannelTCP connection parameters, inChannelOptionClass you can see all the TCP connection parameters supported by Netty.
  • attr-->childAttr: used tochannelSet a key/value that can later be obtained by key

Among them:

The handler, option, and attr methods are all inherited from AbstractBootstrap. The parameters set by these methods will be applied to the NioServerSocketChannel instance. Since NioServerSocketChannel is typically created only one, these parameters will usually only be applied once.

The childHandler, childOption, and childAttr methods are defined by ServerBootStrap. The parameters set by these methods will be applied to the NioSocketChannel instance. A NioSocketChannel instance is created, so each NioSocketChannel instance applies the parameters set by these methods.

2.Call the bind method of ServerBootStrap

Calling bind is equivalent to starting the server. The core logic for starting is all in the BIND method.

Inside the bind method, an instance of NioServerSocketChannel is created and registered in parentGroup. Note that this process is shielded from users.

When a parentGroup receives a registration request, it selects one of its managed NioEventloops to register. In our case, parentGroup has only one NioEventLoop, so it can only be registered to this one.

Once registration is complete, we can detect the arrival of new client connections through NioServerSocketChannel.

If you trace the call chain of the ServerBootstrap. bind method step by step, you will eventually locate the doBind method of the ServerBootStrap parent class AbstractBootstrap.

io.netty.bootstrap.AbstractBootstrap#doBind

private ChannelFuture doBind(final SocketAddress localAddress) {
    final ChannelFuture regFuture = initAndRegister();// Initialize NioServerSocketChannel and register with bossGroup./ / to omit
    return promise;
Copy the code

The most important method to call in the doBind method is initAndRegister, which does three things

1. Create an instance of NioServerSocketChannel using the newChannel method of the ChannelFactory instance you created earlier

2. Initialize NioServerSocketChannel, that is, apply the parameters previously set by handler, option, attr, etc to NioServerSocketChannel

3. Register NioServerSocketChannel with parentGroup. ParentGroup selects one of the NioEventLoops to perform the function that NioServerSocketChannel is intended to perform, that is, listen for client connections.

final ChannelFuture initAndRegister(a) {
    Channel channel = null;
    try {
        channel = channelFactory.newChannel();// create NioServerSocketChannel instance
        init(channel);//2. Initialize NioServerSocketChannel. This is an abstract method that ServerBootStrap overwrites
    } catch (Throwable t) {
        if(channel ! =null){
            channel.unsafe().closeForcibly();
            // We need to force GlobalEventExecutor because we have not yet registered channels
            return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
        }
        return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
    }
 
    ChannelFuture regFuture = group().register(channel);//3. NioServerSocketChannel registered with parentGroup
    if(regFuture.cause() ! =null) {
        if (channel.isRegistered()) {
            channel.close();
        } else{ channel.unsafe().closeForcibly(); }}return regFuture;
}
Copy the code

ServerBootStrap Implements AbstractBootstrap abstract method init, which initializes NioServerSocketChannel. Those familiar with design patterns will recognize that this is a typical template design pattern, in which a parent class calls multiple methods while a subclass overrides a particular method.

In this case, the init method mainly sets the run parameters for NioServerSocketChannel, which we specified earlier by calling ServerBootStrap’s option, attr, handler, and so on.

 @Override
    void init(Channel channel) {
        //1. Set the parameters set by the option method for NioServerSocketChannel
        setChannelOptions(channel, options0().entrySet().toArray(newOptionArray(0)), logger);
         //2. Set attr parameters for NioServerSocketChannel
        setAttributes(channel, attrs0().entrySet().toArray(newAttrArray(0)));

        // set the handler specified by the handler method for NioServerSocketChannel
        ChannelPipeline p = channel.pipeline();

        // Set ServerBootstrapAcceptor as the default handler for NioSocketChannel and pass the parameters to ServerBootstrapAcceptor via the constructor
        final EventLoopGroup currentChildGroup = childGroup;
        final ChannelHandler currentChildHandler = childHandler;
        finalEntry<ChannelOption<? >, Object>[] currentChildOptions = childOptions.entrySet().toArray(newOptionArray(0));
        finalEntry<AttributeKey<? >, Object>[] currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(0));

        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(a) {
                        pipeline.addLast(newServerBootstrapAcceptor( ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs)); }}); }}); }Copy the code

In particular, at the end of the method, in addition to the ChannelHandler we specified for NioServerSocketChannel through the handler method, The ServerBootStrap init method always adds a default handler ServerBootstrapAcceptor to the NioServerSocketChannel handler chain.

As indicated by the name ServerBootstrapAcceptor, it is the handler for client connection requests. When a client request is received, Netty creates a NioSocketChannel object to represent the client. The parameters specified by ServerBoodStrap, such as channelHandler, childOption, childAtrr, and childGroup, also need to be set to NioSocketChannel. But obviously now, since the server has just started, has not received any client requests, and has not yet received any NioSocketChannel instances, these parameters should be saved to ServerBootstrapAcceptor, and then set when the client connection is received. We can see that these parameters are passed to ServerBootstrapAcceptor through the constructor.

After initialization, ServerBootStrap registers NioServerSocketChannel with parentGroup by calling the Register method.

ChannelFuture regFuture = config().group().register(channel);
Copy the code

Further tracking, parentGroup type is NioEventLoopGroup, NioEventLoopGroup register method inherited from MultithreadEventLoopGroup.

public abstract class MultithreadEventLoopGroup extends MultithreadEventExecutorGroup implements EventLoopGroup {...@Override
    public ChannelFuture register(Channel channel) {
        returnnext().register(channel); }...@Override
    public EventExecutor next(a) {
        returnchooser.next(); }... }Copy the code

The return value of the next method is NioEventLoop, and you can see that the actual registration is done by NioEventLoop. The next() method also provides a mechanism for evenly allocating channels in the NioEventLoop.

NioEventLoopGroup created, its parent class instances will create a EventExecutorChooser MultithreadEventExecutorGroup, through its after to ensure NioEventLoop average registered to a different channel.

rotected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args){... chooser = chooserFactory.newChooser(children); . }Copy the code

DefaultEventExecutorChooserFactory chooserFactory USES the default implementation class

public final class DefaultEventExecutorChooserFactory implements EventExecutorChooserFactory{
	 @Override
    public EventExecutorChooser newChooser(EventExecutor[] executors) {
        // Use a round-robin approach to ensure averages
        if (isPowerOfTwo(executors.length)) {// If the specified number of threads is a power of 2
            return new PowerOfTwoEventExecutorChooser(executors);
        } else {
            return newGenericEventExecutorChooser(executors); }}private static boolean isPowerOfTwo(int val) {
        return (val & -val) == val;
    }
    
        private static final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
        private final AtomicInteger idx = new AtomicInteger();
        private final EventExecutor[] executors;

        PowerOfTwoEventExecutorChooser(EventExecutor[] executors) {
            this.executors = executors;
        }

        @Override
        public EventExecutor next(a) {
            return executors[idx.getAndIncrement() & executors.length - 1]; }}private static final class GenericEventExecutorChooser implements EventExecutorChooser {
        private final AtomicInteger idx = new AtomicInteger();
        private final EventExecutor[] executors;

        GenericEventExecutorChooser(EventExecutor[] executors) {
            this.executors = executors;
        }

        @Override
        public EventExecutor next(a) {
            returnexecutors[Math.abs(idx.getAndIncrement() % executors.length)]; }}}Copy the code

As can be seen, for the case where the number of threads is a power of 2, the bit operation & is used to further optimize the modulus taking speed, which is consistent with the familiar HasnMap optimization method.