1. 程式人生 > >【Netty之旅四】你一定看得懂的Netty客戶端啟動原始碼分析!

【Netty之旅四】你一定看得懂的Netty客戶端啟動原始碼分析!

## 前言 前面小飛已經講解了`NIO`和`Netty`服務端啟動,這一講是`Client`的啟動過程。 原始碼系列的文章依舊還是遵循大白話+畫圖的風格來講解,本文`Netty`原始碼及以後的文章版本都基於:**4.1.22.Final** 本篇是以`NettyClient`啟動為切入點,帶大家一步步進入`Netty`原始碼的世界。 ## Client啟動流程揭祕 ### 1、探祕的入口:netty-client demo 這裡用`netty-exmaple`中的`EchoClient`來作為例子: ```java public final class EchoClient { public static void main(String[] args) throws Exception { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer() { @Override public void initChannel(SocketChannel ch) throws Exception { ChannelPipeline p = ch.pipeline(); p.addLast(new EchoClientHandler()); } }); ChannelFuture f = b.connect(HOST, PORT).sync(); f.channel().closeFuture().sync(); } finally { group.shutdownGracefully(); } } } ``` 程式碼沒有什麼獨特的地方,我們上一篇文章時也梳理過`Netty`網路程式設計的一些套路,這裡就不再贅述了。 (忘記的小朋友可以檢視`Netty`系列文章中查詢~) 上面的客戶端程式碼雖然簡單, 但是卻展示了`Netty` 客戶端初始化時所需的所有內容: - `EventLoopGroup`:`Netty`服務端或者客戶端,都必須指定`EventLoopGroup`,客戶端指定的是`NioEventLoopGroup` - `Bootstrap`: `Netty`客戶端啟動類,負責客戶端的啟動和初始化過程 - `channel()`型別:指定`Channel`的型別,因為這裡是客戶端,所以使用的是`NioSocketChannel`,服務端會使用`NioServerSocketChannel` - `Handler`:設定資料的處理器 - `bootstrap.connect()`: 客戶端連線`netty`服務的方法 ### 2、NioEventLoopGroup 流程解析 我們先從`NioEventLoopGroup`開始,一行行程式碼解析,先看看其類結構: ![NioEventLoopGroup類結構.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/41ba8dd2fbba475a87a92ccb030574fc~tplv-k3u1fbpfcp-zoom-1.image) 上面是大致的類結構,而 `EventLoop` 又繼承自`EventLoopGroup`,所以類的大致結構我們可想而知。這裡一些核心邏輯會在`MultithreadEventExecutorGroup`中,包含`EventLoopGroup`的建立和初始化操作等。 接著從`NioEventLoopGroup`構造方法開始看起,一步步往下跟(**程式碼都只展示重點的部分,省去很多暫時不需要關心的程式碼,以下程式碼都遵循這個原則**): ```java EventLoopGroup group = new NioEventLoopGroup(); public NioEventLoopGroup() { this(0); } public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider) { this(nThreads, executor, selectorProvider, DefaultSelectStrategyFactory.INSTANCE); } protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) { super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args); } ``` 這裡通過呼叫`this()`和`super()`方法一路往下傳遞,期間會構造一些預設屬性,一直傳遞到`MultithreadEventExecutorGroup`類中,接著往西看。 ### 2.1、MultithreadEventExecutorGroup 上面建構函式有一個重要的引數傳遞:`DEFAULT_EVENT_LOOP_THREADS`,這個值預設是`CPU核數 * 2`。 為什麼要傳遞這個引數呢?我們之前說過`EventLoopGroup`可以理解成一個執行緒池,`MultithreadEventExecutorGroup`有一個執行緒陣列`EventExecutor[] children`屬性,而傳遞過來的`DEFAULT_EVENT_LOOP_THREADS`就是陣列的長度。 先看下`MultithreadEventExecutorGroup`中的構造方法: ```java protected MultithreadEventExecutorGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, Object... args) { if (executor == null) { executor = new ThreadPerTaskExecutor(newDefaultThreadFactory()); } children = new EventExecutor[nThreads]; for (int i = 0; i < nThreads; i ++) { children[i] = newChild(executor, args); } // ... 省略 } ``` 這段程式碼執行邏輯可以理解為: - 通過`ThreadPerTaskExecutor`構造一個`Executor`執行器,後面會細說,裡面包含了執行緒執行的`execute()`方法 - 接著建立一個`EventExecutor`陣列物件,大小為傳遞進來的`threads`數量,這個所謂的`EventExecutor`可以理解為我們的`EventLoop`,在這個demo中就是`NioEventLoop`物件 - 最後呼叫 `newChild` 方法逐個初始化`EventLoopGroup`中的`EventLoop`物件 上面只是大概說了下`MultithreadEventExecutorGroup`中的構造方法做的事情,後面還會一個個詳細展開,先不用著急,我們先有個整體的認知就好。 再回到`MultithreadEventExecutorGroup`中的構造方法入參中,有個`EventExecutorChooserFactory`物件,這裡面是有個很亮眼的細節設計,通過它我們來洞悉`Netty`的良苦用心。 ### 2.1、亮點設計:DefaultEventExecutorChooserFactory ![EventExecutorChooserFactory.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/55288098e75648de9cb7672031c1bebb~tplv-k3u1fbpfcp-zoom-1.image) `EventExecutorChooserFactory`這個類的作用是用來選擇`EventLoop`執行器的,我們知道`EventLoopGroup`是一個包含了`CPU * 2`個數量的`EventLoop`陣列物件,那每次選擇`EventLoop`來執行任務是選擇陣列中的哪一個呢? 我們看一下這個類的具體實現,`紅框中`都是需要重點檢視的地方: ![輪詢演算法實現器.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d5dc7952bb5747bd82873561dc952ae9~tplv-k3u1fbpfcp-zoom-1.image) `DefaultEventExecutorChooserFactory`是一個選擇器工廠類,呼叫裡面的`next()`方法達到一個輪詢選擇的目的。 >
陣列的長度是length,執行第n次,取陣列中的哪個元素就是對length取餘 ![w9llHf.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/573574ed88024179b5436094e74a280e~tplv-k3u1fbpfcp-zoom-1.image) 繼續回到程式碼的實現,這裡的優化就是在於先通過`isPowerOfTwo()`方法判斷陣列的長度是否為2的n次冪,判斷的方式很巧妙,使用`val & -val == val`,這裡我不做過多的解釋,網上還有很多判斷2的n次冪的優秀解法,我就不班門弄斧了。(**可參考:https://leetcode-cn.com/problems/power-of-two/solution/2de-mi-by-leetcode/**) 當然我認為這裡還有更容易理解的一個演算法:`x & (x - 1) == 0` 大家可以看下面的圖就懂了,這裡就不延展了: ![2的冪次方演算法.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/919d850174ba47fabe7ebcb9f09b1029~tplv-k3u1fbpfcp-zoom-1.image) **BUT!!! 這裡為什麼要去煞費苦心的判斷陣列的長度是2的n次冪?** 不知道小夥伴們是否還記得**大明湖畔**的`HashMap`?一般我們要求`HashMap`陣列的長度需要是2的n次冪,因為在`key`值尋找陣列位置的方法:`(n - 1) & hash` n是陣列長度,這裡如果陣列長度是2的n次冪就可以通過位運算來提升效能,當`length`為2的n次冪時下面公式是等價的: >
n & (length - 1) <=> n % length 還記得上面說過,陣列的長度預設都是`CPU * 2`,而一般伺服器CPU核心數都是2、4、8、16等等,所以這一個小優化就很實用了,再仔細想想,原來陣列長度的初始化也是很講究的。 這裡位運算的好處就是效率遠遠高於與運算,`Netty`針對於這個小細節都做了優化,真是太棒了。 ### 2.3、執行緒執行器:ThreadPerTaskExecutor 接著看下`ThreadPerTaskExecutor`執行緒執行器,每次執行任務都會通過它來建立一個執行緒實體。 ```java public final class ThreadPerTaskExecutor implements Executor { private final ThreadFactory threadFactory; public ThreadPerTaskExecutor(ThreadFactory threadFactory) { if (threadFactory == null) { throw new NullPointerException("threadFactory"); } this.threadFactory = threadFactory; } @Override public void execute(Runnable command) { threadFactory.newThread(command).start(); } } ``` 傳遞進來的`threadFactory`為`DefaultThreadFactory`,這裡面會構造`NioEventLoop`執行緒命名規則為`nioEventLoop-1-xxx`,我們就不細看這個了。當執行緒執行的時候會呼叫`execute()`方法,這裡會建立一個`FastThreadLocalThread`執行緒,具體看程式碼: ```java public class DefaultThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { Thread t = newThread(FastThreadLocalRunnable.wrap(r), prefix + nextId.incrementAndGet()); return t; } protected Thread newThread(Runnable r, String name) { return new FastThreadLocalThread(threadGroup, r, name); } } ``` 這裡通過`newThread()`來建立一個執行緒,然後初始化執行緒物件資料,最終會呼叫到`Thread.init()`中。 ### 2.4、EventLoop初始化 接著繼續看`MultithreadEventExecutorGroup`構造方法: ```java protected MultithreadEventExecutorGroup(int nThreads, Executor executor, EventExecutorChooserFactory chooserFactory, Object... args) { children = new EventExecutor[nThreads]; for (int i = 0; i < nThreads; i ++) { children[i] = newChild(executor, args); // .... 省略部分程式碼 } } ``` 上面程式碼的最後一部分是 `newChild` 方法, 這個是一個抽象方法, 它的任務是例項化 `EventLoop` 物件. 我們跟蹤一下它的程式碼, 可以發現, 這個方法在 `NioEventLoopGroup` 類中實現了, 其內容很簡單: ```java @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]); } NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider, SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) { super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler); if (selectorProvider == null) { throw new NullPointerException("selectorProvider"); } if (strategy == null) { throw new NullPointerException("selectStrategy"); } provider = selectorProvider; final SelectorTuple selectorTuple = openSelector(); selector = selectorTuple.selector; unwrappedSelector = selectorTuple.unwrappedSelector; selectStrategy = strategy; } ``` 其實就是例項化一個 `NioEventLoop` 物件, 然後返回。`NioEventLoop`建構函式中會儲存`provider`和事件輪詢器`selector`,在其父類中還會建立一個`MpscQueue佇列`,然後儲存執行緒執行器`executor`。 再回過頭來想一想,`MultithreadEventExecutorGroup` 內部維護了一個 `EventExecutor[] children`陣列, `Netty` 的 `EventLoopGroup` 的實現機制其實就建立在 `MultithreadEventExecutorGroup` 之上。 每當 `Netty` 需要一個 `EventLoop` 時, 會呼叫 `next()` 方法從`EventLoopGroup`陣列中獲取一個可用的 `EventLoop`物件。其中`next`方法的實現是通過`NioEventLoopGroup.next()`來完成的,就是用的上面有過講解的通過輪詢演算法來計算得出的。 最後總結一下整個 `EventLoopGroup` 的初始化過程: ![EventLoopGroup構造流程.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4e8529ce9fa74bcfb5ec973d0eafc7eb~tplv-k3u1fbpfcp-zoom-1.image) - `EventLoopGroup`(其實是`MultithreadEventExecutorGroup`) 內部維護一個型別為 `EventExecutor children` 陣列,陣列長度是`nThreads` - 如果我們在例項化 `NioEventLoopGroup` 時, 如果指定執行緒池大小, 則 `nThreads` 就是指定的值, 反之是`處理器核心數 * 2` - `MultithreadEventExecutorGroup` 中會呼叫 `newChild` 抽象方法來初始化 `children` 陣列 - 抽象方法 `newChild` 是在 `NioEventLoopGroup` 中實現的, 它返回一個 `NioEventLoop` 例項. - `NioEventLoop` 屬性: - `SelectorProvider provider` 屬性: `NioEventLoopGroup` 構造器中通過 `SelectorProvider.provider()` 獲取一個 `SelectorProvider` - `Selector selector` 屬性: `NioEventLoop` 構造器中通過呼叫通過 `selector = provider.openSelector()` 獲取一個 `selector` 物件. ### 2.5、NioSocketChannel 在`Netty`中,`Channel`是對`Socket`的抽象,每當`Netty`建立一個連線後,都會有一個與其對應的`Channel`例項。 我們在開頭的`Demo`中,設定了`channel(NioSocketChannel.class)`,`NioSocketChannel`的類結構如下: ![NioSocketChannel類結構.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e5605eeca94e45e1b2207085d5edeb88~tplv-k3u1fbpfcp-zoom-1.image) 接著分析程式碼,當我們呼叫`b.channel()`時實際上會進入`AbstractBootstrap.channel()`邏輯,接著看`AbstractBootstrap`中程式碼: ```java public B channel(Class channelClass) { if (channelClass == null) { throw new NullPointerException("channelClass"); } return channelFactory(new ReflectiveChannelFactory(channelClass)); } public ReflectiveChannelFactory(Class clazz) { if (clazz == null) { throw new NullPointerException("clazz"); } this.clazz = clazz; } public B channelFactory(ChannelFactory channelFactory) { if (channelFactory == null) { throw new NullPointerException("channelFactory"); } if (this.channelFactory != null) { throw new IllegalStateException("channelFactory set already"); } this.channelFactory = channelFactory; return self(); } ``` 可以看到,這裡`ReflectiveChannelFactory`其實就是返回我們指定的`channelClass:NioSocketChannel`, 然後指定`AbstractBootstrap`中的`channelFactory = new ReflectiveChannelFactory()`。 ### 2.6、Channel初始化流程 到了這一步,我們已經知道`NioEventLoopGroup`和`channel()`的流程,接著來看看`Channel`的 初始化流程,這也是`Netty`客戶端啟動的的核心流程之一: ```java ChannelFuture f = b.connect(HOST, PORT).sync(); ``` 接著就開始從`b.connect()`為入口一步步往後跟,先看下`NioSocketChannel`構造的整體流程: ![NioSocketChannel構造流程.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5d42568d9e7245868e3151e02ffdeb39~tplv-k3u1fbpfcp-zoom-1.image) 從`connet`往後梳理下整體流程: > Bootstrap.connect -> Bootstrap.doResolveAndConnect -> AbstractBootstrap.initAndRegister ```java final ChannelFuture initAndRegister() { Channel channel = channelFactory.newChannel(); init(channel); ChannelFuture regFuture = config().group().register(channel); return regFuture; } ``` 為了更易讀,這裡程式碼都做了簡化,只保留了一些重要的程式碼。 緊接著我們看看`channelFactory.newChannel()`做了什麼,這裡`channelFactory`是`ReflectiveChannelFactory`,我們在上面的章節分析過: ```java @Override public T newChannel() { try { return clazz.getConstructor().newInstance(); } catch (Throwable t) { throw new ChannelException("Unable to create Channel from class " + clazz, t); } } ``` 這裡的`clazz`是`NioSocketChannel`,同樣是在上面章節講到過,這裡是呼叫`NioSocketChannel`的建構函式然後初始化一個`Channel`例項。 ```java public class NioSocketChannel extends AbstractNioByteChannel implements io.netty.channel.socket.SocketChannel { public NioSocketChannel() { this(DEFAULT_SELECTOR_PROVIDER); } public NioSocketChannel(SelectorProvider provider) { this(newSocket(provider)); } private static SocketChannel newSocket(SelectorProvider provider) { try { return provider.openSocketChannel(); } catch (IOException e) { throw new ChannelException("Failed to open a socket.", e); } } } ``` 這裡其實也很簡單,就是建立一個`Java NIO SocketChannel`而已,接著看看`NioSocketChannel`的父類還做了哪些事情,這裡梳理下類的關係: > NioSocketChannel -> extends AbstractNioByteChannel -> exntends AbstractNioChannel ```java public abstract class AbstractNioChannel extends AbstractChannel { protected AbstractNioByteChannel(Channel parent, SelectableChannel ch) { super(parent, ch, SelectionKey.OP_READ); } protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) { super(parent); ch.configureBlocking(false); } } ``` 這裡會呼叫父類的構造引數,並且傳遞`readInterestOp = SelectionKey.OP_READ:`,這裡還有一個很重要的點,配置 `Java NIO SocketChannel` 為非阻塞的,我們之前在`NIO`章節的時候講解過,這裡也不再贅述。 接著繼續看`AbstractChannel`的建構函式: ```java public abstract class AbstractChannel extends DefaultAttributeMap implements Channel { protected AbstractChannel(Channel parent) { this.parent = parent; id = newId(); unsafe = newUnsafe(); pipeline = newChannelPipeline(); } } ``` 這裡建立一個`ChannelId`,建立一個`Unsafe`物件,這裡的`Unsafe`並不是Java中的Unsafe,後面也會講到。然後建立一個`ChannelPipeline`,後面也會講到,到了這裡,一個完整的`NioSocketChannel` 就初始化完成了,我們再來總結一下: - `Netty` 的 `SocketChannel` 會與 `Java` 原生的 `SocketChannel` 繫結在一起; - 會註冊 `Read` 事件; - 會為每一個 `Channel` 分配一個 `channelId`; - 會為每一個 `Channel` 建立一個`Unsafe`物件; - 會為每一個 `Channel` 分配一個 `ChannelPipeline`; ### 2.7、Channel 註冊流程 還是回到最上面`initAndRegister`方法,我們上面都是在分析裡面`newChannel`的操作,這個方法是`NioSocketChannel`建立的一個流程,接著我們在繼續跟`init()`和`register()`的過程: ```java public abstract class AbstractBootstrap, C extends Channel> implements Cloneable { final ChannelFuture initAndRegister() { Channel channel = channelFactory.newChannel(); init(channel); ChannelFuture regFuture = config().group().register(channel); } } ``` `init()`就是將一些引數`options`和`attrs`設定到`channel`中,我們重點需要看的是`register`方法,其呼叫鏈為: > AbstractBootstrap.initAndRegister -> MultithreadEventLoopGroup.register -> SingleThreadEventLoop.register -> AbstractUnsafe.register 這裡最後到了`unsafe`的`register()`方法,最終呼叫到`AbstractNioChannel.doRegister()`: ```java @Override protected void doRegister() throws Exception { boolean selected = false; for (;;) { selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this); return; } } ``` `javaChannel()`就是`Java NIO`中的`SocketChannel`,這裡是將`SocketChannel`註冊到與`eventLoop`相關聯的`selector`上。 ![Channel註冊流程.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c123c0afdaad4a039138b02cc82ca17d~tplv-k3u1fbpfcp-zoom-1.image) 最後我們整理一下服務啟動的整體流程: 1. `initAndRegister()`初始化並註冊什麼呢? - `channelFactory.newChannel()` - 通過反射建立一個 `NioSocketChannel` - 將 `Java` 原生 `Channel` 繫結到 `NettyChannel` 中 - 註冊 `Read` 事件 - 為 `Channel` 分配 `id` - 為 `Channel` 建立 `unsafe`物件 - 為 `Channel` 建立` ChannelPipeline`(預設是 `head<=>tail` 的雙向連結串列) 2. `init(channel)`` - 把 `Bootstrap` 中的配置設定到 `Channel` 中 3. `register(channel)` - 把 `Channel` 繫結到一個 `EventLoop` 上 - 把 `Java` 原生 `Channel、Netty` 的 `Channel、Selector` 繫結到 `SelectionKey` 中 - 觸發 `Register` 相關的事件 ### 2.8 unsafe初始化 上面有提到過在初始化`Channel`的過程中會建立一個`Unsafe`的物件,然後繫結到`Channel`上: ```java protected AbstractChannel(Channel parent) { this.parent = parent; id = newId(); unsafe = newUnsafe(); pipeline = newChannelPipeline(); } ``` `newUnsafe`直接呼叫到了`NioSocketChannel`中的方法: ```java @Override protected AbstractNioUnsafe newUnsafe() { return new NioSocketChannelUnsafe(); } ``` `NioSocketChannelUnsafe`是`NioSocketChannel`中的一個內部類,然後向上還有幾個父類繼承,這裡主要是對應到相關`Java`底層的`Socket`操作。 ### 2.9 pipeline初始化 我們還是回到`pipeline`初始化的過程,來看一下`newChannelPipeline()`的具體實現: ```java protected DefaultChannelPipeline newChannelPipeline() { return new DefaultChannelPipeline(this); } protected DefaultChannelPipeline(Channel channel) { this.channel = ObjectUtil.checkNotNull(channel, "channel"); succeededFuture = new SucceededChannelFuture(channel, null); voidPromise = new VoidChannelPromise(channel, true); tail = new TailContext(this); head = new HeadContext(this); head.next = tail; tail.prev = head; } ``` 我們呼叫 `DefaultChannelPipeline` 的構造器, 傳入了一個 `channel`, 而這個 `channel` 其實就是我們例項化的 `NioSocketChannel`。 `DefaultChannelPipeline` 會將這個 `NioSocketChannel` 物件儲存在`channel` 欄位中. `DefaultChannelPipeline` 中, 還有兩個特殊的欄位, 即 `head` 和 `tail`, 而這兩個欄位是一個雙向連結串列的頭和尾. 其實在 `DefaultChannelPipeline` 中, 維護了一個以 `AbstractChannelHandlerContext` 為節點的雙向連結串列, 這個連結串列是 `Netty` 實現 `Pipeline` 機制的關鍵. 關於 `DefaultChannelPipeline` 中的雙向連結串列以及它所起的作用, 我們會在後續章節詳細講解。這裡只是對`pipeline`做個初步的認識。 `HeadContext` 的繼承層次結構如下所示: ![HeadContext繼承結構.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9ee9ec5ba77b4a06b8a680cfdb491635~tplv-k3u1fbpfcp-zoom-1.image) `TailContext` 的繼承層次結構如下所示: ![TailContext繼承結構.png](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0f90108e9bb94926895e794c123d5e2b~tplv-k3u1fbpfcp-zoom-1.image) 我們可以看到, 連結串列中 `head` 是一個 `ChannelOutboundHandler`, 而 `tail` 則是一個 `ChannelInboundHandler`. ### 3.0、客戶端connect過程 客戶端連線的入口方法還是在`Bootstrap.connect()`中,上面也分析過一部分內容,請求的具體流程是: > Bootstrap.connect() -> AbstractChannel.coonnect() -> NioSocketChannel.doConnect() ```java public static boolean connect(final SocketChannel socketChannel, final SocketAddress remoteAddress) throws IOException { try { return AccessController.doPrivileged(new PrivilegedExceptionAction() { @Override public Boolean run() throws IOException { return socketChannel.connect(remoteAddress); } }); } catch (PrivilegedActionException e) { throw (IOException) e.getCause(); } } ``` 看到這裡,還是用`Java NIO SocketChannel`傳送的`connect`請求進行客戶端連線請求。 ## 總結 本篇文章以一個`Netty Client demo`為入口,然後解析了`NioEventLoopGroup`建立的流程、`Channel`的建立和註冊的流程,以及客戶端發起`connect`的具體流程,這裡對於很多細節並沒有很深的深入下去,這些會放到後續的原始碼分析文章,敬請期待~ ![原創乾貨分享.png](https://user-gold-cdn.xitu.io/2020/6/13/172ae207fb69c572?w=900&h=383&f=png&s=