像以往一樣,繼續回顧這幅圖。目前為止,我們學習了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示意圖如下:
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對象實例與之對應。