文章目录
- 一、梳理Java中NIO代码
- 二、Netty服务端代码
- 1、new NioEventLoopGroup()
- 2、group
- 3、channel
- 4、NioServerSocketChannel.class
- 5、childHandler
- 6、bind(重点)
一、梳理Java中NIO代码
Java中的NIO其本质是网络层面定义中的多路复用IO模型(一定要和NIO模型区分开)。NIO代码主要分为下列几步:
- 初始ServerSocketChannel
- 初始化Selector,
- 完成Selector和Channel的绑定,并且注册对应的事件
- 用一个死循环遍历selector监控的事件对应的IO请求
- 处理监控到的对应事件的数据信息
public class 基础Selector和Channel绑定 {
public static void main(String[] args) throws IOException {
// 初始化channel
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
channel.bind(new InetSocketAddress(9090));
// 初始化selector,并完成两者的绑定
Selector boss = Selector.open();
SelectionKey selectionKey = channel.register(boss, 0, null);
selectionKey.interestOps(SelectionKey.OP_ACCEPT);
while (true) {
// selector监听事件,可传入对应的超时时间
boss.select();
// 遍历所有接收到的事件
Iterator<SelectionKey> iterator = boss.selectedKeys().iterator();
if (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
if (key.isAcceptable()) {
// 触发对应事件后,获取channel中的数据
SocketChannel accept = channel.accept();
accept.configureBlocking(false);
// 其他业务处理
}
}
}
}
}
上述的NIO代码你可以不需要明白每一步的代码写法,但是你一定要明白每一步的具体含义。如果你对含义都没有弄清楚,那我是不建议你继续向下看的,因为Netty底层的代码逻辑就是完成对上述代码的再次封装。理解了上述代码的执行逻辑,对于后期理解Netty的源码启动流程源码起到事半功倍的效果。
二、Netty服务端代码
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast(new StringEncoder());
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// xxx
}
});
}
})
.bind(8099);
先上一张整体的流程图,仅包含bind后的执行逻辑。在阅读源码的时候,你可以对着这个流程图,一步一步进行比对查看。理解完本文,也就理解了这张流程图。
1、new NioEventLoopGroup()
- MultithreadEventExecutorGroup
在实例化NioEventLoopGroup类的时候,会先去实例化它的父类MultithreadEventLoopGroup,最后会实例到它父类的父类——MultithreadEventExecutorGroup
根据已有的代码,我们不难发现,其底层会创建一个叫children的名字的线程组,其大小为指定的线程数量。并且它还会依次调用newChild方法,完成对线程组的赋值。
补充:上图中的nThreads的值为
nThreads == 0 ?
(Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)))
:
nThreads
- newChild
由于我们是实例化的NioEventLoopGroup类,所以会跳转到NioEventLoopGroup中的newChild方法
@Override
protected EventLoop newChild(Executor executor, Object... args) throws Exception {
EventLoopTaskQueueFactory queueFactory = args.length == 4 ? (EventLoopTaskQueueFactory) args[3] : null;
return new NioEventLoop(this, executor, (SelectorProvider) args[0],
((SelectStrategyFactory) args[1]).newSelectStrategy(), (RejectedExecutionHandler) args[2], queueFactory);
}
- NioEventLoop
实例化NioEventLoop,到这一步我们不难发现一个关联点,那就是我们最开始是实例化一个NioEventLoopGroup,其底层逻辑是在实例化一个一个的NioEventLoop
- TaskQueue 和 selector
调用newTaskQueue(queueFactory)方法,创建一个队列用于后期存放消息
创建selector,这里可以类比Java里NIO中代码的写法,即第一节中第二点初始化Selector(Selector.open()),它们两者底层代码是一样的
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
2、group
熟悉链式编程的小伙伴应该都知道,主对象的链式方法一般都是一个执行一个赋值操作,真正的对这些数据进行逻辑处理的,都在最后一个方法。基于对链式编程的这种常识性的了解之后,我们对group方法就有一个基本的认识了
group方法主要是完成对this.group和this.childGroup两个参数的赋值
这里需要补充一个额外的知识点:
group和childGroup可以理解为是两组线程池,从名字也能看出来前者是老大,后者是child。老大线程池中的线程专门负责接收客户端的连接,child线程池中的线程专门负责网络的读写。如果我们在编写服务端代码的时候,没有去分别指定对应的EventLoopGroup,那么它们两个就会使用同一个EventLoopGroup。一个EventLoopGroup底层又会去创建很多个NioEventLoop,并且其内部含有对应的Selector…这就是第二章第一节的内容。
调用父类的group方法,完成老大NioEventLoopGroup对象的赋值操作
3、channel
- 获取传入类的构造方法
那这里获取到的无参构造方法必然就是NioServerSocketChannel类里面的构造方法
- 把前面创建出来的无参构造方法封装为一个工厂类,并且完成赋值
4、NioServerSocketChannel.class
第三步的group方法存放了NioServerSocketChannel类的无参构造方法,虽然目前不知道有什么用,但是我们可以肯定的是,这个无参构造方法后面一定会用到,那不然存它干嘛?所以我们来看看它的无参构造方法。
- 调用newSocket方法
在NioServerSocketChannel类的无参构造方法中,第一步就是根据SelectorProvider.provider()这样一个静态常量去new一个Socket。在该方法中,会调用对应的openServerSocketChannel方法。
我们不妨联想一个NIO的代码,我们NIO代码第一步就是初始化ServerSocketChannel,即调用对应的open方法。既然说到Netty是对Java中NIO的封装,你是否能联想到什么?其实下面的newSocket方法完成的逻辑,和最开始的ServerSocketChannel.open方法完成的逻辑是一样,换句话说open方法的底层就是下面的逻辑。好了Java中创建NIO的第一步代码位置找到了
- ch.configureBlocking(false)
点击对应的super方法,就会跟到下面的代码中。类比Java中的NIO代码,这一步就是完成非阻塞状态的设置,同样对应Java中NIO的第一步
- newChannelPipeline()
继续向下点击super方法,我可以发现在实例化的过程中,还会创建出对应的pipeline。即我们可以理解为一个 Channel中包含了一个 ChannelPipeline,而 ChannelPipeline中又维护了一个由ChannelHandlerContext(TailContext和HeadContext实现了ChannelHandlerContext接口)组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。
该属性与我们ch.pipeline().addLast(new xxxHandler)相关,此处不再展开,你只需要记住在这里创建了一个这个pipeline对象
5、childHandler
该方法符合我们最初最链式编程的设想,仅仅完成了childHandler这个成员变量的赋值操作。即我们明白真正的核心逻辑在后面的ChannelInitializer类中,这就是我们真正的处理业务逻辑的地方,这一个点我放在最后进行讲解。
6、bind(重点)
- doBind方法
链式编程的最后一个方法一定是关键,根据代码逻辑,我们会进到下面的这个doBind方法,一看名字就是重点,该方法中主要的核心方法是initAndRegister和doBind0
- 调用NioServerSocketChannel的无参构造方法
看到channelFactory就可以直接跳转到第二章的第4节,即实例化对应的对象,此时你就可以直接跳转到第二章的第4节,在回忆一遍,看看查看创建了哪些对象
- init方法
紧接着上面的实例化对应的无参构造,紧接着又执行init方法。
在该方法中,它会先去获取我们第4步创建出来的pipeline(ChannelPipeline p = channel.pipeline();),然后向pipeline中添加handler,是否感觉这个代码的逻辑似曾相识,没错,这个和我们自己编写的netty的业务逻辑部分的代码相同,即向pipelin中添加对应的handler处理事件。
- register方法(重点)
register方法包含逻辑较多。详细的代码执行流程,可直接参考最前面给出了的整体的一个流程图。接下来我只对流程图中几个关键的节点进行说明。(流程图中特殊颜色标记的位置分别对应下面的讲解代码位置)
1)
这里有两个方法一个execute方法还有一个register0。execute方法可以理解为是和客户端连接的入口,在该方法中会使用死循环去不断的select获取数据,继而达到监听客户端发送给服务端的事件信息,最终完成客户端和服务端之间的交互(该方法可以一直跟到,客户端和服务端事件的交互位置)。register0方法可以理解为将服务端的select和channel的绑定,然后回调pipeline中的initChannel方法,最终处理服务端添加到pipeline中handler中的处理逻辑。
2)
这个就是前面提到的会回调pipeline中所有handler的方法入口,底下再调用callHandlerAddedForAllHandlers,然后再顺着双向链表依次调用,代码比较负责,这里不再进行说明
3)
在1)中有说明过,在execute方法底下,会调用到如下的代码位置。该代码位于一段死循环内部,也可以理解为是不断的轮询。触发该方法有两个事件场景,SelectionKey.OP_READ和SelectionKey.OP_ACCEPT,即对应的READ事件和ACCEPT事件。当客户端和Netty服务端建立连接之后,就会触发这个连接事件;当客户端发送数据给服务端的时候,会触发READ事件。两个事件的不同,会导致调用的read方法的实现类不一样。
这个read方法有不同的实现类,分别是NioByteUnsafe类和NioMessageUnsafe类。前者是用于处理READ读取消息事件,后者用于ACCEPT连接事件。
4)
当服务端启动之后,老大线程组会利用线程去完成服务端的注册,即你前面看到的1)2)3)的代码,这些都是老大线程组里面的线程干的。如果客户端发起了和服务端的连接信息,那么老大线程组的线程就会触发3)中的read方法,然后调用到下图中的代码。该代码可以类比服务端注册NioServerSocketChannel这个类的初始化流程,这里是直接实例化这个对象。
同样类比NioServerSocketChannel这个类,在实例化NioSocketChannel这个方法过程中也会实例化出对应的ChannelPileline(内部为一个含有头尾节点的双向链表,可以不断增加handler)
5)
前面第4)步中说到会实例化NioSocketChannel对象,即下图中的第一个框。实例化完对应的对象后,紧接着会调用fireChannelRead方法。在Netty中,但凡看到fire方法开头的,你第一反应就是回调pipeline中的所有的channelRead(去掉fire前缀后的名字)方法。根据我们前面提到的流程图,此时的pipeline肯定是服务端的pipeline,并且pipeline中的类是ServerBootstrapAcceptor对象,即就会回调到这个类的channelRead方法,即出现了下面的第二幅图片中的代码位置
这个channelRead方法至关重要,能否正确理解这个方法,就直接决定了你能否理解Netty客户端和服务端之间的连接绑定。明确我们代码的位置,老大线程组中的一个线程,在死循环中接收到了ACCEPT事件,然后会回调server中pipeline中的这个方法。
这里需要明确childGroup和child这两个参数的含义。我们在很早的时候就有说过,我们在初始化服务端的NioEventLoopGroup的时候会初始化两个线程组,第一个就是老大线程组,第二个就是儿子线程组,这里就使用到了这个儿子线程组(如果不指定儿子线程组,儿子线程组就和老大线程组是一样的两个线程组)。child参数为调用这个方法传过来的msg,也即是上一幅图的readBuf.get(i)的值,而再往前走,这个readBuf就是第4)步中的那个含有实例化出NioSocketChannel的list集合。那么这个child就是客户端的channel。
明白了这两个参数的含义,这个方法就可以描述为,当客户端和服务端发生连接的时候,服务端老大线程组中的一个线程,就会负责将要进行连接的客户端中的channel和服务端初始化过程中的另一个线程组(儿子线程组)进行绑定。完成绑定后,已经连接好的客户端发送数据给服务端后,就将由这个儿子线程组分配线程进行消息的处理
在大概理顺了上面的逻辑之后,你应该就能明白下面这张图片的流程。(如果不明白,建议多看几遍开头的那张流程图,我觉得我画的很细了)
- 客户端发送消息给服务端
- 服务端会初始化两个线程组,即Boss Group线程组(老大线程组)和Worker Group线程组(儿子线程组)
- Boss Group(老大线程组)中的线程会获取监听事件
- 当Boss Group(老大线程组)中的线程监听到ACCEPT事件之后,就会为连接过来的客户端初始化一个SocketChannel
- 然后将这个SocketChannel交由Worker Group(儿子线程组),完成两者之间的一个绑定关系
- 后续客户端发送消息给服务端之后,Worker Group(儿子线程组)就会分配对应的线程去处理对象的消息,此时就会回调channel中添加的所有handler,最终完成两者之间的数据交互
- doBind0方法
这里可以类比第一章中Java中NIO的第1点,即channel.bind(new InetSocketAddress(9090)),完成对应端口的绑定