参考文章:
《超详细的 Netty 入门》
《Netty框架学习之(一):Netty框架简介》
《Netty框架学习之(二):Netty组件简介》
《图解高性能网络架构:Reactor 和 Proactor》
相关文章:
《IO 模型与多路复用》
《Java NIO》
《性能优化:文件传输之DMA、零拷贝与直接IO》
写在开头:本文为学习后的总结,可能有不到位的地方,错误的地方,欢迎各位指正。
前言
在此前的文章中,我们已经对NIO等内容做了基本介绍,但是java NIO类库和API繁杂,学习成本高,还需要熟悉Java多线程编程才能写出高质量的NIO程序。因此便有了封装好的网络编程框架Netty。(建议在阅读本文前先了解完《IO 模型与多路复用》)。
目录
前言
一、线程模型
1、传统阻塞IO线程模
2、Reactor模型
2.1、单 Reactor 单进程 / 线程
2.2、单 Reactor 多线程 / 多进程
2.3、多 Reactor 多进程 / 线程
二、Netty的基本介绍
1、Netty是什么
2、Netty的运行步骤
2.1、内部流程介绍
2.2、样例展示
2.3、核心组件
一、线程模型
目前存在的线程模式:
- 传统阻塞IO的服务模型
- Reactor模型
1、传统阻塞IO线程模
这种模型采用阻塞IO获取输入的数据,每个连接都需要独立的线程来处理逻辑。存在的问题就是,当并发数很大时,就需要创建很多的线程,占用大量的资源。连接创建后,如果当前线程没有数据可读,该线程将会阻塞在读数据的方法上,造成线程资源浪费。
2、Reactor模型
与传统阻塞IO线程模对应的就是基于NIO封装而成的Reactor事件驱动模型。事实上,Reactor 模式也叫 Dispatcher 模式,即 I/O 多路复用监听事件,收到事件后,根据事件类型分配(Dispatch)给某个进程 / 线程。
先来复习下NIO使用的多路复用技术,只需要一个线程就可以监控所有的连接,正如下图这样。
Reactor模式主要由 Reactor 和处理资源池这两个核心部分组成,它俩负责的事情如下:
- Reactor 负责监听和分发事件,事件类型包含连接事件、读写事件;
- 处理资源池负责处理事件,如 read -> 业务逻辑 -> send;
Reactor模式是灵活多变的,可以应对不同的业务场景,灵活在于:
- Reactor 的数量可以只有一个,也可以有多个;
- 处理资源池可以是单个进程 / 线程,也可以是多个进程 /线程;
于是Reactor模式就有以下实现
- 单 Reactor 单进程 / 线程;
- 单 Reactor 多线程 / 进程;
- 多 Reactor 多进程 / 线程;
2.1、单 Reactor 单进程 / 线程
可以看到进程里有 Reactor、Acceptor、Handler 这三个对象:
- Reactor 对象的作用是监听和分发事件;
- Acceptor 对象的作用是获取连接;
- Handler 对象的作用是处理业务;
运行流程如下:
(1)Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型; (2)如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件; (3)如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应; Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
单 Reactor 单进程的方案因为全部工作都在同一个进程内完成,所以实现起来比较简单,不需要考虑进程间通信,也不用担心多进程竞争。
但是,这种方案存在 2 个缺点:
(1)因为只有一个进程,无法充分利用 多核 CPU 的性能; (2)Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟;
2.2、单 Reactor 多线程 / 多进程
如果要克服单 Reactor 单线程 / 进程方案的缺点,那么就需要引入多线程 / 多进程,这样就产生了单 Reactor 多线程 / 多进程的方案。
(1)Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型; (2)如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件; (3)如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
与前一种模式不同之处在于:
(1)Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给子线程里的 Processor 对象进行业务处理; (2)子线程里的 Processor 对象就进行业务处理,处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将响应结果发送给 client;
单Reator多线程的方案优势在于能够充分利用多核 CPU 的能,那既然引入多线程,那么自然就带来了多线程竞争资源的问题。 另外,因为一个 Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
2.3、多 Reactor 多进程 / 线程
要解决单 Reactor的问题,就是将单 Reactor实现成多 Reactor,这样就产生了第 多 Reactor 多进程 / 线程的方案。
(1)主线程中的 MainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 对象中的 accept 获取连接,将新的连接分配给某个子线程; (2)子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入 select 继续进行监听,并创建一个 Handler 用于处理连接的响应事件。 (3)如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应。 (4)Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
多 Reactor 多线程的方案虽然看起来复杂的,但是实际实现时比单 Reactor 多线程的方案要简单的多,原因如下:
- 主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理。
- 主线程和子线程的交互很简单,主线程只需要把新连接传给子线程,子线程无须返回数据,直接就可以在子线程将处理结果发送给客户端。
Netty 正是采用了多Reactor 多线程的方案,而Nginx采用了多 Reactor 多进程的方案,不过方案与标准的多 Reactor 多进程有些差异。
具体差异表现在主进程中仅仅用来初始化 socket,并没有创建 mainReactor 来 accept 连接,而是由子进程的 Reactor 来 accept 连接,通过锁来控制一次只有一个子进程进行 accept(防止出现惊群现象),子进程 accept 新连接后就放到自己的 Reactor 进行处理,不会再分配给其他子进程。
二、Netty的基本介绍 1、Netty是什么
Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。
Netty 对 JDK 自带的 NIO 的 API 进行了良好的封装,解决了上述问题。且Netty拥有高性能、 吞吐量更高,延迟更低,减少资源消耗,最小化不必要的内存复制等优点。Netty 现在都在用的是4.x,5.x版本已经废弃,Netty 4.x 需要JDK 6以上版本支持。
2、Netty的运行步骤
2.1、内部流程介绍
Netty模型是基于主从Reactor多线程模型设计的。
Netty有两组线程池,一个Boss Group,它专门负责客户端连接,另一个Work Group,专门负责网络读写;
- Boss Group和Work Group的类型都是NIOEventLoopGroup;
- NIOEventLoopGroup相当于一个事件循环组,这个组包含了多个事件循环,每一个循环都是NIOEventLoop;
- NIOEventLoop表示一个不断循环执行处理任务的线程,每个NIOEventLoop都有一个Selector,用于监听绑定在其上的ocketChannel的网络通讯;
- Boss Group下的每个NIOEventLoop的执行步骤有3步:
(1). 轮询accept连接事件;
(2). 处理accept事件,与client建立连接,生成一个NioSocketChannel,并将其注册到某个work group下的NioEventLoop的selector上;
(3). 处理任务队列的任务,即runAllTasks;
- 每个Work Group下的NioEventLoop循环执行以下步骤:
(1). 轮询read、write事件;
(2). 处理read、write事件,在对应的NioSocketChannel处理;
(3). 处理任务队列的任务,即runAllTasks;
- 每个Work Group下的NioEventLoop在处理NioSocketChannel业务时,会使用pipeline(管道),管道中维护了很多 handler 处理器用来处理 channel 中的数据。
2.2、样例展示
Netty服务段代码样例
Netty客户端代码样例
运行流程如下
2.3、核心组件
Bootstrap:主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类。
Channel:数据的通道,这一点与java NIO一样,所有的数据流转都需要经过channel。 EventLoop:NioEventLoop 中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务。
I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。
非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发。
NioEventLoopGroup:主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。
每个EventLoopGroup里包括一个或多个EventLoop,每个EventLoop中维护一个Selector实例。
ChannelFuture:Netty 框架中所有的 I/O 操作都为异步的,因此我们需要 ChannelFuture 的 addListener()注册一个 ChannelFutureListener 监听事件,当操作执行成功或者失败时,监听就会自动触发返回结果。 ChannelHandler:充当了所有处理入站和出站数据的逻辑容器。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。 ChannelPipeline:为 ChannelHandler 链提供了容器,当 channel 创建时,就会被自动分配到它专属的 ChannelPipeline,这个关联是永久性的。