学习本章需要先知道IO多路复用,不清楚的请移步:IO多路复用
网络通信中,阻塞IO两大阻塞的地方:socket链接阻塞,等待读取文件阻塞。 本地文件io就只有一个等待文件阻塞
一.Reactor模型(Netty线程模型)
说Netty之前先说一下高性能网络模式Reactor。由于NIO是面向过程编写,效率太低。大佬们基于面向对象的思想,对 I/O 多路复用作了一层封装,让使用者不用考虑底层网络 API 的细节,只需要关注应用代码的编写。大佬们还为这种模式取了个让人第一时间难以理解的名字:Reactor 模式。意思对事件的反应,来了一个事件(连接请求,读写请求),Reactor 就有相对应的反应/响应。
Reactor有3种经典方案:
单Reactor单线程:
- Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
- 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
- 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
- Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
缺点:
- 因为只有一个进程,无法充分利用 多核 CPU 的性能;
- Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟;
适用场景:单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景。
单Reactor多线程:
前面的三个步骤和单 Reactor 单线程方案是一样的,我们只看不一样的部分:
- Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给线程池中的线程进行业务处理;
- 处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将响应结果发送给 client;
缺点:
- 虽然充分了利用了多核CPU,但是会涉及共享资源的竞争。
- 因为一个 Reactor 对象承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。
多Reactor多线程(主从Reactor多线程):
和单Reactor多线程唯一不同的是,将连接请求和读写请求分别发给不同的Reactor进行处理。该模式应用于Netty,Memcache等项目中
那么我们看看Netty到底是什么样的模型:
- Netty将主Reactor和从Reactor抽象成俩个组:Boss Group专门负责连接请求,Worker Group专门负责读写请求。
- 每一个组包含多个NioEventLoopGroup,这是一个事件循环组,这个组内有多个事件循环(NioEventLoop),每一个NioEventLoop都有一个selector,用于监听网络的连接,读写请求。
- 每一个连接请求被Boss Group的selector监测到,都会交给Worker Group中的一个事件循环组,由Worker Group的selector监测是否有读写操作。
- 最后交给Pipeline的handler进行业务处理
具体细节下面详细说明!
二.Netty概述
Netty 是什么?
- Netty 是一个 基于 NIO 的 client-server(客户端服务器)框架,使用它可以快速简单地开发网络应用程序。
- 它极大地简化并优化了 TCP 和 UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好。
- 支持多种协议 如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协议。
为什么要用 Netty?因为 Netty 具有下面这些优点,并且相比于直接使用 JDK 自带的 NIO 相关的 API 来说更加易用。
- 统一的 API,支持多种传输类型,阻塞和非阻塞的。
- 简单而强大的线程模型。
- 自带编解码器解决 TCP 粘包/拆包问题。
- 自带各种协议栈。
- 真正的无连接数据包套接字支持。
- 比直接使用 Java 核心 API 有更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制。
- 安全性不错,有完整的 SSL/TLS 以及 StartTLS 支持。
- 社区活跃
- 成熟稳定,经历了大型项目的使用和考验,而且很多开源项目都使用到了 Netty, 比如我们经常接触的 Dubbo、RocketMQ 等等。
- ......
Netty 应用场景了解么?理论上来说,NIO 可以做的事情 ,使用 Netty 都可以做并且更好。Netty 主要用来做网络通信 :
- 作为 RPC 框架的网络通信工具
- 实现一个自己的 HTTP 服务器:说到 HTTP 服务器的话,作为 Java 后端开发,我们一般使用 Tomcat 比较多。一个最基本的 HTTP 服务器可要以处理常见的 HTTP Method 的请求,比如 POST 请求、GET 请求等等。
- 实现一个即时通讯系统 :使用 Netty 我们可以实现一个可以聊天类似微信的即时通讯系统,这方面的开源项目还蛮多的,可以自行去 Github 找一找。
- 实现消息推送系统 :市面上有很多消息推送系统都是基于 Netty 来做的。
- ...
三.Netty 核心组件有哪些?分别有什么作用?
Bootstrap,ServerBootstrap:
引导,一个Netty通常又一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件。
Bootstrap
是客户端的启动引导类/辅助类,具体使用方法如下:
EventLoopGroup group = new NioEventLoopGroup(); try { //创建客户端启动引导/辅助类:Bootstrap Bootstrap b = new Bootstrap(); //指定线程模型 b.group(group). ...... // 尝试建立连接 ChannelFuture f = b.connect(host, port).sync(); f.channel().closeFuture().sync(); } finally { // 优雅关闭相关线程组资源 group.shutdownGracefully();
ServerBootstrap
客户端的启动引导类/辅助类,具体使用方法如下:
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理 EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //2.创建服务端启动引导/辅助类:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.给引导类配置两大线程组,确定了线程模型 b.group(bossGroup, workerGroup). ...... // 6.绑定端口 ChannelFuture f = b.bind(port).sync(); // 等待连接关闭 f.channel().closeFuture().sync(); } finally { //7.优雅关闭相关线程组资源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }
说明:
-
Bootstrap
通常使用connet()
方法连接到远程的主机和端口,作为一个 Netty TCP 协议通信中的客户端。另外,Bootstrap
也可以通过bind()
方法绑定本地的一个端口,作为 UDP 协议通信中的一端。 -
ServerBootstrap
通常使用bind()
方法绑定本地的端口上,然后等待客户端的连接。 -
Bootstrap
只需要配置一个线程组—EventLoopGroup
,而ServerBootstrap
需要配置两个线程组—EventLoopGroup
,一个用于接收连接,一个用于具体的处理。
Channel:(在代码层面,读写,连接操作看似交给BootStrap,其实是交给Channel)
Channel
接口是 Netty 对网络操作抽象类,它除了包括基本的 I/O 操作,如 bind()
、connect()
、read()
、write()
等。
比较常用的Channel
接口实现类是NioServerSocketChannel
(服务端)和NioSocketChannel
(客户端),这两个 Channel
可以和 BIO 编程模型中的ServerSocket
以及Socket
两个概念对应上。
ChannelFuture:
Netty 是异步非阻塞的,所有的 I/O 操作都为异步的。因此,我们不能立刻得到操作是否执行成功,但是,可通过下面2种方式知道:
- 可以通过
ChannelFuture
接口的addListener()
方法注册一个ChannelFutureListener
,当操作执行成功或者失败时,监听就会自动触发返回结果。 -
可以通过ChannelFuture
接口的sync()
方法让异步的操作变成同步的。
EventLoop 与 EventLoopGroup:
- EventLoopGroup 是一个 EventLoop 池,包含很多的 EventLoop。
- EventLoop的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。因为每一个EventLoop有一个自己的selector。
- 每一个EventLoop都对应一个Thread。
- 每一个Channel都需要注册到EventLoop上进行IO操作。
- 对应到的实现类就是NioEventLoop和NioEventLoopGroup。
Selector和TaskQueue:
- 都属于EventLoop 内的组件。
- Selector就是NIO 的selector,这里不多介绍,详细请看NIO多路复用
- 关于TaskQueue:当handler里面有长时间的任务时候,可以把这个任务放到TaskQueue中,先响应客户端,再执行长任务,,相当于异步执行,但是其实还是这一个线程在执行。
ChannelPipeline和ChannelHandler :
- ChannelPipeline是一个双向链表,里面就是一些ChannelHandler,用来处理对应的Channel的业务。
- ChannelPipeline有Channel的信息,Channel中也有ChannelPipeline的信息。
- 我们可以在 ChannelPipeline上通过
addLast()
方法添加一个或者多个ChannelHandler,因为一个数据或者事件可能会被多个 Handler 处理。当一个ChannelHandler 处理完之后就将数据交给下一个ChannelHandler 。
四.Netty服务端和客户端的启动过程了解么?
// 1.bossGroup 用于接收连接,workerGroup 用于具体的处理 EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //2.创建服务端启动引导/辅助类:ServerBootstrap ServerBootstrap b = new ServerBootstrap(); //3.给引导类配置两大线程组,确定了线程模型 b.group(bossGroup, workerGroup) // (非必备)打印日志 .handler(new LoggingHandler(LogLevel.INFO)) // 4.指定 IO 模型 .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) { ChannelPipeline p = ch.pipeline(); //5.可以自定义客户端消息的业务处理逻辑 p.addLast(new HelloServerHandler()); } }); // 6.绑定端口,调用 sync 方法阻塞直到绑定完成 ChannelFuture f = b.bind(port).sync(); // 7.阻塞等待直到服务器Channel关闭(closeFuture()方法获取Channel 的CloseFuture对象,然后调用sync()方法) f.channel().closeFuture().sync(); } finally { //8.优雅关闭相关线程组资源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); }
五.其他小问题
Netty 长连接?
- 我们知道 TCP 在进行读写之前,server 与 client 之间必须提前建立一个连接。建立连接的过程,需要我们常说的三次握手,释放/关闭连接的话需要四次挥手。这个过程是比较消耗网络资源并且有时间延迟的。
- 所谓,短连接说的就是 server 端 与 client 端建立连接之后,读写完成之后就关闭掉连接,如果下一次再要互相发送消息,就要重新连接。短连接的有点很明显,就是管理和实现都比较简单,缺点也很明显,每一次的读写都要建立连接必然会带来大量网络资源的消耗,并且连接的建立也需要耗费时间。
- 长连接说的就是 client 向 server 双方建立连接之后,即使 client 与 server 完成一次读写,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。长连接的可以省去较多的 TCP 建立和关闭的操作,降低对网络资源的依赖,节约时间。对于频繁请求资源的客户来说,非常适用长连接。
Netty心跳机制了解么?
- 在 TCP 保持长连接的过程中,可能会出现断网等网络异常出现,异常发生的时候, client 与 server 之间如果没有交互的话,它们是无法发现对方已经掉线的。为了解决这个问题, 我们就需要引入 心跳机制 。
- 心跳机制的工作原理是: 在 client 与 server 之间在一定时间内没有数据交互时, 即处于 idle 状态时, 客户端或服务器就会发送一个特殊的数据包给对方, 当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文, 回应发送方, 此即一个 PING-PONG 交互。所以, 当某一端收到心跳消息后, 就知道了对方仍然在线, 这就确保 TCP 连接的有效性.
- TCP 实际上自带的就有长连接选项,本身是也有心跳包机制,也就是 TCP 的选项:
SO_KEEPALIVE
。但是,TCP 协议层面的长连接灵活性不够。所以,一般情况下我们都是在应用层协议上实现自定义心跳机制的,也就是在 Netty 层面通过编码实现。通过 Netty 实现心跳机制的话,核心类是IdleStateHandler
。
Netty 的零拷贝了解么?
在 OS 层面上的 Zero-copy
通常指避免在 用户态(User-space)
与 内核态(Kernel-space)
之间来回拷贝数据。而在 Netty 层面 ,零拷贝主要体现在对于数据操作的优化。
- 使用 Netty 提供的
CompositeByteBuf
类, 可以将多个ByteBuf
合并为一个逻辑上的ByteBuf
, 避免了各个ByteBuf
之间的拷贝。 -
ByteBuf
支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的ByteBuf
, 避免了内存的拷贝。 - 通过
FileRegion
包装的FileChannel.tranferTo
实现文件传输, 可以直接将文件缓冲区的数据发送到目标Channel
, 避免了传统通过循环 write 方式导致的内存拷贝问题。
NioEventLoopGroup 默认的构造函数会起多少线程?
- 默认是cpu核数*2个子线程(也就是nioEventLoop)
寄语:当努力到一定程度,幸运自会与你不期而遇