Netty 的几个核心组件。
Bootstrap:引导器
Bootstrap 的意思是引导,一个 Netty 应用通常是从 Bootstrap 开始的,所以在 hello world 的示例中,我第一步就是 new 一个 Bootstrap 对象。
Bootstrap的作用就是配置整个Netty的组件,把这些组件构建成一个可运行的整体。
在 Netty 中,Bootstrap 有两个:
Bootstrap:用于引导客户端。ServerBootstrap:用于引导服务端。
这里我们组装了第一个组件:
ByteBuf:数据传输的载体
几乎可以说所有的网络通信底层都是基于字节流来传输的,然而在实际应用中我们不可能直接使用底层的 byte 来进行数据交互,因为实在是太不方便了。基于实际应用,我们要求数据传输所使用的数据接口除了效率高外还需要使用方便。
所有的网络通信数据都有一个载体,Netty的数据传输载体则是ByteBuf,它提供了一个底层Byte的抽象视图。
ByteBuf 有几个非常重要的属性:
capacity:容量readerIndex:读索引位置writeIndex:写索引位置
由于有了两个索引,一个用于读,一个用于写,所以在使用 ByteBuf 的时候就不需要进行读写模式的切换了。readerIndex 和 writeIndex 的初始值都是 0,如下:
+-------------------------------+
| writable bytes |
+-------------------------------+
| |
0=readerIndex=writerIndex capacity
当我们往 ByteBuf 中写入 N 个字节后:
+------------------+-------------------+
| readable bytes | writable bytes |
+------------------+-------------------+
| | |
0=readerIndex N=writerIndex capacity
然后在读取 M 个字节,注意,这里 M 是不可能大于 N 的:
+-------------------+------------------+------------------+
| discardable bytes | readable bytes | writable bytes |
+-------------------+------------------+------------------+
| | | |
0 M=readerIndex N=writerIndex capacity
readerIndex 和 writeIndex 的初始值为 0 ,当我们写入 N 个字节时,writeIndex + N,当从 ByteBuf 读取 M 个字节时,readerIndex + M。
组装下一个组件 :ByteBuf。
Channel:数据传输的通道
有了 ByteBuf 后,我们就有了数据的源头,但是还需要通道传输,所以 Netty 提供了另外一个组件:Channel。
Channel是Netty的核心概念之一,是Netty网络IO操作的抽象,即 Netty 网络通信的主体,由它来负责对端进行网络通信、注册、数据操作等一切IO相关的操作。
其主要功能包括:
- 网络
IO的读写。 - 客户端发起连接。
- 关闭连接。
- 网络连接的相关参数。
- 绑定端口。
Netty框架相关操作,如获取Channel相关联的EventLoop、pipeline等。
Channel 提供的 API 非常丰富,主要包括下面几类:
- getter相关API:主要用于获取
Channel相关的属性,如绑定地址,相关配置等等。 - Future相关API:
Channel所有的操作都是异步的,使用Channel的时候并不能立刻知道操作的结果,所以Netty在完成IO调用后会返回一个Future对象,该对象就是Channel异步IO的结果。 - 判断状态 API:
Channel有四种状态,分别是open、register、active、close,Channel提供了相对应的方法来判断当前Channel处于哪种状态,毕竟一些操作是需要在特定的状态下才能进行的。 - 事件触发类方法:触发
IO事件的方法。
组装 Netty 的下一个组件:Channel。
ChannelHandler:数据加工厂
有了 Channel 后,数据就能够流通了,那么下一步我们就需要对数据进行加工了,这个对数据加工的就是 ChannelHandler。ChannelHandler 是我们在 Netty 开发过程中打交道最多的组件,我们的所有业务逻辑几乎都写在 ChannelHandler 里面。
ChannelHandler 充当了所有处理入站和出站数据的应用程序逻辑的容器,其家族如下:
从 ChannelHandler 的类图可以看出,它有两类子接口:
ChannelInboundHandler:处理入站事件和数据。ChannelOutboundHandler:处理出站事件和数据。
在实际开发过程中,我们一般都不直接使用这两个子接口,而是使用他们的适配器:
ChannelInboundHandlerAdapter:处理入站 I/O 事件。ChannelOutboundHandlerAdapter:处理出站 I/O 事件。
我们只需要继承这两个对应的适配器,重写自己感兴趣的方法就可以了,如下:
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 进行业务逻辑处理,一条完美的流程就出来了。但是真的只有如此吗?我们认真考虑几个问题:
- 一个
I/O请求就开一条线程处理吗?使用线程池能解决问题吗? Channel与线程的关系如何?- 没有空闲
CPU了,I/O请求就傻傻地在那等着吗? - 这条简单的数据加工线如何应对高并发?
- 有多线程就有线程安全问题,如何来保证线程安全?
我想这条简单数据加工线并不能很好地解决上面的问题,就单单一个线程安全我估计就得处理疯吧。所以如果 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 结构图如下:
总结
服务端组件执行流程
- 服务端在启动时,绑定本地端口,会初始化两个
EventLoopGroup,一个BossEventLoopGroup和WorkEventLoopGroup,其中BossEventLoopGroup专门负责接受客户端的连接(Accept事件),WorkEventLoopGroup专门负责网络读写。 - 当客户端连接服务端时,
BossEventLoopGroup响应请求,它会该客户端创建一个Channel,该Channel会调用EventLoopGroup的register()方法,BossEventLoopGroup会将该Channel与WorkEventLoopGroup中的某个EventLoop进行绑定,这种绑定关系是永久的,即在该Channel整个生命周期内的所有I/O读写事件都由该EventLoop来处理。 - 在创建
Channel的时候,会创建一个ChannelPipeline,并将Channel与该ChannelPipeline绑定起来,ChannelPipeline里面会构建一条完整的ChannelHandler处理链。 - 当有
I/O读写事件发生时,则由EventLoop绑定的线程来执行,当然执行的主体还是ChannelHandler。
服务端各组件的关系
- 一个客户端对应一个
Channel。 - 一个
Channel与一个EventLoop绑定,且这种绑定关系是永久的,在Channel的整个生命周期内的所有I/O读写事件都由该EventLoop处理。 - 一个
EventLoop只与一个Thread线程进行绑定,该EventLoop的所有事件都由该Thread处理,所以EventLoop是一个单线程执行器,也就不会存在线程安全问题了。 - 一个
EventLoop可以绑定多个Channel,由于EventLoop的单线程执行器,所以如果单个EventLoop处理的Channel,I/O读写事件较多则需要进行资源竞争了。 - 一个
Channel与一个ChannelPipeline绑定,ChannelPipeline里面包含了多个ChannelHandler。
关系图如下: