Netty學習筆記之ChannelHandler


ChannelHandler是netty中的核心處理部分,我們使用netty的絕大部分代碼都寫在這部分,所以了解它的一些機制和特性是很有必要的

Channel

Channel接口抽象了底層socket的一些狀態屬性以及調用方法


 
image

針對不同類型的socket提供不同的子類實現。


 
image

Channel生命周期

 
image

ChannelHandler

ChannelHandler用於處理Channel對應的事件
ChannelHandler接口里面只定義了三個生命周期方法,我們主要實現它的子接口ChannelInboundHandler和ChannelOutboundHandler,為了便利,框架提供了ChannelInboundHandlerAdapter,ChannelOutboundHandlerAdapter和ChannelDuplexHandler這三個適配類,在使用的時候只需要實現你關注的方法即可

ChannelHandler生命周期方法

 
image

ChannelHandler里面定義三個生命周期方法,分別會在當前ChannelHander加入ChannelHandlerContext中,從ChannelHandlerContext中移除,以及ChannelHandler回調方法出現異常時被回調

ChannelInboundHandler

 
image

介紹一下這些回調方法被觸發的時機

回調方法 觸發時機 client server
channelRegistered 當前channel注冊到EventLoop true true
channelUnregistered 當前channel從EventLoop取消注冊 true true
channelActive 當前channel激活的時候 true true
channelInactive 當前channel不活躍的時候,也就是當前channel到了它生命周期末 true true
channelRead 當前channel從遠端讀取到數據 true true
channelReadComplete channel read消費完讀取的數據的時候被觸發 true true
userEventTriggered 用戶事件觸發的時候
channelWritabilityChanged channel的寫狀態變化的時候觸發

可以注意到每個方法都帶了ChannelHandlerContext作為參數,具體作用是,在每個回調事件里面,處理完成之后,使用ChannelHandlerContext的fireChannelXXX方法來傳遞給下個ChannelHandler,netty的codec模塊和業務處理代碼分離就用到了這個鏈路處理

ChannelOutboundHandler

 
image.png
回調方法 觸發時機 client server
bind bind操作執行前觸發 false true
connect connect 操作執行前觸發 true false
disconnect disconnect 操作執行前觸發 true false
close close操作執行前觸發 false true
deregister deregister操作執行前觸發
read read操作執行前觸發 true true
write write操作執行前觸發 true true
flush flush操作執行前觸發 true true

注意到一些回調方法有ChannelPromise這個參數,我們可以調用它的addListener注冊監聽,當回調方法所對應的操作完成后,會觸發這個監聽
下面這個代碼,會在寫操作完成后觸發,完成操作包括成功和失敗

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { ctx.write(msg,promise); System.out.println("out write"); promise.addListener(new GenericFutureListener<Future<? super Void>>() { @Override public void operationComplete(Future<? super Void> future) throws Exception { if(future.isSuccess()){ System.out.println("OK"); } } }); } 

ChannelInboundHandler和ChannelOutboundHandler的區別

個人感覺in和out的區別主要在於ChannelInboundHandler的channelRead和channelReadComplete回調和ChannelOutboundHandler的write和flush回調上,ChannelOutboundHandler的channelRead回調負責執行入棧數據的decode邏輯,ChannelOutboundHandler的write負責執行出站數據的encode工作。其他回調方法和具體觸發邏輯有關,和in與out無關。

ChannelHandlerContext

每個ChannelHandler通過add方法加入到ChannelPipeline中去的時候,會創建一個對應的ChannelHandlerContext,並且綁定,ChannelPipeline實際維護的是ChannelHandlerContext 的關系
在DefaultChannelPipeline源碼中可以看到會保存第一個ChannelHandlerContext以及最后一個ChannelHandlerContext的引用

final AbstractChannelHandlerContext head; final AbstractChannelHandlerContext tail; 

而在AbstractChannelHandlerContext源碼中可以看到

volatile AbstractChannelHandlerContext next; volatile AbstractChannelHandlerContext prev; 

每個ChannelHandlerContext之間形成雙向鏈表

ChannelPipeline

在Channel創建的時候,會同時創建ChannelPipeline

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

在ChannelPipeline中也會持有Channel的引用

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

ChannelPipeline會維護一個ChannelHandlerContext的雙向鏈表

final AbstractChannelHandlerContext head; final AbstractChannelHandlerContext tail; 

鏈表的頭尾有默認實現

protected DefaultChannelPipeline(Channel channel) { this.channel = ObjectUtil.checkNotNull(channel, "channel"); succeededFuture = new SucceededChannelFuture(channel, null); voidPromise = new VoidChannelPromise(channel, true); tail = new TailContext(this); head = new HeadContext(this); head.next = tail; tail.prev = head; } 

我們添加的自定義ChannelHandler會插入到head和tail之間,如果是ChannelInboundHandler的回調,根據插入的順序從左向右進行鏈式調用,ChannelOutboundHandler則相反

具體關系如下,但是下圖沒有把默認的head和tail畫出來,這兩個ChannelHandler做的工作相當重要


 
image

上面的整條鏈式的調用是通過Channel接口的方法直接觸發的,如果使用ChannelContextHandler的接口方法間接觸發,鏈路會從ChannelContextHandler對應的ChannelHandler開始,而不是從頭或尾開始

HeadContext

HeadContext實現了ChannelOutboundHandler,ChannelInboundHandler這兩個接口

class HeadContext extends AbstractChannelHandlerContext implements ChannelOutboundHandler, ChannelInboundHandler 

因為在頭部,所以說HeadContext中關於in和out的回調方法都會觸發
關於ChannelInboundHandler,HeadContext的作用是進行一些前置操作,以及把事件傳遞到下一個ChannelHandlerContext的ChannelInboundHandler中去
看下其中channelRegistered的實現

public void channelRegistered(ChannelHandlerContext ctx) throws Exception { invokeHandlerAddedIfNeeded(); ctx.fireChannelRegistered(); } 

從語義上可以看出來在把這個事件傳遞給下一個ChannelHandler之前會回調ChannelHandler的handlerAdded方法
而有關ChannelOutboundHandler接口的實現,會在鏈路的最后執行,看下write方法的實現

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { unsafe.write(msg, promise); } 

這邊的unsafe接口封裝了底層Channel的調用,之所以取名為unsafe,是不需要用戶手動去調用這些方法。這個和阻塞原語的unsafe不是同一個
也就是說,當我們通過Channel接口執行write之后,會執行ChannelOutboundHandler鏈式調用,在鏈尾的HeadContext ,在通過unsafe回到對應Channel做相關調用
從netty Channel接口的實現就能論證這個

public ChannelFuture write(Object msg) { return pipeline.write(msg); } 

TailContext

TailContext實現了ChannelInboundHandler接口,會在ChannelInboundHandler調用鏈最后執行,只要是對調用鏈完成處理的情況進行處理,看下channelRead實現

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { onUnhandledInboundMessage(msg); } 

如果我們自定義的最后一個ChannelInboundHandler,也把處理操作交給下一個ChannelHandler,那么就會到TailContext,在TailContext會提供一些默認處理

protected void onUnhandledInboundMessage(Object msg) { try { logger.debug( "Discarded inbound message {} that reached at the tail of the pipeline. " + "Please check your pipeline configuration.", msg); } finally { ReferenceCountUtil.release(msg); } } 

比如channelRead中的onUnhandledInboundMessage方法,會把msg資源回收,防止內存泄露

強調一點的是,如果要執行整個鏈路,必須通過調用Channel方法觸發,ChannelHandlerContext引用了ChannelPipeline,所以也能間接操作channel的方法,但是會從當前ChannelHandlerContext綁定的ChannelHandler作為起點開始,而不是ChannelHandlerContext的頭和尾
這個特性在不需要調用整個鏈路的情況下可以使用,可以增加一些效率

上述組件之間的關系

 
 
  1. 每個Channel會綁定一個ChannelPipeline,ChannelPipeline中也會持有Channel的引用
  2. ChannelPipeline持有ChannelHandlerContext鏈路,保留ChannelHandlerContext的頭尾節點指針
  3. 每個ChannelHandlerContext會對應一個ChannelHandler,也就相當於ChannelPipeline持有ChannelHandler鏈路
  4. ChannelHandlerContext同時也會持有ChannelPipeline引用,也就相當於持有Channel引用
  5. ChannelHandler鏈路會根據Handler的類型,分為InBound和OutBound兩條鏈路

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM