一、 Netty 的介绍
Netty是由JBOSS 提供的一个 java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。Netty整合了网络编程、多线程处理和并发等多个领域,极大地简化了网络开发的流程。
Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景,它能够快速和轻松地开发网络应用程序,如协议服务器和客户端。它极大地简化了TCP和UDP套接字服务器等网络编程。 要透彻理解 Netty,需要先学习 NIO,这样我们才能阅读 Netty 的源码。
二、BIO、NIO、AIO
Java 共支持3种网络编程模型 IO 模式: BIO、NIO、AIO
1、BIO( blocking I/O ) : 同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)
工作原理图:
存在的问题:
1)、每 个请求都需 要创建独 立的线 程,与对应的客户端 进 行数 据 Read ,业务处理 Write
2)、当 并发数较大时,需要 创建大量线程来处理连接 ,系统资源占用较大
3)、连 接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费
2、NIO: 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理,面向缓冲区,或者面向块编程的。数据读取到一个它稍后去处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络, Reactor线程模型
NIO 有三大核心部分: Channel( 通道 ) , Buffer( 缓冲区 ) , Selector( 选择器 )
工作原理图:
3、AIO: 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。 AIO采用的是Proactor模式
同步阻塞:你到饭馆点餐,然后在那等着,还要一边喊:好了没啊!(快餐排队结账)
同步非阻塞:在饭馆点完餐,就去遛狗了。不过溜一会儿,就回饭馆喊一声:好了没啊!(麻辣烫,做好叫你)
异步阻塞:遛狗的时候,接到饭馆电话,说饭做好了,让您亲自去拿。(到店取餐)
异步非阻塞:饭馆打电话说,我们知道您的位置,一会给你送过来,安心遛狗就可以了。(外卖)
注意: proactor模式在异步操作完成后触发服务请求的分配和分发 。
三、NIO三大组件
1、缓冲区 (Buffer)
缓冲区(Buffer): 缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象,该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer
Capacity:容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
Limit:表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的
Position:位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写作准备
Mark:标记
2、通道(Channel)
NIO 的通道类似于流,但有些区别如下:
通道可以从缓冲读数据,也可以写数据到缓冲
通道可以同时进行读写,而流只能读或者只能写
通道可以实现异步读写数据
BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道 (Channel) 是双向的,可以读操作,也可以写操作
3、Selector (选择器)
Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求,只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,避免了多线程之间的上下文切换导致的开销
SelectionKey ,表示 Selector 和网络通道的注册关系,共四种:
int OP_ACCEPT:有新的网络连接可以 accept,值为 16
int OP_CONNECT:代表连接已经建立,值为 8
int OP_READ:代表读操作,值为 1
int OP_WRITE:代表写操作,值为 4
4、Selector 、Channel 和 Buffer 的关系
连接过程:
- 当客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel
- Selector 进行监听select方法,返回有事件发生的通道的个数。
- 将socketChannel 注册到Selector上,register(Selector sel, int ops),一个 selector 上可以注册多个 SocketChannel
- 注册后返回一个 SelectionKey,会和该 Selector 关联(集合)
- 进一步得到各个 SelectionKey(有事件发生)
- 在通过 SelectionKey 反向获取 SocketChannel,方法 channel ()
- 得到的 channel,完成业务处理。
四、线程模型
Reactor 模式
I/O 复用结合线程池,就是 Reactor 模式基本设计思想
- Reactor 模式,通过一个或多个输入同时传递给服务处理器的模式(基于事件驱动)
- 服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程,因此Reactor 模式也叫 Dispatcher 模式
- Reactor 模式使用 IO 复用监听事件,收到事件后,分发给某个线程(进程),这点就是网络服务器高并发处理关键
Reactor 模式中核心组成:
- Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人
- Handlers:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。 Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。
Reactor模式分类:
根据 Reactor 的数量和处理资源池线程的数量不同,有3种典型的实现:
- 单Reactor单线程
- 单Reactor多线程
- 主从Reactor多线程
1、单Reactor单线程:
Select是前面 I/O 复用模型介绍的标准网络编程 API ,可以实现应用程序通过一个阻塞对象监听多路连接请求
通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发
如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理,如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应
Handler 会完成 Read→业务处理→ Send的完整业务流程
优缺点:
- 优点: 模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成
- 缺点: 性能问题,只有一个线程,无法完全发挥多核 CPU 的性能。 Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈
- 缺点: 可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
- 使用场景: 客户端的数量有限,业务处理非常快速,比如 Redis 在业务处理的时间复杂度 O(1) 的情况
2 、单 Reactor多线程
方案说明:
- Reactor 对象通过 select 监控客户端请求事件,收到事件后,通过dispatch进行分发
- 如果建立连接请求,则由 Acceptor 通过accept 处理连接请求,然后创建一个Handler对象处理完成连接后的各种事件
- 如果不是连接请求,则由 reactor 分发调用连接对应的 handler 来处理
- handler 只负责响应事件,不做具体的业务处理,通过read读取数据后,会分发给后面的worker线程池的某个线程处理业务
- worker 线程池会分配独立线程完成真正的业务,并将结果返回给 handler
- handler 收到响应后,通过 send 将结果返回给 client
方案优缺点分析:
- 优点: 可以充分的利用多核 cpu 的处理能力
- 缺点: 多线程数据共享和访问比较复杂, reactor 处理所有的事件的监听和响应,在单线程运行,在高并发场景容易出现性能瓶颈。
3、主从Reactor多线程
方案说明:
- 当 Acceptor处理连接事件后,MainReactor 将连接分配给 SubReactor
- subreactor 将连接加入到连接队列进行监听,并创建 handler 进行各种事件处理
- Reactor 主线程 MainReactor 对象通过 select 监听连接事件,收到事件后,通过 Acceptor 处理连接事件
- 当有新事件发生时, subreactor 就会调用对应的 handler 处理
- handler 通过 read 读取数据,分发给后面的 worker 线程处理
- worker 线程池分配独立的 worker 线程进行业务处理,并返回结果
- handler 收到响应的结果后,再通过 send 将结果返回给 client
- Reactor 主线程可以对应多个 Reactor 子线程 , 即 MainRecator 可以关联多个 SubReactor
方案优缺点说明:
- 优点: 父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。
- 优点: 父线程与子线程的数据交互简单, Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据。
- 缺点: 编程复杂度较高
实例: 这种模型在许多项目中广泛使用,包括 Nginx 主从Reactor多进程模型, Memcached主从多线程, Netty主从多线程模型的支持
3 种模式用生活案例来理解
- 单 Reactor 单线程,前台接待员和服务员是同一个人,全程为顾客服
- 单 Reactor 多线程,1个前台接待员,多个服务员,接待员只负责接待
- 主从 Reactor 多线程,多个前台接待员,多个服务生
Reactor 模式具有如下的优点:
- 响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的
- 可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程 / 进程的切换开销
- 扩展性好,可以方便的通过增加 Reactor 实例个数来充分利用 CPU资源
- 复用性好,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性
五、Netty
1、Netty 模型
Netty 主要基于主从Reactors多线程模型(如图)做了一定的改进,其中主从 Reactor 多线程模型有多个 Reactor
- BossGroup线程维护Selector,只关注 Accecpt
- 当接收到 Accept 事件,获取到对应的SocketChannel,封装成 NIOScoketChannel 并注册到 Worker 线程(事件循环),并进行维护
- 当 Worker 线程监听到 selector 中通道发生自己感兴趣的事件后,就进行处理(就由 handler) ,注意handler已经加入到通道
2、工作原理示意图 - 详细版
- Netty 抽象出两组线程池 BossGroup 专门负责接收客户端的连接 , WorkerGroup 专门负责网络的读写
- BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
- NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是 NioEventLoop
- NioEventLoop 表示一个不断循环的执行处理任务的线程,每个NioEventLoop 都有一个selector,用于监听绑定在其上的 socket 的网络通讯
- NioEventLoopGroup 可以有多个线程 , 即可以含有多个 NioEventLoop
- 每个 Boss NioEventLoop 循环执行的步骤有 3 步
<1>、轮询 accept 事件
<2>、处理 accept 事件,与 client 建立连接,生成 NioScocketChannel,并将其注册到某个 worker NIOEventLoop 上的 selector
<3>、处理任务队列的任务,即 runAllTasks - 每个 Worker NIOEventLoop 循环执行的步骤
<1>、轮询read,write 事件
<2>、处理i/o事件,即read,write 事件,在对应NioScocketChannel 处理
<3>、处理任务队列的任务,即 runAllTasks - 每个 Worker NIOEventLoop 处理业务时,会使用 pipeline(管道 ),pipeline 中包含了 channel,即通过 pipeline可以获取到对应通道,管道中维护了很多的处理器
3、原生NIO存在的问题
- NIO的类库和 API 繁杂,使用麻烦:需要熟练掌握 Selector 、ServerSocketChannel、SocketChannel、ByteBuffer 等
- 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序
- 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等
- JDK NIO 的 Bug:例如臭名昭著的Epoll Bug,它会导致 Selector 空轮询,最终导致CPU 100%。直到 JDK 1.7版本该问题仍旧存在,没有被根本解决
Netty的解决办法
对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数,超过512次,则重建Selector没有阻塞就立马返回了,对应代码中处理时间小于超时时间,这个就叫JDK空轮询BUG若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭
六、Netty核心组件
Bootstrap、ServerBootstrap
Bootstrap 意思是引导,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个 Netty 程序,串联各个组件, Netty 中 Bootstrap 类是客户端程序的启动引导类, ServerBootstrap 是服务端启动引导类
- 创建ServerBootStrap实例
- 设置并绑定Reactor线程池:EventLoopGroup,EventLoop就是处理所有注册到本线程的Selector上面的Channel
- 设置并绑定服务端的channel
- 创建处理网络事件的ChannelPipeline和handler,网络时间以流的形式在其中流转,handler完成多数的功能定制:比如编解码 SSl安全认证
- 绑定并启动监听端口
- 当轮训到准备就绪的channel后,由Reactor线程:NioEventLoop执行pipline中的方法,最终调度并执行channelHandler
Future、ChannelFuture
Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 Future 和 ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件
Channel
- Netty 网络通信的组件,能够用于执行网络 I/O 操作。
- 通过 Channel 可获得当前网络连接的通道的状态
- 通过 Channel 可获得网络连接的配置参数(例如接收缓冲区大小)
- Channel 提供异步的网络 I/O操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成
- 调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方
- 支持关联 I/O 操作与对应的处理程序
- 不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应,常用的 Channel 类型:
Selector
- Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。
- 当向一个 Selector 中注册 Channel 后, Selector 内部的机制就可以自动不断地查询 (Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel
ChannelHandler
- ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。
- ChannelHandler 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类
Netty为什么传输快
Netty的传输快其实也是依赖了NIO的一个特性——零拷贝。我们知道,Java的内存有堆内存、栈内存和字符串常量池等等,其中堆内存是占用内存空间最大的一块,也是Java对象存放的地方,一般我们的数据如果需要从IO读取到堆内存,中间需要经过Socket缓冲区,也就是说一个数据会被拷贝两次才能到达他的终点,如果数据量大,就会造成不必要的资源浪费。
Netty针对这种情况,使用了NIO中的另一大特性——零拷贝,当他需要接收数据的时候,他会在堆内存之外开辟一块内存,数据就直接从IO读到了那块内存中去,在netty里面通过ByteBuf可以直接对这些数据进行直接操作,从而加快了传输速度。
零拷贝方式讲解