netty入门之服务端启动过程分析

111 阅读11分钟

Netty 是一款用于高效开发网络应用的 NIO 网络框架,具有高性能、易使用、安全稳定、高可扩展等特点,并且被广泛的应用于各类中间件框架中,比如Dubbo,gRPC,ES,RocketMQ等。
本文以netty的服务端启动的Demo代码为入口,对netty的源码进行学习并加以分析,下面对server的启动过程进行记录。
以下netty代码为4.1.49。

一、基础知识

Future与Promise在netty的代码中会大量使用到Futrue与Promise,理解它们是阅读netty代码的基本门槛之一,下面对它们进行简单的回顾学习。

Future

Runnable vs Callable 在Java 1.5之前,创建线程有两种方式,一种直接通过new Thread创建,另一种是通过实现Runnable接口创建,当然Thread类也是Runnable接口的实现类,也可以是同一种方式。这种方式最大的缺点就是不能拿到线程执行的结果,为了弥补这个缺点,从Java 1.5开始引入接口Callable、Future来实现多线程中取到线程的执行结果。 如下为Callable的实用案例:

public class FactorialTask implements Callable<Integer> {
    int number;

    // standard constructors

    public Integer call() throws InvalidParamaterException {
        int fact = 1;
        // ...
        for(int count = number; count > 1; count--) {
            fact = fact * count;
        }

        return fact;
    }
}

@Test
public void whenTaskSubmitted_ThenFutureResultObtained(){
    FactorialTask task = new FactorialTask(5);
	ExecutorService executorService = Executors.newFixedThreadPool(1);
    Future<Integer> future = executorService.submit(task);
 
    assertEquals(120, future.get().intValue());
}

在JDK的并发包J.U.C中定义了一个接口Future,先从这个Future说起,它用于代表异步操作的结果。Future提供了检查线程是否执行完成的方法isDone(),提供了取消的方法cancel()以及检查是否已取消的isCanceled(),还提供了获取线程执行结果的方法get()。Future的get方法获取结果时会阻塞。

既然JDK已提供了Future接口,为何netty要再定义一个Future接口且继承JDK的Future接口,看下netty的Future接口的定义:

/**
 * The result of an asynchronous operation.
 */
@SuppressWarnings("ClassNameSameAsAncestorName")
public interface Future<V> extends java.util.concurrent.Future<V> {

    /**
     * Returns {@code true} if and only if the I/O operation was completed
     * successfully.
     */
    boolean isSuccess();

    /**
     * returns {@code true} if and only if the operation can be cancelled via {@link #cancel(boolean)}.
     */
    boolean isCancellable();

    /**
     * Returns the cause of the failed I/O operation if the I/O operation has
     * failed.
     *
     * @return the cause of the failure.
     *         {@code null} if succeeded or this future is not
     *         completed yet.
     */
    Throwable cause();

    /**
     * Adds the specified listener to this future.  The
     * specified listener is notified when this future is
     * {@linkplain #isDone() done}.  If this future is already
     * completed, the specified listener is notified immediately.
     */
    Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener);

    /**
     * Adds the specified listeners to this future.  The
     * specified listeners are notified when this future is
     * {@linkplain #isDone() done}.  If this future is already
     * completed, the specified listeners are notified immediately.
     */
    Future<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners);

    /**
     * Removes the first occurrence of the specified listener from this future.
     * The specified listener is no longer notified when this
     * future is {@linkplain #isDone() done}.  If the specified
     * listener is not associated with this future, this method
     * does nothing and returns silently.
     */
    Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener);

    /**
     * Removes the first occurrence for each of the listeners from this future.
     * The specified listeners are no longer notified when this
     * future is {@linkplain #isDone() done}.  If the specified
     * listeners are not associated with this future, this method
     * does nothing and returns silently.
     */
    Future<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... listeners);

    /**
     * Waits for this future until it is done, and rethrows the cause of the failure if this future
     * failed.
     */
    Future<V> sync() throws InterruptedException;

    /**
     * Waits for this future until it is done, and rethrows the cause of the failure if this future
     * failed.
     */
    Future<V> syncUninterruptibly();

    /**
     * Waits for this future to be completed.
     *
     * @throws InterruptedException
     *         if the current thread was interrupted
     */
    Future<V> await() throws InterruptedException;

    /**
     * Waits for this future to be completed without
     * interruption.  This method catches an {@link InterruptedException} and
     * discards it silently.
     */
    Future<V> awaitUninterruptibly();

    /**
     * Waits for this future to be completed within the
     * specified time limit.
     *
     * @return {@code true} if and only if the future was completed within
     *         the specified time limit
     *
     * @throws InterruptedException
     *         if the current thread was interrupted
     */
    boolean await(long timeout, TimeUnit unit) throws InterruptedException;

    /**
     * Waits for this future to be completed within the
     * specified time limit.
     *
     * @return {@code true} if and only if the future was completed within
     *         the specified time limit
     *
     * @throws InterruptedException
     *         if the current thread was interrupted
     */
    boolean await(long timeoutMillis) throws InterruptedException;

    /**
     * Waits for this future to be completed within the
     * specified time limit without interruption.  This method catches an
     * {@link InterruptedException} and discards it silently.
     *
     * @return {@code true} if and only if the future was completed within
     *         the specified time limit
     */
    boolean awaitUninterruptibly(long timeout, TimeUnit unit);

    /**
     * Waits for this future to be completed within the
     * specified time limit without interruption.  This method catches an
     * {@link InterruptedException} and discards it silently.
     *
     * @return {@code true} if and only if the future was completed within
     *         the specified time limit
     */
    boolean awaitUninterruptibly(long timeoutMillis);

    /**
     * Return the result without blocking. If the future is not done yet this will return {@code null}.
     *
     * As it is possible that a {@code null} value is used to mark the future as successful you also need to check
     * if the future is really done with {@link #isDone()} and not rely on the returned {@code null} value.
     */
    V getNow();

    /**
     * {@inheritDoc}
     *
     * If the cancellation was successful it will fail the future with a {@link CancellationException}.
     */
    @Override
    boolean cancel(boolean mayInterruptIfRunning);
}

netty的Future接口提供了更多的方法,与JDK最主要的区别是增加了用于监听器相关的方法,可用于当future执行完成之后立即执行监听器。(观察者模式)

Promise

Promise,中文翻译为承诺或者许诺,含义是人与人之间,一个人对另一个人所说的具有一定憧憬的话,一般是可以实现的。

netty定义的Promise接口继承自其Future,是可以写操作的Future,通过setSuccess,setFailure等方法来设置返回对象。

public class PromiseDemo {

    public static void main(String[] args) {

        EventExecutor executor = GlobalEventExecutor.INSTANCE;
        Promise<Map<String, String>> promise = new DefaultPromise<>(executor);
        promise.addListener(new MyListener());

        Thread myThread = new Thread(new MyThread(promise));
        myThread.start();

        executor.shutdownGracefully();
    }


    static class MyThread implements Runnable {
        Promise promise;

        public MyThread(Promise promise) {
            this.promise = promise;
        }

        @Override
        public void run() {
            // 模拟处理业务逻辑
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Map<String, String> result = new HashMap<>();
            result.put("completeTime", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss")));

            promise.setSuccess(result);
        }
    }

    static class MyListener implements GenericFutureListener<Future<Map<String, String>>> {

        @Override
        public void operationComplete(Future<Map<String, String>> future) throws Exception {
            if (future.isSuccess()) {
                Map<String, String> result = future.getNow();
                System.out.println("执行完成,输出结果:");
                for (Map.Entry<String, String> entry : result.entrySet()) {
                    System.out.println(entry.getKey() + ":" + entry.getValue());
                }
            }
        }
    }
}

二、理解EventLoopGroup、EventLoop

直接以netty的入门使用代码作为入口,如下:

ServerBootstrap server = new ServerBootstrap();
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
server.group(bossGroup, workerGroup);

这里看到里EventLoopGroup,直接从NioEventLoopGroup的构造函数跟进。由于这里涉及到的类较多,命名看起来有点绕,所以先给出一张类图,方便在跟进代码的时候更清晰,如下: Ciqc1F-NPHWAH8ExAAkQr2tQ9xM350.png

NioEventLoopGroup初始化

NioEventLoopGroup最常用的两个构造函数为:

public NioEventLoopGroup() {
        this(0);
}

public NioEventLoopGroup(int nThreads) {
        this(nThreads, (Executor) null);
}

线程的数量决定了EventExecutor[] children属性,也就是EventLoopGroup中用于的EventLoop数组的长度。

** NioEventLoopGroup初始化过程中,其属性EventExecutor[] children初始化的过程?**
1)数组的大小是NioEventLoopGroup的构造函数传入的数组,当没传的时候(无参构造函数)会取默认数量,如下:

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

优先取配置io.netty.eventLoopThreads,没有这个配置时会使用系统可用的处理器的两倍。

2)确定了数组的大小,然后就是进行初始化,即初始化NioEventLoop,在其构造函数中executor为new ThreadPerTaskExecutor(newDefaultThreadFactory())

3)NioEventLoopGroup的属性chooser,用于选择下一个EventLoop,也就是在children数组中选择下一个NioEventLoop,chooser会根据数组的值是否为2的幂次方有两种方式,如果是2的幂次方会使用&运算来计算下标(PowerOfTwoEventExecutorChooser),否则使用%运算来计算下标(GenericEventExecutorChooser)

关于EventLoop的内部实现这里不再展开,包括内部的任务队列、执行方式等。

三、server端启动过程

先看下简单的server端启动的代码:

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

        server.group(bossGroup, workerGroup);
        server.channel(NioServerSocketChannel.class);
        server.localAddress(8080);
        server.option(ChannelOption.SO_KEEPALIVE, true);
        server.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

        server.childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                ch.pipeline().addLast("codec", new HttpServerCodec())
                        .addLast("compressor", new HttpContentCompressor())
                        .addLast("aggregator", new HttpObjectAggregator(65536))
                .addLast("handler", new HttpServerHandler())
                ;
            }
        });

        ChannelFuture future = server.bind().sync();

        System.out.println("服务器启动成功,监听端口:" + future.channel().localAddress());

        future.channel().closeFuture().sync();

代码中包含的内容分为两部分,一部分是初始化并设置一些属性,比如设置channel类型、端口号、连接的参数、处理类、流水线等;另一部分是启动服务,也就是server.bind().sync()这行代码。下面主要看下启动服务这一行代码,netty做了哪些事情,分为bind和sync两部分来看。

bind()

在使用Java NIO时,启动服务端的过程需要将Selector注册到Channel上,代码实现如下:

		Selector selector = Selector.open();

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 绑定端口号
        serverSocketChannel.bind(new InetSocketAddress(9090));
        // 设置为非阻塞
        serverSocketChannel.configureBlocking(false);

        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        selectionKey.attach(new Acceptor(serverSocketChannel, new SubReactor()));

        while (true) {
            int select = selector.select();
            System.out.println("select收到事件数:" + select);
            if (select > 0) {
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

                while (iterator.hasNext()) {
                    SelectionKey next = iterator.next();
                    dispatch(next);
                    // 移除
                    iterator.remove();
                }
            }
        }

上面的代码省略dispatch方法,Acceptor的代码,通过Java NIO的实现服务端的方式包括了以下步骤:

  1. 创建Channel,新建ServerSocketChannel并绑定端口号,设置相关的属性;
  2. 创建Selector
  3. 将Selector注册到Channel上,并设置监听的类型、设置附件
  4. 开启循环通过Selectot.select判断是否有连接,并依次处理各连接

下面与netty的实现过程进行对比与参照,来看netty对bind的过程:

  1. 初始化Channel
  2. Channel与EventLoop的关联
  3. 执行绑定

初始化Channel

初始化Channel,在服务端就是实例化NioServerSocketChannel,包含设置Channel的参数(也就是server.option中设置的)、为Channel创建流水线Pipline。 调用链路:

AbstarctBootstrap#bind( ) AbstarctBootstrap#doBind( ) AbstarctBootstrap#initAndRegister( )

关键代码:

	final ChannelFuture initAndRegister() {
        Channel channel = null;
        try {
			// 通过反射实例化,在Bootstrap.channel(Class<? extends C> channelClass)方法设置要实例化的类
            channel = channelFactory.newChannel();
            // 设置Channel的属性Option,关联新的流水线Pipline
			init(channel);
        } catch (Throwable t) {
           // ....
        }
		// ....
    }

在创建Channel实例时做了两件事情:

  1. 通过反射实例化NioServerSocketChannel
  2. 设置Channel的属性Option(也就是server.option中设置的),关联新的流水线Pipline

Channel与EventLoop关联

换句话说,也就是讲Channel通过EventLoopGroup注册到某个EventLoop上,也就是AbstractBootstrap中initAndRegister方法中的下面一行代码:

    final ChannelFuture initAndRegister() {
        // 省略Channel的初始化及相关设置
		// ....

        ChannelFuture regFuture = config().group().register(channel);
        
		// 省略返回等代码
		// ....
    }

首先会从EventLoopGroup中通过next()选择一个EventLoop(选择的方式在上面的chooser有说明),然后将Channel注册到EventLoop,此处也就是NioEventLoop。

	// SingleThreadEventLoop是NioEventLoop的父类,注册channel的方法如下

	@Override
    public ChannelFuture register(Channel channel) {
        return register(new DefaultChannelPromise(channel, this));
    }

    @Override
    public ChannelFuture register(final ChannelPromise promise) {
        ObjectUtil.checkNotNull(promise, "promise");
        promise.channel().unsafe().register(this, promise);
        return promise;
    }

如上面的代码所示,会将EventLoop和Promise注册到Channel的unsafe实例上,对于netty的unsafe后面找机会再进行具体说明,这里NioServerSocketChannel的unsafe可以定位到其父类抽象类AbstractNioMessageChannel的私有内部类NioMessageUnsafe。也就是说,注册的过程要查看NioMessageUnsafe的register方法,该方法在其父类AbstractChannel的私有内部类AbstractUnsafe中,代码如下:

		// AbstractChannel.java中的内部类AbstractUnsafe类		

		@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;

            if (eventLoop.inEventLoop()) {
                register0(promise);
            } else {
                try {
                    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);
                }
            }
        }

        private void register0(ChannelPromise promise) {
            try {
                // check if the channel is still open as it could be closed in the mean time when the register
                // call was outside of the eventLoop
                if (!promise.setUncancellable() || !ensureOpen(promise)) {
                    return;
                }
                boolean firstRegistration = neverRegistered;
                doRegister();
                neverRegistered = false;
                registered = true;

                // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
                // user may already fire events through the pipeline in the ChannelFutureListener.
                pipeline.invokeHandlerAddedIfNeeded();

                safeSetSuccess(promise);
                pipeline.fireChannelRegistered();
                // Only fire a channelActive if the channel has never been registered. This prevents firing
                // multiple channel actives if the channel is deregistered and re-registered.
                if (isActive()) {
                    if (firstRegistration) {
                        pipeline.fireChannelActive();
                    } else if (config().isAutoRead()) {
                        // This channel was registered before and autoRead() is set. This means we need to begin read
                        // again so that we process inbound data.
                        //
                        // See https://github.com/netty/netty/issues/4805
                        beginRead();
                    }
                }
            } catch (Throwable t) {
                // Close the channel directly to avoid FD leak.
                closeForcibly();
                closeFuture.setClosed();
                safeSetFailure(promise, t);
            }
        }

调用EventLoop的execute方法,如上代码会最终会调用eventLoop.execute,并将一个执行register0的线程(任务task)传入。

到此时,将提交一个register任务到EventLoop中(EventLoop包含任务队列,具体执行方法此处不展开),然后返回一个ChannelFuture,再次回到AbstractBootstrap的initAndRegister方法的代码中:

	final ChannelFuture initAndRegister() {
		// 省略Channel的初始化及相关设置
		// ....        
		
		// 返回DefaultChannelPromise
        ChannelFuture regFuture = config().group().register(channel);
        if (regFuture.cause() != null) {
            if (channel.isRegistered()) {
                channel.close();
            } else {
                channel.unsafe().closeForcibly();
            }
        }

        // If we are here and the promise is not failed, it's one of the following cases:
        // 1) If we attempted registration from the event loop, the registration has been completed at this point.
        //    i.e. It's safe to attempt bind() or connect() now because the channel has been registered.
        // 2) If we attempted registration from the other thread, the registration request has been successfully
        //    added to the event loop's task queue for later execution.
        //    i.e. It's safe to attempt bind() or connect() now:
        //         because bind() or connect() will be executed *after* the scheduled registration task is executed
        //         because register(), bind(), and connect() are all bound to the same thread.

        return regFuture;
    }

netty源码中的注释显示,这个方法返回regFuture时,有两个情况,一种是注册的过程已经处理成功,另一种是提交给异步的线程,也就是添加到EventLoop的任务队列中等待被执行。所以,在这里返回的ChannelFuture(Promise)可以等待注册完成后的一个回调处理。接下来就在具体的bind过程中继续查看。

执行绑定:doBind()

最后在执行doBind操作时会进入到AbstractChannel的AbstractUnsafe中的bind方法,如下:

		@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 {
				// 执行绑定,类似serverSocketChannel.bind(new InetSocketAddress(9090));
                doBind(localAddress);
            } catch (Throwable t) {
                safeSetFailure(promise, t);
                closeIfClosed();
                return;
            }

            if (!wasActive && isActive()) {
                invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.fireChannelActive();
                    }
                });
            }
			// 绑定成功后,设置promise为成功状态
            safeSetSuccess(promise);
        }

最终返回的ChannelFuture是PendingRegistrationPromise的实例。

sync()

sync的作用通过查看DefaultPromise的sync方法可知,主要作用是调用wait()进入休眠,等待上面的doBind()执行完成之后,通过调用safeSetSuccess(promise)来唤醒。其作用防止执行main方法的主线程在调用server.bind()之后就退出,具体代码细节就不再进行分析。

四、总结

netty服务端的启动过程简单概括为,首先初始化Channel,包括EventLoopGroup初始化设置、Channel属性设置、Pipline初始化设置,然后通过EventLoopGroup分配EventLoop与Channel进行关联注册,并开启绑定Selector和循环监听。 从上面的过程中来看,netty大量的使用异步的方式执行,并通过Promise来完成回调,所以理解Promise对理解netty的代码。