Netty 啟動過程源碼分析 (本文超長慎讀)(基于4.1.23)

前言

作為一個 Java 程序員,必須知道Java社區最強網絡框架-------Netty,且必須看過源碼,才能說是了解這個框架,否則都是無稽之談。今天樓主不會講什么理論和概念,而是使用debug 的方式,走一遍 Netty (服務器)的啟動過程。

1. demo 源碼

樓主 clone 的 netty 的源碼,值得一提的是,netty 提供了大量的 demo 供用戶使用和測試。今天我們就通過netty的例子,來逐步 debug。ok ,開始吧。

啟動類源碼

public final class EchoServer {

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

    /**
     * @see io.netty.channel.nio.NioEventLoop
     */
    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 the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(8);
        EventLoopGroup workerGroup = new NioEventLoopGroup(16);
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)//new ReflectiveChannelFactory<C>(channelClass)
             .option(ChannelOption.SO_BACKLOG, 100)
             .handler(new LoggingHandler(LogLevel.INFO))// ServerSocketChannel 專屬
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception { // SocketChannel 專屬
                     ChannelPipeline p = ch.pipeline();
                     if (sslCtx != null) {
                         p.addLast(sslCtx.newHandler(ch.alloc()));
                     }
                     //p.addLast(new LoggingHandler(LogLevel.INFO));
                     p.addLast(new EchoServerHandler());
                 }
             });
           // Start the server.
            ChannelFuture f = b.bind(PORT).sync();

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

處理器源碼

@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg)
        throws UnsupportedEncodingException {
      ByteBuf buf = (ByteBuf)msg;
      byte[] req = new byte[buf.readableBytes()];
      buf.readBytes(req);
      String body = new String(req,"UTF-8");

      System.err.println(body);

      String reqString = "Hello I am Server";
      ByteBuf resp = Unpooled.copiedBuffer(reqString.getBytes());
      ctx.writeAndFlush(resp);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}


這兩個源碼都在 io.netty.example.echo 包下,大家可以自行下載。

2. demo 分析

我們先分析一下我們的 demo 源碼,知道他們有哪些作用。

先看啟動類

main 方法中,首先創建了關于SSL 的配置類,這個不是我們今天的重點。略過。

重點來了,創建了兩個EventLoopGroup 對象:

      // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(8);
        EventLoopGroup workerGroup = new NioEventLoopGroup(16);

這兩個對象是整個 Netty 的核心對象,可以說,整個 Netty 的運作都依賴于他們。bossGroup 用于接受 Tcp 請求,他會將請求交給 workerGroup ,workerGroup 會獲取到真正的連接,然后和連接進行通信,比如讀寫解碼編碼等操作。

try 塊中創建了一個 ServerBootstrap 對象,他是一個引導類,用于啟動服務器和引導整個程序的初始化。
隨后,變量 b 調用了 group 方法將兩個 group 放入了自己的字段中,用于后期引導使用。
然后添加了一個 channel,其中參數一個Class對象,引導類將通過這個 Class 對象反射創建 Channel。
然后添加了一些TCP的參數。
再添加了一個服務器專屬的日志處理器 handler。
再添加一個 SocketChannel(不是 ServerSocketChannel)的 handler。
然后綁定端口并阻塞至連接成功。
最后main線程阻塞等待關閉。
finally 塊中的代碼將在服務器關閉時優雅關閉所有資源。

再看 EchoServerHandler 類

這是一個普通的處理器類,用于處理客戶端發送來的消息,在我們這里,我們簡單的解析出客戶端傳過來的內容,然后打印,最后發送字符串給客戶端。

好,我們已經大致講解了我們的 demo 源碼的作用。當然,這里講的很簡單,我們將在后面的debug 的時候詳細介紹他們的作用。

3. 首先看創建 EventLoopGroup 的過程:

上面的這些都是一些重載的構造方法,并加入了一些默認值,比如為null 的 executor,還有熟悉的 NIO 的 SelectorProvider.provider(),也有一個單例的選擇策略工廠,還有一個默認的線程池拒絕策略,最后還有一個線程的默認數量:CPU 核心數 * 2。最后還有一個默認的線程選擇策略工廠。

最后,才是 NioEventLoopGroup 真正的構造方法,在抽象父類MultithreadEventExecutorGroup中,這里我們看到了模板模式,代碼如下 :

    // 1.默認0,2executor 默認null, 3.nio provider,4.new DefaultSelectStrategyFactory() 是個單例,5.默認拒絕策略:拋出異常
    // args : 3-5, 線程數默認: NettyRuntime.availableProcessors() * 2,也就是 CPU core * 2

    // 1.默認 core *2, 2.null, 3. 單例new DefaultEventExecutorChooserFactory(), 4, 3-5
    protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
                                            EventExecutorChooserFactory chooserFactory, Object... args) {
        if (nThreads <= 0) {
            throw new IllegalArgumentException(String.format("nThreads: %d (expected: > 0)", nThreads));
        }

        if (executor == null) {
            // 類名為名稱的線程工廠
            // 該線程池沒有任何隊列,提交任務后,創建任何線程類型都是 FastThreadLocalRunnable, 并且立即start。
            executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
        }
        // 創建一個事件執行組
        children = new EventExecutor[nThreads];
        // 初始化線程數組
        for (int i = 0; i < nThreads; i ++) {
            boolean success = false;
            try {
                // 創建 new 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);
    }

雖然代碼很長,實際上就那么幾件事情,我們拆分來看:

  1. 如果 executor 是null,創建一個默認的 ThreadPerTaskExecutor,使用 Netty 默認的線程工廠。
  2. 根據傳入的線程數(CPU*2)創建一個線程池(單例線程池)數組。
  3. 循環填充數組中的元素。如果異常,則關閉所有的單例線程池。
  4. 根據線程選擇工廠創建一個 線程選擇器,默認是對2取余(位運算),也可以順序獲取。
  5. 為每一個單例線程池添加一個關閉監聽器。
  6. 將所有的單例線程池添加到一個 HashSet 中。

本篇先于篇幅,不會再繼續拆解這些步驟,后面我們找機會繼續拆解。

4. 再看 ServerBootstrap 創建和構造過程

ServerBootstrap 是個空構造,什么都沒有,但注意,有默認的變量:

注意其中的 ServerBootstrapConfig 對象,這個對象將會在后面起很大作用。

再看后面的鏈式調用:group 方法,將 boss 和 worker 傳入,boss 賦值給 group 屬性,worker 賦值給 childGroup 屬性。

channel 方法傳入 NioServerSocketChannel class 對象。會根據這個 class 創建 channel 對象。

option 方法傳入 TCP 參數,放在一個LinkedHashMap 中。

handler 方法傳入一個 handler 中,這個hanlder 只專屬于 ServerSocketChannel 而不是 SocketChannel。

childHandler 傳入一個 hanlder ,這個handler 將會在每個客戶端連接的時候調用。供 SocketChannel 使用。

5. 再看 bind 方法

注意,整個服務器就是在這個方法里啟動完成的,從這里開始,將會是一段跳來跳去的過程(因為是異步的),我們將使用強大的 IDEA 編輯器進行調試。

開始吧!

上面很簡單,創建了一個端口對象,并做了一些空判斷,就不講了,最重要的是下面的 doBind 方法。

 private ChannelFuture doBind(final SocketAddress localAddress) {
        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 = ((NioServerSocketChannel)channel).newPromise();
            doBind0(regFuture, channel, localAddress, promise);
            return promise;
        } else {
            // Registration future is almost always fulfilled already, but just in case it's not.
            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;
        }
    }

ServerBootstrap 的這個方法不長也不短,但經過我們的拆解,可以分為以下 2 部分:

  1. initAndRegister 初始化 NioServerSocketChannel 通道并注冊各個 handler,返回一個 future。
  2. 執行 doBind0 方法,完成對端口的綁定。

看著很簡單,就兩個步驟。

但實際上,這兩個步驟蜿蜒曲折,各種異步跳轉,請做好準備。

6. initAndRegister 方法

 final ChannelFuture initAndRegister() {
        Channel channel = null;
        try {
            channel = channelFactory.newChannel();//NioServerSocketChannel
            init(channel);
        } 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);
        if (regFuture.cause() != null) {
            if (channel.isRegistered()) {
                channel.close();
            } else {
                channel.unsafe().closeForcibly();
            }
        }
        return regFuture;
    }

同樣的,先進行拆解:

  1. 通過 ServerBootstrap 的通道工廠反射創建一個 NioServerSocketChannel。
  2. init 初始化這個 NioServerSocketChannel。
  3. config().group().register(channel) 通過 ServerBootstrap 的 bossGroup 注冊 NioServerSocketChannel。
  4. 最后,返回這個異步執行的占位符。

4個步驟,調用層次極深,非戰斗人員請注意安全。

第 1 個步驟,反射創建。

channelFactory.newChannel() 方法

反射創建。
傳入一個 NIO 的 provider 實例并調用 newSocket 方法
可以看到這個靜態變量且是 final 的
熟悉的 NIO,創建了一個 ServerSocketChannel 實例
調用父類的構造器,并傳入 ACCEPT,并創建了一個 config 對象用于展示自己
繼續調用父類
這里也很熟悉了,調用父類,設置非阻塞,設置感興趣的事件,設置 Channel 屬性,也就是 JDK 的 ServerSocketChannelImpl ,Netty 的 NioServerSocketChannel 代理了 JDK 的 Socket。
這里繼續給父類賦值, 通過 debug 看到,parent 是 null, id 是通過算法生成唯一ID, 并創建了一個 unsafe和 pipeline
一個工廠
創建 channelID
回到創建 unsafe 的過程,NIO 的 message 的操作類,是 Netty 的核心組件
回到創建 pipeline 的方法,一個默認的 pipeline ,參數是 this ,即 NioServerSocketChannel
DefaultChannelPipeline 的構造方法,是一個雙向鏈表,并將 NioServerSocketChannel 設置為自己的屬性

回到 NioServerSocketChannel 的構造方法,還有 config 的構造方法沒看。

這里返回的是我們各個設置的 JDK 的 ServerSocketChannel
繼續調用父類,但是,限于篇幅,我們不再深入,我們只需知道,這個 config 對象用于配置這個 NioServerSocketChannel ,用于外部獲取參數和配置

到此為止,我們看到了整個 NioServerSocketChannel 的構造過程,可謂非常的復雜,盡管我們貼了很多圖,但仍然沒有到底。但這不妨礙我們這次的主體內容。我們總結一下構造過程:

  1. 通過 NIO 的SelectorProvider 的 openServerSocketChannel 方法得到JDK 的 channel。目的是讓 Netty 包裝 JDK 的 channel。同時設置剛興趣的事件為 ACCEPT和非阻塞
  2. 創建了一個唯一的 ChannelId,創建了一個 NioMessageUnsafe,用于操作消息,創建了一個 DefaultChannelPipeline 管道,是個雙向鏈表結構,用于過濾所有的進出的消息。
  3. 創建了一個 NioServerSocketChannelConfig 對象,用于對外展示一些配置。

好,NioServerSocketChannel 對象創建完了, 現在進入到第二個步驟,init 方法,這是個抽象方法,由 ServerBootstrap 自己實現。

init 方法

代碼如下:

 @Override
    void init(Channel channel) throws Exception {
        final Map<ChannelOption<?>, Object> options = options0();
        synchronized (options) {
            setChannelOptions(channel, options, logger);
        }
        final Map<AttributeKey<?>, Object> attrs = attrs0();
        synchronized (attrs) {
            for (Entry<AttributeKey<?>, Object> e: attrs.entrySet()) {
                @SuppressWarnings("unchecked")
                AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
                channel.attr(key).set(e.getValue());
            }
        }
        ChannelPipeline p = ((NioServerSocketChannel)channel).pipeline();//NioServerSocketChannel

        final EventLoopGroup currentChildGroup = childGroup;
        final ChannelHandler currentChildHandler = childHandler;
        final Entry<ChannelOption<?>, Object>[] currentChildOptions;
        final Entry<AttributeKey<?>, Object>[] currentChildAttrs;
        synchronized (childOptions) {
            currentChildOptions = childOptions.entrySet().toArray(newOptionArray(childOptions.size()));
        }
        synchronized (childAttrs) {
            currentChildAttrs = childAttrs.entrySet().toArray(newAttrArray(childAttrs.size()));
        }

        (p).addLast(new ChannelInitializer<Channel>() {
            @Override
            public void initChannel(final Channel ch) throws Exception {
                final ChannelPipeline pipeline = ch.pipeline();
                ChannelHandler handler = config.handler();// ServerBootstrapConfig
                if (handler != null) {
                    pipeline.addLast(handler);
                }
                ch.eventLoop().execute(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.addLast(new ServerBootstrapAcceptor(
                                ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
                    }
                });
            }
        });
    }

同樣的,該方法不長也不短,繼續拆解,不能陷入細節中:

  1. 設置 NioServerSocketChannel 的 TCP 屬性。
  2. 由于 LinkedHashMap 是非線程安全的,使用同步進行處理。
  3. 對 NioServerSocketChannel 的 ChannelPipeline 添加 ChannelInitializer 處理器。

從上面的步驟可以看出, init 的方法的精華在和 ChannelPipeline 相關。

從 NioServerSocketChannel 的初始化過程中,我們知道,pipeline 是一個雙向鏈表,并且,他本身就初始化了 head 和 tail,這里調用了他的 addLast 方法,也就是將整個 handler 插入到 tail 的前面,因為 tail 永遠會在后面,需要做一些系統的固定工作。

我們進入到 addLast 方法內查看:

循環添加

addLast(EventExecutorGroup group, String name, ChannelHandler handler) 方法:

 @Override
    public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
        final AbstractChannelHandlerContext newCtx;
        synchronized (this) {
            checkMultiplicity(handler);
            newCtx = newContext(group, filterName(name, handler), handler);
            addLast0(newCtx);
            if (!registered) {
                newCtx.setAddPending();
                callHandlerCallbackLater(newCtx, true);
                return this;
            }
            EventExecutor executor = newCtx.executor();
            if (!executor.inEventLoop()) {
                newCtx.setAddPending();
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        callHandlerAdded0(newCtx);
                    }
                });
                return this;
            }
        }
        callHandlerAdded0(newCtx);
        return this;
    }

這里就是 pipeline 方法的精髓了,拆解如下:

  1. 檢查該 handler 是否符合標準,如果沒有 Sharable 注解且已經被使用過了,就拋出異常。
  2. 創建一個 AbstractChannelHandlerContext 對象,這里說一下,ChannelHandlerContext 對象是 ChannelHandler 和 ChannelPipeline 之間的關聯,每當有 ChannelHandler 添加到 Pipeline 中時,都會創建 Context。Context 的主要功能是管理他所關聯的 Handler 和同一個 Pipeline 中的其他 Handler 之間的交互。
  3. 然后將 Context 添加到鏈表中。也就是追加到 tail 節點的前面。
  4. 最后,同步或者異步或者晚點異步的調用 callHandlerAdded0 方法,在該方法中,調用之前的 handler 的 handlerAdded 方法,而該方法內部調用了之前的 ChannelInitializer 匿名類的 initChannel 方法,并且參數就是 context 的 channel(通過 pipeline 獲取),也就是 NioServerSocketChannel。這個 Context 的標準實現就是 DefaultChannelHandlerContext。這個 Context 內部會包含一些重要的屬性,比如 pipeline,handler,屬于出站類型還是入站類型等。
最終調用 initChannel 方法
傳入NioServerSocketChannel
獲取 pipeline 的 NioServerSocketChannel

從上面的分析我們可以看出,pipeline 的 addLast 方法,實際上創建一個 Context 對象包裝了 pipeline 和 handler,然后通過同步或者異步的方式,間接執行 handler 的 自定義方法-------initChannel 方法。而這個 context 也加入到了 pipeline 的鏈表節點中。

好了,針對 addLast 方法,我們暫且就分析到這里。

config().group().register(channel) 方法

回到 initAndRegister 方法中,繼續看 config().group().register(channel) 這行代碼,config 方法返回了 ServerBootstrapConfig,這個 ServerBootstrapConfig 調用了 group 方法,實際上就是 bossGroup。bossGroup 調用了 register 方法。

這個 next 方法調用的是 EventExecutorChooser 的 next 方法,我們看看該方法的實現:

注意,這里是著名的 Netty 對性能壓榨的一個例子,Netty 對于選取數組中的線程有著2套策略。

  1. 如果數組是偶數,則使用位運算獲取下一個EventLoop(單例線程池)(效率高)。
  2. 如果是奇數,使用取余(效率低)。

所以,如果是自定義數組長度的話,最好是偶數,默認的就是CPU 核心的2倍,即偶數。

并且,在判斷數組是否是偶數的算法中,也沒有使用取余,而是位運算。如下:

isPowerTwo 方法

回到正題。拿到下一個單例線程池后,調用他的 register 方法:

this 就是 EventLoop(單例線程池),這里多次提到單例線程池,為什么使用單例線程池呢?一個線程還使用線程池有什么意義呢?答:需要任務隊列,有很多任務需要進行調度,所以需要線程池的特性。但為了多線程的切換導致的性能損耗和為了消除同步,所以使用單個線程。

繼續,這里創建了一個 DefaultChannelPromise ,這里需要說一下 Promise 的作用,其實類似 Future,事實上也繼承了 JDK 的 Future,但增加了很多功能,比如 JDK 的 Future 雖然是異步的,但仍需要 get 方法 阻塞獲取結果才能坐之后的事情,而 Promise 可以通過設置監聽器的方式,在方法執行成功或者失敗的情況下無需等待,就能執行監聽器中的任務,效率幣 Future 高很多。從某種程度上說,Future 是非阻塞,而Promise 才是正在的異步。

他的構造方法也很簡單:

    public DefaultChannelPromise(Channel channel, EventExecutor executor) {
        super(executor);
        this.channel = checkNotNull(channel, "channel");
    }
   
    // 父構造
    public DefaultPromise(EventExecutor executor) {
        this.executor = checkNotNull(executor, "executor");
    }

好了,回到 register 方法,有了剛剛創建的 Promise,EventLoop 繼續調用自己的 register 方法:

register

通過調用 promise 的 channel 方法獲取了 NioServerSocketChannel ,然后再調用 NioServerSocketChannel 的 unsafe方法獲取創建NioServerSocketChannel對象時同時創建的 NioMessageUnsafe 對象,最后調用 NioMessageUnsafe 的 register 方法,參數時 promise 和 NioEventLoop。最后返回了這個 promise 方法。

我們可以先思考一下,之所以使用 promise ,register 內部肯定時異步執行了某個方法,讓 promise 立刻返回。執行完畢后再執行設置的監聽器的方法。

我們去看個究竟:

      @Override
        public final void register(EventLoop eventLoop, final ChannelPromise promise) {
            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() {// 開始真正的異步,boss 線程開始啟動
                        @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);
                }
            }
        }

該方法不長也不短,我們拆解一下:

  1. 先是一系列的判斷。
  2. 判斷當前線程是否是給定的 eventLoop 線程。注意:這點很重要,Netty 線程模型的高性能取決于對于當前執行的Thread 的身份的確定。如果不在當前線程,那么就需要很多同步措施(比如加鎖),上下文切換等耗費性能的操作。
  3. 異步(因為我們這里直到現在還是 main 線程在執行,不屬于當前線程)的執行 register0 方法。

實際上,聰明如你,一定知道 register0(promise) 才是最重要的方法。我們來看看該方法邏輯:

private void register0(ChannelPromise promise) {
            try {
                if (!promise.setUncancellable() || !ensureOpen(promise)) {
                    return;
                }
                boolean firstRegistration = neverRegistered;
                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) {
                closeForcibly();
                closeFuture.setClosed();
                safeSetFailure(promise, t);
            }
        }

繼續拆解:

  1. 首先狀態判斷。
  2. 執行 doRegister 方法。
  3. 執行 pipeline.invokeHandlerAddedIfNeeded() 方法。
  4. 執行 pipeline.fireChannelRegistered() 方法。

可以看到 doRegister 應該就是真正的執行方法,而后面的就是管道開始調用 handller 的一些注冊成功之后的回調方法。先看doRegister 方法:


    @Override
    protected void doRegister() throws Exception {
        boolean selected = false;
        for (;;) {
            try {
                selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
                return;
            } catch (CancelledKeyException e) {
                if (!selected) {
                    // Force the Selector to select now as the "canceled" SelectionKey may still be
                    // cached and not removed because no Select.select(..) operation was called yet.
                    eventLoop().selectNow();
                    selected = true;
                } else {
                    // We forced a select operation on the selector before but the SelectionKey is still cached
                    // for whatever reason. JDK bug ?
                    throw e;
                }
            }
        }
    }

是不是很熟悉?該方法在一個死循環中向 JDK 中注冊感興趣的事件。如果成功,則直接結束,如果失敗,則 調用 EventLoop 內部的 JDK 的 select 的 selectNow 方法立即返回,然后嘗試第二次注冊,如果還是報錯,則拋出異常。注意,這里同時還把自己(NioServerSocketChannel)作為attach 綁定了該 selectKey 上。大家可能奇怪,為什么注冊的是0,而不是16 Accpet 事件呢?樓主也不知道。但是最后還是會刪除這個讀事件,重新注冊 accpet 事件的。netty 不知道是怎么想的。

好了,回到 register0 方法,還有2個步驟,分別是執行 pipeline 的 invokeHandlerAddedIfNeeded 方法和 fireChannelRegistered 方法,同時設置 promise 為成功,這個時候,promise就會執行監聽器的方法。

invokeHandlerAddedIfNeeded 方法是做什么的呢?還記得我們之前在 pipeline 的 addLast 方法中,添加了一個 handler 嗎?我們說該方法可能會晚點執行,因為這個方法被包裝成了 task,這里就會執行該方法。這個方法的意思就是,如果管道中有需要執行的任務,就去執行。我們回憶一下那個方法:

addLast 方法,added 屬性為true

callHandlerCallbackLater 方法會根據 added 屬性包裝成一個 task(add 任務或 removed 任務),成為任務鏈表上的一個節點。

而 add 任務和 removed 任務的不同在于,add 任務是pipeline 初始化之后調用的任務(通過 Channel 的handlerAdded 方法),removed 是pipeline 結束后執行(通過 Handler 的 handlerRemoved 方法)。

回到 register0 方法,在執行完 invokeHandlerAddedIfNeeded 方法后,也就是我們剛開始的init 方法里的 ChannelInitializer 匿名類的 initChannel 方法。

safeSetSuccess(promise) 方法就是通知 promise 已經成功了,你可以執行監聽器的方法了,而這里的監聽器則是我們的 dobind 方法中設置的:

doBind方法

至于內部執行,我們稍后再說,先回到我們的 register0 方法中,在 safeSetSuccess 方法執行后,執行 pipeline.fireChannelRegistered() 方法。看名字是執行 handler 的注冊成功之后的回調方法。我們跟進去看看:

Pipeline 中的 靜態方法,并傳入了 head Context
獲取 head 的 執行器EventLoop,用于判斷是否在當前線程,如果在當前線程,則立即執行 invokeChannelRegistered 方法,否則異步執行,我們這里當然是在當前線程。所以同步執行
在這里執行 Context 對應的 handler 的 channelRegistered 方法,
和之前一樣,會檢查通道中是否有需要延遲執行的任務,如果有,就執行,然后調用 Context 的 fireChannelRegistered 方法,而不是 pipeline 的 fireChannelRegistered 方法
該方法調用的是 head Context 的 invokeChannelRegistered 靜態方法,注意,這里的參數很重要,我們進入 findContextInbound 方法內部查看
注意,我們說 pipeline 是一個雙向鏈表,這里鏈表起作用了,通過找到當前節點的下一個節點,并返回,但這判斷的是:必須是入站類型的

回到 fireChannelRegistered 方法,看看 invokeChannelRegistered 是如何調用的?

注意到了嗎,next 節點就是我設置的 LoggingHandler 對應的 Context,獲取對應的 EventLoop。從這里我們總結一下 netty 的 Handler 設計:Netty 初始了一個 pipeline,pipeline 內部維護著一個 ChannelContextContext 雙向鏈表,Context 是對 Handler 的封裝,是 pipeline 和 Handler 溝通的關鍵,每次信息入站,從 head 節點開始,執行 context 的 handler 的對應方法,執行結束通過 findContextInbound() 方法找到下一個節點,繼續執行

tail 節點的 channelRegistered 什么都不做。

好,終于可以回到我們 ServerBootStrap 的 initAndRegister 方法中了。

還沒有完。我們上面對于第三個步驟 對 NioServerSocketChannel 的 ChannelPipeline 添加 ChannelInitializer 處理器。 只是一筆帶過。也就是說,我們從NioServerSocketChannel 的pipeline 的的addLast 方法中一直分析到現在。我們再回頭看看該方法:

ServerBootStrap init 方法

我們分析了 addLast 方法,但下面還有一個回調方法,什么呢?想 NioServerSocketChannel 的 EventLoop 提交了一個任務,也就是 pipeline 的 addLast 方法。是一個 ServerBootstrapAcceptor 對象,而這個 ServerBootstrapAcceptor 也是一個 handler,你可以想到了吧,從該 handler 名字就可以看出來,該 handler 是用于處理 accept 事件的。我們看看他的構造方法:

上面沒有上面好說的,下面有一個 task,任務內容是 設置 該 channel 的autoread 屬性為 true,這里我們記一下。還要注意一點,有一個 childHandler 屬性,是什么呢?就是我們 main 方法中的 ChannelInitializer 匿名內部類,聰明的你應該想到了,既然該 handler 是接受 accept 事件的,那么,肯定需要初始化管道等操作,不然我們怎么在管道中操作我們的邏輯呢?所以就需要這個 ChannelInitializer 通道初始化對象了。

好了,到這里,我們的 initAndRegister 方法終于算是結束了。

回到我們的 doBind 方法。

7. 回到 doBind 方法

doBind 方法

終于回來了,繼續分析,我們上面說完了 dobind 方法有2個重要的步驟,initAndRegister 說完了,接下來看 doBind0 方法,該方法的參數為 initAndRegister 的 future,NioServerSocketChannel,端口地址,NioServerSocketChannel 的 promise。我們進入看看:

    private static void doBind0(
            final ChannelFuture regFuture, final Channel channel,
            final SocketAddress localAddress, final ChannelPromise promise) {
        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());
                }
            }
        });
    }

該方法想 NioServerSocketChannel 的 eventLoop 提交了一個任務,當 future(其實就是 promise) 成功后執行
NioServerSocketChannel 的 bind 方法,并添加一個關閉監聽器。我們主要關注 bind 方法。

層層調用來到了 NioServerSocketChannel 的 pipeline 的 tail 節點的 bind 方法,該方法首先找到出站節點,然后執行出站節點的 invokeBind 方法。

尋找 tail 節點的上一個節點,且必須是出站類型的,根據我們的設置,tail 的上一個節點應該是 LoggingHandler 因為他既是是出站類型也是入站類型
根據 UML 可知 LoggingHandler 類型

接下來,將調用 LoggingHandler 的 invokeBind 方法。

context 調用 handler 的 bind 方法
當然就是打印日志而已
繼續循環,ctx 的 bind 方法就是先尋找下一個節點或者下一個節點,然后調用節點的 invokeBind 方法,然后調用 handler 的 bind 方法
來到了head 節點的bind方法,這里調用了 unsafe 的 bind方法
這里的 unsafe 來自 NioServerSocketChannel 的 unsafe

我們看看 unsafe 的 bind 方法:


       @Override
        public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
            assertEventLoop();
            if (!promise.setUncancellable() || !ensureOpen(promise)) {
                return;
            }
            if (Boolean.TRUE.equals(config().getOption(ChannelOption.SO_BROADCAST)) &&
                localAddress instanceof InetSocketAddress &&
                !((InetSocketAddress) localAddress).getAddress().isAnyLocalAddress() &&
                !PlatformDependent.isWindows() && !PlatformDependent.maybeSuperUser()) {
  
                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 {
                doBind(localAddress);
            } catch (Throwable t) {
                safeSetFailure(promise, t);
                closeIfClosed();
                return;
            }
            if (!wasActive && isActive()) {
                invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        pipeline.fireChannelActive();
                    }
                });
            }

            safeSetSuccess(promise);
        }

可以看到,這里最終的方法就是 doBind 方法,執行成功后,執行通道的 fireChannelActive 方法,告訴所有的 handler,已經成功綁定。那我們就進入 doBind 方法查看:

很熟悉,獲取 JDK 的channel 進行綁定。

回到 bind 方法,然后調用 invokeLater 方法,代碼如下:

        private void invokeLater(Runnable task) {
            try {
                eventLoop().execute(task);
            } catch (RejectedExecutionException e) {
                logger.warn("Can't invoke task later as EventLoop rejected it", e);
            }
        }

將這個任務提交。而這個 fireChannelActive 和之前 pipeline 的所有方法都類似,遍歷所有節點,執行 ChannelActive 方法。

回到 bind 方法,最后一步:safeSetSuccess(promise),告訴 promise 任務成功了。其可以執行監聽器的方法了。雖然這個 promise 沒有任何監聽方法。

如果到這里,樓主告訴你,整個啟動過程已經結束了,你肯定和詫異,什么?服務器不應該是監聽 Accept 事件嗎,我們分析了這么多,只發現在 doRegister 方法中注冊了 0 (read) 事件,竟然沒有監聽 Accept 事件,和我們平時寫的 Nio 代碼不同啊?

是的,如果你想到了這里,說明你思考了。

一切就在上面的 fireChannelActive 方法中。該方法回先調用 head 節點的 channelActive 方法,而 head 節點的 channelActive 代碼如下:

readIfIsAutoRead 默認返回 true。然后像之前的 pipeline 一樣,繼續在鏈表中調用。最后,來到了一個關鍵的地方:

Head 節點 的read 方法

調用的是 NioServerSocketChannel 的 unsafe 的 beginRead 方法。繼續查看:



最后來到了這里

我們看看 NioServerSocketChannel 的 doBeginRead 方法。
拿到 selectionKey ,如果 key 的監聽事件是0 的話,就改為 readInterestOp ,也就是我們初始化NioServerSocketChannel 時設置的值:

好了,到這里,整個服務器就完整的啟動了,可謂艱難。但整體而言,Netty 的結構設計還是很緊湊的。雖然調用層次很深,但這是所有源碼的特點。

8. 總結啟動過程

好了,從源碼層面已經分析完了,我們來總結一下啟動的過程。

  1. 首先創建2個 EventLoopGroup 線程池數組。數組默認大小CPU*2,方便chooser選擇線程池時提高性能。
  2. BootStrap 將 boss 設置為 group屬性,將 worker 設置為 childer 屬性。
  3. 通過 bind 方法啟動,內部重要方法為 initAndRegister 和 dobind 方法。
  4. initAndRegister 方法會反射創建 NioServerSocketChannel 及其相關的 NIO 的對象, pipeline , unsafe,同時也為 pipeline 初始了 head 節點和 tail 節點。同時也含有 NioServerSocketChannelConfig 對象。然后向 pipeline 添加自定義的處理器和 ServerBootstrapAcceptor 處理器。這個處理器用于分配接受的 請求給 worker 線程池。每次添加處理器都會創建一個相對應的 Context 作為 pipeline 的節點并包裝 handler 對象。注冊過程中會調用 NioServerSocketChannel 的 doRegister 方法注冊讀事件。
  5. 在register0 方法成功以后調用在 dobind 方法中調用 doBind0 方法,該方法會 調用 NioServerSocketChannel 的 doBind 方法對 JDK 的 channel 和端口進行綁定,之后在調用 pipeline 的fireChannelActive 最后會調用 NioServerSocketChannel 的 doBeginRead 方法,將感興趣的事件設置為Accept,完成 Netty 服務器的所有啟動,并開始監聽連接事件。

好了,時間不早了,good luck! 請期待后續關于 Netty 源碼分析的文章!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容