Receiving request of Netty source code analysis

Posted by dirkie on Fri, 11 Feb 2022 23:37:11 +0100

1, Entrance

In the second section( Netty simple startup process )In, we introduced the simple startup of netty. After we know that the port binding is completed, we begin to wait for requests from the client and process requests from the client. Now let's see how netty operates?
When we review the source code of the channel registration process in Section 2, we can find the following code.

    public final void register(EventLoop eventLoop, final ChannelPromise promise) {
            AbstractChannel.this.eventLoop = eventLoop;//Here is to assign the current EventLoop to
            //eventLoop property of the current channel
             eventLoop.execute(new Runnable() {
                    @Override
                     public void run() {
                         register0(promise);
                     }
               });
            }
    }

It can be seen from the code that the registration of the channel is realized through EventLoop (the current EventLoop is an instance of NioEventLoop, and how to allocate this instance will be explained separately later. One thing you can know is that the current EventLoop is allocated through group). The execute method is mainly to add tasks to the task queue, Then start the thread (if the current thread is not started). Back to the doBind0 method

    private static void doBind0(
            final ChannelFuture regFuture, final Channel channel,
            final SocketAddress localAddress, final ChannelPromise promise) {
        //The eventLoop here is assigned by the registration method above
        channel.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                    channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); 
            }
        });
    }

The specific port binding is also completed through EventLoop. At this time, continue to add a port binding task to the task queue. The thread has been started when registering. Here, we can roughly guess where the entry to receive the client's request is.
Both registration and binding are completed through EventLoop, which means that the request from the receiving client is also completed here (when registering, we know that the thread is started), so let's first see how the run method in EventLoop executes.

    protected void run() {
        int selectCnt = 0;
        for (;;) {
        	//Most of the code is omitted
              select(curDeadlineNanos);              
              processSelectedKeys();
          }            
    }

Here, the key two lines of code of the whole run method are extracted. The first line is the familiar code select (curdeadline nanos); That is, the entry to call the select method of the underlying selector. The second line is the method entry processSelectedKeys() for processing specific requests. If we find that processSelectedKey(k, (AbstractNioChannel) a) will be called in the end when we degug code.

    private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
        try {
           if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();//This is where the client accept s and reads
            }
 
    }

Let's take a look at the read method. From the perspective of the method, it is mainly to read information and then process information.

        public void read() {
                doReadMessages(readBuf);
                pipeline.fireChannelRead(readBuf.get(i));
 
        }

Looking at the source code of the doReadMessages method, we can know that the current method is to add NioSocketChannel object (buf.add(new NioSocketChannel(this, ch)), where ch is the underlying SocketChannel) to the readBuf collection. pipeline. The method of firechannelread (readBuf. Get (I)) is to process all the elements in the current collection. Here we can see that it is through the pipeline to obtain a handler for execution. We also briefly introduced the pipeline in Section 3 and reviewed the initialization method in Section 2, calling init:

    void init(Channel channel) {
        ChannelPipeline p = channel.pipeline();
        p.addLast(new ChannelInitializer<Channel>() {
            @Override
            public void initChannel(final Channel ch) {
                ch.eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.addLast(new ServerBootstrapAcceptor(
                                ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });
    }

The source code can know that when initialization, we have added a ChannelInitializer handler to the pipeline attribute of NioServerSocketChannel, and its initChannel needs to be implemented by ourselves. This initChannel method is called in channelRegistered method and handlerAdded method, for NioServerSocketChannel, The first time we call it in the channelRegistered method, we know that channel will call pipeline. after the registration is completed. Firechannelregistered() method. Therefore, after the registration is completed, the initchannel method of the upper channelinitializer will be called to add the serverbootstrap acceptor to the pipeline. So when the fireChannelRead method is invoked in the read method, the channelRead method in ServerBootstrapAcceptor will be called directly.

        public void channelRead(ChannelHandlerContext ctx, Object msg) {
            final Channel child = (Channel) msg;//You can see the channel here above
            //NioSocketChannel
            child.pipeline().addLast(childHandler);//The childHandler here is
            //The first thing to start is the custom added handler
            setChannelOptions(child, childOptions, logger);//Set sub parameters
            setAttributes(child, childAttrs);//Set sub attributes
            try {
                childGroup.register(child).addListener(new ChannelFutureListener() {
                //Register the channel through the sub group. The channel registration here is very simple, that is, execute the register and active methods of the handler
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        if (!future.isSuccess()) {
                            forceClose(child, future.cause());
                        }
                    }
                });
            } catch (Throwable t) {
                forceClose(child, t);
            }
        }

From here, we can know that the real reception of client data is realized through childGroup, that is, it is processed from the EventLoop allocated in childGroup. The processing logic is the same as above.

2, Details

The first part only briefly introduces the entrance of data receiving and processing. I believe most people are still confused and confused in logic. So this part mainly clarifies the logic of the first part.

  • Back to the first quarter( First met Netty )In the example of bootstrap, there are two groups. These two groups are very important. From the naming point of view, one is the main group and the other is the working group. Here we need to remember that there are these two groups. From the group method of bootstrap, we can know that boss is assigned to the group attribute in AbstractBootstrap and work is assigned to the childGroup attribute in ServerBootstrap.
      EventLoopGroup boss=new NioEventLoopGroup();
      EventLoopGroup work=new NioEventLoopGroup();
      ServerBootstrap b=new ServerBootstrap();
      b.group(boss,work)

Let's not worry about its function, but remember the relationship above.

  • Review the binding process and find out where the two group s are used in the binding process
    1. In initializing the init(channel) method, childGroup is passed into the serverbootstrap acceptor as a construction parameter
    2. When initAndRegister() calls config() group(). Register (channel). First, configure config to get group, config() Group () returns the group attribute, that is, the boss parameter above. Check the register method of group and actually call next() register(channel).
    public ChannelFuture register(Channel channel) {
        return next().register(channel);
    }

The return value of the next() method is an EventLoop object (which is a specific thread object). If you continue to track the next method of the parent class, you will know that the group maintains an EventLoop array (EventExecutor[] executors. The initialization of the array is realized through the group. How to allocate this is described separately later), Group makes specific allocation through certain algorithms, which will be described in detail when analyzing the group source code separately in the future.

    public EventLoop next() {
        return (EventLoop) super.next();
    }

Combined with the first part, it can be seen that the channel registration and port binding are implemented through the same EventLoop allocated by the group attribute (i.e. boss), and the receiving of client requests is also in the run method of this instance. It can be seen that the group attribute (i.e. boss) in AbstractBootstrap will allocate an EventLoop instance according to a certain algorithm to handle channel registration, port binding and client requests.
3. As can be seen from the first part, in the processSelectedKeys() method of the EventLoop instance, this method will eventually call the following methods.

 private void processSelectedKeysOptimized() {
     for (int i = 0; i < selectedKeys.size; ++i) {
           final Object a = k.attachment();
           //Here is the channel obtained through the attachment. We have analyzed it when registering
           //The current NioServerSocketChannel instance will be registered on the underlying channel in the form of attachment
            //Therefore, the current AbstractNioChannel is an instance of NioServerSocketChannel
             processSelectedKey(k, (AbstractNioChannel) a);
     }
 }
 private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
     final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
     //Therefore, the instance here is also the NioServerSocketChannel instance. From this, we can see that the ch.unsafe() method returns
     //new NioMessageUnsafe() instance, which is in [section 3]( https://blog.csdn.net/solayang/article/details/116641699 )It has been analyzed in
     try {
        if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0
         || readyOps == 0) {
             unsafe.read();//This is where the client accept s and reads
         }

 }

So here's unsafe Read() uses the read method in the NioMessageUnsafe instance, which was also analyzed in the first part. Finally, it will call the channelRead method of ServerBootstrapAcceptor. This method mainly does the following things:
1) Add a custom childHandler to childChannel
2) Set sub parameters and sub attributes
3) Register the childChannel through the childGroup. The registration principle here is the same as that of the group. The only difference is that the current channel is the NioSocketChannel instance, and the underlying channel in the doRegister method is SocketChannel instead of ServerSocketChannel. It should be noted here that childGroup, like group, will allocate an EventLoop instance to handle the current registration and start the current thread.
Here, let's sort out the current processing flow:

  1. Starting from bind(), first instantiate NioServerSocketChannel, then initialize the channel, assign childGroup as the construction parameter to serverbootstrap acceptor, and then add this handler to the pipeline of the channel
  2. Register the channel. The group allocates an EventLoop instance (and assigns it to the channel), registers the current channel as an attachment to the underlying ServerSocketChannel through this instance (NIO programming is involved here), and starts the thread
  3. Port binding, port binding is also achieved through the eventLoop property of channel, and finally calls the bottom port binding method.
  4. Receive the request from the client. The customer service side requests that in the run method of the EventLoop instance, the user form a NioSocketChannel instance object, call the channelRead method of ServerBootstrapAcceptor, register the childChannel through the childGroup, and wait for the client to send data
  5. Process the data of the client through the custom handler

Therefore, through the above repeated analysis, it is clear that the group will allocate an EventLoop instance to bind ports and handle connection requests from the current port. After the group handles the request, the childGroup will allocate an EventLoop to handle the read-write events of the client.
So the general logic of netty processing requests is analyzed here.
Please correct any mistakes in the above. Please understand.