揭开Netty的神秘面纱之accept连接过程

Netty 版本 : 4.1.101.Final
Dubbo 版本 : 3.3
协议:dubbo(后续会单独分析triple协议以及作者提的connection window size的pr)
说明 : Netty的源码需要依据具体的使用场景来分析,脱离了场景来分析Netty的代码都是耍流氓(虽然Netty框架流程很固定,但是对代码的理解与记忆没有场景驱动来的深),本文就以Dubbo这个微服务框架为例来分析Netty的底层实现
注意:本文默认读者了解java nio中相关核心组件

Server端到底是怎么处理连接请求的?

  • 我们用dubbo-demo项目来剖析下整个源码的执行过程:源码地址
image.png

1. 启动dubbo-demo-spring-boot-provider下面的ProviderApplication提供者应用,找到传输层(netty)的初始化入口类NettyServer,根据上一篇介绍的netty框架的相关组件,我们可以直接找到包含ServerBootstrap的位置并找到红框所在的方法:

org.apache.dubbo.remoting.transport.netty4.NettyServer#doOpen

initServerBootstrap

org.apache.dubbo.remoting.transport.netty4.NettyEventLoopFactory#eventLoopGroup

1️⃣ bossGroup线程组属于parent(EventLoopGroup),workerGroup属于child(EventLoopGroup),加入到启动配置中,默认bossGroup只有一个线程,workerGroup的线程数取CPU核心数+1跟32的较小值 。EventLoopGroup此处默认请求下会创建NioEventLoopGroup,只有在Linux系统下并且设置netty.epoll.enable=true时才会创建EpollEventLoopGroup(两者区别的话读者可自行查阅资料),

NioEventLoopGroup.png

大致路径是:NioEventLoopGroup -> MultithreadEventLoopGroup -> MultithreadEventExecutorGroup,可以看到线程组中包含了一个线程数组EventExecutor[],最终的子类实现是NioEventLoop(主角登场🎤),而NioEventLoop也存在特殊的继承关系:NioEventLoop -> SingleThreadEventLoop -> SingleThreadEventExecutor -> AbstractScheduledEventExecutor(又继承并抽象了java中线程池处理逻辑) -> EventLoop
MultithreadEventExecutorGroup构造器

io.netty.channel.nio.NioEventLoopGroup#newChild

2️⃣ channel(NettyEventLoopFactory.serverSocketChannelClass()) 方法就是用来初始化ServerSocketChannel属于哪种类型:(1)EpollSocketChannel (2)NioSocketChannel,当然,默认情况下会创建NioSocketChannel,EpollSocketChannel同上述EpollEventLoopGroup的创建条件

ServerSocketChannel类型选择

NioServerSocketChannel.png

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());最终会进入到如下的绑定逻辑:

io.netty.bootstrap.AbstractBootstrap#doBind 图1
io.netty.bootstrap.AbstractBootstrap#initAndRegister 图2

1️⃣ 根据第1步里面配置的Channel类型创建channel实例(NioServerSocketChannel),注意这里是默认是通过ReflectiveChannelFactory调用channel的无参构造反射获取实例

NioServerSocketChannel构造器 图3

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。

io.netty.bootstrap.ServerBootstrap#init 图4

4️⃣ 初始化channel的配置之后,再看图2中有一行重要的代码ChannelFuture regFuture = config().group().register(channel); 其中config().group()获取的是当前bossGroup(此处是NioEventLoopGroup),那么自然会走NioEventLoopGroup这一条链路的register逻辑。从上面的继承关系,最终会走到此处来选择一个EventExecutor(具体的某个线程,实际上就是我们上面的主角NioEventLoop,代码继承关系比较复杂,建议读者跟着截图自行debug)

io.netty.util.concurrent.MultithreadEventExecutorGroup#next 图5
所以会进到NioEventLoop的register逻辑,方法继承自父类SingleThreadEventLoop,如下:
SingleThreadEventLoop的register方法

5️⃣ 搞来搞去(🥚都要碎了),最终会调用register0模版方法最终注册
io.netty.channel.AbstractChannel.AbstractUnsafe#register0 图6

其中doRegister方法是用来给子类扩展的,可以看到在AbstractNioChannel中这段代码,是不是似曾相识!!! 没错就是原始社会中手撕java nio,channel注册到Selector的方式,另外还有典型的netty处理风格:事件驱动,沿着pipeline顺序执行handler,(1)通知handler增加了(2)通知channel注册了。
io.netty.channel.nio.AbstractNioChannel#doRegister 图7

3. 注册完Channel之后,接下来就进入到bind端口逻辑

io.netty.bootstrap.AbstractBootstrap#doBind0 图8

由于我们之前创建的是NioServerSocketChannel,所以会走到其绑定的实现逻辑,javaChannel().bind(localAddress, config.getBacklog()); 此行代码是不是依然似曾相识!!!
io.netty.channel.socket.nio.NioServerSocketChannel#doBind 图9

什么?这就结束了?扯了这么多,问题还是没有解决啊!!!看官别急,且跟我继续探索😊

4. 不知道你有没有留心到,作者前面提到的主角NioEventloop,既然是主角为什么一笔带过了。。。(我可以怪Netty逻辑上太能绕了吗,前面出场的时候,并没有场景来驱动,切入不了事件处理流程😂),再仔细看一下图8,doBind0中通过channel.eventLoop().execute(...)触发的端口绑定,这行代码怎么这么眼熟? 线程池提交任务不就是这么搞的嘛!!!只不过channel.eventLoop()获取的是主角NioEventLoop,主角出来必定会震惊九天十地(又跑偏了...)。调用链过长,先看一下类的UML:

NioEventLoop.png

最终会执行到如下的方法:

io.netty.util.concurrent.SingleThreadEventExecutor#execute(java.lang.Runnable, boolean) 图10

1️⃣ 将任务添加到队列中(类似于java中线程池添加任务的逻辑)
2️⃣ 如果提交的线程跟当前NioEventLoop中的线程不是同一个(外部提交的任务,一般程序初始创建绑定端口的时候),则启动当前主角NioEventLoop下的线程并运行,逻辑如下:
io.netty.util.concurrent.SingleThreadEventExecutor#doStartThread 图11

3️⃣ 当前executor是被包装之后的ThreadPerTaskExecutor线程池,顾名思义该线程池只有一个线程来处理任务,另外比较关键的一行代码是:SingleThreadEventExecutor.this.run(); 该方法是个抽象方法,需要子类来完成,即通过当前线程对应的NioEventLoop来执行run方法,方法如下:
io.netty.channel.nio.NioEventLoop#run 图12-1

io.netty.channel.nio.NioEventLoop#run 图12-2

4️⃣ 此时启动consumer应用ConsumerApplication,咱们只看监听到连接事件之后的处理,方法


io.netty.channel.nio.NioEventLoop#processSelectedKeys 图13

进入到处理就绪事件的方法:


io.netty.channel.nio.NioEventLoop#processSelectedKey(java.nio.channels.SelectionKey, io.netty.channel.nio.AbstractNioChannel) 图14

5️⃣ 由于我们之前启动server时设置的感兴趣事件就是SelectionKey.OP_ACCEPT,所以此处会走到unsafe.read(),方法如下:

io.netty.channel.nio.AbstractNioMessageChannel.NioMessageUnsafe#read 图15

可以看到此处时do{}while()循环的方式来读取请求数据,其中doReadMessages方法是读取请求消息的核心方法,如下:
io.netty.channel.socket.nio.NioServerSocketChannel#doReadMessages 图16-1

可以看到通过java nio中的channel的accept方法来接收连接(是不是又似曾相识!!!🤣),然后添加到读取缓冲区集合readBuf中,方法如下:
io.netty.channel.socket.nio.NioServerSocketChannel#doReadMessages 图16-2

此时我们readBuf中是NioSocketChannel,通过pipeline.fireChannelRead(readBuf.get(i))方法,会传播到上面已经添加的ServerBootstrapAcceptor 处理器的channelRead方法,如下:
io.netty.bootstrap.ServerBootstrap.ServerBootstrapAcceptor#channelRead
,然后这个时候就会初始化workerGroup等之前配置的child的相关handler以及会把channel注册到系统监听中(原理同BossGroup,不在细述)

本篇文章写到这里要告一段落了!至此,我们就应该很清晰的知道了服务端的accept过程!

  1. ☛ 文章要是勘误或者知识点说的不正确,欢迎评论,毕竟这也是作者通过阅读源码及相关文档获得的知识,难免会有疏忽!
  2. 要是感觉文章对你有所帮助,不妨点个关注,或者移驾看一下作者的其他文集,也都是干活多多哦,文章也在全力更新中。
  3. 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容