目录
ChannelHandler 与 ChannelPipeline
Netty怎么解决 Java 的 epoll 空轮询 bug?
NIO的不足
- NIO的类库和API繁杂,使用麻烦,需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等
- 需要具备其它的额外技能做铺垫,例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序
- 可靠性能力补齐,开发工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大
- 存在一些BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该bug发生概率降低了一些而已,它并没有被根本解决
Netty特点
Netty的对JDK自带的NIO的API进行封装,解决上述问题,主要特点为:
- 设计优雅适用于各种传输类型的统一API - 阻塞和非阻塞Socket基于灵活且可扩展的事件模型,可以清晰地分离关注点,高度可定制的线程模型 - 单线程,一个或多个线程池真正的无连接数据报套接字支持
- 使用方便详细记录的Javadoc,用户指南和示例没有其他依赖项,JDK 5(Netty 3.x)或6(Netty 4.x)就足够了
- 高性能吞吐量更高,延迟更低,减少资源消耗,最小化不必要的内存复制
- 安全完整的SSL / TLS和StartTLS支持
- 社区活跃,不断更新社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会被加入
功能特性
- 传输服务,支持 BIO 和 NIO。
- 容器集成,支持 OSGI、JBossMC、Spring、Guice 容器。
- 协议支持,HTTP、Protobuf、二进制、文本、WebSocket 等一系列常见协议都支持。还支持通过实行编码解码逻辑来实现自定义协议。
- Core 核心,可扩展事件模型、通用通信 API、支持零拷贝的 ByteBuf 缓冲对象。
线程模型
事件驱动模型
通常,我们设计一个事件处理模型的程序有两种思路:
- 轮询方式:线程不断轮询访问相关事件发生源有没有发生事件,有发生事件就调用事件处理逻辑
- 事件驱动方式:发生事件,主线程把事件放入事件队列,在另外线程不断循环消费事件列表中的事件,调用事件对应的处理逻辑处理事件。事件驱动方式也被称为消息通知方式,其实是设计模式中观察者模式的思路。
事件驱动模型主要包括 4 个基本组件:
- 事件队列(event queue):接收事件的入口,存储待处理事件。
- 分发器(event mediator):将不同的事件分发到不同的业务逻辑单元。
- 事件通道(event channel):分发器与处理器之间的联系渠道。
- 事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作。
可以看出,相对传统轮询模式,事件驱动有如下优点:
- 可扩展性好,分布式的异步架构,事件处理器之间高度解耦,可以方便扩展事件处理逻辑。
- 高性能,基于队列暂存事件,能方便并行异步处理事件。
Reactor 线程模型
Reactor 模型是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式
关键组成
- Reactor,Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人
- Handlers,处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作
分类
根据Reactor 的数量和 Hanndler 线程数量的不同,Reactor 模型有 3 个变种:
- 单 Reactor 单线程
- 单 Reactor 多线程
- 主从 Reactor 多线程
Netty线程模型
主要基于主从 Reactors 多线程模型,但是做了一定的修改,其中主从 Reactor 多线程模型有多个 Reactor(实际上 SubReactor 和 Worker 线程在同一个线程池中):
- MainReactor 负责客户端的连接请求,并将请求转交给 SubReactor
- SubReactor 负责相应通道的 IO 读写请求(异步IO)
- 非 IO 请求(具体逻辑处理)的任务则会直接写入队列,等待 worker threads 进行处理
线程模型概述
- Netty抽象出两组线程池:BossGroup 和 WorkerGroup(都是NioEventLoopGroup)
- BossGroup 专门负责接收客户端的连接
- WorkerGroup 专门负责网络的读写
- BossGroup 线程维护Selector , 只关注Accecpt
- 当接收到Accept事件,获取到对应的SocketChannel, 封装成 NIOScoketChannel并注册到 Worker 线程(事件循环),并进行维护
- 当Worker线程监听到selector 中通道发生自己感兴趣的事件后,就进行处理(就由handler), 注意 handler 已经加入到通道
Netty架构
- ServerBootstrap 与 Bootstrap
- Future 与 ChannelFuture
- Channel
- Selector
- EventLoop 与 EventLoopGroup
- ChannelHandler 与 ChannelPipeline
组成
ServerBootstrap 与 Bootstrap
Bootstarp 和 ServerBootstrap 被称为引导类,指对应用程序进行配置,并使他运行起来的过程。Netty处理引导的方式是使你的应用程序和网络层相隔离。
Bootstrap 是客户端的引导类,Bootstrap 在调用 bind()(连接UDP)和 connect()(连接TCP)方法时,会新创建一个 Channel,仅创建一个单独的、没有父 Channel 的 Channel 来实现所有的网络交换。
ServerBootstrap 是服务端的引导类,ServerBootstarp 在调用 bind() 方法时会创建一个 ServerChannel 来接受来自客户端的连接,并且该 ServerChannel 管理了多个子 Channel 用于同客户端之间的通信。
Future 与 ChannelFuture
Netty 中所有的 I/O 操作都是异步的,即操作不会立即得到返回结果,所以 Netty 中定义了一个 ChannelFuture 对象作为这个异步操作的“代言人”,表示异步操作本身。如果想获取到该异步操作的返回值,可以通过该异步操作对象的addListener() 方法为该异步操作添加监 NIO 网络编程框架 Netty 听器,为其注册回调:当结果出来后马上调用执行。
Netty 的异步编程模型都是建立在 Future 与回调概念之上的。
Channel
Channel是 Java NIO 的一个基本构造。可以看作是传入或传出数据的载体。因此,它可以被打开或关闭,连接或者断开连接。
Selector
Netty基于Selector对象实现I/O多路复用,通过 Selector, 一个线程可以监听多个连接的Channel事件, 当向一个Selector中注册Channel 后,Selector 内部的机制就可以自动不断地查询(select) 这些注册的Channel是否有已就绪的I/O事件(例如可读, 可写, 网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。
EventLoop 与 EventLoopGroup
EventLoop 定义了Netty的核心抽象,用来处理连接的生命周期中所发生的事件,在内部,将会为每个Channel分配一个EventLoop。
EventLoopGroup 是一个 EventLoop 池,包含很多的 EventLoop。
Netty 为每个 Channel 分配了一个 EventLoop,用于处理用户连接请求、对用户请求的处理等所有事件。EventLoop 本身只是一个线程驱动,在其生命周期内只会绑定一个线程,让该线程处理一个 Channel 的所有 IO 事件。
一个 Channel 一旦与一个 EventLoop 相绑定,那么在 Channel 的整个生命周期内是不能改变的。一个 EventLoop 可以与多个 Channel 绑定。即 Channel 与 EventLoop 的关系是 n:1,而 EventLoop 与线程的关系是 1:1。
ChannelHandler 与 ChannelPipeline
ChannelHandler 是对 Channel 中数据的处理器,这些处理器可以是系统本身定义好的编解码器,也可以是用户自定义的。这些处理器会被统一添加到一个 ChannelPipeline 的对象中,然后按照添加的顺序对 Channel 中的数据进行依次处理。
工作原理
初始化基本过程
- 初始化创建 2 个 NioEventLoopGroup,其中 boosGroup 用于 Accetpt 连接建立事件并分发请求,workerGroup 用于处理 I/O 读写事件和业务逻辑。
- 基于 ServerBootstrap(服务端启动引导类),配置 EventLoopGroup、Channel 类型,连接参数、配置入站、出站事件 handler。
- 绑定端口,开始工作。
Boss EventLoop循环执行任务内容
- 轮询 Accept 事件
- 处理 Accept I/O 事件,与 Client 建立连接,生成 NioSocketChannel,并将 NioSocketChannel 注册到某个 Worker NioEventLoop 的 Selector 上
- 处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用 eventloop.execute 或 schedule 执行的任务,或者其他线程提交到该 eventloop 的任务。
Worker NioEventLoop 循环执行任务内容
- 轮询 Read、Write 事件。
- 处理 I/O 事件,即 Read、Write 事件,在 NioSocketChannel 可读、可写事件发生时进行处理。
- 处理任务队列中的任务,runAllTasks。
Netty怎么解决 Java 的 epoll 空轮询 bug?
初始化一个selectCnt变量, 每次轮询进行++操作, 后面会将其进行归零, 如果没有归零selectCnt, 会执行unexpectedSelectorWakeup()方法, 即如果达到了512, 会进行selector重建
Netty中零拷贝的应用
Netty 中的零拷贝体现在以下几个方面:
- 堆外内存,避免 JVM 堆内存到堆外内存的数据拷贝
- 组合Buffer
- 使用 Netty 提供的 CompositeByteBuf 类, 可以将多个ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝
- 通过 Unpooled.wrappedBuffer 可以将 byte 数组包装成 ByteBuf 对象,包装过程中不会产生内存拷贝
- ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝
- transferTo方法, 通过 FileRegion 包装的FileChannel#tranferTo() 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题
Netty涉及到的设计模式
装饰器模式:对现有的类对象进⾏包裹和封装,以期望在不改变类对象 及其类定义的情况下,为对象添加额外功能
io流相关的FilterInputStream 就是个装饰器。什么都不做,委托给内部的InputStream成员对象
观察者模式:当⼀个对象状态发⽣变化时,所有该对象的关注者均能收到状态变化通知,以进⾏相应的处理
ChannelFuture中有个重要的addListener方法,会添加特定的监听器到future中,这些监听器会在future isDone返回true的时候立刻被通知。
Netty堆外内存问题
NIO中的堆外内存 DirectByteBuffer ,利用虚引用的cleaner和GC来顺带回收堆外内存
Netty中的堆外内存,从根本上分为两种:
- 池化的堆外内存 PooledDirectByteBuf 由内存池来负责管理,实际上底层的回收跟非池化的相同
- 非池化的堆外内存 UnpooledDirectByteBuf
- hasCleaner:和 DirectByteBuf一样,利用虚引用的cleaner和GC来顺带回收堆外内存
- noCleaner:利用 UnSafe.freeMemory(内存地址)来回收内存,减少了GC和cleaner的开销
-
Netty使用引用计数机制来确定一个ByteBuf是否应该被回收,当引用计数为0时就回收
-
Netty提供了一种内存泄漏检测机制,用来提醒程序员哪些ByteBuf内存泄漏了
-