引言
上一篇文章介紹了Netty
的線程模型及EventLoop
機制,相信大家對Netty
已經有一個基本的認識。那么本篇文章我會根據Netty提供的Demo
來分析一下Netty
啟動流程。
啟動流程概覽
開始之前,我們先來分析下Netty
服務端的啟動流程,下面是一個簡單的流程圖
啟動流程大致分為五步
- 創建
ServerBootstrap
實例,ServerBootstrap
是Netty服務端的啟動輔助類,其存在意義在于其整合了Netty可以提供的所有能力,并且盡可能的進行了封裝,以方便我們使用 - 設置并綁定
EventLoopGroup
,EventLoopGroup
其實是一個包含了多個EventLoop
的NIO線程池,在上一篇文章我們也有比較詳細的介紹過EventLoop
事件循環機制,不過值得一提的是,Netty 中的EventLoop
不僅僅只處理IO讀寫事件,還會處理用戶自定義或系統的Task任務 - 創建服務端Channel
NioServerSocketChannel
,并綁定至一個EventLoop
上。在初始化NioServerSocketChannel
的同時,會創建ChannelPipeline
,ChannelPipeline
其實是一個綁定了多個ChannelHandler
的執行鏈,后面我們會詳細介紹 - 為服務端
Channel
添加并綁定ChannelHandler
,ChannelHandler
是Netty開放給我們的一個非常重要的接口,在觸發網絡讀寫事件后,Netty都會調用對應的ChannelHandler
來處理,后面我們會詳細介紹 - 為服務端
Channel
綁定監聽端口,完成綁定之后,Reactor線程(也就是第三步綁定的EventLoop線程)就開始執行Selector
輪詢網絡IO事件了,如果Selector
輪詢到網絡IO事件了,則會調用Channel
對應的ChannelPipeline
來依次執行對應的ChannelHandler
啟動流程源碼分析
下面我們就從啟動源碼來進一步分析 Netty 服務端的啟動流程
入口
首先來看下常見的啟動代碼
// 配置bossEventLoopGroup 配置大小為1
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 配置workEventLoopGroup 配置大小默認為cpu數*2
EventLoopGroup workerGroup = new NioEventLoopGroup();
// 自定義handler
final EchoServerHandler serverHandler = new EchoServerHandler();
try {
// 啟動輔助類 配置各種參數(服務端Channel類,EventLoopGroup,childHandler等)
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
// 配置 channel通道,會反射實例化
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
//p.addLast(new LoggingHandler(LogLevel.INFO));
p.addLast(serverHandler);
}
});
// 綁定監聽端口 啟動服務器
ChannelFuture f = b.bind(PORT).sync();
// 等待服務器關閉
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
- 可以看到上面的代碼首先創建了兩個
EventLoopGroup
,在上一篇文章我們有介紹過Netty
的線程模型有三種,而不同的EventLoopGroup
配置對應了三種不同的線程模型。這里創建的兩個EventLoopGroup
則是用了多線程Reactor模型
,其中bossEventLoopGroup
對應的就是處理Accept
事件的線程組,而workEventLoopGroup
則負責處理IO讀寫事件。 - 然后就是創建了一個啟動輔助類
ServerBootstrap
,并且配置了如下幾個重要參數- group 兩個Reactor線程組(bossEventLoopGroup, workEventLoopGroup)
- channel 服務端Channel
- option 服務端socket參數配置 例如
SO_BACKLOG
指定內核未連接的Socket連接排隊個數 - handler 服務端Channel對應的Handler
- childHandler 客戶端請求Channel對應的Handler
- 綁定服務端監聽端口,啟動服務 ->
ChannelFuture f = b.bind(PORT).sync();
這篇文章主要是分析Netty的啟動流程。so我們直接看b.bind(PORT).sync()
的源碼
bind
發現該方法內部實際調用的是doBind(final SocketAddress localAddress)
方法
doBind
private ChannelFuture doBind(final SocketAddress localAddress) {
// 初始化服務端Channel
final ChannelFuture regFuture = initAndRegister();
final Channel channel = regFuture.channel();
if (regFuture.cause() != null) {
return regFuture;
}
if (regFuture.isDone()) {
// 初始化一個 promise(異步回調)
ChannelPromise promise = channel.newPromise();
// 綁定監聽端口
doBind0(regFuture, channel, localAddress, promise);
return promise;
}
.... // 省略其他代碼
}
doBind
主要做了兩個事情
-
initAndRegister()
初始化Channel -
doBind0
綁定監聽端口
initAndRegister()
final ChannelFuture initAndRegister() {
Channel channel = null;
try {
// new一個新的服務端Channel
channel = channelFactory.newChannel();
// 初始化Channel
init(channel);
} catch (Throwable t) {
...
}
// 將Channel注冊到EventLoopGroup中一個EventLoop上
ChannelFuture regFuture = config().group().register(channel);
if (regFuture.cause() != null) {
if (channel.isRegistered()) {
channel.close();
} else {
channel.unsafe().closeForcibly();
}
}
return regFuture;
}
channelFactory.newChannel()
其實就是通過反射創建配置的服務端Channel類,在這里是NioServerSocketChannel
創建完成的
NioServerSocketChannel
進行一些初始化操作,例如將我們配置的Handler
加到服務端Channel
的pipeline
中將
Channel
注冊到EventLoopGroup
中一個EventLoop上
下面我們來看下NioServerSocketChannel
類的構造方法,看看它到底初始化了哪些東西,先看下其繼承結構
NioServerSocketChannel初始化
下面是它的構造方法的調用順序,依次分為了四步
// 1
public NioServerSocketChannel() {
// 通過 SelectProvider來初始化一個Java NioServerChannel
this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}
// 2.
public NioServerSocketChannel(ServerSocketChannel channel) {
super(null, channel, SelectionKey.OP_ACCEPT);
// 創建一個配置類,持有Java Channel
config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}
// 3.
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
super(parent);
this.ch = ch;
this.readInterestOp = readInterestOp;
try {
// 設置Channel為非阻塞
ch.configureBlocking(false);
} catch (IOException e) {
try {
ch.close();
} catch (IOException e2) {
logger.warn(
"Failed to close a partially initialized socket.", e2);
}
throw new ChannelException("Failed to enter non-blocking mode.", e);
}
}
// 4
protected AbstractChannel(Channel parent) {
this.parent = parent;
// 生成一個channel Id
id = newId();
// 創建一個 unSafe 類,unsafe封裝了Netty底層的IO讀寫操作
unsafe = newUnsafe();
// 創建一個 pipeline類
pipeline = newChannelPipeline();
}
可以看到NioServerSocketChannel
的構造函數主要是初始化并綁定了以下3類
- 綁定一個Java
ServerSocketChannel
類 - 綁定一個
unsafe
類,unsafe封裝了Netty底層的IO讀寫操作 - 綁定一個
pipeline
,每個Channel都會唯一綁定一個pipeline
init(Channel channel)
void init(Channel channel) {
// 設置Socket參數
setChannelOptions(channel, newOptionsArray(), logger);
setAttributes(channel, attrs0().entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY));
ChannelPipeline p = channel.pipeline();
// 子EventLoopGroup用于完成Nio讀寫操作
final EventLoopGroup currentChildGroup = childGroup;
// 為workEventLoop配置的自定義Handler
final ChannelHandler currentChildHandler = childHandler;
final Entry<ChannelOption<?>, Object>[] currentChildOptions;
synchronized (childOptions) {
currentChildOptions = childOptions.entrySet().toArray(EMPTY_OPTION_ARRAY);
}
// 設置附加參數
final Entry<AttributeKey<?>, Object>[] currentChildAttrs = childAttrs.entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY);
// 為服務端Channel pipeline 配置 對應的Handler
p.addLast(new ChannelInitializer<Channel>() {
@Override
public void initChannel(final Channel ch) {
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));
}
});
}
});
}
這里主要是為服務端Channel配置一些參數,以及對應的處理器ChannelHandler
,注意這里不僅僅會把我們自定義配置的ChannelHandler
加上去,同時還會自動幫我們加入一個系統Handler
(ServerBootstrapAcceptor
),這就是Netty用來接收客戶端請求的Handler
,在ServerBootstrapAcceptor
內部會完成SocketChannel
的連接,EventLoop
的綁定等操作,之后我們會著重分析這個類
Channel的注冊
// MultithreadEventLoopGroup
public ChannelFuture register(Channel channel) {
// next()會選擇一個EventLoop來完成Channel的注冊
return next().register(channel);
}
// SingleThreadEventLoop
public ChannelFuture register(Channel channel) {
return register(new DefaultChannelPromise(channel, this));
}
// AbstractChannel
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);
}
....
}
完成注冊流程
- 完成實際的Java
ServerSocketChannel
與Select
選擇器的綁定 - 并觸發
channelRegistered
以及channelActive
事件
到這里為止,其實Netty服務端已經基本啟動完成了,就差綁定一個監聽端口了。可能讀者會很詫異,怎么沒有看到Nio線程輪詢 IO事件的循環呢,講道理肯定應該有一個死循環才對?那我們下面就把這段代碼找出來
在之前的代碼中,我們經常會看到這樣一段代碼
// 往EventLoop中丟了一個異步任務(其實是同步的,因為只有一個Nio線程,不過因為是事件循環機制(丟到一個任務隊列中),看起來像是異步的)
eventLoop.execute(new Runnable() {
@Override
public void run() {
...
}
});
eventLoop.execute
到底做了什么事情?
private void execute(Runnable task, boolean immediate) {
boolean inEventLoop = inEventLoop();
// 把當前任務添加到任務隊列中
addTask(task);
// 不是Nio線程自己調用的話,則表明是初次啟動
if (!inEventLoop) {
// 啟動EventLoop的Nio線程
startThread();
...
}
...
}
/**
* 啟動EventLoop的Nio線程
*/
private void doStartThread() {
assert thread == null;
// 啟動Nio線程
executor.execute(new Runnable() {
@Override
public void run() {
thread = Thread.currentThread();
if (interrupted) {
thread.interrupt();
}
boolean success = false;
updateLastExecutionTime();
try {
SingleThreadEventExecutor.this.run();
success = true;
} catch (Throwable t) {
logger.warn("Unexpected exception from an event executor: ", t);
} finally {
...
}
通過上面的代碼可以知道這里主要做了兩件事情
- 創建的任務被丟入了一個隊列中等待執行
- 如果是初次創建,則啟動Nio線程
- SingleThreadEventExecutor.this.run(); 調用子類的Run實現(執行IO事件的輪詢)
看下NioEventLoop
的Run
方法實現
protected void run() {
int selectCnt = 0;
for (;;) {
try {
int strategy;
try {
// 獲取IO事件類型
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
...
default:
}
} catch (IOException e) {
// 出現異常 重建Selector
rebuildSelector0();
selectCnt = 0;
handleLoopException(e);
continue;
}
selectCnt++;
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
boolean ranTasks;
if (ioRatio == 100) {
try {
if (strategy > 0) {
// 處理對應事件,激活對應的ChannelHandler事件
processSelectedKeys();
}
} finally {
// 處理完事件了才執行全部Task
ranTasks = runAllTasks();
}
}
...
}
}
}
到這里的代碼是不是就非常熟悉了,熟悉的死循環輪詢事件
- 通過Selector來輪詢IO事件
- 觸發Channel所綁定的Handler處理對應的事件
- 處理完IO事件了 會執行系統或用戶自定義加入的Task
doBind0
實際的Bind邏輯在 NioServerSocketChannel
中執行,我們直接省略前面一些冗長的調用,來看下最底層的調用代碼,發現其實就是調用其綁定的Java Channel來執行對應的監聽端口綁定邏輯
protected void doBind(SocketAddress localAddress) throws Exception {
// 如果JDK版本大于7
if (PlatformDependent.javaVersion() >= 7) {
javaChannel().bind(localAddress, config.getBacklog());
} else {
javaChannel().socket().bind(localAddress, config.getBacklog());
}
}
尾言
本篇文章把Netty的啟動流程粗略的捋了一遍,目的不是為了摳細節,而是大致能夠清楚Netty服務端啟動時主要做了哪些事情,所以有些地方難免會比較粗略一筆帶過。在后面的文章我會把一些細節的源碼單獨拎出來深入分析