Netty Bootstrap(圖解)
瘋狂創客圈 Java 分布式聊天室【 億級流量】實戰系列之18 【 博客園 總入口 】
源碼工程
源碼IDEA工程獲取鏈接:Java 聊天室 實戰 源碼
寫在前面
大家好,我是作者尼恩。目前和幾個小伙伴一起,組織了一個高並發的實戰社群【瘋狂創客圈】。正在開始 高並發、億級流程的 IM 聊天程序 學習和實戰,此文是是百萬級流量 Netty 聊天器 打造的系列文章的第18篇,這是一個基礎篇,介紹Bootstrap。
順便說明下:
本文的內容只是一個初稿、初稿,本文的知識,在《Netty Zookeeper Redis 高並發實戰》一書時,進行大篇幅的完善和更新,並且進行的源碼的升級。 博客和書不一樣,書的內容更加系統化、全面化,更加層層升入、層次分明、更多次的錯誤排查,請大家以書的內容為准。
本文的最終內容, 具體請參考瘋狂創客圈 傾力編著,機械工業出版社出版的 《Netty Zookeeper Redis 高並發實戰》一書 。
圖解幾個重要概念
下面的幾個概念,非常重要。
之前沒有認真介紹,下面圖解說明一下。
父子 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 面試題 一網打盡
- 瘋狂創客圈 【 博客園 總入口 】