一、簡介
ChannelHandler 用來處理 Channel 上的各種事件,分爲入站、出站兩種。
所有 ChannelHandler 被連成一串,就是 Pipeline。
- 入站處理器通常是 ChannelInboundHandlerAdapter 的子類,主要用來讀取客戶端數據,寫回結果。
- 出站處理器通常是 ChannelOutboundHandlerAdapter 的子類,主要對寫回結果進行加工。
可以將netty中的各種組件進行一個比如,如下所示:
二、代碼分析
我們通過代碼的形式,來展示Handler和Pipeline之間的關係,以及ChannelInboundHandlerAdapter 和ChannelOutboundHandlerAdapter 的使用方式。
客戶端代碼,使用前面文章用到的客戶端,支持控制檯輸入內容:
public static void main(String[] args) throws Exception {
// 將group提出來,不能匿名方式,爲了後面調動shutdownGracefully()方法
NioEventLoopGroup group = new NioEventLoopGroup();
ChannelFuture channelFuture = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) {
ch.pipeline().addLast(new StringEncoder());
}
})
.connect("localhost", 8080);
// 同步等待連接
Channel channel = channelFuture.sync().channel();
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
if ("q".equals(line)) {
System.out.println("關閉channel");
// close 異步操作 1s 之後
channel.close();
break;
}
channel.writeAndFlush(line);
}
}, "input").start();
// 處理channel關閉後的操作
ChannelFuture closeFuture = channel.closeFuture();
//異步 - EventLoopGroup線程優雅關閉
closeFuture.addListener((ChannelFutureListener) future -> group.shutdownGracefully());
}
2.1 ChannelInboundHandlerAdapter
服務端代碼,首先添加三個入棧處理器,並分別指定名稱爲h1,h2,h3,分別打印1,2,3
public static void main(String[] args) {
new ServerBootstrap().group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioServerSocketChannel) {
ChannelPipeline pipeline = nioServerSocketChannel.pipeline();
pipeline.addLast("h1", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("1");
super.channelRead(ctx, msg);
}
});
pipeline.addLast("h2", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("2");
super.channelRead(ctx, msg);
}
});
pipeline.addLast("h3", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("3");
super.channelRead(ctx, msg);
}
});
}
}).bind(8080);
}
分別啓動客戶端和服務端,客戶端隨便輸入內容,服務端得到如下輸出:
1
2
3
由上所示我們可以得到一個結論,我們添加的入站handler是按照添加順序進行執行的。
通過簡單的源碼跟蹤:
private void addLast0(AbstractChannelHandlerContext newCtx) {
//獲取當前處理器鏈表中的尾節點的前一個處理器
AbstractChannelHandlerContext prev = this.tail.prev;
//將prev設置爲新處理器的前一個處理器
newCtx.prev = prev;
//將新處理器的尾結點設置爲tail
newCtx.next = this.tail;
//將prev的下一個節點設置爲新處理器
prev.next = newCtx;
//將尾結點的前一個處理器設置爲新處理器
this.tail.prev = newCtx;
}
我們能得到一個結論,我們通過addLast方法添加的handler其實是添加在鏈表當中,其中每一個節點都有其對象的頭和尾節點,所以我們前面添加的三個處理器會如下所示排列:
2.2 ChannelOutboundHandlerAdapter
接下來我們在上面的基礎上,增加ChannelOutboundHandlerAdapter 處理器,此處理器只有在進行channel數據寫入纔會執行,這個就不掩飾了,我們直接在代碼當中添加寫入代碼。
public static void main(String[] args) {
new ServerBootstrap().group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel nioServerSocketChannel) {
ChannelPipeline pipeline = nioServerSocketChannel.pipeline();
// 入站處理器
pipeline.addLast("h1", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("1");
super.channelRead(ctx, msg);
}
});
pipeline.addLast("h2", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("2");
super.channelRead(ctx, msg);
}
});
pipeline.addLast("h3", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("3");
super.channelRead(ctx, msg);
//服務端channel調用寫入方法,出站處理器纔會生效
nioServerSocketChannel.writeAndFlush(ctx.alloc().buffer().writeBytes("helloworld".getBytes()));
}
});
//出站處理器
pipeline.addLast("h4", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println("4");
super.write(ctx, msg, promise);
}
});
pipeline.addLast("h5", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println("5");
super.write(ctx, msg, promise);
}
});
pipeline.addLast("h6", new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println("6");
super.write(ctx, msg, promise);
}
});
}
}).bind(8080);
}
結果:
1
2
3
6
5
4
如上所示,發現出站處理器的處理順序是6、5、4,從後向前處理的。
其實ChannelPipeline的實現是一個雙向鏈表,所以實現了上述出站、入站的功能。
2.3 上述代碼過程分析
如前面的代碼所示,我們可以在Pipeline中添加很多個handler,並使其按照一定的順序去執行,其主要的一一四就在於,我可以在每個處理器當中,對收到的數據進行不同類型的處理。
比如h1中,我對收到的內容轉字符串,將其發送給h2處理器,然後h2處理器將收到的字符串轉成一個java對象,等等。
那麼handler之間是如何傳遞處理後數據的呢?
在前面的入站handler當中,都有一行如下代碼:
super.channelRead(ctx, msg);
此方法如下所示:
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg);
}
內部執行了ctx.fireChannelRead(msg) ,也就是通過這行代碼將處理過的消息傳遞給下一個處理器進行處理的。
並且此方法只能喚醒的是下一個入站處理器,如我們前面的代碼,在h3使用這行代碼喚醒h4是無效的,因爲h4是一個出站處理器。所以我們可以刪除該行代碼,只使用以下這個即可:
nioServerSocketChannel.writeAndFlush(ctx.alloc().buffer().writeBytes("helloworld".getBytes()));
注意:
我們上面的代碼使用的服務端的channel(nioServerSocketChannel)的writeAndFlush方法。
而在每個處理器當中傳遞過來的對象有一個ctx,而這個ctx也有writeAndFlush方法:
如果你使用了這個ctx,並將其放在h3中,那麼我們會發現執行結果只會輸除前三個入站處理器的結果。
爲什麼?
因爲使用ctx這個writeAndFlush方法,pipeline會從當前處理器向前去尋找有沒有出站處理器,而我們的h3、h2、h1都是入站處理器,所以沒有出站處理器的輸出。
而nioServerSocketChannel會從尾巴向前面找,所以找到了h6、h5、h4。
驗證
在h3當中執行ctx的writeAndFlush方法,註釋其中的nioServerSocketChannel調用,並將h3放到h5的後面去執行,按照前面的結論,應該會打印1、2、3、5、4。
pipeline.addLast("h3", new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("3");
//h4是一個出站處理器,此處調用沒有效果。
//super.channelRead(ctx, msg);
//服務端channel調用寫入方法,出站處理器纔會生效
//nioServerSocketChannel.writeAndFlush(ctx.alloc().buffer().writeBytes("helloworld".getBytes()));
ctx.writeAndFlush(ctx.alloc().buffer().writeBytes("helloworld".getBytes()));
}
});
執行代碼結果,符合預期:
1
2
3
5
4