揭开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. 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容