本文開始分析Netty的源碼,由于目標是自頂向下分析,在這一節將分析Netty是如何構建起如上圖所示的整體框架。首先將使用一個示例展示怎么使用Bootstarp構建服務端應用,然后將深入源碼了解底層機制和原理。
1.使用示例
首先使用Netty構造如圖所示的框架,源碼如下:
// 指定mainReactor
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
// 指定subReactor
EventLoopGroup workerGroup = new NioEventLoopGroup();
// 用戶自定義的ThreadPool
EventExecutorGroup threadPool = new ThreadPool();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100) // 設置TCP參數
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(threadPool,
new DecoderHandler(), // 解碼處理器
new ComputeHandler()); // 計算處理器
new EncoderHandler(), // 編碼處理器
}
});
// 綁定到本地端口等待客戶端連接
ChannelFuture f = b.bind(PORT).sync();
// 等待接受客戶端連接的Channel被關閉
f.channel().closeFuture().sync();
} finally {
// 關閉兩個線程組
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
threadPool.shutdown();
}
逐行分析代碼,EventLoopGroup
是Netty實現的線程池接口,兩個線程池:bossGroup和workerGroup分別對應mainReactor和subReactor,其中boss專門用于接受客戶端連接,worker也就是常說的IO線程專門用于處理IO事件。IO事件包括兩類,一類如服務端接收到客戶端數據的Read事件,另一類如用戶線程主動向客戶端發送數據的Write事件。在4.0版本中,用戶自定義的業務線程池須實現EventExecutorGroup
接口,4.1版本則可以直接使用JAVA自帶的線程池。
為了幫助用戶快速構建基于Netty的服務,Netty提供了兩個啟動器ServerBootstrap
和Bootstrap
,分別用于啟動服務器端和客戶端程序。group(EventLoopGroup...)
方法用于指定一個或兩個Reactor,本例中指定為兩個。channel(Channel)
方法本質用來指定一個Channel工廠,本例中該工廠生產服務端用于accept客戶端連接的Channel,將默認使用Channel的無參構造方法。如果用戶需要自定義有參數的Channel,可自定義所需的工廠實現。option(Key, Value)
用于指定TCP相關的參數以及一些Netty自定義的參數。childHandler()
用于指定subReactor中的處理器,類似的,handler()
用于指定mainReactor的處理器,只是默認情況下mainReactor中已經添加了acceptor處理器,所以無需再指定。需要注意的是:這兩個方法并不能累積調用而達到增加多個處理器的目的,所以引入了 ChannelInitializer
,它是一個特殊的Handler,功能是初始化多個Handler,如本例中的DecoderHandler
,ComputeHandler
,EncoderHandler
。完成初始化工作后,ChannelInitializer
會從Handler鏈中刪除。至此,如圖所示的框架已經構建完畢。
最后臨門一腳,bind(int)
方法將服務端Channel綁定到本地端口,成功后將accept客戶端的連接,從而是整個框架運行起來。使用sync()
方法是由于Netty中的事件都是異步的,所以需要同步等待結果。準確的說,這個方法在這里使用是有問題的,sync()
完成后只能表明綁定事件運行完畢,但并不能說明綁定成功,雖然失敗的可能性微乎其微。
f.channel().closeFuture().sync()
方法僅僅是為了使當前main線程阻塞而不立即執行之后的各種shutdown()
方法,其語義是等到服務端接受客戶端連接的Channel被關閉時,才執行后面代碼的操作。在實際應用中,這樣的代碼并不實用,我們可能需要接受諸如kill
命令后,優雅關閉線程組。
一些情況下,我們并不使用如圖所示的結構,比如當業務邏輯都很簡單,也就是如圖所示的decode,compute,encode能在短時間完成(數十毫秒或更少),那么可以不使用業務線程池。代碼也很簡單,只需要改動ChannelInitializer
即可:
b.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new DecoderHandler()); // 解碼處理器
p.addLast(new ComputeHandler()); // 計算處理器
p.addLast(new EncoderHandler()); // 編碼處理器
}
});
事實上這是Netty的默認方法,也就是說不在addLast(Handler)
方法中指定線程池,那么將使用默認的subReacor即woker線程池也即IO線程池執行處理器中的業務邏輯代碼。
又比如,如開始的例子只讓IO線程池處理read,write等IO事件會覺得有點大材小用,于是將decode和encode交給IO線程處理,如果此時的compute查詢需要數據庫中的數據,那么代碼可改動為如下:
b.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new DecoderHandler()); // 解碼處理器
p.addLast(new EncoderHandler()); // 編碼處理器
p.addLast(threadPool, new ComputeWithSqlHandler()); // 附帶SQL查詢的計算
}
});
最佳實踐
簡單快速的業務邏輯可由IO線程池執行,復雜耗時的業務(如查詢數據庫,取得網絡連接等)使用新的業務邏輯線程池執行。
本文介紹了Bootstrap的使用,如果還想知道背后的原理,可移步后續文章:自頂向下深入分析Netty(三)--Bootstrap源碼分析。