一個疑問
首先一切的一切,是從一次意外開始。
在寫一個netty的server的時候,這裏有四個handler,inboundHandler實現的類EchoInHandler1與EchoInHandler2,outboundHandler實現的類EchoOutHandler1與EchoOutHandler2;
在添加到pipeline的時候,如果這些handler的存放到pipeline的位置爲EchoOutHandler1-EchoOutHandler2-EchoInHandler1-EchoInHandler2,那麼一切就正常了。
開始監聽,端口爲:/127.0.0.1:20000
in1
in2
接收客戶端數據:QUERY TIME ORDER
server向client發送數據
out2
out1
Complete1
但是如果存放的順序是EchoInHandler1-EchoInHandler2-EchoOutHandler1-EchoOutHandler2,那麼會出現在出站的時候,EchoOutHandler1與EchoOutHandler2卻沒有執行。
開始監聽,端口爲:/127.0.0.1:20000
in1
in2
接收客戶端數據:QUERY TIME ORDER
server向client發送數據
Complete1
這是爲什麼呢?
PS:如果想知道答案可以直接看最後一節
public void start() throws Exception {
EventLoopGroup eventLoopGroup = null;
try {
//server端引導類
ServerBootstrap serverBootstrap = new ServerBootstrap();
//連接池處理數據
eventLoopGroup = new NioEventLoopGroup();
serverBootstrap.group(eventLoopGroup)
.channel(NioServerSocketChannel.class)
//指定通道類型爲NioServerSocketChannel,一種異步模式,OIO阻塞模式爲OioServerSocketChannel
.localAddress("localhost",port)
//設置InetSocketAddress讓服務器監聽某個端口已等待客戶端連接。
.childHandler(new ChannelInitializer<Channel>() {
//設置childHandler執行所有的連接請求
@Override
protected void initChannel(Channel ch) throws Exception {
// 註冊兩個InboundHandler,執行順序爲註冊順序,所以應該是InboundHandler1 InboundHandler2
// 註冊兩個OutboundHandler,執行順序爲註冊順序的逆序,所以應該是OutboundHandler2 OutboundHandler1
ch.pipeline().addLast(new EchoInHandler1());
ch.pipeline().addLast(new EchoInHandler2());
ch.pipeline().addLast(new EchoOutHandler1());
ch.pipeline().addLast(new EchoOutHandler2());
}
});
// 最後綁定服務器等待直到綁定完成,調用sync()方法會阻塞直到服務器完成綁定,
// 然後服務器等待通道關閉,因爲使用sync(),所以關閉操作也會被阻塞。
ChannelFuture channelFuture = serverBootstrap.bind().sync();
System.out.println("開始監聽,端口爲:" + channelFuture.channel().localAddress());
channelFuture.channel().closeFuture().sync();
} finally {
eventLoopGroup.shutdownGracefully().sync();
}
}
EchoInHandler1
package com.aguicode.practice.netty.mutilhandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* @author aguicode
* @since 2020-3-8
*/
public class EchoInHandler1 extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
System.out.println("in1");
// 通知執行下一個InboundHandler
ctx.fireChannelRead(msg);
//ctx.writeAndFlush(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("Complete1");
//ctx.flush();//刷新後纔將數據發出到SocketChannel
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}
EchoInHandler2
package com.aguicode.practice.netty.mutilhandler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.util.Date;
/**
* @author aguicode
* @since 2020-3-8
*/
public class EchoInHandler2 extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
System.out.println("in2");
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8");
System.out.println("接收客戶端數據:" + body);
//向客戶端寫數據
System.out.println("server向client發送數據");
String currentTime = new Date(System.currentTimeMillis()).toString();
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
//ctx.write(resp);
ctx.writeAndFlush(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("Complete2");
//ctx.flush();//刷新後纔將數據發出到SocketChannel
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}
EchoOutHandler1
package com.aguicode.practice.netty.mutilhandler;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import java.util.Date;
/**
* @author aguicode
* @since 2020-3-8
*/
public class EchoOutHandler1 extends ChannelOutboundHandlerAdapter {
@Override
// 向client發送消息
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println("out1");
/*System.out.println(msg);*/
String response = "\nI am ok!\n";
ByteBuf encoded = ctx.alloc().buffer(4 * response.length());
encoded.writeBytes(response.getBytes());
String currentTime = new Date(System.currentTimeMillis()).toString();
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.write(resp);
ctx.writeAndFlush(encoded);
ctx.flush();
}
}
EchoOutHandler2
package com.aguicode.practice.netty.mutilhandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
/**
* @author aguicode
* @since 2020-3-8
*/
public class EchoOutHandler2 extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println("out2");
// 執行下一個OutboundHandler
/*System.out.println("at first..msg = "+msg);
msg = "hi newed in out2";*/
// 通知執行下一個OutboundHandler
super.write(ctx, msg, promise);
super.flush(ctx);
}
}
幾個重要的概念
netty中有以下幾個重要的概念,首先是server與client,它們中有channel、channelPipeline、channelHandler、channelHandlerContext、ServerBootStrap、bootStrap、channelFuture、selector、Eventloop;
關於Netty的組件中的介紹會安排到另外一篇詳細解答,這裏只是分析in與out boundHandler執行順序
原理解析
channelHandler 中定義outboundhandler和inboundhandler,表示一個請求進來時通過入站inboundhandler,而內部進行一些業務的邏輯處理之後出站使用outboundhandler,
這裏handler是定義在channelPipeline裏邊的,handler之間是一種雙向鏈表的關係,inBound事件從head節點傳播到tail節點,outBound事件從tail節點傳播到head節點。
/**
* I/O Request
* via {@link Channel} or
* {@link ChannelHandlerContext}
* |
* +---------------------------------------------------+---------------+
* | ChannelPipeline | |
* | \|/ |
* | +---------------------+ +-----------+----------+ |
* | | Inbound Handler N | | Outbound Handler 1 | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ | |
* | | \|/ |
* | +----------+----------+ +-----------+----------+ |
* | | Inbound Handler N-1 | | Outbound Handler 2 | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ . |
* | . . |
* | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
* | [ method call] [method call] |
* | . . |
* | . \|/ |
* | +----------+----------+ +-----------+----------+ |
* | | Inbound Handler 2 | | Outbound Handler M-1 | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ | |
* | | \|/ |
* | +----------+----------+ +-----------+----------+ |
* | | Inbound Handler 1 | | Outbound Handler M | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ | |
* +---------------+-----------------------------------+---------------+
* | \|/
* +---------------+-----------------------------------+---------------+
* | | | |
* | [ Socket.read() ] [ Socket.write() ] |
* | |
* | Netty Internal I/O Threads (Transport Implementation) |
* +-------------------------------------------------------------------+
*/
例如在建立三次握手之後,開始讀數據,從head節點發起,準確來說是head的unsafe方法發起,inbound尋找下一個inbound時,調用invokeChannelActive(next),一個個遞歸調用,直到最後一個inBound節點—即tail節點,並且tail節點作爲尾節點,會終止inbound事件的傳播,讀事件就結束了,
這個時候,經過一段業務邏輯的處理,就需要處理outbound事件,轉而反向傳播,outbound則調用的是writeAndFlush(),直到head節點,數據最終會落在head節點的unsafe.write方法。
我是分割線
執行順序的分析
那麼原理都懂了,這裏就重點分析一下inboundHandler與outboundHandler添加順序不同,帶來執行順序的問題
- inbound事件在pipeline中傳輸方向是head->tail,即從頭到尾,而且會忽略outbound事件
invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
重要的是find方法
private AbstractChannelHandlerContext findContextInbound(int mask) {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.next;
} while ((ctx.executionMask & mask) == 0);
return ctx;
}
或者類似這樣子:
- outbound事件在pipeline傳輸方向正好相反,會從tail->head,即從尾到頭,同時也會忽略inbound事件
final AbstractChannelHandlerContext next = findContextOutbound(flush ?
(MASK_WRITE | MASK_FLUSH) : MASK_WRITE);
重要的是find方法
private AbstractChannelHandlerContext findContextOutbound(int mask) {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.prev;
} while ((ctx.executionMask & mask) == 0);
return ctx;
}
或者類似這樣子:
但是 需要關注的是:AbstractChannelHandlerContext ctx = this;
其實AbstractChannelHandlerContext是上下文都共享的,所以,
如果是EchoInHandler1-EchoInHandler2-EchoOutHandler1-EchoOutHandler2,那麼一開始入站執行了EchoInHandler1-EchoInHandler2,因爲do-while循環跳出,ctx留在了EchoInHandler2的位置,在出站的時候,在EchoInHandler2的位置反向遍歷,只會遍歷EchoInHandler2-EchoInHandler1,那麼自然就不會去讀取-EchoOutHandler1-EchoOutHandler2了。
相反,如果是EchoOutHandler1-EchoOutHandler2-EchoInHandler1-EchoInHandler2的順序,一開始入站ctx到了EchoInHandler2的位置,反向遍歷就會經過EchoInHandler2-EchoInHandler1-EchoOutHandler2-EchoOutHandler1