Netty入門與實戰——pipeline 與 channelHandler

Ps:此係列文章來源於Netty 入門與實戰:仿寫微信 IM 即時通訊系統,有能力的還請支持正版,一頓飯錢,絕對物有所值

*如何避免 else 氾濫

不管是服務端還是客戶端,處理流程大致分爲以下幾個步驟

把這三類邏輯都寫在一個類裏面,客戶端寫在 ClientHandler,服務端寫在 ServerHandler,如果要做功能的擴展(比如,我們要校驗 magic number,或者其他特殊邏輯),只能在一個類裏面去修改, 這個類就會變得越來越臃腫。

另外,我們注意到,每次發指令數據包都要手動調用編碼器編碼成 ByteBuf,對於這類場景的編碼優化,我們能想到的辦法自然是模塊化處理,不同的邏輯放置到單獨的類來處理,最後將這些邏輯串聯起來,形成一個完整的邏輯處理鏈。

Netty 中的 pipeline 和 channelHandler 正是用來解決這個問題的:它通過責任鏈設計模式來組織代碼邏輯,並且能夠支持邏輯的動態添加和刪除 ,Netty 能夠支持各類協議的擴展,比如 HTTP,Websocket,Redis,靠的就是 pipeline 和 channelHandler。

*pipeline 與 channelHandler 的構成

無論是從服務端來看,還是客戶端來看,在 Netty 整個框架裏面,一條連接對應着一個 Channel,這條 Channel 所有的處理邏輯都在一個叫做 ChannelPipeline 的對象裏面,ChannelPipeline 是一個雙向鏈表結構,他和 Channel 之間是一對一的關係。

ChannelPipeline 裏面每個節點都是一個 ChannelHandlerContext 對象,這個對象能夠拿到和 Channel 相關的所有的上下文信息,然後這個對象包着一個重要的對象,那就是邏輯處理器 ChannelHandler

*channelHandler 的分類

 

可以看到 ChannelHandler 有兩大子接口:

第一個子接口是 ChannelInboundHandler,從字面意思也可以猜到,他是處理讀數據的邏輯,比如,我們在一端讀到一段數據,首先要解析這段數據,然後對這些數據做一系列邏輯處理,最終把響應寫到對端, 在開始組裝響應之前的所有的邏輯,都可以放置在 ChannelInboundHandler 裏處理,它的一個最重要的方法就是 channelRead()。讀者可以將 ChannelInboundHandler 的邏輯處理過程與 TCP 的七層協議的解析聯繫起來,收到的數據一層層從物理層上升到我們的應用層。

第二個子接口 ChannelOutBoundHandler 是處理寫數據的邏輯,它是定義我們一端在組裝完響應之後,把數據寫到對端的邏輯,比如,我們封裝好一個 response 對象,接下來我們有可能對這個 response 做一些其他的特殊邏輯,然後,再編碼成 ByteBuf,最終寫到對端,它裏面最核心的一個方法就是 write(),讀者可以將 ChannelOutBoundHandler 的邏輯處理過程與 TCP 的七層協議的封裝過程聯繫起來,我們在應用層組裝響應之後,通過層層協議的封裝,直到最底層的物理層。

這兩個子接口分別有對應的默認實現,ChannelInboundHandlerAdapter,和 ChanneloutBoundHandlerAdapter,它們分別實現了兩大接口的所有功能,默認情況下會把讀寫事件傳播到下一個 handler。

*ChannelInboundHandler 的事件傳播

 

在服務端的 pipeline 添加三個 ChannelInboundHandler

NettyServer.java

serverBootstrap
        .childHandler(new ChannelInitializer<NioSocketChannel>() {
            protected void initChannel(NioSocketChannel ch) {
                ch.pipeline().addLast(new InBoundHandlerA());
                ch.pipeline().addLast(new InBoundHandlerB());
                ch.pipeline().addLast(new InBoundHandlerC());
            }
        });

每個 inBoundHandler 都繼承自 ChannelInboundHandlerAdapter,然後實現了 channelRead() 方法

public class InBoundHandlerA extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("InBoundHandlerA: " + msg);
        super.channelRead(ctx, msg);
    }
}

public class InBoundHandlerB extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("InBoundHandlerB: " + msg);
        super.channelRead(ctx, msg);
    }
}

public class InBoundHandlerC extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("InBoundHandlerC: " + msg);
        super.channelRead(ctx, msg);
    }
}

 

channelRead() 方法裏面,我們打印當前 handler 的信息,然後調用父類的 channelRead() 方法,而這裏父類的 channelRead() 方法會自動調用到下一個 inBoundHandler 的 channelRead() 方法,並且會把當前 inBoundHandler 裏處理完畢的對象傳遞到下一個 inBoundHandler,我們例子中傳遞的對象都是同一個 msg。

我們通過 addLast() 方法來爲 pipeline 添加 inBoundHandler,inBoundHandler 的執行順序與我們通過 addLast() 方法 添加的順序保持一致。

*ChannelOutboundHandler 的事件傳播

在服務端的 pipeline 添加三個 ChanneloutBoundHandler

serverBootstrap
        .childHandler(new ChannelInitializer<NioSocketChannel>() {
            protected void initChannel(NioSocketChannel ch) {
                // inBound,處理讀數據的邏輯鏈
                ch.pipeline().addLast(new InBoundHandlerA());
                ch.pipeline().addLast(new InBoundHandlerB());
                ch.pipeline().addLast(new InBoundHandlerC());
                
                // outBound,處理寫數據的邏輯鏈
                ch.pipeline().addLast(new OutBoundHandlerA());
                ch.pipeline().addLast(new OutBoundHandlerB());
                ch.pipeline().addLast(new OutBoundHandlerC());
            }
        });

每個 outBoundHandler 都繼承自 ChanneloutBoundHandlerAdapter,然後實現了 write() 方法

public class OutBoundHandlerA extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println("OutBoundHandlerA: " + msg);
        super.write(ctx, msg, promise);
    }
}

public class OutBoundHandlerB extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println("OutBoundHandlerB: " + msg);
        super.write(ctx, msg, promise);
    }
}

public class OutBoundHandlerC extends ChannelOutboundHandlerAdapter {
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println("OutBoundHandlerC: " + msg);
        super.write(ctx, msg, promise);
    }
}

write() 方法裏面,我們打印當前 handler 的信息,然後調用父類的 write() 方法,而這裏父類的 write() 方法會自動調用到下一個 outBoundHandler 的 write() 方法,並且會把當前 outBoundHandler 裏處理完畢的對象傳遞到下一個 outBoundHandler。outBoundHandler 的執行順序與我們添加的順序相反

*pipeline 的結構

不管我們定義的是哪種類型的 handler, 最終它們都是以雙向鏈表的方式連接,這裏實際鏈表的節點是 ChannelHandlerContext,這裏爲了讓結構清晰突出,可以直接把節點看作 ChannelHandlerContext

*pipeline 的執行順序

image.png

 

 

雖然兩種類型的 handler 在一個雙向鏈表裏,但是這兩類 handler 的分工是不一樣的,inBoundHandler 的事件通常只會傳播到下一個 inBoundHandler,outBoundHandler 的事件通常只會傳播到下一個 outBoundHandler,兩者相互不受干擾。

*總結

  1. 通過前面編寫客戶端服務端處理邏輯,引出了 pipeline 和 channelHandler 的概念。
  2. channelHandler 分爲 inBound 和 outBound 兩種類型的接口,分別是處理數據讀與數據寫的邏輯,可與 tcp 協議棧聯繫起來。
  3. 兩種類型的 handler 均有相應的默認實現,默認會把事件傳遞到下一個,這裏的傳遞事件其實說白了就是把本 handler 的處理結果傳遞到下一個 handler 繼續處理。
  4. inBoundHandler 的執行順序與我們實際的添加順序相同,而 outBoundHandler 則相反。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章