Netty15 -- Analysis of netty startup process

Posted by karikamiya on Mon, 06 Dec 2021 20:44:42 +0100

  there are many Netty cases under Netty's io.netty.example package, which can be used for source code analysis and learning. Now use the Echo case under the package to analyze the startup process of Netty. The entry code is as follows:

public final class EchoServer {

    static final boolean SSL = System.getProperty("ssl") != null;
    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));

    public static void main(String[] args) throws Exception {
        // Configure SSL
        final SslContext sslCtx;
        if (SSL) {
            SelfSignedCertificate ssc = new SelfSignedCertificate();
            sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
        } else {
            sslCtx = null;
        }

        // Configure server
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .option(ChannelOption.SO_BACKLOG, 100)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     if (sslCtx != null) {
                         p.addLast(sslCtx.newHandler(ch.alloc()));
                     }
                     //p.addLast(new LoggingHandler(LogLevel.INFO));
                     // Add processor
                     p.addLast(new EchoServerHandler());
                 }
             });

            // Bind the listening port and start the service
            ChannelFuture f = b.bind(PORT).sync();

            // Wait until the server socket is closed.
            f.channel().closeFuture().sync();
        } finally {
            // Close all event loops to terminate all threads
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

  1. Two EventLoopGroup objects are created:

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();

  these two objects are the core objects of Netty, and the whole running process of Netty depends on them. The bossGroup is used to receive TCP requests and submit the received requests to the worker group for processing (i.e. handler); The worker group will get the real connection, and then communicate with the client through the connection to complete reading, writing, encoding and decoding.
  EventLoopGroup, i.e. event loop group (program group). It contains multiple eventloops, which can register channel s for selection in the event loop (related to selectors).
  1 here means that only one thread is opened in the bossGroup event loop group to receive requests, while the workerGroup will open two times the number of CPU cores by default:

private static final int DEFAULT_EVENT_LOOP_THREADS;
static {
    DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
            "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));

    if (logger.isDebugEnabled()) {
        logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
    }
}

  from the above code, we can define the number of threads opened by default by setting the value of io.netty.eventLoopThreads.
  go deep into the internal of new NioEventLoopGroup(), and the code is as follows:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                         EventExecutorChooserFactory chooserFactory, Object... args) {
     if (nThreads <= 0) {
         throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
     }
	 //Initialize thread pool
     if (executor == null) {
         executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
     }
     // Create EventExecutor array
     children = new EventExecutor[nThreads];
     for (int i = 0; i < nThreads; i ++) {
         boolean success = false;
         try {
             // Assign a value to each element in the EventExecutor array, and each object assigned is of type NIOEventLoop
             children[i] = newChild(executor, args);
             success = true;
         } catch (Exception e) {
             // TODO: Think about if this is a good exception type
             throw new IllegalStateException("failed to create a child event loop", e);
         } finally {
             if (!success) {
                 for (int j = 0; j < i; j ++) {
                     children[j].shutdownGracefully();
                 }

                 for (int j = 0; j < i; j ++) {
                     EventExecutor e = children[j];
                     try {
                         while (!e.isTerminated()) {
                             e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);
                         }
                     } catch (InterruptedException interrupted) {
                         // Let the caller handle the interruption.
                         Thread.currentThread().interrupt();
                         break;
                     }
                 }
             }
         }
     }

     chooser = chooserFactory.newChooser(children);

     final FutureListener<Object> terminationListener = new FutureListener<Object>() {
         @Override
         public void operationComplete(Future<Object> future) throws Exception {
             if (terminatedChildren.incrementAndGet() == children.length) {
                 terminationFuture.setSuccess(null);
             }
         }
     };

     for (EventExecutor e: children) {
         e.terminationFuture().addListener(terminationListener);
     }

     Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);
     Collections.addAll(childrenSet, children);
     readonlyChildren = Collections.unmodifiableSet(childrenSet);
 }

  initialize an EventExecutor array first, and then assign values to each element of EventExecutor []:

// Initialize EventExecutor array
children = new EventExecutor[nThreads];
for (int i = 0; i < nThreads; i ++) {
    ...        
    try {
        //Assign a value to each element of EventExecutor []
        children[i] = newChild(executor, args);
        success = true;
    } catch (Exception e) {
        // TODO: Think about if this is a good exception type
        throw new IllegalStateException("failed to create a child event loop", e);
    } finally {
        ...
    }
    ...
}

  the value assigned to each element of EventExecutor [] here is a NioEventLoop object: NioEventLoop implements the EventLoop and EventExecutor interfaces

@Override
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
    return new NioEventLoop(this, executor, (SelectorProvider) args[0],
        ((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2]);
}

  2. After creating two EventLoopGroup objects, create a ServerBootstrap object:

try {
    ServerBootstrap b = new ServerBootstrap();
    b.group(bossGroup, workerGroup)
     .channel(NioServerSocketChannel.class)
     // Set some TCP parameters. option() can be called multiple times to set multiple parameters
     .option(ChannelOption.SO_BACKLOG, 100)
     .handler(new LoggingHandler(LogLevel.INFO))
     .childHandler(new ChannelInitializer<SocketChannel>() {
         @Override
         public void initChannel(SocketChannel ch) throws Exception {
             ChannelPipeline p = ch.pipeline();
             if (sslCtx != null) {
                 p.addLast(sslCtx.newHandler(ch.alloc()));
             }
             //p.addLast(new LoggingHandler(LogLevel.INFO));
             // Add processor
             p.addLast(new EchoServerHandler());
         }
     });
     ...
} finally {
    ...
}

  ServerBootstrap is a boot class used to start the server and boot the initialization of the whole program. It is associated with ServerChannel. ServerChannel inherits the channel (the channel type is specified through the method channel(NioServerSocketChannel.class), that is, the channel type of communication between the server and the client) and can obtain remote IP and other information through ServerChannel:

public interface ServerSocketChannel extends ServerChannel {
    @Override
    ServerSocketChannelConfig config();
    @Override
    InetSocketAddress localAddress();
    @Override
    InetSocketAddress remoteAddress();
}

  here, only a ChannelFactory object is created through reflection, and the Channel object is not really created (the creation of the Channel object is completed in the bind() method), which is equivalent to only specifying the type for the ChannelFactory to determine the type of Channel to be created through the ChannelFactory later:

public B channel(Class<? extends C> channelClass) {
    if (channelClass == null) {
        throw new NullPointerException("channelClass");
    }
    return channelFactory(new ReflectiveChannelFactory<C>(channelClass));
}

  set TCP parameters through option() (the option() method can be called multiple times to add or remove configuration from options). The essence of options is a Map:

private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>();

public <T> B option(ChannelOption<T> option, T value) {
    if (option == null) {
        throw new NullPointerException("option");
    }
    if (value == null) {
        synchronized (options) {
            options.remove(option);
        }
    } else {
        synchronized (options) {
            options.put(option, value);
        }
    }
    return self();
}

  after setting the TCP parameters, add a handler to handle business or events for bossGroup and workerGroup. The handler set in handler() method is associated with bossGroup, and the handler set in childHandler() method is associated with workerGroup.
  3. Bind the port and wait for the client connection (when there is a client connection, a channel channel of the type previously specified through the channel() method will be created for the communication between the server and the client)

// Bind the listening port and start the server
ChannelFuture f = b.bind(PORT).sync();

  the bind() method will eventually call the doBind() method:

private ChannelFuture doBind(final SocketAddress localAddress) {
    final ChannelFuture regFuture = initAndRegister();
    final Channel channel = regFuture.channel();
    if (regFuture.cause() != null) {
        return regFuture;
    }

    if (regFuture.isDone()) {
        // Here, the registration is completed successfully
        ChannelPromise promise = channel.newPromise();
        doBind0(regFuture, channel, localAddress, promise);
        return promise;
    } else {
        // At this point, the registration work is basically always successful, but fault tolerance is carried out just in case
        final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
        regFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                Throwable cause = future.cause();
                if (cause != null) {
                    // Registration on the EventLoop failed so fail the ChannelPromise directly to not cause an
                    // IllegalStateException once we try to access the EventLoop of the Channel.
                    promise.setFailure(cause);
                } else {
                    // Registration was successful, so set the correct executor to use.
                    // See https://github.com/netty/netty/issues/2586
                    promise.registered();

                    doBind0(regFuture, channel, localAddress, promise);
                }
            }
        });
        return promise;
    }
}

  the first line of the doBind() method is to create a channel with the ChannelFactory initialized through reflection (channel(NioServerSocketChannel.class)), and register the channel after creation:

final ChannelFuture initAndRegister() {
    Channel channel = null;
    try {
        //Create channel
        channel = channelFactory.newChannel();
        init(channel);
    } catch (Throwable t) {
        ...
    }
    // Register channel
    ChannelFuture regFuture = config().group().register(channel);
    if (regFuture.cause() != null) {
        if (channel.isRegistered()) {
            channel.close();
        } else {
            channel.unsafe().closeForcibly();
        }
    }
    return regFuture;
}

  in this way, the server starts, and then blocks until the socket of the server is closed (the server stops running). Finally, all thread resources in the event loop group will be gracefully closed.

Topics: Java Netty SSL https