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.