Pipeline Handler HandlerContext 創建源碼剖析
ChannelPipeline 調度 handler 的源碼剖析
源碼剖析目的
Netty 中的 ChannelPipeline 、 ChannelHandler 和 ChannelHandlerContext 是非常核心的組件, 我們從源碼來分析Netty 是如何設計這三個核心組件的,並分析是如何創建和協調工作的.
ChannelPipeline
| ChannelHandler
| ChannelHandlerContext
介紹
1.1 三者關係
-
每當 ServerSocket 創建一個新的連接,就會創建一個 Socket,對應的就是目標客戶端。
-
每一個新創建的 Socket 都將會分配一個全新的 ChannelPipeline(以下簡稱 pipeline)
-
每一個 ChannelPipeline 內部都含有多個 ChannelHandlerContext(以下簡稱 Context)
-
他們一起組成了雙向鏈表,這些 Context 用於包裝我們調用 addLast 方法時添加的 ChannelHandler(以下簡稱 handler)
- 上圖中:ChannelSocket 和 ChannelPipeline 是一對一的關聯關係,而 pipeline 內部的多個 Context 形成了鏈表,Context 只是對 Handler 的封裝。
- 當一個請求進來的時候,會進入 Socket 對應的 pipeline,並經過 pipeline 所有的 handler,對,就是設計模式中的過濾器模式。
1.2 ChannelPipeline 作用及設計
1)pipeline 的接口設計
部分源碼
可以看到該接口繼承了 inBound,outBound,Iterable 接口,表示他可以調用數據出站的方法和入站的方法,同時也能遍歷內部的鏈表, 看看他的幾個代表性的方法,基本上都是針對 handler 鏈表的插入,追加,刪除,替換操作,類似是一個 LinkedList。同時,也能返回 channel(也就是 socket)
1)在 pipeline 的接口文檔上,提供了一幅圖
數據流向pipeline 是入棧,流出pipeline 是出棧
對上圖的解釋說明:
-
這是一個 handler 的 list,handler 用於處理或攔截入站事件和出站事件,pipeline 實現了過濾器的高級形式,以便用戶控制事件如何處理以及 handler 在 pipeline 中如何交互。
-
上圖描述了一個典型的 handler 在 pipeline 中處理 I/O 事件的方式,IO 事件由 inboundHandler 或者 outBoundHandler 處理,並通過調用
ChannelHandlerContext.fireChannelRead
方法轉發給其最近的handler 處理程序 。
-
入站事件由入站處理程序以自下而上的方向處理,如圖所示。入站處理程序通常處理由圖底部的I / O線程生成入站數據。入站數據通常從如SocketChannel.read(ByteBuffer) 獲取。
-
通常一個 pipeline 有多個 handler,例如,一個典型的服務器在每個通道的管道中都會有以下處理程序
協議解碼器 - 將二進制數據轉換爲Java對象。
協議編碼器 - 將Java對象轉換爲二進制數據。
業務邏輯處理程序 - 執行實際業務邏輯(例如數據庫訪問) -
你的業務程序不能將線程阻塞,會影響 IO 的速度,進而影響整個 Netty 程序的性能。如果你的業務程序很快,就可以放在 IO 線程中,反之,你需要異步執行。或者在添加 handler 的時候添加一個線程池,
例如:
// 下面這個任務執行的時候,將不會阻塞 IO 線程,執行的線程來自 group 線程池
pipeline.addLast(group,“handler”,new MyBusinessLogicHandler());
或者放taskQueue或者scheduleTaskQueue中中
1.3 ChannelHandler 作用及設計
- 源碼
public interface ChannelHandler {
//當把 ChannelHandler 添加到 pipeline 時被調用
void handlerAdded(ChannelHandlerContext ctx) throws Exception;
//當從 pipeline 中移除時調用
void handlerRemoved(ChannelHandlerContext ctx) throws Exception;
// 當處理過程中在 pipeline 發生異常時調用
@Deprecated
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;
}
ChannelHandler 的作用就是處理 IO 事件或攔截 IO 事件,並將其轉發給下一個處理程序 ChannelHandler。
Handler 處理事件時分入站和出站的,兩個方向的操作都是不同的,因此,Netty 定義了兩個子接口繼承 ChannelHandler
2)ChannelInboundHandler
入站事件接口
-
channelActive 用於當 Channel 處於活動狀態時被調用;
-
channelRead 當從Channel 讀取數據時被調用等等方法。
-
程序員需要重寫一些方法,當發生關注的事件,需要在方法中實現我們的業務邏輯,因爲當事件發生時,Netty 會回調對應的方法。
3)ChannelOutboundHandler
出站事件接口
- bind 方法,當請求將 Channel 綁定到本地地址時調用
- close 方法,當請求關閉 Channel 時調用等等
- 出站操作都是一些連接和寫出數據類似的方法。
4)ChannelDuplexHandler
處理出站和入站事件
- ChannelDuplexHandler 間接實現了入站接口並直接實現了出站接口。
- 是一個通用的能夠同時處理入站事件和出站事件的類。
1.4 ChannelHandlerContext 作用及設計
- ChannelHandlerContext UML圖
ChannelHandlerContext 繼承了出站方法調用接口和入站方法調用接口
1)ChannelOutboundInvoker
和 ChannelInboundInvoker
部分源碼
- 這兩個 invoker 就是針對入站或出站方法來的,就是在 入站或出站 handler 的外層再包裝一層,達到在方法前後攔截並做一些特定操作的目的
2)ChannelHandlerContext
部分源碼
- ChannelHandlerContext 不僅僅時繼承了他們兩個的方法,同時也定義了一些自己的方法
- 這些方法能夠獲取 Context 上下文環境中對應的比如 channel,executor,handler ,pipeline,內存分配器,關聯的 handler 是否被刪除。
- Context 就是包裝了 handler 相關的一切,以方便 Context 可以在 pipeline 方便的操作 handler
ChannelPipeline
| ChannelHandler
| ChannelHandlerContext
創建過程
分爲3個步驟來看創建的過程:
-
任何一個 ChannelSocket 創建的同時都會創建 一個 pipeline。
-
當用戶或系統內部調用 pipeline 的 add*** 方法添加 handler 時,都會創建一個包裝這 handler 的 Context。
-
這些 Context 在 pipeline 中組成了雙向鏈表。
2.1 Socket 創建的時候創建 pipeline;在 SocketChannel 的抽象父類 AbstractChannel 的構造方法中
protected AbstractChannel(Channel parent) {
this.parent = parent; //斷點測試
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
Debug 一下, 可以看到代碼會執行到這裏, 然後繼續追蹤到
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;
}
說明:
1) 將 channel 賦值給 channel 字段,用於 pipeline 操作 channel。
2) 創建一個 future 和 promise,用於異步回調使用。
3) 創建一個 inbound 的 tailContext
,創建一個既是 inbound 類型又是 outbound 類型的 headContext
.
4) 最後,將兩個 Context 互相連接,形成雙向鏈表。
5) tailContext 和 HeadContext 非常的重要,所有 pipeline 中的事件都會流經他們,
2.2 在 add** 添加 Handler 處理器的時候創建 Context** 看下 DefaultChannelPipeline 的 addLast 方法如何創建的 Context,代碼如下
@Override
public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) {
if (handlers == null) { //斷點
throw new NullPointerException("handlers");
}
for (ChannelHandler h: handlers) {
if (h == null) {
break;
}
addLast(executor, null, h);
}
return this;
}
繼續Debug
public final ChannelPipeline addLast(EventExecutorGroup group, String name, ChannelHandler handler) {
final AbstractChannelHandlerContext newCtx;
synchronized (this) {
checkMultiplicity(handler);
newCtx = newContext(group, filterName(name, handler), handler);//
addLast0(newCtx);
if (!registered) {
newCtx.setAddPending();
callHandlerCallbackLater(newCtx, true);
return this;
}
EventExecutor executor = newCtx.executor();
if (!executor.inEventLoop()) {
newCtx.setAddPending();
executor.execute(new Runnable() {
@Override
public void run() {
callHandlerAdded0(newCtx);
}
});
return this;
}
}
callHandlerAdded0(newCtx);
return this;
}
說明
- pipeline 添加 handler,參數是線程池,name 是null, handler 是我們或者系統傳入的handler。Netty 爲了防止多個線程導致安全問題,同步了這段代碼,步驟如下:
- 檢查這個 handler 實例是否是共享的,如果不是,並且已經被別的 pipeline 使用了,則拋出異常。
- 調用
newContext(group, filterName(name, handler), handler)
方法,創建一個 Context。從這裏可以看出來了,每次添加一個 handler 都會創建一個關聯 Context。 - 調用 addLast 方法,將 Context 追加到鏈表中。
- 如果這個通道還沒有註冊到 selecor 上,就將這個 Context 添加到這個 pipeline 的待辦任務中。當註冊好了以後,就會調用 callHandlerAdded0 方法(默認是什麼都不做,用戶可以實現這個方法)。
- 到這裏,針對三對象創建過程,瞭解的差不多了,和最初說的一樣,每當創建 ChannelSocket 的時候都會創建一個綁定的 pipeline,一對一的關係,創建 pipeline 的時候也會創建 tail 節點和 head 節點,形成最初的鏈表。tail 是入站 inbound 類型的 handler, head 既是 inbound 也是 outbound 類型的 handler。在調用 pipeline 的 addLast 方法的時候,會根據給定的 handler 創建一個 Context,然後,將這個 Context 插入到鏈表的尾端(tail 前面)。
ChannelPipeline
是如何調度 handler 的源碼剖析
DefaultChannelPipeline 是如何實現這些 fire 方法的
1.1 DefaultChannelPipeline
源碼
public class DefaultChannelPipeline implements ChannelPipeline {
@Override
public final ChannelPipeline fireChannelActive() {
AbstractChannelHandlerContext.invokeChannelActive(head);
return this;
}
@Override
public final ChannelPipeline fireChannelInactive() {
AbstractChannelHandlerContext.invokeChannelInactive(head);
return this;
}
@Override
public final ChannelPipeline fireExceptionCaught(Throwable cause) {
AbstractChannelHandlerContext.invokeExceptionCaught(head, cause);
return this;
}
@Override
public final ChannelPipeline fireUserEventTriggered(Object event) {
AbstractChannelHandlerContext.invokeUserEventTriggered(head, event);
return this;
}
@Override
public final ChannelPipeline fireChannelRead(Object msg) {
AbstractChannelHandlerContext.invokeChannelRead(head, msg);
return this;
}
@Override
public final ChannelPipeline fireChannelReadComplete() {
AbstractChannelHandlerContext.invokeChannelReadComplete(head);
return this;
}
@Override
public final ChannelPipeline fireChannelWritabilityChanged() {
AbstractChannelHandlerContext.invokeChannelWritabilityChanged(head);
return this;
}
}
說明:
可以看出來,這些方法都是 inbound 的方法,也就是入站事件,調用靜態方法傳入的也是 inbound 的類型 head handler。這些靜態方法則會調用 head 的 ChannelInboundInvoker 接口的方法,再然後調用 handler 的真正方法
1.2 再看下piepline 的 outbound 的 fire 方法實現源碼
public class DefaultChannelPipeline implements ChannelPipeline {
@Override
public final ChannelFuture bind(SocketAddress localAddress) {
return tail.bind(localAddress);
}
@Override
public final ChannelFuture connect(SocketAddress remoteAddress) {
return tail.connect(remoteAddress);
}
@Override
public final ChannelFuture connect(SocketAddress remoteAddress, SocketAddress localAddress) {
return tail.connect(remoteAddress, localAddress);
}
@Override
public final ChannelFuture disconnect() {
return tail.disconnect();
}
@Override
public final ChannelFuture close() {
return tail.close();
}
@Override
public final ChannelFuture deregister() {
return tail.deregister();
}
@Override
public final ChannelPipeline flush() {
tail.flush();
return this;
}
@Override
public final ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {
return tail.bind(localAddress, promise);
}
@Override
public final ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
return tail.connect(remoteAddress, promise);
}
@Override
public final ChannelFuture connect(
SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
return tail.connect(remoteAddress, localAddress, promise);
}
@Override
public final ChannelFuture disconnect(ChannelPromise promise) {
return tail.disconnect(promise);
}
}
說明:
- 這些都是出站的實現,但是調用的是 outbound 類型的 tail handler 來進行處理,因爲這些都是 outbound 事件。
- 出站是 tail 開始,入站從 head 開始。因爲出站是從內部外面寫,從tail 開始,能夠讓前面的 handler 進行處理,防止由 handler 被遺漏,比如編碼。反之,入站當然是從 head 往內部輸入,讓後面的 handler 能夠處理這些輸入的數據。比如解碼。因此雖然 head 也實現了 outbound 接口,但不是從 head 開始執行出站任務
2.關於如何調度,用一張圖來表示:
說明:
- pipeline 首先會調用 Context 的靜態方法 fireXXX,並傳入 Context
- 然後,靜態方法調用 Context 的 invoker 方法,而 invoker 方法內部會調用該 Context 所包含的 Handler 的真正的 XXX 方法,調用結束後,如果還需要繼續向後傳遞,就調用 Context 的 fireXXX2 方法,循環往復。責任鏈模式