转载

对Netty的一些理解

Netty 是一个高性能、异步事件驱动的 NIO 框架,它提供了对 TCPUDP 和文件传输的支持。作为当前最流行的 NIO 框架, Netty 在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也是基于 NettyNIO 框架构建。

Netty 利用 Java 高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 构建一个客户端/服务端,其具有高并发、传输快、封装好等特点。

高并发 Netty 是一款基于 NIONonblocking I/O ,非阻塞 IO )开发的网络通信框架,对比于 BIOBlocking I/O ,阻塞 IO ),它的并发性能得到了很大提高 。

传输快 Netty 的传输快其实也是依赖了 NIO 的一个特性—— 零拷贝

封装好Netty封装了NIO操作的很多细节,提供易于使用的API,还有心跳、重连机制、拆包粘包方案等特性,使开发者能能够快速高效的构建一个稳健的高并发应用。

为什么要用 Netty ?

JDK 原生 NIO 程序的问题

JDK 原生也有一套网络应用程序 API ,但是存在一系列问题,主要如下:

  • NIO 的类库和 API 繁杂,使用麻烦。你需要熟练掌握 SelectorServerSocketChannelSocketChannelByteBuffer 等。
  • 需要具备其他的额外技能做铺垫。例如熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的 NIO 程序。
  • 可靠性能力补齐,开发工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等。 NIO 编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大。
  • JDK NIOBug 。例如臭名昭著的 Epoll Bug ,它会导致 Selector 空轮询,最终导致 CPU 100% 。 官方声称在 JDK 1.6 版本的 update 18 修复了该问题,但是直到 JDK 1.7 版本该问题仍旧存在,只不过该 Bug 发生概率降低了一些而已,它并没有被根本解决。

Netty 的特点

NettyJDK 自带的 NIOAPI 进行封装,解决上述问题,主要特点有:

  • 设计优雅,适用于各种传输类型的统一 API 阻塞和非阻塞 Socket ;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型 - 单线程,一个或多个线程池;真正的无连接数据报套接字支持(自 3.1 起)。
  • 使用方便,详细记录的 Javadoc ,用户指南和示例;没有其他依赖项, JDK 5Netty 3.x )或 6Netty 4.x )就足够了。
  • 高性能,吞吐量更高,延迟更低;减少资源消耗;最小化不必要的内存复制。

安全,完整的 SSL/TLSStartTLS 支持。

  • 社区活跃,不断更新,社区活跃,版本迭代周期短,发现的 Bug 可以被及时修复,同时,更多的新功能会被加入。

Netty 内部执行流程

服务端:

对Netty的一些理解

对Netty的一些理解

  • 创建 ServerBootStrap 实例
  • 设置并绑定 Reactor 线程池: EventLoopGroupEventLoop 就是处理所有注册到本线程的 Selector 上面的 Channel
  • 设置并绑定服务端的 Channel
  • 创建处理网络事件的 ChannelPipelinehandler ,网络时间以流的形式在其中流转, handler 完成多数的功能定制:比如编解码 SSl 安全认证
  • 绑定并启动监听端口
  • 当轮训到准备就绪的 channel 后,由 Reactor 线程: NioEventLoop 执行 pipline 中的方法,最终调度并执行 channelHandler

客户端:

对Netty的一些理解

对Netty的一些理解

Netty 架构设计

主要功能特性如下图:

对Netty的一些理解

Netty 功能特性如下:

  • 传输服务,支持 BIONIO
  • 容器集成,支持 OSGIJBossMCSpringGuice 容器。
  • 协议支持, HTTPProtobuf 、二进制、文本、 WebSocket 等一系列常见协议都支持。还支持通过实行编码解码逻辑来实现自定义协议。
  • Core 核心,可扩展事件模型、通用通信 API 、支持零拷贝的 ByteBuf 缓冲对象。

模块组件

BootstrapServerBootstrap

Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件, NettyBootstrap 类是客户端程序的启动引导类, ServerBootstrap 是服务端启动引导类。

FutureChannelFuture

正如前面介绍,在 Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。

但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 FutureChannelFutures ,它们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。

Channel

Netty 网络通信的组件,能够用于执行网络 I/O 操作。

Channel 为用户提供:

  • 当前网络连接的通道的状态(例如是否打开?是否已连接?)
  • 网络连接的配置参数 (例如接收缓冲区大小)
  • 提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成。调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以在 I/O 操作成功、失败或取消时回调通知调用方。
  • 支持关联 I/O 操作与对应的处理程序。

不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应。下面是一些常用的 Channel 类型:

  • NioSocketChannel ,异步的客户端 TCP Socket 连接。
  • NioServerSocketChannel ,异步的服务器端 TCP Socket 连接。
  • NioDatagramChannel ,异步的 UDP 连接。
  • NioSctpChannel ,异步的客户端 Sctp 连接。
  • NioSctpServerChannel ,异步的 Sctp 服务器端连接,这些通道涵盖了 UDPTCP 网络 IO 以及文件 IO

Selector

Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。

当向一个 Selector 中注册 Channel 后, Selector 内部的机制就可以自动不断地查询( Select ) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel

NioEventLoop

NioEventLoop 中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLooprun 方法,执行 I/O 任务和非 I/O 任务:

  • I/O 任务,即 selectionKeyready 的事件,如 acceptconnectreadwrite 等,由 processSelectedKeys 方法触发。
  • IO 任务,添加到 taskQueue 中的任务,如 register0bind0 等任务,由 runAllTasks 方法触发。

两种任务的执行时间比由变量 ioRatio 控制,默认为 50 ,则表示允许非 IO 任务执行的时间与 IO 任务的执行时间相等。

NioEventLoopGroup

NioEventLoopGroup ,主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程( NioEventLoop )负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。

ChannelHandler

ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline (业务处理链)中的下一个处理程序。

ChannelHandler 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类:

  • ChannelInboundHandler 用于处理入站 I/O 事件。
  • ChannelOutboundHandler 用于处理出站 I/O 操作。

或者使用以下适配器类:

  • ChannelInboundHandlerAdapter 用于处理入站 I/O 事件。
  • ChannelOutboundHandlerAdapter 用于处理出站 I/O 操作。
  • ChannelDuplexHandler 用于处理入站和出站事件。
  • ChannelHandlerContext 保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象。

ChannelPipline

保存 ChannelHandlerList ,用于处理或拦截 Channel 的入站事件和出站操作。

ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互。

Netty 高性能设计

Netty 作为异步事件驱动的网络,高性能之处主要来自于其 I/O 模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据。

I/O 模型

用什么样的通道将数据发送给对方, BIONIO 或者 AIOI/O 模型在很大程度上决定了框架的性能。

阻塞 I/O

传统阻塞型 I/O ( BIO )可以用下图表示:

对Netty的一些理解

特点以及缺点如下:

  • 每个请求都需要独立的线程完成数据 Read ,业务处理,数据 Write 的完整操作问题。
  • 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
  • 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。

I/O 复用模型

对Netty的一些理解

I/O 复用模型中,会用到 Select ,这个函数也会使进程阻塞,但是和阻塞 I/O 所不同的是这个函数可以同时阻塞多个 I/O 操作。

而且可以同时对多个读操作,多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用 I/O 操作函数。

Netty 的非阻塞 I/O 的实现关键是 基于 I/O 复用模型 ,这里用 Selector 对象表示:

对Netty的一些理解

NettyIO 线程 NioEventLoop 由于聚合了多路复用器 Selector ,可以同时并发处理成百上千个客户端连接。

当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。

线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。

由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。

一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

Netty 线程模型

Netty 主要基于 主从 Reactors 多线程模型 (如下图)做了一定的修改,其中主从 Reactor 多线程模型有多个 Reactor

MainReactor 负责客户端的连接请求,并将请求转交给 SubReactor

SubReactor 负责相应通道的 IO 读写请求。

IO 请求(具体逻辑处理)的任务则会直接写入队列,等待 worker threads 进行处理。

这里引用 Doug Lee 大神的 Reactor 介绍: Scalable IO in Java 里面关于主从 Reactor 多线程模型的图:

对Netty的一些理解

特别说明的是:虽然 Netty 的线程模型基于主从 Reactor 多线程,借用了 MainReactorSubReactor 的结构。但是实际实现上 SubReactorWorker 线程在同一个线程池中。

Netty 的零拷贝

是在发送数据的时候,传统的实现方式是:

File.read(bytes);
Socket.send(bytes);

这种方式需要四次数据拷贝和四次上下文切换:

read buffer
socket buffer
socket buffer

零拷贝的概念

明显上面的第二步和第三步是没有必要的,通过 javaFileChannel.transferTo 方法,可以避免上面两次多余的拷贝(当然这需要底层操作系统支持)

  • 调用 transferTo ,数据从文件由 DMA 引擎拷贝到内核 read buffer
  • 接着 DMA 从内核 read buffer 将数据拷贝到网卡接口 buffer

上面的两次操作都不需要 CPU 参与,所以就达到了零拷贝。

Netty 中的零拷贝主要体现在三个方面:

  • bytebuffer
Netty 发送和接收消息主要使用 bytebufferbytebuffer 使用对外内存( DirectMemory )直接进行 Socket 读写。

原因:如果使用传统的堆内存进行 Socket 读写, JVM 会将堆内存 buffer 拷贝一份到直接内存中然后再写入 socket ,多了一次缓冲区的内存拷贝。 DirectMemory 中可以直接通过DMA发送到网卡接口。

  • Composite Buffers

传统的 ByteBuffer ,如果需要将两个 ByteBuffer 中的数据组合到一起,我们需要首先创建一个 size=size1+size2 大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。

但是使用 Netty 提供的组合 ByteBuf ,就可以避免这样的操作,因为 CompositeByteBuf 并没有真正将多个 Buffer 组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。

  • 对于 FileChannel.transferTo 的使用

Netty 中使用了 FileChannel的transferTo 方法,该方法依赖于操作系统实现零拷贝。

本文由博客一文多发平台 OpenWrite 发布!

更多内容请点击我的博客 沐晨

原文  https://segmentfault.com/a/1190000021288510
正文到此结束
Loading...