Netty learning notes netty source code - start the process

Posted by firemankurt on Fri, 28 Jan 2022 18:52:41 +0100

preface

The notes are based on black horse's Netty teaching handout and some of my own understanding. I feel that this is a very good video I've seen. There's basically no nonsense. Video address: Black horse Netty . From here on, we try to look at the source code to understand the underlying operating mechanism of Netty.



1. nio startup process review

Let's review what nio needs to do to start the server

//1. NioEventLoopGroup (nio boss thread for short) is used in netty to encapsulate threads and selector s
Selector selector = Selector.open(); 

//2 create NioServerSocketChannel, initialize its associated handler and store config for native ssc
NioServerSocketChannel attachment = new NioServerSocketChannel();

//3 when creating NioServerSocketChannel, a java Native serversocketchannel is created. The function of serversocketchannel can be understood as a registrar. The ssc in netty is registered on the native ssc as an attachment
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); 
serverSocketChannel.configureBlocking(false);

//4 start the nio boss thread to perform the following operations

//5 Registration (only the selector and NioServerSocketChannel are associated), and events are not followed
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, attachment);

//6 head - > initializer - > serverbootstrap acceptor - > tail. The initializer is one-time and only for adding acceptors

//7 binding port
serverSocketChannel.bind(new InetSocketAddress(8080));

//8 trigger the channel active event and focus on the op in the head_ Accept event
//Indicates that this event is triggered when the client accesses
selectionKey.interestOps(SelectionKey.OP_ACCEPT);

The main steps are as follows:

1. Selector selector = Selector.open();

2. ServerSocketChannel ssc = ServerSocketChannel.open();
3. SelectionKey key = ssc.register(selector, 0, nettySsc);
4. ssc.bind(new InetSocketAddress("localhost", 8080));
5. key.interestOps(SelectionKey.OP_ACCRPT);



2. Source code method description

Use the following code to illustrate:

public class TestSourceServer {
    public static void main(String[] args) {
        new ServerBootstrap()
                //EventLoop has a thread and actuator selector, which is used to focus on events and solve some tasks
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>(){

                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new LoggingHandler());
                    }
                }).bind(8080);
    }
}



1. Selector selector = Selector.open();

This part is created in new NioEventLoopGroup(). Because EventLoop maintains a selector and a thread, the selector is created here



2. The following four methods
Some processes of bind method mainly skip unimportant steps:
	bind() -> return bind(new InetSocketAddress(inetPort)) ->
	return doBind(ObjectUtil.checkNotNull(localAddress, "localAddress"));

Important method: doBind(ObjectUtil.checkNotNull(localAddress, "localAddress"))

private ChannelFuture doBind(final SocketAddress localAddress) {
		//1. Initialize NioServerSocketChannel and register with ssc
		// regFuture is actually a promise object. When the result is generated, the following doBind0 will be called
        final ChannelFuture regFuture = initAndRegister();
        final Channel channel = regFuture.channel();
        if (regFuture.cause() != null) {
            return regFuture;
        }

        if (regFuture.isDone()) {
            // At this point we know that the registration was complete and successful.
            ChannelPromise promise = channel.newPromise();
            //Binding port
            doBind0(regFuture, channel, localAddress, promise);
            return promise;
        } else {
            final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
            regFuture.addListener(new ChannelFutureListener() {
            //Nio thread call
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    Throwable cause = future.cause();
                    if (cause != null) {
                                     
                        promise.setFailure(cause);
                    } else {
                   
                        promise.registered();
						//2. Binding port
                        doBind0(regFuture, channel, localAddress, promise);
                    }
                }
            });
            return promise;
        }
    }

When looking at the code below, note that the dobind method is executed in the main thread, but thread switching occurs when the initAndRegister method is executed.

The following is a brief introduction to the two methods (initAndRegister and doBind0):

  1. init and register regFuture processing

    1. init (main thread execution)
      a. Create NioServerSocketChannel (main thread execution)
      b. Add NioServerSocketChannel initialization handler (main thread execution)
      Initialize handler and wait for call (main not called)
      Add accept handler to nio ssc (establish connection after accept event)
    2. register (switch thread)
      a. Start nio boss thread (executed by main thread)
      b. The native ssc(ServerSocketChannel) registers with the selector and does not pay attention to events (NiO thread execution)
      c. Execute NioServerSocketChannel initialization handler (NiO thread execution)
  2. regFuture waiting for callback doBind0

    1. Native ServerSocketChannel binding (NiO thread execution)
    2. Trigger NioServerSocketChannel active event (NiO thread execution)



3. initAndRegister

The form of method annotation is based on a general method and assisted by internal methods

final ChannelFuture initAndRegister() {
		//Initialize channel
        Channel channel = null;
        try {
        	//Create a channel through the channelFactory factory
        	//Created reflective channelfactory (nioserversocketchannel. Class)
        	//In fact, the interior is created by using the reflection method to get the constructor
            channel = channelFactory.newChannel();	// 1
            init(channel);							// 2
        } catch (Throwable t) {
            if (channel != null) {
        
                channel.unsafe().closeForcibly();
                return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
            } 
            return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
        }

        ChannelFuture regFuture = config().group().register(channel);//3	
        if (regFuture.cause() != null) {
            if (channel.isRegistered()) {
                channel.close();
            } else {
                channel.unsafe().closeForcibly();
            }
        }
        return regFuture;
    }



1. channel = channelFactory.newChannel()

1. channel = channelFactory.newChannel();

This method uses the channel factory to create it internally. newChannel uses the reflection mechanism to call the constructor of NioServerSocketChannel class. We go directly to NioServerSocketChannel class to see the source code

public NioServerSocketChannel() {
    this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}

Enter newSocket(DEFAULT_SELECTOR_PROVIDER): in fact, the provider is called Openserversocketchannel() method

In fact, after careful observation, this line of code is the execution process of the open method of ServerSocketChannel. The following is the open method of ServerSocketChannel:


Summary: this line of code actually calls serversocketchannel Open creates a NioServerSocketChannel, and internally creates the ServerSockerChannel of JDK



2. init(channel)

2. init(channel)
@Override
    void init(Channel channel) {
        setChannelOptions(channel, newOptionsArray(), logger);
        setAttributes(channel, attrs0().entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY));
		//p here is the pipeline of NioServerSocketChannel
        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);
		
		//Starting from here is the point
		//An initialized handler is added to NioServerSocketChannel
		//The method inside will only be executed once
        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));
                    }
                });
            }
        });
    }

Summary: in this line of code, an initialization processor is created for NioServerSocketChannel, waiting to be called. This processor is called only once



3. ChannelFuture regFuture = config().group().register(channel)

3. ChannelFuture regFuture = config().group().register(channel)
The following is the call chain. Finally, it goes to the real register method, in which the thread is switched:



@Override
  public final void register(EventLoop eventLoop, final ChannelPromise promise) {
       ObjectUtil.checkNotNull(eventLoop, "eventLoop");
       if (isRegistered()) {
           promise.setFailure(new IllegalStateException("registered to an event loop already"));
           return;
       }
       if (!isCompatible(eventLoop)) {
           promise.setFailure(
                   new IllegalStateException("incompatible event loop type: " + eventLoop.getClass().getName()));
           return;
       }

       AbstractChannel.this.eventLoop = eventLoop;
	   //This method is to check whether the current thread is equal to Nio thread. The current thread is definitely not equal to Nio thread, because the main thread is walking
       if (eventLoop.inEventLoop()) {
           register0(promise);
       } else {
           try {
           	   //Create a task object and give it to the Nio thread in eventLoop to execute
           	   //Then, the thread is switched from the main thread to the Nio thread
           	   //register0 is lazy loading, and the thread is created only when it is called here
               eventLoop.execute(new Runnable() {
                   @Override
                   public void run() {
                   	
                       register0(promise);
                   }
               });
           } catch (Throwable t) {
               logger.warn(
                       "Force-closing a channel whose registration task was not accepted by an event loop: {}",
                       AbstractChannel.this, t);
               closeForcibly();
               closeFuture.setClosed();
               safeSetFailure(promise, t);
           }
       }
   }

Enter the register0(promise) method:

 private void register0(ChannelPromise promise) {
  try {
         if (!promise.setUncancellable() || !ensureOpen(promise)) {
             return;
         }
         boolean firstRegistration = neverRegistered;
         //The core code is here
         doRegister();
         neverRegistered = false;
         registered = true;

         pipeline.invokeHandlerAddedIfNeeded();

         safeSetSuccess(promise);
         pipeline.fireChannelRegistered();
         if (isActive()) {
             if (firstRegistration) {
                 pipeline.fireChannelActive();
             } else if (config().isAutoRead()) {
                 beginRead();
             }
         }
     } catch (Throwable t) {
         // Close channel
         closeForcibly();
         closeFuture.setClosed();
         safeSetFailure(promise, t);
     }
 }



Enter the doRegister() method of register0, which completes the registration, that is, selectionkey = SSc Register (selector, 0, nettyssc)

@Override
    protected void doRegister() throws Exception {
        boolean selected = false;
        for (;;) {
            try {
            //Get the java Native ServerSocketChannel and register it with the selector
            //0: indicates that there is no event of interest
            //this: the current NioServerSocketChannel object
                selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
                return;
            } catch (CancelledKeyException e) {
                if (!selected) {
                    eventLoop().selectNow();
                    selected = true;
                } else { 
                    throw e;
                }
            }
        }
    }



Enter the pipeline of register0 In the invokehandleraddedifneeded () method, this method is actually called internally The ChannelInitializer set in init (channel) initializes the initChannel method of the processor, so the function of this method is to call the initialization method not called in the second step
So this method is actually called in initialization.

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);
                }
				
				//A serverbootstrap acceptor processor is added to NioServerSocketChanel to establish a connection after the accept event
				//The task of adding a processor is submitted to the channel and guaranteed to run in the thread in the channel
                ch.eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.addLast(new ServerBootstrapAcceptor(
                                ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });



Enter the safeSetSuccess(promise) method of register0. In fact, this method is used to set the result of promise. Note: the promise in doBind of the source method comes from the same promise, so it will enter doBind0 method after this step is completed.


Summary: config () in this line of code group(). Register (channel) completes the registration of ssc, that is, the register method, and sets a handler in it. This handler is executed after the accept event, and sets the result for promise in the source doBind, resulting in entering doBind0 of the doBind method



4. doBind0

doBind0(regFuture, channel, localAddress, promise) is still a familiar routine inside. The task is handed over to the nio ssc thread for execution

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

The following is the execution chain of the method: layer by layer






Finally came to the working method, which is too deep in the call chain: similarly, we only focus on two places

@Override
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
    assertEventLoop();

    if (!promise.setUncancellable() || !ensureOpen(promise)) {
        return;
    }

    // See: https://github.com/netty/netty/issues/576
    if (Boolean.TRUE.equals(config().getOption(ChannelOption.SO_BROADCAST)) &&
        localAddress instanceof InetSocketAddress &&
        !((InetSocketAddress) localAddress).getAddress().isAnyLocalAddress() &&
        !PlatformDependent.isWindows() && !PlatformDependent.maybeSuperUser()) {
        // Warn a user about the fact that a non-root user can't receive a
        // broadcast packet on *nix if the socket is bound on non-wildcard address.
        logger.warn(
                "A non-root user can't receive a broadcast packet if the socket " +
                "is not bound to a wildcard address; binding to a non-wildcard " +
                "address (" + localAddress + ") anyway as requested.");
    }

    boolean wasActive = isActive();
    try {
    	//1. Core calling method
        doBind(localAddress);		// 1
    } catch (Throwable t) {
        safeSetFailure(promise, t);
        closeIfClosed();
        return;
    }
	//Determine if it is isActive
    if (!wasActive && isActive()) {		// 2
        invokeLater(new Runnable() {
            @Override
            public void run() {
                pipeline.fireChannelActive();
            }
        });
    }

    safeSetSuccess(promise);
}



1. doBind(localAddress)

@SuppressJava6Requirement(reason = "Usage guarded by java version check")
@Override
 protected void doBind(SocketAddress localAddress) throws Exception {
 	//Judge whether the java version is > 7
     if (PlatformDependent.javaVersion() >= 7) {
     	//javaChannel is actually ServerSocketChannel	
     	//register
         javaChannel().bind(localAddress, config.getBacklog());
     } else {
         javaChannel().socket().bind(localAddress, config.getBacklog());
     }
 }

Summary: this method is used to bind according to different java versions

2. pipeline.fireChannelActive()

To enter this method, first judge whether it is isActive, that is, whether the current ServerSocketChannel is available after a series of operations.

Up to now, there are three processors on the pipeline:
head --> acceptor --> tail

Call pipeline After firechannelactive(), the methods of all processors will be executed in sequence. Of course, acceptor and tail have little impact at this time, because acceptor has been handled in initAndRegister. So now navigate directly to the HeadContext, in the defaultchannelpipeline Java class. Navigate to the ChannelActice method:

 @Override
 public void channelActive(ChannelHandlerContext ctx) {
     ctx.fireChannelActive();
	 //Focus on events
     readIfIsAutoRead();
 }

We trace the calling process of readIfIsAutoRead():







Find the final calling method:

@Override
protected void doBeginRead() throws Exception {
     // Get selectionKey
     final SelectionKey selectionKey = this.selectionKey;
     //Judge whether it is illegal
     if (!selectionKey.isValid()) {
         return;
     }
		
     readPending = true;
	//Get events of interest
     final int interestOps = selectionKey.interestOps();
     //If the event of interest is not
     if ((interestOps & readInterestOp) == 0) {
     	// Paying attention to reading events is actually equivalent to adding, for example, 0000 | 0010 = 0010
     	//Of course, readInterestOp = 16 can be added only when it is 0
         selectionKey.interestOps(interestOps | readInterestOp);
     }
 }




Conclusion: this method is actually binding the event of interest





If there is any error, please point it out!!!

Topics: Java Netty Back-end source code analysis