1. 程式人生 > >自頂向下深入分析Netty(三)--Bootstrap

自頂向下深入分析Netty(三)--Bootstrap

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,如本例中的DecoderHandlerComputeHandlerEncoderHandler。完成初始化工作後,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執行緒池執行,複雜耗時的業務(如查詢資料庫,取得網路連線等)使用新的業務邏輯執行緒池執行