Netty Bootstrap(秒懂) - 圖解Netty系列
Netty Bootstrap(圖解)
瘋狂創客圈 Java 分散式聊天室【 億級流量】實戰系列之18 【 部落格園 總入口 】
文章目錄
原始碼工程
原始碼IDEA工程獲取連結:Java 聊天室 實戰 原始碼
寫在前面
大家好,我是作者尼恩。
今天是百萬級流量 Netty 聊天器 打造的系列文章的第18篇,這是一個基礎篇,介紹Bootstrap。
力爭以圖文並茂的形式,做到非常的易懂。
圖解幾個重要概念
下面的幾個概念,非常重要。
之前沒有認真介紹,下面圖解說明一下。
父子 channel
在 Netty 中, Channel 是一個 Socket 連線的抽象, 它為使用者提供了關於底層 Socket 狀態(是否是連線還是斷開) 以及對 Socket 的讀寫等操作。
每當 Netty 建立了一個連線後, 都會有一個對應的 Channel 例項。
並且,有父子channel 的概念。 伺服器連線監聽的channel ,也叫 parent channel。 對應於每一個 Socket 連線的channel,也叫 child channel。
EventLoop 執行緒與執行緒組
在看本文之前,如果不明白 reactor 執行緒和reactor模式,請 檢視 瘋狂創客圈的專門文章:Reactor模式 。
在Netty 中,每一個 channel 綁定了一個thread 執行緒。
一個 thread 執行緒,封裝到一個 EventLoop , 多個EventLoop ,組成一個執行緒組 EventLoopGroup。
反過來說,EventLoop 這個相當於一個處理執行緒,是Netty接收請求和處理IO請求的執行緒。 EventLoopGroup 可以理解為將多個EventLoop進行分組管理的一個類,是EventLoop的一個組。
他們的對應關係,大致如下:
通道與Reactor執行緒組
這裡主要是涉及的是伺服器端。
伺服器端,一般有設定兩個執行緒組,監聽連線的 parent channel 工作在一個獨立的執行緒組,這裡名稱為boss執行緒組(有點像負責招人的包工頭)。
連線成功後,負責客戶端連線讀寫的 child channel 工作在另一個執行緒組,這裡名稱為 worker 執行緒組,專門負責搬資料(有點兒像搬磚)。
Channel 通道的型別
除了 TCP 協議以外, Netty 還支援很多其他的連線協議, 並且每種協議還有 NIO(非同步 IO) 和 OIO(Old-IO, 即傳統的阻塞 IO) 版本的區別。
不同協議不同的阻塞型別的連線都有不同的 Channel 型別與之對應,下面是一些常用的 Channel 型別:
- NioSocketChannel, 代表非同步的客戶端 TCP Socket 連線.
- NioServerSocketChannel, 非同步的伺服器端 TCP Socket 連線.
- NioDatagramChannel, 非同步的 UDP 連線
- NioSctpChannel, 非同步的客戶端 Sctp 連線.
- NioSctpServerChannel, 非同步的 Sctp 伺服器端連線.
- OioSocketChannel, 同步的客戶端 TCP Socket 連線.
- OioServerSocketChannel, 同步的伺服器端 TCP Socket 連線.
- OioDatagramChannel, 同步的 UDP 連線
- OioSctpChannel, 同步的 Sctp 伺服器端連線.
- OioSctpServerChannel, 同步的客戶端 TCP Socket 連線.
啟動器初步介紹
Bootstrap 是 Netty 提供的一個便利的工廠類,可以通過它來完成 Netty 的客戶端或伺服器端的 Netty 初始化。
當然,Netty 的官方解釋說,可以不用這個啟動器。
但是,一點點去手動建立channel 並且完成一些的設定和啟動,會非常麻煩。還是使用這個便利的工具類,會比較好。
有兩個啟動器,分別應用在伺服器和客戶端。
如下圖:
兩個啟動器大致的配置,都是相同的。
下面以伺服器serverBootstrap 啟動類為主要的介紹物件。
圖解 Bootstrap執行流程
首先,建立了一個引導器 ServerBootstrap 例項,這個專門用於引導服務端的啟動工作,直接new 建立即可。(客戶端的引導器差不多,不過是建立Bootstrap 例項)
// 啟動引導器
private static ServerBootstrap b = new ServerBootstrap();
啟動一個Bootstrap,大致有8步,如下圖:
程式碼如下:
try { //1 設定reactor 執行緒
b.group(bossLoopGroup, workerLoopGroup);
//2 設定nio型別的channel
b.channel(NioServerSocketChannel.class);
//3 設定監聽埠
b.localAddress(new InetSocketAddress(port));
//4 設定通道選項
b.option(ChannelOption.SO_KEEPALIVE, true);
b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
//5 裝配流水線
b.childHandler(new ChannelInitializer<SocketChannel>()
{
//有連線到達時會建立一個channel
protected void initChannel(SocketChannel ch) throws Exception
{
ch.pipeline().addLast(new ProtobufDecoder());
ch.pipeline().addLast(new ProtobufEncoder());
// pipeline管理channel中的Handler
// 在channel佇列中新增一個handler來處理業務
ch.pipeline().addLast("serverHandler", serverHandler);
}
});
// 6 開始繫結server
// 通過呼叫sync同步方法阻塞直到繫結成功
ChannelFuture channelFuture = b.bind().sync();
LOGGER.info(ChatServer.class.getName() +
" started and listen on " +
channelFuture.channel().localAddress());
// 7 監聽通道關閉事件
// 應用程式會一直等待,直到channel關閉
ChannelFuture closeFuture= channelFuture.channel().closeFuture();
closeFuture.sync();
} catch (Exception e)
{
e.printStackTrace();
} finally
{
// 8 優雅關閉EventLoopGroup,
// 釋放掉所有資源包括建立的執行緒
workerLoopGroup.shutdownGracefully();
bossLoopGroup.shutdownGracefully();
}
接下來就是精彩的8個步驟。
1:設定reactor 執行緒組
在設定 reactor 反應器執行緒組之前,建立了兩個 NioEventLoopGroup 執行緒組:
-
bossLoopGroup 表示伺服器連線監聽執行緒組,專門接受 accept 新的客戶端client 連線
-
workerGroup 表示處理每一條連線的資料收發的執行緒組
線上程組和啟動器都建立完成後,就可以開始設定執行緒組:通過 b.group(bossGroup, workerGroup) 方法,給引導器配置兩大執行緒組。
配置完成之後,整個引導類的 reactor 執行緒正式確定。這裡確定的工作模式,為父子執行緒的模型。
也可以不設定兩個執行緒組,只設置一個執行緒組。
如果只設置一個執行緒組,具體的方法為 —— b.group( workerGroup) 。
配置完成一個執行緒組,則所有的 channel ,包括服務監聽通道父親channel 和所有的子channel ,都工作在同一個執行緒組中。
說明一下,一個執行緒組,可不止一條執行緒哈。
2 :設定通道的IO型別
Netty 不止支援 Java NIO ,也支援阻塞式的 BIO (在Netty 中 叫做OIO)。
這裡配置的是NIO,方法如下。
//2 設定nio型別的channel
b.channel(NioServerSocketChannel.class);
如果想指定 IO 模型為 BIO,那麼這裡配置上Netty的 OioServerSocketChannel.class 型別即可。由於NIO 的優勢巨大,通常不會在Netty中使用BIO。
3:設定監聽埠
//3 設定監聽埠
b.localAddress(new InetSocketAddress(port));
這是最為簡單的一步操作。
4:設定通道引數
-
childOption() 方法
給每條child channel 連線設定一些TCP底層相關的屬性,比如上面,我們設定了兩種TCP屬性,其中 ChannelOption.SO_KEEPALIVE表示是否開啟TCP底層心跳機制,true為開
-
option() 方法
對於server bootstrap而言,這個方法,是給parent channel 連線設定一些TCP底層相關的屬性。
TCP連線的引數詳細介紹如下。
option設定的引數:
SO_RCVBUF ,SO_SNDBUF
這兩個選項就是來設定TCP連線的兩個buffer尺寸的。
每個TCP socket在核心中都有一個傳送緩衝區和一個接收緩衝區,TCP的全雙工的工作模式以及TCP的滑動視窗便是依賴於這兩個獨立的buffer以及此buffer的填充狀態。
SO_SNDBUF
Socket引數,TCP資料傳送緩衝區大小。該緩衝區即TCP傳送滑動視窗,linux作業系統可使用命令:cat /proc/sys/net/ipv4/tcp_smem 查詢其大小。
TCP_NODELAY
TCP引數,立即傳送資料,預設值為Ture(Netty預設為True而作業系統預設為False)。該值設定Nagle演算法的啟用,改演算法將小的碎片資料連線成更大的報文來最小化所傳送的報文的數量,如果需要傳送一些較小的報文,則需要禁用該演算法。Netty預設禁用該演算法,從而最小化報文傳輸延時。
這個引數,與是否開啟Nagle演算法是反著來的,true表示關閉,false表示開啟。通俗地說,如果要求高實時性,有資料傳送時就馬上傳送,就關閉,如果需要減少傳送次數減少網路互動,就開啟。
SO_KEEPALIVE
底層TCP協議的心跳機制。Socket引數,連線保活,預設值為False。啟用該功能時,TCP會主動探測空閒連線的有效性。可以將此功能視為TCP的心跳機制,需要注意的是:預設的心跳間隔是7200s即2小時。Netty預設關閉該功能。
SO_REUSEADDR
Socket引數,地址複用,預設值False。有四種情況可以使用:
(1).當有一個有相同本地地址和埠的socket1處於TIME_WAIT狀態時,而你希望啟動的程式的socket2要佔用該地址和埠,比如重啟服務且保持先前埠。
(2).有多塊網絡卡或用IP Alias技術的機器在同一埠啟動多個程序,但每個程序繫結的本地IP地址不能相同。
(3).單個程序繫結相同的埠到多個socket上,但每個socket繫結的ip地址不同。(4).完全相同的地址和埠的重複繫結。但這隻用於UDP的多播,不用於TCP。
SO_LINGER
Socket引數,關閉Socket的延遲時間,預設值為-1,表示禁用該功能。-1表示socket.close()方法立即返回,但OS底層會將傳送緩衝區全部發送到對端。0表示socket.close()方法立即返回,OS放棄傳送緩衝區的資料直接向對端傳送RST包,對端收到復位錯誤。非0整數值表示呼叫socket.close()方法的執行緒被阻塞直到延遲時間到或傳送緩衝區中的資料傳送完畢,若超時,則對端會收到復位錯誤。
SO_BACKLOG
Socket引數,服務端接受連線的佇列長度,如果佇列已滿,客戶端連線將被拒絕。預設值,Windows為200,其他為128。
b.option(ChannelOption.SO_BACKLOG, 1024)
表示系統用於臨時存放已完成三次握手的請求的佇列的最大長度,如果連線建立頻繁,伺服器處理建立新連線較慢,可以適當調大這個引數.
SO_BROADCAST
Socket引數,設定廣播模式。
5: 裝配流水線
ChannelPipeline 這是Netty處理請求的責任鏈,這是一個ChannelHandler的連結串列,而ChannelHandler就是用來處理網路請求的內容的。
每一個channel ,都有一個處理器流水線。
裝配 child channel 流水線,呼叫 childHandler()方法,傳遞一個ChannelInitializer 的例項。
在 child channel 建立成功,開始通道初始化的時候,在bootstrap啟動器中配置的 ChannelInitializer 例項就會被呼叫。
這個時候,才真正的執行去執行 initChannel 初始化方法,開始通道流水線裝配。
流水線裝配,主要是在流水線pipeline 的後面,增加負責資料讀寫、處理業務邏輯的handler。
b.childHandler(new ChannelInitializer<SocketChannel>()
{
//有連線到達時會建立一個channel
protected void initChannel(SocketChannel ch) throws Exception
{
ch.pipeline().addLast(new ProtobufDecoder());
ch.pipeline().addLast(new ProtobufEncoder());
// pipeline管理channel中的Handler
// 在channel佇列中新增一個handler來處理業務
ch.pipeline().addLast("serverHandler", serverHandler);
}
});
說明一下,ChannelInitializer這個類中,有一個泛型引數 SocketChannel,這裡的型別,需要和前面的Channel型別對應上。
順便說一下處理器。
處理器 ChannelHandler 用來處理網路請求內容,有ChannelInboundHandler和ChannelOutboundHandler兩種,ChannlPipeline會從頭到尾順序呼叫ChannelInboundHandler處理網路請求內容,從尾到頭呼叫ChannelOutboundHandler 處理網路請求內容。
pipeline 流水線的圖,大致如下:
如何裝配parent 通道呢?
使用serverBootstrap.handler() 方法 。 handler()方法,可以和前面分析的childHandler()方法對應起來。childHandler()用於指定處理新連線資料的讀寫處理邏輯。 handler()方法裝配parent 通道。
比方說:
serverBootstrap.handler(new ChannelInitializer()
{
protected void initChannel(NioServerSocketChannel ch)
{
System.out.println("服務端啟動中");
}
}
)
handler()用於指定在服務端啟動過程中的一些邏輯,通常情況下呢,我們用不著這個方法。
6: 開始繫結server
// 通過呼叫sync同步方法阻塞直到繫結成功
ChannelFuture channelFuture = b.bind().sync();
LOGGER.info(ChatServer.class.getName() +
" started and listen on " +
channelFuture.channel().localAddress());
這個也很簡單。
7: ChannelFuture
ChannelFuture 在Netty中的所有的I/O操作都是非同步執行的,這就意味著任何一個I/O操作會立刻返回,不保證在呼叫結束的時候操作會執行完成。因此,會返回一個ChannelFuture的例項,通過這個例項可以獲取當前I/O操作的狀態。
// 7 監聽通道關閉事件
// 應用程式會一直等待,直到channel關閉
ChannelFuture closeFuture= channelFuture.channel().closeFuture();
closeFuture.sync();
對於客戶端來說,Bootstrap是開發netty客戶端的基礎,通過Bootstrap的connect方法來連線伺服器端。該方法返回的也是ChannelFuture。
8 優雅關閉EventLoopGroup
// 8 優雅關閉EventLoopGroup,
// 釋放掉所有資源包括建立的執行緒
workerLoopGroup.shutdownGracefully();
bossLoopGroup.shutdownGracefully();
這個,會關閉所有的child channel,這是非常重要的。
關閉之後,會釋放掉底層的資源,如TCP Socket 檔案描述符,等等。
瘋狂創客圈 Java 死磕系列
- Java (Netty) 聊天程式【 億級流量】實戰 開源專案實戰
- Netty 原始碼、原理、JAVA NIO 原理
- Java 面試題 一網打盡
- 瘋狂創客圈 【 部落格園 總入口 】