Netty 版本 : 4.1.101.Final
Dubbo 版本 : 3.3
协议:dubbo(后续会单独分析triple协议以及作者提的connection window size的pr)
说明 : Netty的源码需要依据具体的使用场景来分析,脱离了场景来分析Netty的代码都是耍流氓(虽然Netty框架流程很固定,但是对代码的理解与记忆没有场景驱动来的深),本文就以Dubbo这个微服务框架为例来分析Netty的底层实现
注意:本文默认读者了解java nio中相关核心组件
Server端到底是怎么处理连接请求的?
- 我们用dubbo-demo项目来剖析下整个源码的执行过程:源码地址
1. 启动dubbo-demo-spring-boot-provider
下面的ProviderApplication提供者应用,找到传输层(netty)的初始化入口类NettyServer
,根据上一篇介绍的netty框架的相关组件,我们可以直接找到包含ServerBootstrap
的位置并找到红框所在的方法:
1️⃣ bossGroup线程组属于parent(EventLoopGroup),workerGroup属于child(EventLoopGroup),加入到启动配置中,默认bossGroup只有一个线程
,workerGroup的线程数取CPU核心数+1跟32的较小值
。EventLoopGroup此处默认请求下会创建NioEventLoopGroup,只有在Linux系统下并且设置netty.epoll.enable=true时才会创建EpollEventLoopGroup(两者区别的话读者可自行查阅资料),
大致路径是:
NioEventLoopGroup
-> MultithreadEventLoopGroup
-> MultithreadEventExecutorGroup
,可以看到线程组中包含了一个线程数组EventExecutor[],最终的子类实现是NioEventLoop
(主角登场🎤),而NioEventLoop也存在特殊的继承关系:NioEventLoop
-> SingleThreadEventLoop
-> SingleThreadEventExecutor
-> AbstractScheduledEventExecutor
(又继承并抽象了java中线程池处理逻辑) -> EventLoop
2️⃣ channel(NettyEventLoopFactory.serverSocketChannelClass()) 方法就是用来初始化ServerSocketChannel属于哪种类型:(1)EpollSocketChannel (2)NioSocketChannel,当然,默认情况下会创建NioSocketChannel,EpollSocketChannel同上述EpollEventLoopGroup的创建条件
3️⃣ childOption的TCP配置项(为什么是child的配置项?根据作者上一篇中提到Netty线程模型的特点:workerGroup才用来读写请求,boosGroup只用来接收连接
):(1)SO_REUSEADDR:开启地址重用 (2)TCP_NODELAY:数据是否延迟发送,一般设置为true,实时性更高(3)SO_KEEPALIVE:连接保活,注意是TCP层面的,对现在的微服务架构一般没啥用,应用层还需要做连接存活check (4)ALLOCATOR:buffer分配器,默认是直接内存方法分配。
4️⃣ childHandler
:此处是Netty框架的重点但不是本篇文章的重点部分,作用是配置workerGroup对应的handler处理器,接收请求时的自定义handler,核心编解码等相关逻辑都是在这里配置的
2. 初始化server启动配置之后,紧接着开始绑定到指定的端口上: ChannelFuture channelFuture = bootstrap.bind(getBindAddress());
最终会进入到如下的绑定逻辑:
1️⃣ 根据第1步里面配置的Channel类型创建channel实例(NioServerSocketChannel),注意这里是默认是通过ReflectiveChannelFactory
调用channel的无参构造反射获取实例
2️⃣ 图3构造器中,我们需要特别关注的是newChannel方法,该方法会创建一个java.nio.channels.ServerSocketChannel(原始的nio ServerSocketChannel,这也验证了上一篇说的特性是基于java nio),另外再看第四个构造器,parent等于null,言外之意当前channel就是parent,并且只对SelectionKey.OP_ACCEPT事件感兴趣(连接事件),传到父类AbstractNioMessageChannel中
,大致继承关系:NioServerSocketChannel
->AbstractNioMessageChannel
-> AbstractNioChannel
-> AbstractChannel
⚠️注意:此channel被保存在AbstractNioChannel
(毕竟是nio的东西嘛),通过 io.netty.channel.nio.AbstractNioChannel#javaChannel方法可以在子类需要的时候直接获取
3️⃣ 然后调用图4中的init方法初始化一些channel的配置,我们可以看到一些netty中比较常见的类:ChannelPipeline(保存在AbstractChannel中,属于最顶层,连接级别,netty的整个请求处理生命周期都在其内部完成),这里红框中有一个比较重要的地方就是往当前pipeline(parent)中添加了一个handler叫做ServerBootstrapAcceptor
,光看这个名字大概就猜出来就是作为acceptor来处理连接的handler。
4️⃣ 初始化channel的配置之后,再看图2中有一行重要的代码:ChannelFuture regFuture = config().group().register(channel);
其中config().group()获取的是当前bossGroup(此处是NioEventLoopGroup),那么自然会走NioEventLoopGroup这一条链路的register逻辑。从上面的继承关系,最终会走到此处来选择一个EventExecutor(具体的某个线程,实际上就是我们上面的主角NioEventLoop,代码继承关系比较复杂,建议读者跟着截图自行debug)
5️⃣ 搞来搞去(🥚都要碎了),最终会调用register0模版方法最终注册
其中doRegister方法是用来给子类扩展的,可以看到在AbstractNioChannel中这段代码,是不是似曾相识!!! 没错就是原始社会中手撕java nio,channel注册到Selector的方式
,另外还有典型的netty处理风格:事件驱动
,沿着pipeline顺序执行handler,(1)通知handler增加了(2)通知channel注册了。3. 注册完Channel之后,接下来就进入到bind端口逻辑
由于我们之前创建的是NioServerSocketChannel,所以会走到其绑定的实现逻辑,
javaChannel().bind(localAddress, config.getBacklog());
此行代码是不是依然似曾相识!!!什么?这就结束了?扯了这么多,问题还是没有解决啊!!!看官别急,且跟我继续探索😊
4. 不知道你有没有留心到,作者前面提到的主角NioEventloop
,既然是主角为什么一笔带过了。。。(我可以怪Netty逻辑上太能绕了吗,前面出场的时候,并没有场景来驱动,切入不了事件处理流程😂),再仔细看一下图8,doBind0中通过channel.eventLoop().execute(...)触发的端口绑定
,这行代码怎么这么眼熟? 线程池提交任务不就是这么搞的嘛!!!只不过channel.eventLoop()获取的是主角NioEventLoop
,主角出来必定会震惊九天十地(又跑偏了...)。调用链过长,先看一下类的UML:
最终会执行到如下的方法:
1️⃣ 将任务添加到队列中(类似于java中线程池添加任务的逻辑)
2️⃣ 如果提交的线程跟当前NioEventLoop中的线程不是同一个(外部提交的任务,一般程序初始创建绑定端口的时候),则启动当前主角
NioEventLoop
下的线程并运行,逻辑如下:3️⃣ 当前
executor
是被包装之后的ThreadPerTaskExecutor
线程池,顾名思义该线程池只有一个线程来处理任务,另外比较关键的一行代码是:SingleThreadEventExecutor.this.run();
该方法是个抽象方法,需要子类来完成,即通过当前线程对应的NioEventLoop来执行run方法,方法如下:4️⃣ 此时启动consumer应用ConsumerApplication,咱们只看监听到连接事件之后的处理,方法
进入到处理就绪事件的方法:
5️⃣ 由于我们之前启动server时设置的感兴趣事件就是SelectionKey.OP_ACCEPT,所以此处会走到unsafe.read()
,方法如下:
可以看到此处时do{}while()循环的方式来读取请求数据,其中
doReadMessages
方法是读取请求消息的核心方法,如下:可以看到通过java nio中的channel的accept方法来接收连接
(是不是又似曾相识!!!🤣),然后添加到读取缓冲区集合readBuf中,方法如下:此时我们readBuf中是
NioSocketChannel
,通过pipeline.fireChannelRead(readBuf.get(i))
方法,会传播到上面已经添加的ServerBootstrapAcceptor
处理器的channelRead
方法,如下:本篇文章写到这里要告一段落了!至此,我们就应该很清晰的知道了服务端的accept过程!
- ☛ 文章要是勘误或者知识点说的不正确,欢迎评论,毕竟这也是作者通过阅读源码及相关文档获得的知识,难免会有疏忽!
- ☛ 要是感觉文章对你有所帮助,不妨点个关注,或者移驾看一下作者的其他文集,也都是干活多多哦,文章也在全力更新中。
- ☛ 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处!