在netty中有我們一般有兩種發送數據的方式,即使用ChannelHandlerContext或者Channel的write方法,這兩種方法都能發送數據,那么其有什么區別呢。這兒引用netty文檔中的解釋如下。

這個通俗一點的解釋呢可以說ChannelHandlerContext執行寫入方法時只會執行當前handler之前的OutboundHandler。而Channel則會執行所有的OutboundHandler。下面我們可以通過例子來理解
1.建立一個netty服務端
public class Server { public static void main(String[] args) throws InterruptedException { ServerBootstrap serverBootstrap = new ServerBootstrap(); ChannelFuture channelFuture = serverBootstrap.group(new NioEventLoopGroup(1) , new NioEventLoopGroup(10)) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler()) .childHandler(new InitialierHandler()) //即步驟2中的類 .bind(8080) .sync(); channelFuture.channel().closeFuture().sync(); } }
2.創建ChannelInitializer
在這個類中我們添加了四個處理器 這兒注意順序 (具體類在步驟3)
public class InitialierHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new RequestChannelHandler1()); socketChannel.pipeline().addLast(new ResponseChannelHandler1()); socketChannel.pipeline().addLast(new RequestChannelHandler2()); socketChannel.pipeline().addLast(new ResponseChannelHandler2()); } }
順序分別為 in1 →out1→ in2 →out2 這兒用圖來增加理解 (netty會自動區分in或是out類型)

3. 分別創建2個 int Handler 2個out handler
RequestChannelHandler1(注意后面業務會修改方法具體內容)
public class RequestChannelHandler1 extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx , Object msg) throws Exception { System.out.println("請求處理器1"); super.channelRead(ctx,msg); } }
RequestChannelHandler2(注意后面業務會修改方法具體內容)
public class RequestChannelHandler2 extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx , Object msg) throws Exception { System.out.println("請求處理器2");super.channelRead(ctx,msg); } }
ResponseChannelHandler1
public class ResponseChannelHandler1 extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx , Object msg , ChannelPromise promise) throws Exception { System.out.println("響應處理器1"); ByteBuf byteMsg = (ByteBuf) msg; byteMsg.writeBytes("增加請求1的內容".getBytes(Charset.forName("gb2312"))); super.write(ctx,msg,promise); } }
ResponseChannelHandler2
public class ResponseChannelHandler2 extends ChannelOutboundHandlerAdapter { @Override public void write(ChannelHandlerContext ctx , Object msg , ChannelPromise promise) throws Exception { System.out.println("響應處理器2"); ByteBuf byteMsg = (ByteBuf) msg; byteMsg.writeBytes("增加請求2的內容".getBytes(Charset.forName("gb2312"))); super.write(ctx,msg,promise); } }
4.檢驗
可以使用調試器來調試請求,例如網絡調試助手

我們一共創建了四個Handler 且類型以及順序為 in1 → out1 →in2 →out2 ,按照netty的定義。可實驗如下
4.1 in1中調用ChannelHandlerContext(只會調用其之前的handler)的方法則out1,out2都不會調用,in1中調用Channel(所有都會調用)的方法則會out1,out2都調用
我們將in1 read方法內容改為如下
@Override public void channelRead(ChannelHandlerContext ctx , Object msg) throws Exception { System.out.println("請求處理器1"); ctx.writeAndFlush(Unpooled.copiedBuffer("hello word1" , Charset.forName("gb2312"))); super.channelRead(ctx,msg); }
in2 read方法改為如下
public void channelRead(ChannelHandlerContext ctx , Object msg) throws Exception { System.out.println("請求處理器2"); super.channelRead(ctx,msg); }
使用網絡調試后發現控制台打印如下 並沒有經過out1和out2

而網絡調試控制台打印如下 ,我們只接收到了hello word原始內容

然后將in1中的ChannelHandlerContext改為Channel后則控制台和網絡調試控制台打印分別如下
public void channelRead(ChannelHandlerContext ctx , Object msg) throws Exception { System.out.println("請求處理器1"); ctx.channel().writeAndFlush(Unpooled.copiedBuffer("hello word1" , Charset.forName("gb2312"))); super.channelRead(ctx,msg); }


控制台中兩次響應的處理已經打印,並且返回內容已經被分別加上out處理器中的信息
4.2 in2中調用ChannelHandlerContext(只會調用其之前的handler)的方法則out1會調用,out2不會調用,in2中調用Channel(所有都會調用)的方法則會out1,out2都調用
這兒我們將in1 與in2稍作修改
in1 read方法改為如下
public void channelRead(ChannelHandlerContext ctx , Object msg) throws Exception { System.out.println("請求處理器1"); super.channelRead(ctx,msg); }
in2 read方法改為如下
public void channelRead(ChannelHandlerContext ctx , Object msg) throws Exception { System.out.println("請求處理器2"); ctx.writeAndFlush(Unpooled.copiedBuffer("hello word2" , Charset.forName("gb2312"))); super.channelRead(ctx,msg); }
使用網絡調試工具訪問后控制台和網絡調試控制台分別打印如下


可以發現idea控制台只打印了out1 網絡調試控制台也只增加了請求1的內容。
至於將ChannelHandlerContext則和4.1中效果一致,out1和out2都會執行,這兒就不在寫了
5.源碼簡略分析
通過上面的案例應該就很明確這兩者的差別了,我們這兒可以簡要看下源碼步驟。
5.1 pipeline.addLast()
上面我們通過socketChannel.pipeline().addLast 添加了我們的Handler,顧名思義就是將我們的處理器添加到末尾(netty內部使用一個鏈表存儲)。我們可以看下其源碼
public final ChannelPipeline addLast(EventExecutorGroup executor, ChannelHandler... handlers) { ObjectUtil.checkNotNull(handlers, "handlers"); for (ChannelHandler h: handlers) { if (h == null) { break; } addLast(executor, null, h); } return this; }
上面循環是可能傳入多個,根據這個可以得知,我們傳入多個的時候也是根據參數順序來的。我們可以接着看addLast(executor,null,h)方法。
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); .... } callHandlerAdded0(newCtx); return this; }
這兒就是將我們傳入的handler包裝了成了一個AbstractChannelHandlerContext (數據類型是一個雙向鏈表),然后執行了addLast0方法。
private void addLast0(AbstractChannelHandlerContext newCtx) { AbstractChannelHandlerContext prev = tail.prev; newCtx.prev = prev; newCtx.next = tail; prev.next = newCtx; tail.prev = newCtx; }
這兒的代碼就比較簡單,就是將當前的handler插入到tail節點與倒數第二個節點之間。這樣當前的handler就成為了倒數第二個節點,以后每加一個handler都會成為新的倒數第2個節點。這兒注意tail節點由一個專門的TailContext維護。
既然處理器已經添加,我們就可以看下其如何工作的吧
5.2 ChannelHandlerContext.writeAndFlush方法
private void write(Object msg, boolean flush, ChannelPromise promise) { //注意flush為true ........ final AbstractChannelHandlerContext next = findContextOutbound(flush ? (MASK_WRITE | MASK_FLUSH) : MASK_WRITE); final Object m = pipeline.touch(msg, next); EventExecutor executor = next.executor(); if (executor.inEventLoop()) { if (flush) { next.invokeWriteAndFlush(m, promise); } else { next.invokeWrite(m, promise); } } else { ....... } }
這里面的邏輯可以發現主要分為兩步,第一步找到下一個執行的handler,第二部執行這個handler的write方法。我們主要看下查找next的方法,即這個findContextOutbound()方法,點進去看下
private AbstractChannelHandlerContext findContextOutbound(int mask) { AbstractChannelHandlerContext ctx = this; do { ctx = ctx.prev; } while ((ctx.executionMask & mask) == 0); return ctx; }
可以看到 這里面會不斷的查找當前handlerContext的前一個滿足寫操作的handler。找到滿足的后就會返回。
比如我們現在有個handler鏈是這樣的head→in1→out1→in2→out2→in3→out3→tail 。我們在in3中寫入,那in3的pre就是out2,如果滿足條件就會將out2返回,不滿足就會→in2→out1→in1這樣不斷往前查找
5.3Channel.writeAndFlush方法
這個方法根據上面的結論會從handler鏈的tail開始調用,其實這個也很好理解,上面的ChannelHanderContext本身就是鏈表結構,所以支持查找當前的節點的前后節點。而這個Channel並不是鏈表結構,所以只能從tail開始一個一個找了。
@Override public final ChannelFuture writeAndFlush(Object msg) { return tail.writeAndFlush(msg); }
這里面調用的tail的write方法,我們看下tail
final AbstractChannelHandlerContext tail;
這就是5.2中我們說的tail節點,那最終會調用的也就是5.2中的write方法,只是這個this從當前的channelContextHandler變為了tail
