一文讲透Netty核心源码

avatar
@海尔优家智能科技(北京)有限公司

深入分析 Netty Reactor 线程模型的实现原理
以生活中场景为例,类比说明 Netty 各核心组件的原理、交互流程
详细分析 Netty 核心源码流程

1. Netty是什么

Netty是 一个异步的,事件驱动的网络应用程序框架,基于Java Nio的Api实现 ...

定义较长,看完估计 懂的人感觉没营养,不懂的人依然茫然;不妨换个角度理解下

大部分同学对Spring Mvc、Mybatis框架很熟悉,对其所实现功能、提供哪些Api 都了然于大脑。

Netty框架与Java Nio之间关系, 类似于 Spring Mvc框架与Java Servlet ,Mybatis框架与Jdbc 之间的关系; 框架会让对应的功能开发更简洁、高效。

基于Spring Mvc框架开发web服务端应用,通常只需开发出 Controller、通用拦截器、参数/返回值解析器(框架有已提供多个选项)。

基于netty框架开发 Nio服务端应用,通常也只需开发出 请求处理器、通用过滤器、编/解码器(框架有已提供多个选项)。

netty与spring mvc 工作层次和原理很不一样,以上只是从框架对功能封装角度进行类比

2. 换个方式理解Netty Nio通讯

2.1 借助一个生活中的例子

假设你是老板,需要开家电话客服公司;提供的服务很简单:接电话并与用户沟通。

你为了更高效的服务大量用户,准备设置两个团队A和B,职责分派如下:

  • A团队:守着呼叫总台,接听并转交给B团队某一个人的电话
  • B团队:守着自己的工位电话,记录转交来的用户电话号码并与用户进行沟通

A团队活少,理论上可多个人干,你公司初期接听业务量不大,一个人即可;

B团队活多,理论上一个人干也行,通常会多个人一起干(按公司业务量分配)。

在开工前你准备好办公场所,为了节省人力成本、你分别告诉A和B职位的中介,等有活时在招聘对应人过来,一旦人招聘来之后,就按对应工位的工作职责夜以继日的干活。

等一切就绪之后,你给A团队工位1扔去第一个任务(开机)并告诉中介A找人干活,然后就坐到一边休息去了。

A1员工开启呼叫台后,就能接到广大用户的咨询电话,当来电话后,A1员工将对应呼叫信息包装成任务【和老板的沟通方式一致】扔给B团队中的某一个工位任务表,若工位没人则告诉中介B找人干活。

【故事end】

故事核心角色有 老板、A团队员工、A团队人力中介、B团队员工、B团队人力中介;重点要理解各角色的职责、交互方式(若已阅读过源码应该能对应到netty的各个组件)。

AB团队的员工在处理大多数任务时,会按照固定的流程 顺序倒序执行,下文用netty组件描述;暂不用故事描述了(否则题主讲的难受,读者看的更难受)

2.2 Netty交互流程

BossGroup(A团队)主要处理连接事件,各组件间关系,核心交互流程如下

  • ThreadPreTaskExecutor: 每次执行任务会创建新线程执行任务,NioEventLoopGroup 持有该线程池属性
  • NioEventLoopGroup:是线程池组 或 线程池集群,启动netty server的线程(幕后老板)提交任务到bossGroup,bossGroup轮询选择一个NioEventLoop并提交任务到任务队列中
  • NioEventLoop:仅有一个线程的线程池,当往线程池第一次提交任务时,通过ThreadPreTaskExecutor创建线程并赋值给NioEventLoop.thread属性,并且持有nio selector
  • 连接op_accept事件select出之后,会交给NioServerSocketChannel的内部Unsafe对象进行read(op_accept事件读取的是连接),之后会通过pipeline方法传播channelRead事件
  • Channel:netty server程序中有NioServerSocketChannel(端口监听channel),NioSocketChannel(与client通讯channel);
  • 各自封装了jdk nio channel,扩展了bind、connect、register、close、read、write等io事件;其内部类UnSafe主要用于和jdk nio 中的类进行交互
  • ChannelPipeline:与channel对象相互引用 ,内部包装了ChannelHandler链,类似于过滤器中的 FilterChain。

workGroup读取client消息与bossGroup读取client连接 流程基本一致,内容不一样而已;

业务handler处理完请求后通常会调用netty Channel的writeAndFlush方法、再经过ChannelPipeline中的ChannelHandler链传播到内部Unsafe对象,最终调用javachannel的write方法写出响应给client。

read 与 write对比

  • read消息,消息到网卡,经过nio selector 后传播到应用程序的ChannelHandler,从外向内传播(inbound 事件),对应用程序来说被动接收

jdkChannel(read到了消息) → unsafe → pipeline → head → businessHandler → tail

  • write返回值,应用程序调用channel.write方法后传播到Javachannel.write 写出到网卡并发送,从内向外传播(outbound 事件),是应用程序主动发起

nettychannel → pipeline → tail → businessHandler → head → unsafe → jdkChannel

3. Netty组件详解

inbound、outbound 唯一区别是事件传播方向不一样,从Java nio层 向 应用程序层(netty代码和用户代码) 方法传播 是inbound事件,从应用程序层方法 向 Java nio层传播 是 outbound事件。

inbound事件不一定都是由client发起,也可能是netty server自身发起的。

如:channel注册功能,从netty框架代码经过pipeline传播到Java nio层 属于outbound事件

当注册完成之后,netty框架会反方向(从Java nio层向应用程序层方法)传播 channelRegistered (inbound事件)

  • ChannelOutboundInvoker :用于传播outbound事件(如:writeAndFlush)
  • ChannelInboundInvoker :用于传播Inbound事件 (如:fireChannelRead)
  • ChannelOutboundHandler :处理outbound事件业务逻辑(如:bind,write,connect)
  • ChannelInboundHandler :处理inbound事件逻辑 (如:channelActive,channelRead)
  • ChannelPipeline、ChannelHandlerContext 组件都实现了ChannelOutboundInvoker与ChannelInboundInvoker,两者都可传递inbound和outbound事件
  • Channel组件只实现了ChannelOutboundInvoker,若阅读到对应代码,可能会疑问:为什么只实现了ChannelOutboundInvoker?难道不能传播inbound事件?

通过上面流程得知所有的io事件都会经过Channel, 题主认为不实现ChannelInboundInvoker是因为用不到,原因如下:

  1. inbound事件是从 jdk nio 层 向应用程序层传播,通过 javachannel可以引用获取到nettychannel进而能引用到unsafe对象,直接调用unsafe或pipeline进行传播事件即可,netty内部也是这么操作的。
  2. outbound事件是从应用程序层向jdk nio 层传播,需要提供对应的write、bind、close、等方法给调用方使用。

ChannelPipeline中的ChannelHandler在被添加时会包装成ChannelHandlerContext,具体的某一个ChannelHandler可只处理inbound或outbound事件。

但每个ChannelHandlerContext对象都具有传播inbound和outbound事件的能力,才能传播所有事件。

HeadContext特殊性

  1. 实现了inbound与outbound接口但强制设置inbound为false(题主对这里理解还不成熟、暂不表述)

  2. head比tail多了个unsafe属性,从调用层次来看head紧挨着unsafe,事件传播时会相互调用

     final class HeadContext extends AbstractChannelHandlerContext
            implements ChannelOutboundHandler, ChannelInboundHandler {
    
        /**
         * HeadContext比TailContext多了个 unsafe属性
         */
        private final Unsafe unsafe; 
    
        HeadContext(DefaultChannelPipeline pipeline) {
            super(pipeline, null, HEAD_NAME, false(inbound), true(outbound));
            unsafe = pipeline.channel().unsafe();
            setAddComplete();
        }
        。。。
    

    }

4. 走读Netty Nio代码

netty server启动四大步骤

  1. 创建NioServerSocketChannel
  2. 初始化NioServerSocketChannel参数
  3. 注册channel到selector
  4. 绑定端口

端口绑定后,可接受客户端连接,并收发消息

注意Main线程、boss io线程、worker io线程之间的交互

代码流程较长、建议在电脑端对着源码阅读

5. 本文作者及团队介绍

三翼鸟****数字化技术平台-智能运营平台」以用户行为数据为基础,利用推荐引擎为用户提供“千人千面”的个性化推荐服务,改善用户体验,持续提升核心业务指标。通过构建高效、智能的线上运营系统,全面整合数据资产,实现数据分析-人群圈选-用户触达-后效分析-策略优化的运营闭环,并提供可视化报表,一站式操作提升数字化运营效率。