自頂向下深入分析Netty(七)--ChannelPipeline和ChannelHandler總述

Netty架構模式

像以往一樣,繼續回顧這幅圖。目前為止,我們學習了Netty的EventLoop、Channel以及ChannelFuture,還差最后兩個部分:ByteBuf和ChannelHandler。ByteBuf作為通道讀寫數據的緩沖區,Channel底層數據的讀寫細節正是由ByteBuf完成。ChannelHandler作為處理各種事件的處理器,為用戶提供實際的業務邏輯處理功能。在本章中,我們將介紹ChannelHandler以及存儲它的容器ChannelPipeline。使用自頂向下的方法,首先介紹整體ChannePipeline,然后介紹ChannelHandler。

7.1 總述

7.1.1 ChannelPipeline

提到pipeline,我們首先想到的是*nix中的管道,可實現將一個程序的輸出作為另一個程序的輸入。ChannelPipeline也實現類似的功能,不同的是:ChannelPipeline將一個ChannelHandler的處理后的數據作為下一個ChannelHandler處理的數據源。Netty的ChannelPipeline示意圖如下:

ChanelPipeline

Xnix的管道中流動的是數據,ChnanelPipeline中流動的是事件(事件中可能附加數據)。Netty定義了兩種事件類型:入站(inbound)事件和出站(outbound)事件。ChannelPipeline使用攔截過濾器模式使用戶可以掌控ChannelHandler處理事件的流程。注意:事件在ChannelPipeline中不自動流動而需要調用ChannelHandlerContext中諸如fileXXX()或者read()類似的方法將事件從一個ChannelHandler傳播到下一個ChannelHandler。
事實上,ChannelHandler不處理具體的事件,處理具體的事件由相應的子類完成:ChannelInboundHandler處理和攔截入站事件,ChannelOutboundHandler處理和攔截出站事件。那么事件是怎么在ChannelPipeline中流動的呢?我們使用代碼注釋中的例子:

    ChannelPipeline p = ...;
    p.addLast("1", new InboundHandlerA());
    p.addLast("2", new InboundHandlerB());
    p.addLast("3", new OutboundHandlerA());
    p.addLast("4", new OutboundHandlerB());
    p.addLast("5", new InboundOutboundHandlerX());

對于入站事件,處理序列為:1-->2-->5;對于出站事件,處理序列為:5-->4-->3。可見,入站事件與出站事件處理順序正好相反。事件不會在ChannelPipeline中自動流動,而完全由用戶控制,所以ChannelHandler處理的代碼可能如下:

    public class InboundHandlerA implements ChannelInboundHandler {
        @Override
        public void channelActive(ChannelHandlerContext ctx) {
            System.out.println("Connected!"); // 用戶自定義處理邏輯
            ctx.fireChannelActive(); // 將channelActive事件傳播到InboundHandlerB
        }
    }
   
    public class OutboundHandlerB extends ChannelOutboundHandler{
        @Override
        public void close(ChannelHandlerContext ctx, ChannelPromise promise) {
            System.out.println("Closing .."); // 用戶自定義處理邏輯
            ctx.close(promise); // 將close事件傳播到OutboundHandlerA
        }
    }

入站事件一般由I/O線程觸發,以下事件為入站事件:

    ChannelRegistered() // Channel注冊到EventLoop
    ChannelActive()     // Channel激活
    ChannelRead(Object) // Channel讀取到數據
    ChannelReadComplete()   // Channel讀取數據完畢
    ExceptionCaught(Throwable)  // 捕獲到異常
    UserEventTriggered(Object)  // 用戶自定義事件
    ChannelWritabilityChanged() // Channnel可寫性改變,由寫高低水位控制
    ChannelInactive()   // Channel不再激活
    ChannelUnregistered()   // Channel從EventLoop中注銷

出站事件一般由用戶觸發,以下事件為出站事件:

    bind(SocketAddress, ChannelPromise) // 綁定到本地地址
    connect(SocketAddress, SocketAddress, ChannelPromise)   // 連接一個遠端機器
    write(Object, ChannelPromise)   // 寫數據,實際只加到Netty出站緩沖區
    flush() // flush數據,實際執行底層寫
    read()  // 讀數據,實際設置關心OP_READ事件,當數據到來時觸發ChannelRead入站事件
    disconnect(ChannelPromise)  // 斷開連接,NIO Server和Client不支持,實際調用close
    close(ChannelPromise)   // 關閉Channel
    deregister(ChannelPromise)  // 從EventLoop注銷Channel

入站事件一般由I/O線程觸發,用戶程序員也可根據實際情況觸發??紤]這樣一種情況:一個協議由頭部和數據部分組成,其中頭部含有數據長度,由于數據量較大,客戶端分多次發送該協議的數據,服務端接收到數據后需要收集足夠的數據,組裝為更有意義的數據傳給下一個ChannelInboudHandler。也許你已經知道,這個收集數據的ChannelInboundHandler正是Netty中基本的Encoder,Encoder中會處理多次ChannelRead()事件,只觸發一次對下一個ChannelInboundHandler更有意義的ChannelRead()事件。
出站事件一般由用戶觸發,而I/O線程也可能會觸發。比如,當用戶已配置ChannelOption.AutoRead選項,則I/O在執行完ChannelReadComplete()事件,會調用read()方法繼續關心OP_READ事件,保證數據到達時自動觸發ChannelRead()事件。
如果你初次接觸Netty,會對下面的方法感到疑惑,所以列出區別:

    channelHandlerContext.close()   // close事件傳播到下一個Handler
    channel.close()                 // ==channelPipeline.close()
    channelPipeline.close()         // 事件沿整個ChannelPipeline傳播,注意in/outboud的傳播起點

回憶AbstractChannel的構造方法:

    protected AbstractChannel(Channel parent) {
        this.parent = parent;
        unsafe = newUnsafe();
        pipeline = newChannelPipeline();
    }

    protected DefaultChannelPipeline newChannelPipeline() {
        return new DefaultChannelPipeline(this);
    }

可見,新建一個Channel時會自動新建一個ChannelPipeline,也就是說他們之間是一對一的關系。另外需要注意的是:ChannelPipeline是線程安全的,也就是說,我們可以動態的添加、刪除其中的ChannelHandler??紤]這樣的場景:服務器需要對用戶登錄信息進行加密,而其他信息不加密,則可以首先將加密Handler添加到ChannelPipeline,驗證完用戶信息后,主動從ChnanelPipeline中刪除,從而實現該需求。

7.1.2 ChannelHandler

ChannelHandler并沒有方法處理事件,而需要由子類處理:ChannelInboundHandler攔截和處理入站事件,ChannelOutboundHandler攔截和處理出站事件。我們已經明白,ChannelPipeline中的事件不會自動流動,而我們一般需求事件自動流動,Netty提供了兩個Adapter:ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter來滿足這種需求。其中的實現類似如下:

    // inboud事件默認處理過程
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelRegistered();    // 事件傳播到下一個Handler
    }
    
    // outboud事件默認處理過程
    public void bind(ChannelHandlerContext ctx, SocketAddress localAddress,
            ChannelPromise promise) throws Exception {
        ctx.bind(localAddress, promise);  // 事件傳播到下一個Handler
    }

在Adapter中,事件默認自動傳播到下一個Handler,這樣帶來的另一個好處是:用戶的Handler類可以繼承Adapter且覆蓋自己感興趣的事件實現,其他事件使用默認實現,不用再實現ChannelIn/outboudHandler接口中所有方法,提高效率。
我們常常遇到這樣的需求:在一個業務邏輯處理器中,需要寫數據庫、進行網絡連接等耗時業務。Netty的原則是不阻塞I/O線程,所以需指定Handler執行的線程池,可使用如下代碼:

    static final EventExecutorGroup group = new DefaultEventExecutorGroup(16);
    ...
    ChannelPipeline pipeline = ch.pipeline();
    // 簡單非阻塞業務,可以使用I/O線程執行
    pipeline.addLast("decoder", new MyProtocolDecoder());
    pipeline.addLast("encoder", new MyProtocolEncoder());
    // 復雜耗時業務,使用新的線程池
    pipeline.addLast(group, "handler", new MyBusinessLogicHandler());

ChannelHandler中有一個Sharable注解,使用該注解后多個ChannelPipeline中的Handler對象實例只有一個,從而減少Handler對象實例的創建。代碼示例如下:

    public class DataServerInitializer extends ChannelInitializer<Channel> {
       private static final DataServerHandler SHARED = new DataServerHandler();
  
       @Override
       public void initChannel(Channel channel) {
           channel.pipeline().addLast("handler", SHARED);
       }
   }

Sharable注解的使用是有限制的,多個ChannelPipeline只有一個實例,所以該Handler要求無狀態。上述示例中,DataServerHandler的事件處理方法中,不能使用或改變本身的私有變量,因為ChannelHandler是非線程安全的,使用私有變量會造成線程競爭而產生錯誤結果。

7.1.3 ChannelHandlerContext

Context指上下文關系,ChannelHandler的Context指的是ChannleHandler之間的關系以及ChannelHandler與ChannelPipeline之間的關系。ChannelPipeline中的事件傳播主要依賴于ChannelHandlerContext實現,由于ChannelHandlerContext中有ChannelHandler之間的關系,所以能得到ChannelHandler的后繼節點,從而將事件傳播到下一個ChannelHandler。
ChannelHandlerContext繼承自AttributeMap,所以提供了attr()方法設置和刪除一些狀態屬性值,用戶可將業務邏輯中所需使用的狀態屬性值存入到Context中。此外,Channel也繼承自AttributeMap,也有attr()方法,在Netty4.0中,這兩個attr()方法并不等效,這會給用戶程序員帶來困惑并且增加內存開銷,所以Netty4.1中將channel.attr()==ctx.attr()。在使用Netty4.0時,建議只使用channel.attr()防止引起不必要的困惑。
一個Channel對應一個ChannelPipeline,一個ChannelHandlerContext對應一個ChannelHandler,但一個ChannelHandler可以對應多個ChannelHandlerContext。當一個ChannelHandler使用Sharable注解修飾且添加同一個實例對象到不用的Channel時,只有一個ChannelHandler實例對象,但每個Channel中都有一個ChannelHandlerContext對象實例與之對應。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容