Netty整体框架
前面两篇文章对Java NIO进行了详细的介绍和分析,也给下面分析Netty源码打下一定的基础
netty框架参考博文:
https://www.cnblogs.com/imstudy/p/9908791.html
https://blog.csdn.net/u013857458/article/details/82527722
Java已经有了一个原生的NIO框架,为什么还会出现Netty呢,这个原因主要有两个:
- Java的NIO还不够高效,其底层使用selector,而Netty使用Linux下最高效的I/O模式epoll
- Selector多路复用的开发模式较为复杂,需要在程序中自己轮询,而且SelectionKey需要自己进行删除的管理,比较容易出错,而且由很多阻塞操作(select),Java自带的AIO更加难用。Netty是全异步操作,并且将底层IO操作全部封装,简化开发
- Java的NIO内存管理采用ByteBuffer,ByteBuffer是出了名的难用,在使用的时候要是忘记
flip()
很容易出错。Netty提供的ByteBuf就好用了很多,其采用读写双Index,更加易用
Netty的线程模型
Netty 主要基于主从 Reactors 多线程模型,但是做了一定的修改,其中主从 Reactor 多线程模型有主从两个Reactor:
- MainReactor 负责客户端的连接请求,并将请求转交给 SubReactor;
- SubReactor 负责相应通道的 IO 读写请求;
- 3)非 IO 请求(具体逻辑处理)的任务则会直接写入队列,等待 worker threads 进行处理。
这里引用 Doug Lee 大神的 Reactor 介绍——Scalable IO in Java 里面关于主从 Reactor 多线程模型的图:
需要注意的是:虽然 Netty 的线程模型基于主从 Reactor 多线程,借用了 MainReactor 和 SubReactor 的结构。但是实际实现上 SubReactor 和 Worker 线程在同一个线程池中
EventLoopGroup bossGroup = new NioEventLoopGroup(10);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,workerGroup);
b.channel(NioServerSocketChannel.class);
上面代码中的 bossGroup 和 workerGroup 是 Bootstrap 构造方法中传入的两个对象,这两个 group 均是EventLoopGroup
,也就是事件循环组,这里暂且把它认为是线程池
- NioEventLoopGroup: NIO的事件循环组,如果不指定初始化线程数量,将默认初始化CPU内核数 * 2个线程
private static final int DEFAULT_EVENT_LOOP_THREADS;
static {
DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
"io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
if (logger.isDebugEnabled()) {
logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
}
}
/**
* @see MultithreadEventExecutorGroup#MultithreadEventExecutorGroup(int, Executor, Object...)
*/
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
}
- bossGroup: 事件循环组只是在 Bind 某个端口后,获得其中一个线程作为 MainReactor,专门处理端口的 Accept 事件,每个端口对应一个 Boss 线程
- workerGroup: 事件循环组负责处理真正的逻辑会被各个 SubReactor 和 Worker 线程充分利用
Netty Reactor工作框架图
Server 端包含 1 个 Boss NioEventLoopGroup 和 1 个 Worker NioEventLoopGroup。
NioEventLoopGroup 相当于 1 个事件循环组,这个组里包含多个事件循环 NioEventLoop,每个 NioEventLoop 包含 1 个 Selector 和 1 个事件循环线程。
每个 Boss NioEventLoop 循环执行的任务包含 3 步:
1)轮询 Accept 事件;
2)处理 Accept I/O 事件,与 Client 建立连接,生成 NioSocketChannel,并将 NioSocketChannel 注册到某个 Worker NioEventLoop 的 Selector 上;
3)处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用 eventloop.execute 或 schedule 执行的任务,或者其他线程提交到该 eventloop 的任务。
每个 Worker NioEventLoop 循环执行的任务包含 3 步:
1)轮询 Read、Write 事件;
2)处理 I/O 事件,即 Read、Write 事件,在 NioSocketChannel 可读、可写事件发生时进行处理;
3)处理任务队列中的任务,runAllTasks。
Netty的异步处理模式
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者
Netty 中的 I/O 操作都是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture
调用者并不能立刻获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果
当 Future 对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态,注册监听函数来执行完成后的操作
常见有如下操作:
- 通过 isDone 方法来判断当前操作是否完成
- 通过 isSuccess 方法来判断已完成的当前操作是否成功
- 通过 getCause 方法来获取已完成的当前操作失败的原因
- 通过 isCancelled 方法来判断已完成的当前操作是否被取消
- 通过 addListener 方法来注册监听器,当操作已完成(isDone 方法返回完成),将会通知指定的监听器;如果 Future 对象已完成,则理解通知指定的监听器
例如下面的代码中绑定端口是异步操作,当绑定操作处理完,将会调用相应的监听器处理逻辑:
serverBootstrap.bind(port).addListener(future -> {
if(future.isSuccess()) {
System.out.println(newDate() + ": 端口["+ port + "]绑定成功!");
} else{
System.err.println("端口["+ port + "]绑定失败!");
}
});
我们也可用sync方法把异步操作变为同步操作,具体代码如下:
try {
ChannelFuture f = b.bind(12345).sync();
if(f.isSuccess()){
System.out.println("服务器启动成功");
}
f.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
Netty 框架的架构
Netty 功能特性
- 传输服务:支持 BIO 和 NIO
- 容器集成:支持 OSGI、JBossMC、Spring、Guice 容器
- 协议支持:HTTP、Protobuf、二进制、文本、WebSocket 等一系列常见协议都支持。还支持通过实行编码解码逻辑来实现自定义协议
- Core 核心:可扩展事件模型、通用通信 API、支持零拷贝的 ByteBuf 缓冲对象
Netty 模块
Netty 模块组件
1. Bootstrap、ServerBootstrap
Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类。
2. Future、ChannelFuture
正如前面介绍,在 Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。
但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 Future 和 ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。
3. Channel
Netty 网络通信的组件,能够用于执行网络 I/O 操作。Channel是被注册到EventLoop上的。Channel 为用户提供:
- 当前网络连接的通道的状态(例如是否打开?是否已连接?)
- 网络连接的配置参数 (例如接收缓冲区大小)
- 提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成。
- 调用立即返回一个 ChannelFuture 实例,通过注册监听器(addListener)到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方。
- 支持关联 I/O 操作与对应的处理程序。
不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应
下面是一些常用的 Channel 类型:
- NioSocketChannel,异步的客户端 TCP Socket 连接。
- NioServerSocketChannel,异步的服务器端 TCP Socket 连接,
统一使用水平触发
- EpollServerSocketChannel,异步的服务器端 TCP Socket 连接,
统一使用边缘触发
以获得最大性能,这个只能在Linux机器上使用
/**
* {@link ServerSocketChannel} implementation that uses linux EPOLL Edge-Triggered Mode for
* maximal performance.
*/
- NioDatagramChannel,异步的 UDP 连接。
- NioSctpChannel,异步的客户端 Sctp 连接。
- NioSctpServerChannel,异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。
4. NioEventLoop事件执行器
NioEventLoop 就是异步IO处理网络连接的生命周期中发生的各种事件,其中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务:
- I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。
- 非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发。
两种任务的执行时间比由变量 ioRatio 控制,默认为 50,则表示允许非 IO 任务执行的时间与 IO 任务的执行时间相等。
5. NioEventLoopGroup事件循环组
NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解为一个事件执行器的组(线程池),内部维护了一组循环线程,每个线程负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。每个线程执行都调用next函数把任务交给下一个线程执行,这样就可以获得Future,保证了Netty的全异步执行
6. Channel、EventLoop(Group)和ChannelFuture
- Channel:代表一个Socket链接
- ChannelFuture:异步通知,ServerBootstrap bind端口返回ChannelFuture
- EventLoop:循环处理网络连接的生命周期中发生的各种事件
EventLoop就是单线程的事件循环执行器, EventLoop组合成 EventLoopGroup, Channel被创建后就注册在了一个 EventLoop上, Channel在整个生命周期内使用EventLoop处理IO事件
关系说明:
- 一个EventLoopGroup 包含一个或者多个EventLoop
- 一个EventLoop 在它的生命周期内只和一个Thread 绑定
- 所有由EventLoop 处理的I/O 事件都将在它专有的Thread 上被处理
- 一个Channel 在它的生命周期内只注册于一个EventLoop
- 一个EventLoop 可能会被分配给一个或多个Channel
7. ChannelHandler、ChannelPipeline和ChannelHandlerContext
- ChannelHandler:应用程序开发人员的角度来看,Netty 的主要组件是ChannelHandler,它充当了所有处理入站和出站数据的应用程序逻辑的地方。Netty 以适配器类的形式(这里的适配器并不是设计模式中的适配器模式,更像是模版方法)提供了大量默认的ChannelHandler 实现,帮我们简化应用程序处理逻辑的开发过程
ChannelHandler的适配器
-
ChannelPipeline:提供了ChannelHandler 链的容器(
责任链模式
),并定义了用于在该链上传播入站和出站事件流的API。当ChannelHandler 被添加到ChannelPipeline 时,它将会被分配一个ChannelHandlerContext,其代表了ChannelHandler 和ChannelPipeline 之间的绑定Netty会把出站Handler和入站Handler放到一个Pipeline中,物理视图上看是一个,从逻辑视图上看是两个。那么站在逻辑视图的角度,分属出站和入站不同的Handler ,是无所谓顺序的。而同属一个方向的Handler则是有顺序的,因为上一个Handler处理的结果往往是下一个Handler的要求的输入。将图 中的处理器(ChannelHandler)从左到右进行编号,那么入站事件按顺序看到的ChannelHandler 将是1,2,4,而出站事件按顺序看到的ChannelHandler 将是5,3
- ChannelHandlerContext:创建ChannelHandler并绑定到ChannelPipline的时候Netty自动为该ChannelHandler生成相应ChannelHandlerContext,他代表了ChannelHandler和ChannelPipeline之间的绑定,在ChannelHandlerContext中可以拿到相应的Channel和ChannelPipeline
ChannelHandlerContext,Channel,Pipeline都有flush方法,区别:
- Channel,pipeline调用时将遍历所有ChannelHandler然后出站
- ChannelHandlerContext调用flush时将只调用他后面的ChannelHandler,所以一般都调用ChannelHandlerContext的WriteAndFlush函数