Netty 入门 — 要想掌握 Netty,你必须知道它的这些核心组件

在上篇文章(Netty 入门 --- 亘古不变的Hello World)中,我们简单认识了开发一个 Netty 服务端和客户端代码的主要步骤了,在这几大步骤中我们基本上可以看出 Netty 的几个核心组件。

在真正进入 Netty 的学习之前,我们非常有必要先对这些组件进行一个整体的认识,对于 Netty 入门阶段的讲解,大明哥采用整体 ---> 分解 ---> 总结的模式来阐述。对于一头牛,我们需要先知道这是一头牛,了解这头牛有哪些组织,然后再把这些组织一个一个地拆开来认识,清楚里面每一个组织的功能,最后再将这些组织组合成一头牛,是不是就会清晰很多。

Bootstrap:引导器

Bootstrap 的意思是引导,一个 Netty 应用通常是从 Bootstrap 开始的,所以在 hello world 的示例中,我第一步就是 new 一个 Bootstrap 对象。

Bootstrap 的作用就是配置整个 Netty 的组件,把这些组件构建成一个可运行的整体。

在 Netty 中,Bootstrap 有两个:

  1. Bootstrap:用于引导客户端
  2. ServerBootstrap:用于引导服务端

这里我们组装了第一个组件:

ByteBuf:数据传输的载体

几乎可以说所有的网络通信底层都是基于字节流来传输的,然而在实际应用中我们不可能直接使用底层的 byte 来进行数据交互,因为实在是太不方便了。基于实际应用,我们要求数据传输所使用的数据接口除了效率高外还需要使用方便。

所有的网络通信数据都有一个载体,Netty 的数据传输载体则是 ByteBuf,它提供了一个底层 Byte 的抽象视图

有小伙伴就说 Netty 是基于 NIO,在 JDK 原生的 NIO 中就有一个 ByteBuffer,使用起来也挺方便的,为什么不用它呢?因为原生的 ByteBuffer 有几个缺点:

  1. ByteBuffer 长度是固定的,无法动态扩展和收缩。
  2. ByteBuffer 只有一个标识位置的索引,读写的时候我们需要主动调用 flip() 进行模式切换,使用起来不方便又容易出错。
  3. ByteBuffer API 不够丰富,无法满足我们日常的开发需求。

而 Netty 提供的 ByteBuf 则很好地解决了这些问题,比原生的 ByteBuffer 更加灵活、高效。

ByteBuf 有几个非常重要的属性:

  • capacity:容量
  • readerIndex:读索引位置
  • writeIndex:写索引位置

由于有了两个索引,一个用于读,一个用于写,所以在使用 ByteBuf 的时候就不需要进行读写模式的切换了。readerIndex 和 writeIndex 的初始值都是 0,如下:

diff 复制代码
+-------------------------------+
|       writable bytes          |
+-------------------------------+
|                               |
0=readerIndex=writerIndex       capacity

当我们往 ByteBuf 中写入 N 个字节后:

diff 复制代码
+------------------+-------------------+
|  readable bytes  |    writable bytes |
+------------------+-------------------+
|                  |                   |
0=readerIndex      N=writerIndex       capacity

然后在读取 M 个字节,注意,这里 M 是不可能大于 N 的:

python 复制代码
+-------------------+------------------+------------------+
| discardable bytes |  readable bytes  |  writable bytes  |
+-------------------+------------------+------------------+
|                   |                  |                  |
0               M=readerIndex    N=writerIndex       capacity

整体来说 ByteBuf 的工作机制还是非常好理解的:readerIndex 和 writeIndex 的初始值为 0 ,当我们写入 N 个字节时,writeIndex + N,当从 ByteBuf 读取 M 个字节时,readerIndex + M。

至此,我们的版图再增加一个 :ByteBuf。

Channel:数据传输的通道

有了 ByteBuf 后,我们就有了数据的源头,但是没有通道传输啊,所以 Netty 有提供了另外一个组件:Channel。

Channel 是 Netty 的核心概念之一,是 Netty 网络 IO 操作的抽象,即 Netty 网络通信的主体,由它来负责对端进行网络通信、注册、数据操作等一切 IO 相关的操作

其主要功能包括:

  1. 网络 IO 的读写
  2. 客户端发起连接
  3. 关闭连接
  4. 网络连接的相关参数
  5. 绑定端口
  6. Netty 框架相关操作,如获取 Channel 相关联的 EventLoop、pipeline 等。

Channel 提供的 API 非常丰富,主要包括下面几类:

  • 类 getter API:主要用于获取 Channel 相关的属性,如绑定地址,相关配置等等。
  • Future 相关 API:Channel 所有的操作都是异步的,使用 Channel 的时候并不能立刻知道操作的结果,所以Netty 在完成 IO 调用后会返回一个 Future 对象,该对象就是 Channel 异步 IO 的结果。
  • 判断状态 API :Channel 有四种状态,分别是 openregisteractiveclose,Channel 提供了相对应的方法来判断当前 Channel 处于哪种状态,毕竟一些操作是需要在特定的状态下才能进行的。
  • 事件触发类方法:触发 IO 事件的方法。

到这里我们版图再次增加 Netty 的通信通道:Channel。

ChannelHandler:数据加工厂

有了 Channel 后,数据就能够流通了,那么下一步我们就需要对数据进行加工了,这个对数据加工的就是 ChannelHandler。ChannelHandler 是我们在 Netty 开发过程中打交道最多的组件,我们的所有业务逻辑几乎都写在 ChannelHandler 里面。

ChannelHandler 充当了所有处理入站和出站数据的应用程序逻辑的容器,其家族如下:

从 ChannelHandler 的类图可以看出,它有两类子接口:

  • ChannelInboundHandler:处理入站事件和数据
  • ChannelOutboundHandler:处理出站事件和数据

在实际开发过程中,我们一般都不直接使用这两个子接口,而是使用他们的适配器:

  • ChannelInboundHandlerAdapter:处理入站 I/O 事件
  • ChannelOutboundHandlerAdapter:处理出站 I/O 事件

我们只需要继承这两个对应的适配器,重写自己感兴趣的方法就可以了,如下:

scala 复制代码
public class ChannelHandlerTest_1 extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // do something
    }
}

方法有很多,在后面的文章中有详细介绍,这里就不展开阐述了。

至此,Netty 的版图多了一个 Netty 数据加工者:ChannelHandler

ChannelPipeline:服务编排器

ChannelHandler 是我们开发业务逻辑的主战场,但是我们的业务逻辑并不是很简单的,几乎都需要有多个 ChannelHandler 来相互配合完成一个业务逻辑,比如我们用户注册,就可以分为如下几个步骤:数据校验 ---> 保存用户 ---> 通知用户。难道这三个步骤我们都写在一个 ChannelHandler 里面吗?虽然也可以,但是整个逻辑都冗余在一起了,后期怎么维护,所以我们需要将这三个 ChannelHandler 拆分为 3 个,他们三个相互配合完成用户注册。那怎么配合呢?对不起,ChannelHandler 并不知道,他们只知道干活。这个时候就需要引入另外一个组件了:ChannelPipeline,handler 编排者。它来负责编排多个 ChannelHandler ,让他们相互配合完成一个完整的业务。例如注册,它会将 数据校验 ---> 保存用户 ---> 通知用户 三个 ChannelHandler 按照顺序编排在一起,一起来完成注册这个业务逻辑。

ChannelPipeline 我们可以看做是 ChannelHandler 的载体,它是由一组ChannelHandler 组成的,ChannelPipeline 采用责任链的方式将这些 ChannelHandler 组合成一个双向链表。当有 I/O 读写事件触发时,ChannelPipeline 会依次调用 ChannelHandler 来进行处理,事件是从头到尾处理的。

但是实际上 ChannelPipeline 并不是直接维护 ChannelHandler 的关系的,而是通过 ChannelHandlerContext 来维护的,为什么会多加这一层呢?因为在事件传播的过程中我们需要利用 ChannelHandlerContext 来保存上下文,如果没有这层的话,ChannelHandler 除了要处理自身的业务逻辑,还要兼顾维护前后置的关系,以及上下文的处理,这样就会显得整个 ChannelHandler 不够纯粹,不是很优雅。

加入服务编排器 ChannelPipeline 后,Netty 的版图如下:

EventLoop:I/O 事件核心处理引擎

有了数据,有了数据传输的通道,也有数据加工器和编排器,一个简单的网络 I/O 框架基本上就完成了,客户端向服务端发送 I/O 请求,经过 Channel 传输,经由 ChannelPipeline 编排的 ChannelHandler 进行业务逻辑处理,一条完美的流程就出来了。但是真的只有如此吗?我们认真考虑几个问题:

  1. 一个 I/O 请求就开一条线程处理吗?使用线程池能解决问题吗?
  2. Channel 与线程的关系如何?
  3. 没有空闲 CPU了,I/O 请求就傻傻地在那等着吗?
  4. 这条简单的数据加工线如何应对高并发?
  5. 有多线程就有线程安全问题,如何来保证线程安全?
  6. 等等

我想这条简单数据加工线并不能很好地解决上面的问题,就单单一个线程安全我估计就得处理疯吧。所以如果 Netty 的线程模型仅仅只是如何,那 Netty 也称不上一个优秀的网络 I/O 框架。而恰恰 Netty最精华的地方就在这里:基于 Reactor 模型的线程模型

Netty 的线程模型是基于 Reactor 线程模型,即 I/O 多路复用, 而 EventLoop 是 Netty Reactor 线程模型的核心处理引擎,**是 Netty 中最最核心的组件,也是 Netty 最精华的部分,它负责 Netty 中 I/O 事件的分发,也就是说,这个事件谁来做,它说了算。**Netty 为什么能处理成千上万客户端的连接,奥秘就在于这个地方。

一般来说我们都不是直接使用 EventLoop,而是通过 EventLoopGroup 提供的 API 来获取一个 EventLoop,EventLoopGroup 它是一组 EventLoop,它主要是来维护和管理 EventLoop。

Netty 推荐采用主从多线程模型,其中 BossEventLoopGroup 负责 ServerSocketChannel 的 Accept 事件,WorkerEventLoopGroup 负责 I/O 的读写事件。

加入 EventLoop 和 EventLoopGroup 后,Netty 版图如下:

总结

服务端组件执行流程

这里对 Netty 服务端整个流程做一个说明,阐述 Netty 服务端的工作流程,这样我们就清楚知道各个组件在 Netty 中扮演的是什么角色。

  1. 服务端在启动时,绑定本地端口,会初始化两个 EventLoopGroup,一个 BossEventLoopGroup 和 WorkEventLoopGroup ,其中 BossEventLoopGroup 专门负责接受客户端的连接(Accept 事件),WorkEventLoopGroup 专门负责网络读写。
  2. 当客户端连接服务端时,BossEventLoopGroup 响应请求,它会该客户端创建一个 Channel,该 Channel 会调用 EventLoopGroup 的 register() 方法,BossEventLoopGroup 会将该 Channel 与 WorkEventLoopGroup 中的某个 EventLoop 进行绑定,这种绑定关系是永久的,即在该 Channel 整个生命周期内的所有 I/O 读写事件都由该 EventLoop 来处理。
  3. 在创建 Channel 的时候,会创建一个 ChannelPipeline ,并将 Channel 与该 ChannelPipeline 绑定起来,ChannelPipeline 里面会构建一条完整的 ChannelHandler 处理链。
  4. 当有 I/O 读写事件发生时,则由 EventLoop 绑定的线程来执行,当然执行的主体还是 ChannelHandler。

服务端各组件的关系

大明哥这里再来说说各个组件之间的关系。

  1. 一个客户端对应一个 Channel。
  2. 一个 Channel 与一个 EventLoop 绑定,且这种绑定关系是永久的,在 Channel 的整个生命周期内的所有 I/O 读写事件都由该 EventLoop 处理。
  3. 一个 EventLoop 只与一个 Thread 线程进行绑定,该 EventLoop 的所有事件都由该 Thread 处理,所以 EventLoop 是一个单线程执行器,也就不会存在线程安全问题了。
  4. 一个 EventLoop 可以绑定多个 Channel,由于 EventLoop 的单线程执行器,所以如果单个 EventLoop 处理的 Channel I/O 读写事件较多则需要进行资源竞争了。
  5. 一个 Channel 与一个 ChannelPipeline 绑定,ChannelPipeline 里面包含了多个 ChannelHandler。

关系图如下:

到这里,整个 Netty 的核心组件就已经介绍完毕了,大明哥相信你们已经对整个 Netty 框架全貌有了一个初步的了解,下一步我们就来把这个 6 个组件拆分开来详细介绍。

注:Netty 最重要的就是这 6 个核心组件,大明哥将会花 30+ 篇文章来彻底讲清楚这 6 个核心组件的方方面面,分为入门篇和进阶篇。 入门篇:讲解基本概念及核心 API 的使用方法 进阶篇:深入分析核心概念,高阶功能以及用法

相关推荐
y先森5 小时前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy5 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189115 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿6 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡7 小时前
commitlint校验git提交信息
前端
虾球xz7 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇7 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒8 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员8 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐8 小时前
前端图像处理(一)
前端