一個典型的 NIO 服務端應該有哪些東西來支撐他的服務呢?

ServerSocketChannel

首先要有一個 ServerSocketChannel,就像流操作都要基於 Stream 對象一樣, NIO 中的所有 I/O 操作都基於 Channel 對象。

一個 Channel 代表著和某一實體的連接,這個實體可以是硬體設備、文件或者是網路套接字,通過 Channel 可以讀寫數據。

NIO 中的 ServerSocketChannel 相當於普通 IO 中的 ServerSocket,而客戶端的 SocketChannel 則相當於普通 IO 中的 Socket。

Selector

另外還需要有一個 Selector,用來獲取 Channel 的事件,Channel 有4種事件,分別是:

Accept:有可以接受的連接

Connect:已經連接成功

Read:有數據可以讀取

Write:可以進行數據寫入

如果我們直接從 Channel 中讀取數據時,很可能會有問題,因為這時 Channel 中可能根本就沒有數據可以讀,而強行進行讀取的話,會使線程掛起一直等待著數據的到來,直到有數據到達才被喚醒,那該線程在數據到達這段時間內將不做任何事情。

通過 Selector 來檢查 Channel 的狀態變化,當具體的狀態滿足條件時向外發出通知即可,就跟監聽器一樣,我們將 Channel 註冊到 Selector 上,然後告訴 Selector 我這個 Channel 所感興趣的事件列表,接下來 Selector 就會負責把滿足條件的事件通知到 Channel,這樣的話 Channel 就不需要每次傻傻的來讀取數據了。

那 Channel 怎樣才能知道他感興趣的事件已經發生了呢,當 Channel 把自己感興趣的事件註冊到 Selector 上之後,只需要通過 Selector 提供的 select 方法去查詢就好了。

所以一個典型的 NIO 服務端是這樣的:

// 初始化服務端TCP連接通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 設置為非阻塞模式
serverChannel.configureBlocking(false);
// 創建一個selector
Selector selector = Selector.open();
// 把Channel註冊到selector上
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 綁定埠
serverChannel.bind(new InetSocketAddress(8864));

瞭解了 NIO 是怎麼玩的之後,我們來分析下 Netty 服務端是怎麼啟動的,首先看一個最簡單的 EchoServer 的啟動代碼:

EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 1
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) // 2
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO)) // 3
.childHandler(new ChannelInitializer<SocketChannel>() { // 4
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new EchoServerHandler());
}
});
// Start the server.
ChannelFuture f = b.bind(PORT).sync(); // 5
// 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();
}

整個過程主要有五個部分組成,如下圖所示:

簡單解釋一下每個步驟的作用:

1、初始化兩個 NioEventLoopGroup,這兩個對象可以看做是傳統 IO 編程模型中的線程組,bossGroup 主要進行監聽埠,accept 新連接,並將新連接轉交給 worker 線程去執行,workerGroup 主要是處理客戶端請求的。

2、通過指定一個 Channel 的 Class 來創建一個 Channel 工廠,後續通過該工廠來創建 Channel,實際上這裡就統一了服務端的 IO 模型了,通過統一的 api 就能輕鬆的指定服務端的 IO 模型是 NIO 還是 BIO,只需要指定不同的 Channel 類型即可。

3、添加一個 Server 端專屬的 ChannelHandler。

4、添加一個自定義的用來處理客戶端請求的 ChannelHandler,主要用來進行編解碼、數據讀寫、邏輯處理等。

5、綁定埠並啟動服務端。

下面就對這五個步驟進行詳細的分析,不過上面還有一個很重要的類沒有說到,就是 ServerBootstrap 引導,他主要就是負責把所有的對象聚集在一起,然後把服務啟動起來,所以不進行具體的描述。

初始化 NioEventLoopGroup

NioEventLoopGroup 簡單點理解就是線程組,他會持有一組線程,這裡的線程就是 EventLoop。

整個 NioEventLoopGroup 的初始化的過程如下圖所示,具體的流程可以查看 NioEventLoopGroup 的構造方法的執行過程,這裡不貼具體的代碼了:

在 NioEventLoopGroup 初始化時,依次初始化了三個對象,分別是虛線框中黃色部分對應的對象。

其中有一個對象 SelectorProvider 會在後續使用它來創建 Selector 對象。

在這些對象都初始化好之後,NioEventLoopGroup 會依次調用父類的構造方法,最終調用到父類 MultithreadEventExecutorGroup 中,然後再父類的構造方法中完成剩下的初始化工作,這些工作又可以分為四個部分:

我們來看這四個部分具體的實現。

初始化 Executor

首先是初始化一個 Executor 對象,代碼如下所示:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
// 1.初始化 executor
if (executor == null) {
executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());
}
// 省略部分代碼
}

executor 從 NioEventLoopGroup 中未進行初始化,一直到這裡才進行初始化,這裡默認是初始化的一個 ThreadPerTaskExecutor 對象,從類名中我們可以發現,這個 Executor 在接收到新的任務時,會為每個任務都創建一個線程來執行。

創建 EventExecutor 數組

第二步是創建一個 EventExecutor 數組,如下圖所示:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
// 1.初始化 executor

// 2.創建 EventExecutor 數組
children = new EventExecutor[nThreads];
// 省略部分代碼
}

初始化 EventExecutor 數組

第三步就是對這個數組進行實例化,如下圖所示:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
// 1.初始化 executor

// 2.創建 EventExecutor 數組

// 3.初始化每一個 EventExecutor
for (int i = 0; i < nThreads; i ++) {
boolean success = false;
try {
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) {
// 如果當前對象初始化失敗,則將之前初始化的所有對象都關閉
}
}
}
// 省略部分代碼
}

這裡需要注意的是,初始化 EventExecutor 對象的方法是 newChild,而這個方法的實現是在 NioEventLoopGroup 類中,具體的代碼如下所示:

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 對象,並且該對象持有了 SelectorProvider 和 Executor 以及其他對象

但是我們的 children 數組中需要的 EventExecutor 對象,為什麼這裡 new 出來的是一個 NioEventLoop 對象呢?我們可以看一下 NioEventLoop 類的結構,如下圖所示:

從類的結構圖中可以看到, NioEventLoop 類繼承自 SingleThreadEventLoop,而 SingleThreadEventLoop 類繼承自 SingleThreadEventExecutor 類,並且實現了 EventLoop 介面。然後 SingleThreadEventExecutor 又繼承自 AbstractScheduledEventExecutor 類。

AbstractScheduledEventExecutor 類又繼承自 AbstractEventExecutor,該類直接實現了 EvenExecutor 介面,所以 NioEventLoop 也就是成了一個 EventExecutor。

從圖中還可以看出,EventExecutor 還是繼承自 ScheduleExecutorService 介面,這就說明瞭,EventExecutor 除了有線程池的能力,還具備調度的能力。

所以 NioEventLoop 是一個具有線程池功能的事件循環器,並且還具有調度任務的功能,為什麼要擁有調度的功能呢,因為 Netty 中有很多任務,包括需要定時執行的調度任務,和一次性執行的任務。

但是 NioEventLoop 中負責執行具體任務的線程,是在 SingleThreadEventLoop 中創建的,並且只創建了一個線程,這一點從類名中就可以看出來。

那為什麼只創建一個線程呢,線程池如果只有一個線程的話,那意義不就很小了嗎?其實這正是 Netty 設計的精美之處,通過一個線程來支撐所有的事件循環,從而避免了多線程之間的並發問題,也減少了線程切換所帶來的性能損耗。Netty 通過充分壓榨這一個線程的能力,實現了一種無鎖化的高效的編程模型。

初始化 EventExecutorChooser

當上面的三個步驟都完成之後,最後一個步驟就是初始化一個 EventExecutorChooser,如下所示:

protected MultithreadEventExecutorGroup(int nThreads, Executor executor,
EventExecutorChooserFactory chooserFactory, Object... args) {
// 1.初始化 executor

// 2.創建 EventExecutor 數組

// 3.初始化每一個 EventExecutor

// 4.初始化 EventExecutorChooser
chooser = chooserFactory.newChooser(children);
// 省略部分代碼
}

這步主要是通過一個 chooserFactory 創建了一個 EventExecutorChooser,傳遞進去的參數則是 EventExecutor 數組,而 EventExecutorChooser 的作用就是從 EventExecutor 數組中選擇一個可用的 EventExecutor。

需要注意的是 newChooser 創建的 chooser 對象有兩種類型,這取決於 children 的個數,如果個數是偶數,則選擇 PowerOfTwoEventExecutorChooser,該類型的 chooser 在選擇 EventExecutor 時採用位運算,效率非常高;如果個數是奇數則選擇 GenericEventExecutorChooser,該類型的 chooser 在選擇 EventExecutor 時採用取餘運算,效率較低,這充分體現了 Netty 在性能優化上的考慮。

現在我們來總結一下初始化完 NioEventLoopGroup 之後一共創建了哪些對象:

Executor:創建了一個 Executor 對象,具體實例為:ThreadPerTaskExecutor

EventExecutor 數組:創建了一個大小為 nThread 的 EventExecutor 數組,每個實例都是一個 NioEventLoop

NioEventLoop:一個同時具備 execute 和 schedule 能力的 EventExecutor,並且每一個 NioEventLoop 只有一個 Thread 來支撐其運行

EventExecutorChooser:一個可以獲取一個可用的 EventExecutor 的選擇器

指定 Channel 類型

通過 .channel() 方法指定了一個 Channel 的 Class,該方法會創建一個 ChannelFactory,後續創建新的 Channel 則由該 ChannelFactory 來創建。

具體是如何創建 Channel 的,只需要看下 ChannelFactory 是如何實現的即可,實際的 ChannelFactory 實例,是一個 ReflectiveChannelFactory:

@Override
public T newChannel() {
try {
return clazz.getConstructor().newInstance();
} catch (Throwable t) {
throw new ChannelException("Unable to create Channel from class " + clazz, t);
}
}

可以很清楚的知道,是通過反射獲取到該類的構造方法,然後創建了一個實際。

這裡的 Channel 都是服務端的 Channel,另外該方法除了是創建了一個 Channel 工廠之外,更重要的是他指定了 IO 模型。

我們使用 Netty 來進行編寫網路應用時,一般都是使用它的 NIO 模型,因為這樣纔能夠發揮出他最大的價值。但是如果你想使用 BIO 模型的話,也是支持的,只需要指定 Class 為:OioServerSocketChannel 即可。

添加服務端 ChannelHandler

通過 .handler() 方法可以添加一個服務端的 ChannelHandler,該 ChannelHandler 是用來處理服務端的一些邏輯的,比如紀錄一些日誌等等。

添加客戶端 ChannelHandler

通過 .childHandler() 方法就可以添加一個用來處理 Client 端請求的 ChannelHandler,該 ChannelHandler 就是所有實際業務邏輯處理的核心部分。

包括對客戶端發送過來的數據進行解碼,對數據進行邏輯處理,然後生成響應數據後寫回客戶端。

綁定埠並啟動

當以上的所有準備工作都執行完畢之後,服務端啟動過程中最重要的部分就要開始執行了:那就是綁定埠,也就是執行 ServerBootstrap 的 bind 方法。該方法也可以拆分成兩個獨立的部分:

初始化 Channel

initAndRegister 方法,主要做的就是初始化並註冊 Channel 對象,這個 Channel 的類型就是開始的時候通過 .channel() 方法指定的類型。

首先通過 ChannelFactory 創建一個我們所需類型的 Channel 對象,然後對這個 Channel 進行初始化,具體的代碼如下:

final ChannelFuture initAndRegister() {
Channel channel = null;
try {
// 創建一個 channel
channel = channelFactory.newChannel();
init(channel);
} catch (Throwable t) {
// 省略部分代碼
}
}

創建 Channel

我們先看下創建 Channel 的過程,上面我們已經分析過了 channelFactory.newChannel() 是通過反射創建了一個 Channel 的實例,而該 Channel 的 Class 是我們指定的 NioServerSocketChannel,現在我們需要知道該 Channel 是如何被創建的,整個過程也分為四個部分:

第一步,創建一個 ServerSocketChannel:

private static final SelectorProvider DEFAULT_SELECTOR_PROVIDER = SelectorProvider.provider();

private static ServerSocketChannel newSocket(SelectorProvider provider) {
try {
return provider.openServerSocketChannel();
} catch (IOException e) {
throw new ChannelException(
"Failed to open a server socket.", e);
}
}

這裡就是通過 JDK 底層的 SelectorProvider 創建了一個 ServerSocketChannel,後續都是通過該 Channel 對客戶端請求進行 accept 操作。

PS:在創建 NioEventLoopGroup 時也創建了一個 SelectorProvider,不過該 provider 是為了在某個條件下重新創建一個新的 Selector ,通過 rebuildSelector 的方式來解決 JDK 的 epoll 空轉的bug。

第二步,將第一步創建的 ServerSocketChannel 以及一個感興趣的事件傳入並調用父類構造方法:

public NioServerSocketChannel(ServerSocketChannel channel) {
super(null, channel, SelectionKey.OP_ACCEPT);
config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}

這裡的第二行代碼,是將當前 NioServerSocketChannel 和 一個 ServerSocket 封裝成一個 NioServerSocketChannelConfig。

第三步,調用父類構造方法初始化一些變數:

protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);
this.ch = ch;
this.readInterestOp = readInterestOp;
try {
ch.configureBlocking(false);
} catch (IOException e) {
// 省略部分代碼
}
}

這一步主要是將一些變數保存起來,並設置了 channel 的阻塞模式為非阻塞模式

第四步,創建核心對象,包括 channelId,unsafe,pipeline:

protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}

因為我們本篇文章是對伺服器啟動流程的分析,所以這裡不對每一個對象的創建過程進行分析了,只需要知道是在創建 NioServerSocketChannel 的時候,創建了 channelId,unsafe 和 pipeline 對象。後面的操作中會用到這些對象。

初始化 Channel

初始化 Channel 的 init 方法 在 AbstractBootstrap 中是一個抽象方法,具體的實現在 ServerBootstrap 中,我們來看下具體的實現:

void init(Channel channel) throws Exception {

// 獲取Channel的pipeline
ChannelPipeline p = channel.pipeline();

final EventLoopGroup currentChildGroup = childGroup;
// 該childHandler對象是在創建ServerBootstrap對象時,通過childHandler方法創建的
final ChannelHandler currentChildHandler = childHandler;
// 省略部分代碼

// 把在創建ServerBootstrap對象時,創建的channelHandler和childHandler對象都添加到
// channel的pipeline中去
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) throws Exception {
final ChannelPipeline pipeline = ch.pipeline();
ChannelHandler handler = config.handler();
if (handler != null) {
pipeline.addLast(handler);
}

ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
pipeline.addLast(new ServerBootstrapAcceptor(
ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
}
});
}

這裡初始化最主要的工作就是為 NioServerSocketChannel 的 pipeline 添加了一個 ChannelHandler。

該 ChannelHandler 就是來處理客戶端連接的接入的,通過 ServerBootstrapAcceptor 來實現。

其中的 currentChildGroup 就是我們創建的 workerGroup,currentChildHandler 則是我們通過 childHandler 方法指定的處理客戶端請求的 ChannelHandler。

所以 ServerBootstrapAcceptor 主要的工作就是接受客戶端的請求,並將請求轉發給 workGroup 去處理,具體的處理邏輯由用戶自定義的 ChannelHandler 確定。

註冊 Channel

對 Channel 初始化完了之後,現在就需要對 Channel 進行註冊了,如下所示:

final ChannelFuture initAndRegister() {
Channel channel = null;
// 創建並初始化 channel
ChannelFuture regFuture = config().group().register(channel);
}

config().group() 返回的是 bootstrap 的 group 屬性,也就是我們創建 ServerBootstrap 時傳入的 bossGroup,他是一個 NioEventLoopGroup 實例。

整個註冊的過程也分為四個部分:

第一步,調用 NioEventLoopGroup 的註冊方法,如下所示:

public abstract class MultithreadEventLoopGroup
extends MultithreadEventExecutorGroup
implements EventLoopGroup {
@Override
public ChannelFuture register(Channel channel) {
return next().register(channel);
}
}

其中 next() 的方法實現是在父類 MultithreadEventExecutorGroup 中,也就是第二步的操作。

第二步,獲取一個可用的 EventExecutor,如下所示:

public abstract class MultithreadEventExecutorGroup
extends AbstractEventExecutorGroup {
@Override
public EventExecutor next() {
return chooser.next();
}
}

看到 chooser 我們應該馬上反應出來,這個對象是在初始化 NioEventLoopGroup 的時候創建的,同時還通過 newChild 方法創建了 nThreads 個 EventExecutor,並將這些 EventExecutor 都保存在了一個叫 children 的數組中,這裡的 EventExecutor 實例是一個 NioEventLoop 。

chooser 選擇器的 next 方法其實就是從 children 數組中獲取一個可用的 EventExecutor,也就是獲取一個可用的 NioEventLoop,所以 next().register(channel) 的方法,實際上是執行的 NioEventLoop 的 register(channel) 方法。

第三步,執行 NioEventLoop 的 register 方法,該方法是在 NioEventLoop 的父類 SingleThreadEventLoop 中實現的,如下所示:

public abstract class SingleThreadEventLoop
extends SingleThreadEventExecutor
implements EventLoop {
@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;
}
}

第四步,執行 unsafe 的 register 方法,看到 unsafe 我們也應該馬上就想到,該對象是在初始化 NioServerSocketChannel 的時候,在父類 AbstractChannel 中通過 newUnsafe() 方法初始化的,所以我們回到 AbstractChannel 中查看 newUnsafe 方法創建的 unsafe 對象到底是什麼。

但是很可惜,該方法是一個抽象方法,如下所示:

public abstract class AbstractChannel
extends DefaultAttributeMap
implements Channel {
protected abstract AbstractUnsafe newUnsafe();
}

所以需要到 AbstractChannel 的子類中尋找該方法的實現,並且該子類也要滿足是 NIOServerSocketChannel 的父類,所以很容易找到,該實現類是:AbstractNioMessageChannel,現在看下 newUnsafe 方法創建的 unsafe 對象是什麼,如下所示:

@Override
protected AbstractNioUnsafe newUnsafe() {
return new NioMessageUnsafe();
}

private final class NioMessageUnsafe
extends AbstractNioUnsafe {
}

所以現在我們得到了 unsafe 對象的實例是 NioMessageUnsafe,接下來就看 NioMessageUnsafe 的 register 方法了,該方法是在父類 AbstractUnsafe 中實現的,如下所示:

@Override
public final void register(EventLoop eventLoop, final ChannelPromise promise) {
// 省略部分代碼
AbstractChannel.this.eventLoop = eventLoop;

if (eventLoop.inEventLoop()) {
register0(promise);
} else {
try {
eventLoop.execute(new Runnable() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {
// 省略部分代碼
}
}
}

但是該方法仍然不是最終的註冊方法,又調用了一個叫 register0 的方法,但是該方法調用在兩個分支內:

如果 eventLoop.inEventLoop 為 true 則直接調用

否則將該方法封裝成一個 Runnable 交給 eventLoop 去執行

這裡我們需要注意的是 Netty 的 EventLoop 底層是使用的一個單線程來支撐他的工作的,很多操作都會看到對於當前線程的判斷,這正是 Netty 線程模型高效的原因所在。

讓該線程執行某個方法之前,要先判斷,當前是否在 EventLoop 線程之中,如果在的話就直接執行,否則將需要執行的方法封裝成一個 Runnable 交給 EventLoop 去調度,EventLoop 會在下個時間點來執行該任務,並且是在 EventLoop 線程中執行。

該方法可以拆分成兩個部分,如下圖所示:

第一步調用的是 doRegister 方法,如下所示:

private void register0(ChannelPromise promise) {
try {
// 省略部分代碼

// 執行具體的註冊
doRegister();

} catch (Throwable t) {
// 省略部分的代碼
}
}

doRegister 方法在 AbstractChannel 中是一個空實現,所以我們需要到他的子類中去尋找具體的實現,很容易我們在 AbstractNioChannel 中找到了該方法的實現,如下所示:

@Override
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
return;
} catch (CancelledKeyException e) {
if (!selected) {
eventLoop().selectNow();
selected = true;
} else {
throw e;
}
}
}
}

到這裡就是最底層的調用了,這裡的 JavaChannel() 方法獲取到的就是我們創建 NioServerSocketChannel 時通過 provider.openServerSocketChannel() 方法創建的 ServerSocketChannel。

而 eventLoop().unwrappedSelector() 方法獲取到的則是通過 provider.openSelector() 創建的一個原始的 Selector 對象。

換一個寫法這步操作實際上就是:

ServerSocketChannel.register(Selector, interest set);

就是通過這一步操作把 ServerSocketChannel 感興趣的事件註冊到 JDK 底層的一個 Selector 上。

但是有可能這個這個時候,註冊事件有可能失敗,所以需要立即執行一次 selector 的 selectNow 方法,因為這個被「取消」的SelectionKey 可能還緩存著沒有被移除。然後嘗試進行第二次註冊,如果成功的話就直接返回,如果還是失敗的話就拋出異常。

第二步,通過 pipeline 觸發 handler 中的某些回調方法,如下所示:

private void register0(ChannelPromise promise) {
try {
// 省略部分代碼

// 執行具體的註冊

pipeline.invokeHandlerAddedIfNeeded();

safeSetSuccess(promise);
pipeline.fireChannelRegistered();
if (isActive()) {
if (firstRegistration) {
pipeline.fireChannelActive();
} else if (config().isAutoRead()) {
beginRead();
}
}
} catch (Throwable t) {
// 省略部分代碼
}
}

首先通過 pipeline 的 invokeHandlerAddedIfNeeded 方法來觸發調用那些通過 pipeline.addLast() 方法添加的 ChannelHandler 的 channelAdded() 方法。

然後通知 promise 已經成功了,現在可以執行監聽器的 operationComplete 方法了。

最後 isActive() 方法默認是返回的 false,因為到現在我們還沒有綁定埠呢。

綁定埠

當 Channel 已經創建、初始化、並成功註冊好之後,最後就需要執行埠綁定了,現在回到 ServerBootstrap 的 doBind 方法中,如下所示:

private ChannelFuture doBind(final SocketAddress localAddress) {
// 初始化Channel並進行註冊
// 省略部分代碼
if (regFuture.isDone()) {
ChannelPromise promise = channel.newPromise();
doBind0(regFuture, channel, localAddress, promise);
return promise;
} else {
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) {
promise.setFailure(cause);
} else {
promise.registered();
doBind0(regFuture, channel, localAddress, promise);
}
}
});
return promise;
}
}

當註冊的結果 regFuture 已經完成了,那麼就可以直接執行綁定操作了,否則需要在 regFuture 上增加一個監聽器,當註冊完成時再執行綁定操作,不管怎麼樣,具體的綁定操作都是在 doBind0 方法中,如下所示:

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());
}
}
});
}

doBind0 的方法可以拆分成以下幾個部分:

中間的幾步調用過程這裡就不再贅述了,大家可以從源碼中跟一下,我們直接跳到最後一步調用 unsafe 的 bind 方法,不出意外就是在這一步進行了 JDK 底層 Channel 的埠綁定了,最終代碼如下:

public class NioServerSocketChannel
extends AbstractNioMessageChannel
implements io.netty.channel.socket.ServerSocketChannel {
protected void doBind(SocketAddress localAddress) throws Exception {
if (PlatformDependent.javaVersion() >= 7) {
javaChannel().bind(localAddress, config.getBacklog());
} else {
javaChannel().socket().bind(localAddress, config.getBacklog());
}
}
}

看到這裡一切就都清楚了,終於將埠綁定到了 ServerSocketChannel 上去了。

Netty 啟動過程總結

下面我們總結下 Netty 是如何在整個啟動過程中,把 NIO 那一套啟動步驟完美的分散到各個地方去的。

ServerSocketChannel 在哪創建的

首先需要知道 Netty 是在哪裡創建的 ServerSocketChannel,是在初始化 NioServerSocketChannel 對象的時候在 newSocket 方法中,通過 provider.openServerSocketChannel() 創建的。

非阻塞模式在哪設置的

同樣是在初始化 NioServerSocketChannel 對象的時候,在調用到父類 AbstractNioChannel 的構造方法的時候,執行的 ch.configureBlocking(false)。

Selector 在哪創建的

在 NioEventLoop 中通過 provider.openSelector() 創建的,並將該 Selector 對象存放在一個叫 unwrappedSelector 的變數中。

而 NioEventLoop 中的 provider,是在初始化 NioEventLoopGroup 時,通過 SelectorProvider.provider() 創建的,並最終傳遞給了 NioEventLoop。

Channel 是什麼時候註冊到 Selector 上去的

在 initAndRegister 方法中最終執行到了 AbstractNioChannel 中的 doRegister 方法,在該方法中將 Channel 註冊到 Selector 上去的。

埠是什麼時候綁定的

在 ServerBootstrap 的 doBind 方法中會先執行 initAndRegister 方法,執行完之後就會執行 doBind0 來進行埠綁定,最終會執行到 NioServerSocketChannel 類中的 doBind 方法,在該方法中完成了 JDK 底層埠的綁定。

小編福利分享:關注私信我回復「資料」免費領取一套架構師學習視頻資料。


推薦閱讀:
相關文章