java-5.2 netty

121 阅读8分钟

需求

Netty是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能协议服务器和客户端。

nio的缺点

  • NIO的类库和API繁杂,学习成本高,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
  • 需要熟悉Java多线程编程。这是因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能写出高质量的NIO程序。
  • 臭名昭著的epoll bug。它会导致Selector空轮询,最终导致CPU 100%。直到JDK1.7版本依然没得到根本性的解决

设计

功能架构

image.png

  • 用于各种传输类型的统一API - 阻塞和非阻塞套接字
  • 基于灵活和可扩展的事件模型,可以清晰地分离问题
  • 高度可定制的线程模型 - 单线程,一个或多个线程池,如SEDA
  • 零拷贝的Buffer
  • 高性能 吞吐量更好,延迟更低 更少的资源消耗 最小化不必要的内存拷贝

技术架构

image.png

  • Bootstrap 和 ServerBootstrap ServerBootstrap是一个设置服务器的助手类。您可以直接使用通道设置服务器。

  • NioEventLoop NioEventLoopGroup是一个处理I/O操作的多线程事件循环。Netty为不同类型的传输提供了各种EventLoopGroup实现。在本例中,我们将实现一个服务器端应用程序,因此将使用两个NioEventLoopGroup。第一个,通常称为“boss”,接受一个传入连接。第二个,通常称为“worker”,在"boss"接受连接并向worker注册已接受的连接后,处理已接受连接的流量。使用多少线程以及如何将它们映射到创建的通道取决于EventLoopGroup实现,甚至可以通过构造函数进行配置。

  • Channel 基础的IO操作,如绑定、连接、读写等都依赖于底层网络传输所提供的原语,在Java的网络编程中,基础核心类是Socket,而Netty的Channel提供了一组API,极大地简化了直接与Socket进行操作的复杂性,并且Channel是很多类的父类,如EmbeddedChannel、LocalServerChannel、NioDatagramChannel、NioSctpChannel、NioSocketChannel等。

  • ChannelHandler ChannelHandler 为 Netty 中最核心的组件,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。 ChannelHandler 有两个核心子类 ChannelInboundHandler 和 ChannelOutboundHandler,其中 ChannelInboundHandler 用于接收、处理入站数据和事件,而 ChannelOutboundHandler 则相反。

  • ChannelPipeline ChannelPipeline 为 ChannelHandler 链提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API。一个数据或者事件可能会被多个 Handler 处理,在这个过程中,数据或者事件经流 ChannelPipeline,由 ChannelHandler 处理。在这个处理过程中,一个 ChannelHandler 接收数据后处理完成后交给下一个 ChannelHandler,或者什么都不做直接交给下一个 ChannelHandler。

image.png

  • ChannelHandlerContext ChannelHandlerContext对象提供各种操作,使您能够触发各种I/O事件和操作。在这里,我们调用write(Object)来逐字写入接收到的消息。

  • EventLoop Netty 基于事件驱动模型,使用不同的事件来通知我们状态的改变或者操作状态的改变。它定义了在整个连接的生命周期里当有事件发生的时候处理的核心抽象。

Channel 为Netty 网络操作抽象类,EventLoop 主要是为Channel 处理 I/O 操作,两者配合参与 I/O 操作。 当一个连接到达时,Netty 就会注册一个 Channel,然后从 EventLoopGroup 中分配一个 EventLoop 绑定到这个Channel上,在该Channel的整个生命周期中都是有这个绑定的 EventLoop 来服务的。

image.png

  • ChannelFuture Netty 为异步非阻塞,即所有的 I/O 操作都为异步的,因此,我们不能立刻得知消息是否已经被处理了。Netty 提供了 ChannelFuture 接口,通过该接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。

流程

服务端处理逻辑

image.png

  1. 设置服务端ServerBootStrap启动参数
  2. 通过ServerBootStrap的bind方法启动服务端,bind方法会在parentGroup中注册NioServerScoketChannel,监听客户端的连接请求
  3. Client发起连接CONNECT请求,parentGroup中的NioEventLoop不断轮循是否有新的客户端请求,如果有,ACCEPT事件触发
  4. ACCEPT事件触发后,parentGroup中NioEventLoop会通过NioServerSocketChannel获取到对应的代表客户端的NioSocketChannel,并将其注册到childGroup中
  5. childGroup中的NioEventLoop不断检测自己管理的NioSocketChannel是否有读写事件准备好,如果有的话,调用对应的ChannelHandler进行处理

原理

reactor线程模型

Reactor模式一般翻译成"反应器模式",也有人称为"分发者模式"。它是将客户端请求提交到一个或者多个服务处理程序的设计模式。工作原理是由一个线程来接收所有的请求,然后派发这些请求到相关的工作线程中。

在java中,没有NIO出现之前都是使用socket编程。socket的接收请求是阻塞的,需要处理完一个请求才能处理下一个请求,所以在面对高并发的服务请求时,性能就会很差。

那有人就会说使用多线程(如下图所示)。接收到一个请求,就创建一个线程处理,这样就不会阻塞了。实际上这样的确是可以在提升性能上起到一定的作用,但是当请求很多的时候,就会创建大量的线程,维护线程需要资源的消耗,线程之间的切换也需要消耗性能。而且系统创建线程的数量也是有限的,所以当高并发时,会直接把系统拖垮。

image.png

由于以上的问题,提出了Reactor模式。

Reactor:负责响应事件,将事件分发到绑定了对应事件的Handler,如果是连接事件,则分发到Acceptor。 Handler:事件处理器。负责执行对应事件对应的业务逻辑。 Acceptor:绑定了 connect 事件,当客户端发起connect请求时,Reactor会将accept事件分发给Acceptor处理。

  • 单线程模型

image.png

只有一个select循环接收请求,客户端(client)注册进来由Reactor接收注册事件,然后再由reactor分发(dispatch)出去,由下面的处理器(Handler)去处理。

单线程的问题实际上是很明显的。只要其中一个Handler方法阻塞了,那就会导致所有的client的Handler都被阻塞了,也会导致注册事件也无法处理,无法接收新的请求。所以这种模式用的比较少,因为不能充分利用到多核的资源。

  • 多线程模型

image.png

在多线程Reactor中,注册接收事件都是由Reactor来做,其它的计算,编解码由一个线程池来做。从图中可以看出工作线程是多线程的,监听注册事件的Reactor还是单线程。

对比单线程Reactor模型,多线程Reactor模式在Handler读写处理时,交给工作线程池处理,不会导致Reactor无法执行,因为Reactor分发和Handler处理是分开的,能充分地利用资源。从而提升应用的性能。 缺点:Reactor只在主线程中运行,承担所有事件的监听和响应,如果短时间的高并发场景下,依然会造成性能瓶颈。

  • 主从多线程模型

image.png

  1. mainReactor负责监听客户端请求,专门处理新连接的建立,将建立好的连接注册到subReactor。
  2. subReactor 将分配的连接加入到队列进行监听,当有新的事件发生时,会调用连接相对应的Handler进行处理。

mainReactor 主要是用来处理客户端请求连接建立的操作。subReactor主要做和建立起来的连接做数据交互和事件业务处理操作,每个subReactor一个线程来处理。

这样的模型使得每个模块更加专一,耦合度更低,能支持更高的并发量。许多框架也使用这种模式,比如接下来要讲的Netty框架就采用了这种模式。

  • netty中的应用 image.png
  1. BossGroup相当于mainReactor,负责建立连接并且把连接注册到WorkGroup中。WorkGroup负责处理连接对应的读写事件。
  2. BossGroup和WorkGroup是两个线程池,里面有多个NioEventGroup(实际上是线程),默认BossGroup和WorkGroup里的线程数是cpu核数的两倍(源码中有体现)。
  3. 每一个NioEventGroup都是一个无限循环,负责监听相对应的事件。
  4. Pipeline(通道)里包含多个ChannelHandler(业务处理),按顺序执行。

零拷贝的buffer

Netty使用自己的缓冲区API而不是NIO ByteBuffer来表示字节序列。与使用ByteBuffer相比,这种方法有显著的优势。Netty的新缓冲区类型,ChannelBuffer是从头开始设计的,以解决ByteBuffer的问题,并满足网络应用程序开发人员的日常需求。

通过提供Composite(组合)和Slice(切分)两种Buffer来实现零拷贝:

在通信层之间传输数据时,数据经常需要合并或切片。例如,如果一个payload被分割到多个packages上,那么通常需要将其组合起来进行解码。

传统上,来自多个包的数据是通过将它们复制到一个新的字节缓冲区来合并的。

Netty支持一种零拷贝的方法,通过一个ChannelBuffer“指向”所需的缓冲区,从而消除了执行拷贝的需要。

image.png

参看