netty系列之:Event、Handler和Pipeline

flydean發表於2021-08-07

簡介

上一節我們講解了netty中的Channel,知道了channel是事件處理器和外部聯通的橋樑。今天本文將會詳細講解netty的剩下幾個非常總要的部分Event、Handler和PipeLine。

ChannelPipeline

pipeLine是連線Channel和handler的橋樑,它實際上是一個filter的實現,用於控制其中handler的處理方式。

當一個channel被建立的時候,和它對應的ChannelPipeline也會被建立。

先看下ChannelPipeline的定義:

public interface ChannelPipeline
        extends ChannelInboundInvoker, ChannelOutboundInvoker, Iterable 

首先ChannelPipeline繼承自Iterable,表示它是可遍歷的,而遍歷的結果就是其中一個個的Handler。

作為一個合格的Iterable,ChannelPipeline提供了一系列的add和remote方法,通過這些方法就可以向ChannelPipeline中新增或者移除Handler。因為ChannelPipeline是一個filter,而過濾器是需要指定對應的filter的順序的,所以ChannelPipeline中有addFirst和addLast這種新增不同順序的方法。

然後可以看到ChannelPipeline繼承了ChannelInboundInvoker和ChannelOutboundInvoker兩個介面。

先看一張channelPipeline的工作流程圖:

可以看出ChannelPipeline主要有兩種操作,一種是讀入Inbound,一種是寫出OutBound。

對於Socket.read()這樣的讀入操作,呼叫的實際上就是ChannelInboundInvoker中的方法。對於外部的IO寫入的請求,呼叫的就是ChannelOutboundInvoker中的方法。

注意,Inbound和outbound的處理順序是相反的,比如下面的例子:

    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());

上面的程式碼中我們向ChannelPipeline新增了5個handler,其中2個InboundHandler,2個OutboundHandler和一個同時處理In和Out的Handler。

那麼當channel遇到inbound event的時候,就會按照1,2,3,4,5的順序進行處理,但是隻有InboundHandler才能處理Inbound事件,所以,真正執行的順序是1,2,5。

同樣的當channel遇到outbound event的時候,會按照5,4,3,2,1的順序進行執行,但是隻有outboundHandler才能處理Outbound事件,所以真正執行的順序是5,4,3.

簡單的說,ChannelPipeline指定了Handler的執行順序。

ChannelHandler

netty是一個事件驅動的框架,所有的event都是由Handler來進行處理的。ChannelHandler可以處理IO、攔截IO或者將event傳遞給ChannelPipeline中的下一個Handler進行處理。

ChannelHandler的結構很簡單,只有三個方法,分別是:

void handlerAdded(ChannelHandlerContext ctx) throws Exception;
void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;

根據inbound和outbound事件的不同,ChannelHandler可以分為兩類,分別是ChannelInboundHandler 和ChannelOutboundHandler.

因為這兩個都是interface,實現起來比較麻煩,所以netty為大家提供了三個預設的實現:ChannelInboundHandlerAdapter,ChannelOutboundHandlerAdapter和ChannelDuplexHandler。前面兩個很好理解,分別是inbound和outbound,最後一個可以同時處理inbound和outbound。

ChannelHandler是由ChannelHandlerContext提供的,並且和ChannelPipeline的互動也是通過ChannelHandlerContext來進行的。

ChannelHandlerContext

ChannelHandlerContext可以讓ChannelHandler和ChannelPipeline或者其他的Handler進行互動。它就是一個上下文環境,使得Handler和Channel可以相互作用。

如可以在ChannelHandlerContext中,呼叫channel()獲得繫結的channel。可以通過呼叫handler()獲得繫結的Handler。通過呼叫fire*方法來觸發Channel的事件。

看下ChannelHandlerContext的定義:

public interface ChannelHandlerContext extends AttributeMap, ChannelInboundInvoker, ChannelOutboundInvoker 

可以看到他是一個AttributeMap用來儲存屬性,還是一個ChannelInboundInvoker和ChannelOutboundInvoker用來觸發和傳播相應的事件。

對於Inbound來說傳播事件的方法有:

ChannelHandlerContext.fireChannelRegistered()
ChannelHandlerContext.fireChannelActive()
ChannelHandlerContext.fireChannelRead(Object)
ChannelHandlerContext.fireChannelReadComplete()
ChannelHandlerContext.fireExceptionCaught(Throwable)
ChannelHandlerContext.fireUserEventTriggered(Object)
ChannelHandlerContext.fireChannelWritabilityChanged()
ChannelHandlerContext.fireChannelInactive()
ChannelHandlerContext.fireChannelUnregistered()

對於Outbound來說傳播事件的方法有:

ChannelHandlerContext.bind(SocketAddress, ChannelPromise)
ChannelHandlerContext.connect(SocketAddress, SocketAddress, ChannelPromise)
ChannelHandlerContext.write(Object, ChannelPromise)
ChannelHandlerContext.flush()
ChannelHandlerContext.read()
ChannelHandlerContext.disconnect(ChannelPromise)
ChannelHandlerContext.close(ChannelPromise)
ChannelHandlerContext.deregister(ChannelPromise)

這些方法,在一個Handler中呼叫,然後將事件傳遞給下一個Handler,如下所示:

   public class MyInboundHandler extends ChannelInboundHandlerAdapter {
        @Override
       public void channelActive(ChannelHandlerContext ctx) {
           System.out.println("Connected!");
           ctx.fireChannelActive();
       }
   }
  
   public class MyOutboundHandler extends ChannelOutboundHandlerAdapter {
        @Override
       public void close(ChannelHandlerContext ctx, ChannelPromise promise) {
           System.out.println("Closing ..");
           ctx.close(promise);
       }
   }

ChannelHandler中的狀態變數

ChannelHandler是一個Handler類,一般情況下,這個類的例項是可以被多個channel共同使用的,前提是這個ChannelHandler沒有共享的狀態變數。

但有時候,我們必須要在ChannelHandler中保持一個狀態,那麼就涉及到ChannelHandler中的狀態變數的問題,看下面的一個例子:

  public interface Message {
       // your methods here
   }
  
   public class DataServerHandler extends SimpleChannelInboundHandler<Message> {
  
       private boolean loggedIn;
  
        @Override
       public void channelRead0(ChannelHandlerContext ctx, Message message) {
           if (message instanceof LoginMessage) {
               authenticate((LoginMessage) message);
               loggedIn = true;
           } else (message instanceof GetDataMessage) {
               if (loggedIn) {
                   ctx.writeAndFlush(fetchSecret((GetDataMessage) message));
               } else {
                   fail();
               }
           }
       }
       ...
   }

這個例子中,我們需要在收到LoginMessage之後,對訊息進行認證,並儲存認證狀態,因為業務邏輯是這樣的,所以必須要有一個狀態變數。

那麼這樣帶有狀態變數的Handler就只能繫結一個channel,如果繫結多個channel就有可能出現狀態不一致的問題。一個channel繫結一個Handler例項,很簡單,只需要在initChannel方法中使用new關鍵字新建一個物件即可。

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

那麼除了新建handler例項之外,還有沒有其他的辦法呢?當然是有的,那就是 ChannelHandlerContext 中的AttributeKey屬性。還是上面的例子,我們看一下使用AttributeKey應該怎麼實現:

   public interface Message {
       // your methods here
   }
  
    @Sharable
   public class DataServerHandler extends SimpleChannelInboundHandler<Message> {
       private final AttributeKey<Boolean> auth =
             AttributeKey.valueOf("auth");
  
        @Override
       public void channelRead(ChannelHandlerContext ctx, Message message) {
           Attribute<Boolean> attr = ctx.attr(auth);
           if (message instanceof LoginMessage) {
               authenticate((LoginMessage) o);
               attr.set(true);
           } else (message instanceof GetDataMessage) {
               if (Boolean.TRUE.equals(attr.get())) {
                   ctx.writeAndFlush(fetchSecret((GetDataMessage) o));
               } else {
                   fail();
               }
           }
       }
       ...
   }

上例中,首先定義了一個AttributeKey,然後使用ChannelHandlerContext的attr方法將Attribute設定到ChannelHandlerContext中,這樣該Attribute繫結到這個ChannelHandlerContext中了。後續即使使用同一個Handler在不同的Channel中該屬性也是不同的。

下面是使用共享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);
       }
   }

注意,在定義DataServerHandler的時候,我們加上了@Sharable註解,如果一個ChannelHandler使用了@Sharable註解,那就意味著你可以只建立一次這個Handler,但是可以將其繫結到一個或者多個ChannelPipeline中。

注意,@Sharable註解是為java文件準備的,並不會影響到實際的程式碼執行效果。

非同步Handler

之前介紹了,可以通過呼叫pipeline.addLast方法將handler加入到pipeline中,因為pipeline是一個filter的結構,所以加入的handler是順序進行處理的。

但是,我希望某些handler是在新的執行緒中執行該怎麼辦?如果我們希望這些新的執行緒中執行的Handler是無序的又該怎麼辦?

比如我們現在有3個handler分別是MyHandler1,MyHandler2和MyHandler3。

順序執行的寫法是這樣的:

ChannelPipeline pipeline = ch.pipeline();
  
   pipeline.addLast("MyHandler1", new MyHandler1());
   pipeline.addLast("MyHandler2", new MyHandler2());
   pipeline.addLast("MyHandler3", new MyHandler3());

如果要讓MyHandler3在新的執行緒中執行,則可以加入group選項,從而讓handler在新的group中執行:

static final EventExecutorGroup group = new DefaultEventExecutorGroup(16);
ChannelPipeline pipeline = ch.pipeline();
  
   pipeline.addLast("MyHandler1", new MyHandler1());
   pipeline.addLast("MyHandler2", new MyHandler2());
   pipeline.addLast(group,"MyHandler3", new MyHandler3());

但是上例中DefaultEventExecutorGroup加入的Handler也是會順序執行的,如果確實不想順序執行,那麼可以嘗試考慮使用UnorderedThreadPoolEventExecutor 。

總結

本文講解了Event、Handler和PipeLine,並舉例說明他們之間的關係和相互作用。後續會從netty的具體實踐出發,進一步加深對netty的理解和應用,希望大家能夠喜歡。

本文已收錄於 http://www.flydean.com/05-netty-channelevent/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章