深入netty之Pipeline 設計原理
上一篇文章中我們已經知道了在 Netty 中每個 Channel 都有且僅有一個 ChannelPipeline 與之對應,它們的組成關係如下:
通過上圖我們可以看到,一個 Channel 包含了一個 ChannelPipeline,而 ChannelPipeline 中又維護了一個由 ChannelHandlerContext 組成的雙向鏈表。這個鏈表的頭是 HeadContext,鏈表的尾是 TailContext,並且每個 ChannelHandlerContext 中又關聯着一個 ChannelHandler
接下來我們從源碼角度去看一下netty中是如何設計Pipeline 的,從AbstractChannel的構造器方法開始
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
AbstractChannel 有一個 pipeline 字段,在構造器中會初始化它爲 DefaultChannelPipeline 的實例
接着我們跟蹤一下 DefaultChannelPipeline 的初始化過程,首先進入到 DefaultChannelPipeline 構造器中:
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;
}
在 DefaultChannelPipeline 構造器中, 首先將與之關聯的 Channel 保存到字段 channel 中。然後實例化兩個ChannelHandlerContext:一個是 HeadContext 實例 head,另一個是 TailContext 實例 tail。接着將 head 和 tail 互相指向, 構成一個雙向鏈表
TailContext 類層次結構:
HeadContext 類層次結構:
從類層次結構圖中可以很清楚地看到,head 實現了 ChannelInboundHandler 接口,而 tail 實現了 ChannelOutboundHandler 接口,並且它們都實現了 ChannelHandlerContext 接口, 因此可以說 head 和 tail 即是一個 ChannelHandler,又是一個 ChannelHandlerContext
ChannelInitializer 的添加
到目前爲止,這個 Pipeline 還並不能實現什麼特殊的功能,因爲我們還 沒有給它添加自定義的 ChannelHandler
通常來說,我們在初始化 Bootstrap,會添加我們自定義的 ChannelHandler, 以客戶端啓動代碼片段來舉例:
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new ChatClientHandler(nickName));
}
});
上面代碼的初始化過程,相信大家都不陌生。在調用 handler 時,傳入了一個 ChannelInitializer 對象,它提供了一個 initChannel()方法給我我們初始化 ChannelHandler,這個初始化過程是怎樣的呢?又是在什麼時候添加到 ChannelPipeline 中的呢?
Bootstrap 的 init()方法中添加到 ChannelPipeline 中的,其代碼如下:
void init(Channel channel) throws Exception {
ChannelPipeline p = channel.pipeline();
p.addLast(config.handler());
...
}
從上面的代碼可見,Bootstrap 將 config.handler()返回的 ChannelHandler 添加到 Pipeline 中,而 handler()返回的其實就是我們在初始化 Bootstrap 時通過 handler()方法設置的 ChannelInitializer 實例(ChannelInitializer 實現了 ChannelInboundHandlerAdapter),因此這裏其實就是將 ChannelInitializer 插入到了 Pipeline 的末端。此時 Pipeline 的結構如下圖所示
可是我們在客戶端代碼中插入的是一個 ChannelInitializer 實例,爲什麼在 ChannelPipeline 中的雙向鏈表中的元素卻是一個 ChannelHandlerContext 呢?我們需要繼續去源碼中尋找答案
剛纔,我們提到,在 Bootstrap 的 init()方法中會調用 p.addLast()方法,將 ChannelInitializer 插入到鏈表的末端:
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);
}
return this;
}
addLast()方法最終落在DefaultChannelPipeline中,在addLast()方法中首先檢查 ChannelHandler 的名字是否是重複,如果不重複,則調用 newContex()方法爲 Handler 創建一個對應的 DefaultChannelHandlerContext 實例,並將兩者關聯起來(Context 中有一個 handler 屬性保存着對應的 Handler 實例)。
爲了添加一個 handler 到 pipeline 中,必須把此 handler 包裝成 ChannelHandlerContext。
newContext()方法會構建一個DefaultChannelHandlerContext 對象,構造器如下:
DefaultChannelHandlerContext(
DefaultChannelPipeline pipeline, EventExecutor executor, String name, ChannelHandler handler) {
super(pipeline, executor, name, isInbound(handler), isOutbound(handler));
if (handler == null) {
throw new NullPointerException("handler");
}
this.handler = handler;
}
自定義 ChannelHandler 的添加過程
前面我們已經分析了 ChannelInitializer 是如何插入到 Pipeline 中的,接下來就來探討 ChannelInitializer 在哪裏被調用,ChannelInitializer 的作用,以及我們自定義的 ChannelHandler 是如何插入到 Pipeline 中的
先簡單複習一下 Channel 的註冊過程:
1、首先在 AbstractBootstrap 的 initAndRegister()中,通過 group().register(channel),調用 MultithreadEventLoopGroup 的 register()方法。
2、在 MultithreadEventLoopGroup 的 register()中調用 next()獲取一個可用的 SingleThreadEventLoop,然後調用 它的 register()方法。
3、在 SingleThreadEventLoop 的 register()方法中,通過 channel.unsafe().register(this, promise)方法獲取 channel 的 unsafe()底層 IO 操作對象,然後調用它的 register()。
4、在 AbstractUnsafe 的 register()方法中,調用 register0()方法註冊 Channel 對象。
5、在 register0()方法中,調用 AbstractNioChannel 的 doRegister()方法。
6、doRegister()方法將 Channel 對應的 Java NIO 的 SockerChannel 對象註冊到一個 eventLoop 的 Selector 中,並且將當前 Channel 作爲 attachment
自定義 ChannelHandler 的添加過程,就發生在 步驟四 AbstractUnsafe 的 register0()方法中,在這個方法中調用了 pipeline.fireChannelRegistered()方法,其代碼實現如下:
private void register0(ChannelPromise promise) {
...
pipeline.fireChannelRegistered();
...
}
fireChannelRegistered()方法代碼實現如下:
public final ChannelPipeline fireChannelRegistered() {
AbstractChannelHandlerContext.invokeChannelRegistered(head);
return this;
}
再看 AbstractChannelHandlerContext 的 invokeChannelRegistered()方法:
static void invokeChannelRegistered(final AbstractChannelHandlerContext next) {
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
next.invokeChannelRegistered();
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRegistered();
}
});
}
}
顯然,這個代碼會從 head 開始遍歷 Pipeline 的雙向鏈表,然後找到第一個屬性 inbound 爲 true 的 ChannelHandlerContext 實例。而ChannelInitializer 實現的是 ChannelInboudHandler,因此它所對應 的 ChannelHandlerContext 的 inbound 屬性就是 true,因此這裏返回就是 ChannelInitializer 實例所對應的 ChannelHandlerContext 對象,如下圖所示:
當獲取到 inbound(ChannelInitializer) 的 Context 後,就調用它的 invokeChannelRegistered()方法:
private void invokeChannelRegistered() {
if (invokeHandler()) {
try {
((ChannelInboundHandler) handler()).channelRegistered(this);
} catch (Throwable t) {
notifyHandlerException(t);
}
} else {
fireChannelRegistered();
}
}
我們知道每個 ChannelHandler 都和一個 ChannelHandlerContext 關聯,很明顯這裏 handler()返回的對象就是我們實例化的 ChannelInitializer 對象,接着調用了 ChannelInitializer 的 channelRegistered()方法,繼續看代碼:
public final void channelRegistered(ChannelHandlerContext ctx) throws Exception {
if (initChannel(ctx)) {
ctx.pipeline().fireChannelRegistered();
} else {
ctx.fireChannelRegistered();
}
}
private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
if (initMap.putIfAbsent(ctx, Boolean.TRUE) == null) { // Guard against re-entrance.
try {
initChannel((C) ctx.channel());
} catch (Throwable cause) {
exceptionCaught(ctx, cause);
} finally {
remove(ctx);
}
return true;
}
return false;
}
注意initChannel()方法中調用了initChannel(© ctx.channel()),這個initChannel(C ch)方法我們也很熟悉,它就是我們在初始化 Bootstrap 時,調用 handler 方法傳入的匿 名內部類所實現的方法:
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new ChatClientHandler(nickName));
}
});
因此,當調用這個方法之後, 我們自定義的 ChannelHandler 就插入到了 Pipeline,此時 Pipeline 的狀態如下圖所示:
當添加完成自定義的 ChannelHandler 後,在 finally 代碼塊會刪除自定義的 ChannelInitializer,也就是 remove(ctx)最 終調用 ctx.pipeline().remove(this),因此最後的 Pipeline 的狀態如下:
Pipeline 的事件傳播機制
我們已經知道 AbstractChannelHandlerContext 中有 inbound 和 outbound 兩個 boolean 變量,分別用 於標識 Context 所對應的 handler 的類型,即:
1、inbound 爲 true 是,表示其對應的 ChannelHandler 是 ChannelInboundHandler 的子類。
2、outbound 爲 true 時,表示對應的 ChannelHandler 是 ChannelOutboundHandler 的子類。
這兩個字段到底有什麼作用呢? 這還要從 ChannelPipeline 的事件傳播類型說起。 Netty 中的傳播事件可以分爲兩種:Inbound 事件和 Outbound 事件
從上圖可以看出,inbound 事件和 outbound 事件的流向是不一樣的,inbound 事件的流行是從下至上,而 outbound 剛好相反,是從上到下
inbound 類似於是事件回調(響應請求的事件),而 outbound 類似於主動觸發(發起請求的 事件)
Outbound 事件傳播方式
Outbound 事件都是請求事件(request event),即請求某件事情的發生,然後通過 Outbound 事件進行通知。 Outbound 事件的傳播方向是 tail -> customContext -> head。
我們接下來以 connect 事件爲例,分析一下 Outbound 事件的傳播機制。 首先,當用戶調用了 Bootstrap 的 connect()方法時,就會觸發一個 Connect 請求事件,此調用會觸發如下調用鏈:
繼續跟蹤,我們就發現 AbstractChannel 的 connect()其實由調用了 DefaultChannelPipeline 的 connect()方法:
@Override
public final ChannelFuture connect(SocketAddress remoteAddress, ChannelPromise promise) {
return tail.connect(remoteAddress, promise);
}
可以看到,當 outbound 類型的事件(這裏是 connect 事件)傳遞到 Pipeline 後,它其實是以 tail 爲起點開始傳播的。
而 tail.connect()會調用到 AbstractChannelHandlerContext 的 connect()方法:
public ChannelFuture connect( final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
......
final AbstractChannelHandlerContext next = findContextOutbound();
EventExecutor executor = next.executor();
next.invokeConnect(remoteAddress, localAddress, promise);
.....
return promise;
}
findContextOutbound()方法是以當前 Context 爲起點,沿着Pipeline 中的 Context 雙向鏈表尋找第一個 outbound 屬性爲 true 的 Context(即關聯 ChannelOutboundHandler 的 Context),然後返回
當找到了一個 outbound 類型的 Context 後,就調用它的 invokeConnect()方法,這個方法中會調用 Context 關聯的 ChannelHandler 的 connect()方法:
private void invokeConnect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
if (invokeHandler()) {
try {
((ChannelOutboundHandler) handler()).connect(this, remoteAddress, localAddress, promise);
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise);
}
} else {
connect(remoteAddress, localAddress, promise);
}
}
於是這個調用回到了: Context.connect -> Connect.findContextOutbound -> next.invokeConnect -> handler.connect -> Context.connect 這樣的循環中
直到 connect 事件傳遞到 DefaultChannelPipeline 的雙向鏈表的頭節點,即 head 中。爲什麼會傳遞 到 head 中呢?因爲,head 實現了 ChannelOutboundHandler
因爲 head 本身既是一個 ChannelHandlerContext,又實現了 ChannelOutboundHandler 接口,因此最終 connect()事件是在 head 中被處理。head 的 connect()事件處理邏輯如下:
public void connect(
ChannelHandlerContext ctx,
SocketAddress remoteAddress, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
unsafe.connect(remoteAddress, localAddress, promise);
}
connect事件傳播流程總結:
1.Bootstrap調用connect方法,觸發事件事件被Channel.connect接收處理 Bootstrap.connect() -->Channel.connect()
2.Channel會調用其關聯的Pipeline的connect()方法, Channel.connect()–>Pipeline.connect()
3.Pipeline會從雙向鏈表的tail節點開始處理,調用tail節點的connect()方法Pipeline.connect() -->TailContext.connect()
4.TailContext會尋找下一個Outbound的ChannelHandlerContext,然後調用其invokeConnect方法TailContext.connect() -->ChannelHandlerContext.invokeConnect()
5.ChannelHandlerContext會調用其相關聯的ChannelOutboundHandler的connect()方法,ChannelHandlerContext.invokeConnect()–>ChannelOutboundHandler.connect()
6.這樣一直向下傳遞直到HeadContext,HeadContext同時也實現了ChannelOutboundHandler接口,因此最終connect事件被HeadContext的connect()方法處理