channel,channelPipeline,channelhandlerContext是Netty中的核心组件,接下来我们将从源码的角度分析这三大组件是如何协调工作的,本文建立在对三者有一个基本的了解,一些基本知识就不再赘述
前置知识:到底什么是出战和入站
- 服务器端和客户端都有一个装载ChannelHandler链的ChannelPipeline的容器,所以出战和入站我们分为两个角度
- 当数据从socket向pipeline流动时,这个动作称之为入站,当数据从pipeline流向socket时,这个动作称为出站。
- 如果以客户端主动访问服务端为例,则这个过程分别触发:客户端出站 --> 服务端入站 --> 服务端出站 --> 客户端入站;而如果以服务端主动访问客户端为例,则这个过程分别触发:服务端出站 --> 客户端入站 --> 客户端出站 --> 服务端入站
宏观掌控三者的关系
- 每当ServerSocket创建一个新的连接,就会创建一个Socket,对应着目标客户端
- 每一个新建的Socket都会分配一个全新的ChannelPipeline
- 每一个ChannelPipeline内部都含有多个ChannelHandlerContext
- ChannelHandlerContext是ChannelHander的包装实现,ChannelHander是ChannelHandlerContext的一个成员变量,context底层是一个双向链表
深入理解三者关系
首先我们来看看ChannelPipeline的继承关系
可以看到ChannelPipeline可以看到他可以调用数据出战和入站的方法,同时也能遍历内部的链表,下面我们看看一些基本的方法
从接口罗列的方法中也能看出基本上都是对handler的增删改查操作,同时也可以获取到与此pipeline绑定的channel对象,在接口文档中提供了一个入站和出战handler的执行流程,接下来我们对此展开详细的介绍
从源码分析链表中handler的执行流程
我们先来搞清楚handler的执行顺序,为了清晰展示,这里结合一个简单的代码,简单来说就是添加了三个入站处理器以及三个出战处理器,并且在第三个handler处调用write方法触发出战处理器
public class TestContext {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(1);
ctx.fireChannelRead(msg);//调用下一个入站handler的channelRead()方法 @1
}
});
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(2);
ctx.fireChannelRead(msg);//调用下一个入站handler的channelRead()方法 @2
}
});
pipeline.addLast(new ChannelInboundHandlerAdapter(){
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(3);
ctx.fireChannelRead(msg);//@3
ctx.channel().write(msg);//触发出战处理器
}
});
pipeline.addLast(new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println(4);@4
ctx.writeAndFlush(msg,promise);
}
});
pipeline.addLast(new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println(5);@5
ctx.writeAndFlush(msg,promise);
}
});
pipeline.addLast(new ChannelOutboundHandlerAdapter() {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println(6);@6
ctx.writeAndFlush(msg,promise);
}
});
}
})
.bind(8080);
}
}
可以编写客户端程序或者浏览器访问对应端口触发服务端代码,在此就不展示了,可以看到输出的结果是:
1
2
3
6
5
4
可以看到,ChannelInboundHandlerAdapter是按照addLast的顺序执行,而ChannelOutboundAdapter是按照addLast的逆序执行的,ChannelPipeline的底层链表如图所示
- 入站处理器中,
ctx.fireChannelRead(msg)
是调用下一个入站处理器,稍后会从源码分析 - 其中注意ctx.channel和ctx都可以调用write方法,都会触发出战处理器,但有所区别,前者会从尾部开始触发后序出战处理器的执行,而后者是从当前节点找上一个出战处理器
- @3处的代码如果改成ctx.write()则只会打印123,因为第三个handler前没有其他的出战处理器了
- @6处的代码如果改成ctx.channel.write()则会打印123666…因为ctx.channel.write()会从tail开始找下一个出战处理器,下一个又是6号处理器,无线循环
那么channelPipeline是如何分辨出战入站处理器,又是如何调用到下一个处理器的呢?
这时就要我们的channelHandlerContext出场了
channelHandlerContext使得channelHandler能够和它的channelPipeline以及其他的channelHandler交互,ChannelHandler可以通知其所属的channelPipeline中的下一个ChannelHandler,甚至可以动态修改它所欲的ChannelPipeline
– 《netty实战》
从源码角度来看其实就是channelHandlerContext的一系列以fire开头的方法,以fireChannelread()方法为例,此方法会调用处理器链中下一个入站处理器的ChannelRead()方法,其他的方法以此类推
下面就以fireChannelread()方法看看底层是如何实现的
@Override
ChannelHandlerContext fireChannelRead(Object msg);
// 具体类中的方法实现
@Override
public ChannelHandlerContext fireChannelRead(final Object msg) {
invokeChannelRead(findContextInbound(MASK_CHANNEL_READ), msg);
return this;
}
// 继续点findContextInbound方法
private AbstractChannelHandlerContext findContextInbound(int mask) {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.next;
} while ((ctx.executionMask & mask) == 0);
return ctx;
}
我们来看看findContextInbound()
方法,mask即传入的参数MASK_CHANNEL_READ
,在while循环中我们还可以看到一个变量executionMask
,该变量在创建此Handler时(创建handler底层会同步创建一个context对象)就被赋予了初值,具体的值有
这里可以看到可以把标记分为两类,一类是出战处理器的标记,一类是入站处理器的标记,低8位是入站处理器,高8位是出战处理器,所以ctx.executionMask & mask!=0
时,即表明遍历到了下一个入站处理器,就退出循环,返回该处理器
所以寻找出战处理器的逻辑相同,注意ctx.write()和ctx.channel.write()的区别 ,前者会从尾部开始触发后序出战处理器的执行,而后者是从当前节点找上一个出战处理器