【Netty之旅四】你一定看得懂的Netty客戶端啟動原始碼分析!
阿新 • • 發佈:2020-09-22
## 前言
前面小飛已經講解了`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 extends C> channelClass) {
if (channelClass == null) {
throw new NullPointerException("channelClass");
}
return channelFactory(new ReflectiveChannelFactory(channelClass));
}
public ReflectiveChannelFactory(Class extends T> clazz) {
if (clazz == null) {
throw new NullPointerException("clazz");
}
this.clazz = clazz;
}
public B channelFactory(ChannelFactory extends C> 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=