Netty源码分析专题[1]-服务端启动流程
Netty是一个高性能底层网络传输层技术,深入研究其底层实现原理不仅可以领略其优秀的架构思想,还能为用好这个技术打下坚实的基础,正所谓知其然知其所以然,本文使用的netty版本是4.1.52,其中池化内存部分的源码的变动很大,这个版本的代码在内存回收到缓存的时候有个bug,当然新版本已经修复,具体的可以看池化内存源码分析相关的内容。
从本文开始,会有一系列的Netty源码分析相关的分析
1、从Java NIO模型说起
Java NIO类库好像是JDK1.4版本开始引入的,本文只关注网络通信部分,相比于普通的Socket通信,其提供了非阻塞的通信方式,并且封装了操作系统底层复杂的网络实现,对外只提供一个简单的多路复用器(Selector),使用Selector编写网络通信的编程模型如下:
整体来说并不是很复杂,但是其实这种简单实现会有个问题,就是单个Selector的压力过大,既要处理连接,又要处理IO读写,假设读数据很慢,那么势必会积压很多的连接,因为这个模型有这样或者那样的问题,且网络传输这种底层技术大家使用的情况都差不多,所以就有一群大神创造了Netty,采用Reactor变成模型提升网络的并发处理能力
2、Netty基本概念
基本上服务端代码的固定格式就是下面的样子
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
NioEventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY,true)
.childAttr(AttributeKey.valueOf("childAttr"),"childAttrValue")
.handler(new SeverHandler())
.childHandler(new ChannelInitializer<SocketChannel>(){
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
从代码中可以归纳出以下几个重要的变量:
- 1)、parentGroup:服务端SocketChannel线程组,用于接受客户端连接
- 2)、childGroup:客户端SocketChannel管理线程组,负责管理客户端连接,服务端接收到连接后就会交给workerGroup的某个线程
- 3)、ServerBootstrap:服务端启动类,负责初始化工作和整体的管理
- 4)、DefaultChannelPipeline:业务逻辑处理链,负责实际的业务逻辑处理,责任链模式
接下来会分别介绍这几个部分,可以用网络上的一张图来描述上面的流程
2-1 NioEventLoop
图中最明显的就是把接收客户端连接和处理IO事件分开,核心干活的类就是NioEventLoop,这个类有一个run方法,且核心逻辑确实在run方法中,但是这个类没有继承线程相关的接口,所以不是一个线程,但是实际的作用等同于一个线程,所以本文会将其和线程化作等号,NioEventLoop的内部基本机构如下:
每个NioEventLoop都会属于一个NioEventLoopGroup,也就是最开始代码段中定义的parentGroup和childGroup,初始化的时候都是通过初始化NioEventLoopGroup间接得到NioEventLoop,每个NioEventLoopGroup其实就是一个线程组,如果构造的时候不指定线程组中线程的数量【也就是NioEventLoop的数量】,默认个数就是2×Cpu核数
从类成员中可以看出NioEventLoop是一个线程也是一个Selector,所以后续所有的操作都是由run方法执行,前面说过了,其实NioEventLoop并不是一个线程,其内部的run方法是由其父类的SingleThreadEventExecutor在首次调用时创建出新线程,再由新线程内部调用该类的run方法执行,其启动流程及其入口如下:
线程启动之后会直接调用NioEventLoop的run方法,run方法是个死循环,意味着后续的核心业务逻辑都由其完成,run方法的执行流程如下:
看了Netty的源码之后我才知道原来Jdk的Selector有空轮训的bug,就是Selector返回但是没有任何IO事件发生,Netty采用了技术的方式得以解决,具体描述都在图里
2-2 DefaultChannelPipeline
DefaultChannelPipeline是一个默认的业务逻辑处理链,Netty通过PipeLine使得程序员只需要关注顶层业务逻辑而不用关注底层网络的具体实现,每个连接都有一个DefaultChannelPipeline,Netty的连接通道并不是直接使用Java原生的SeverScoketChannel和SocketChannel,而是自己对其进行了扩展和包装,叫NioServerScoketChannel和NioScketChannel,分别对应服务端和客户端,DefaultChannelPipeline就是它们的成员变量,在将连接注册到NioEventLoop的Selector时,会将对应的NioXXXchannel作为附加变量注册才SelectionKey,后续该连接有IO事件时,可以通过附加信息将实际Netty封装的NioXXXChannel取出进行后续操作
DefaultChannelPipeline默认情况下都会创建两个节点,头部和尾部**【可以避免链表操作的判空等操作,Netty内部采用了很多这种操作,比如内存管理的PoolSubPage管理器头寸节点数组】**,处理链的每个节点都属于InbondHandler或者OutBoundHandler或者两种都有,InbondHandler负责处理IO事件【比如连接已经读取到Buf、连接注册完成、读取完成、连接激活,InbondHandler的传播是从链表头部开始】,OutbondHandler负责处理IO操作【比如读取数据,OutbondHandler的传播是从链表尾部部开始】
举个栗子说明一下,请看以下代码
ServerBootstrap b = newServerBootstrap();
b.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.TCP_NODELAY,true)
.childAttr(AttributeKey.valueOf("childAttr"),"childAttrValue")
.handler(new SeverHandler())
.childHandler(new ChannelInitializer<SocketChannel>()
{
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
}
}
);
关注一下上面.handler(new SeverHandler()) 这句代码就是需要添加到服务端Channel内部PipeLine的节点,.childHandler()就是是加到客户端Channel内部PipeLine的节点,假设此时服务端和客户端都有好几个连接,需要在Hadler的实现类上加上Sharable注解,否则会报错
上面添加Hadler的代码我相信使用过Netty的人都很熟悉,这里要说下ChannelInitializer,这个类在Channel没有注册完成之前,其本身也是PipelLine的一个节点,但是这个类主要作用就是添加其他的Handler到Channel的PipeLine【是个工具人】,其内部实现了HandlerAdded方法,HandlerAdded方法会调用该类的InitChannel模板方法将Hadler加到该Channel对应的Pipeline【实际情况是做啥都可以】,这个方法在每个Channel注册完成后会调用,在InitChannel方法执行之后,它会把自己从PipeLine中删除
说了这么多,是时候该展示以下如果以上代码执行完毕之后,PipeLine的结构
2-2-1 NioServerSoketChannel(服务端channel)的PipeLine
直接上图
除了自定义的Hadler,默认会添加一个ServerBootstrapAcceptor,新接收到的连接左后都是由其进行处理,包括注册到工作线程组的某个Selector
2-2-2 NioSoketChannel(客户端channel)的PipeLine
也直接上图
自定义的Handler在服务端PipeLine的ServerBootstrapAcceptor中被添加
2-3 ServerBootStrap
这个类是启动的主类,这个类构造完成后基本既可以开始工作了,这里画个启动的时序图,虽然有点拉,凑合也能看看
2-3-1 主流程时序
主流程时序图如下【图片编号:image-20211227162633866.png】:
里面基本都是异步调用,包括注册和绑定端口,这些动作做完之后,服务端才能开始接收连接,这些任务会被封装成Task交给NIoEventLoop执行,我们只关心具体的业务逻辑
2-3-2 注册服务端网络Channel到Selector【图片编号:image-20211227162945911.png】
2-3-3 绑定网络地址【图片编号:image-20211227163102015.png】
2-3-4 激活服务端channel
绑定好端口和地址之后,说明可以正式提供服务,所以需要发送一个active信号,告知别人,同时也正式开始接收客户端连接,激活信号流转流程如下【图片编号:image-20211227163327235.png】:
2-3-5 接收客户端连接
有新连接接入后的主要时序图如下【图片编号:image-20211227163444846.png】,入口还是服务端Channel对应的NioEventLoop的run方法【木得办法,死循环,出不去】
3、图片地址
服务端的主要流程就这些,图片的gitee地址:https://gitee.com/source-code-note/graph/tree/master/netty