本文核心:
-
JAVA I/O模型是如何演化的?
-
Netty架构设计是怎样的?
-
Netty高性能体现在哪些方面?
一、网络编程框架NIO介绍
JAVA中的网络I/O模型有三种:BIO、NIO、AIO。
传统BIO编程
BIO的名称是同步阻塞I/O模式。
该模型的特点是:该模型特点是:有一个独立的Acceptor线程负责监听客户端的连接,一旦有新的连接请求,就会创建一个新的线程进行处理,然后处理完成之后返回客户端,销毁线程。对应的通信模型图如下:
由于每个连接都要创建一个线程来处理,因此该I/O模型的弊端也比较明显:
-
线程多时占用内存大,并且线程频繁切换开销也大\
-
线程多时会导致性能急剧下降\
因此 BIO只适用与连接数少的场景。
虽然该模型可以引入线程池+等待队列进行优化,避免使用过多的线程,但是依然无法解决线程利用率低的问题。
使用 BIO 实现网络编程使用的 Java 编程组件是 ServerSocket 和 Socket。服务端示例代码为:
public static void main(String[] args) throws IOException { final ExecutorService executorService = Executors.newCachedThreadPool(); final ServerSocket serverSocket = new ServerSocket(8080); while (true) { final Socket socket = serverSocket.accept(); executorService.execute(() -> { try { handler(socket); } catch (IOException e) { e.printStackTrace(); } }); }}
/**
* 处理客户端请求
*/
private static void handler(Socket socket) throws IOException { byte[] bytes = new byte[1024]; InputStream inputStream = socket.getInputStream(); socket.close(); while (true) { int read = inputStream.read(bytes); if (read != -1) { System.out.println("msg from client: " + new String(bytes, 0, read)); } else { break; } }}
NIO编程
NIO的名称是同步非阻塞I/O模式。
在这种模型中,服务器上一个线程处理多个连接,即多个客户端请求都会被注册到多路复用器(Selector)上,多路复用器会轮询这些连接,轮询到连接上有 I/O 活动就进行处理。NIO 降低了线程的需求量,提高了线程的利用率。
NIO的三个核心组件:
- 缓冲区Buffer
Buffer是NIO类库中一个新的概念,在NIO中,所有数据都是用缓冲区处理的。在写入数据时,是先写入缓冲区,在读取数据时,也是从缓冲区中读取。
缓冲区其实是个数组,但是不仅仅是一个数组,它提供了对数据的结构化访问以及维护读写位置的信息。
常见的缓冲区有:
-
ByteBuffer:最常使用,用于操作字节数组。
-
CharBuffer
-
ShortBuffer
-
IntBuffer
-
LongBuffer
-
FloatBuffer
-
DoubleBuffer
除了Boolean类型,JAVA中每种基本数据类型都有对应的缓冲区类型。
2. 通道Channel
通道是指在客户端和服务端连接建立后的传输数据的管道,可以通过它读取和写入数。通道与流不同之处在于流只是在一个方向上写入或者读取,二通道可以用于读也可以用于写,还可以同时用于读写。
Channel主要可以分为两大类:
-
FileChannel:用于文件操作
-
SelectableChannel:用于网络连接,根据网络协议不同,可以分为:
-
-
ServerSocketChannel和SocketChannle:用于TCP协议的数据读写,分别对应服务端和客户端的通道\
-
DatagramChannel:用于UDP协议的数据读写
-
3. 多路复用器Selector
Selector是NIO中一个比较重要的基础概念,Selector会不断轮询注册在其上面的channel,如果某个channel有新的TCP连接接入、读写事件,该channel就变成就绪状态,并且放入名为SelectionKey的选择键集合中,然后迭代SelectionKey集合中的每一个选择键,根据具体I/O事件类型,执行对应的业务操作。
可以注册到Selector上的I/O事件类型枚举有:
-
SelectionKey.OP_READ:可读
-
SelectionKey.OP_WRITE:可写
-
SelectionKey.OP_CONNECT:连接
-
SelectionKey.OP_ACCEPT:接收
一个Selector可以同时轮询多个channel,因此只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端连接,这是NIO模型能够实现高性能的一个关键原因。
使用 NIO 实现网络编程的服务端代码示例:
public static void main(String[] args) throws IOException { ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); Selector selector = Selector.open(); // 绑定端口 serverSocketChannel.socket().bind(new InetSocketAddress(8080)); // 设置 serverSocketChannel 为非阻塞模式 serverSocketChannel.configureBlocking(false); // 注册 serverSocketChannel 到 selector,关注 OP_ACCEPT 事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while (true) { // 没有事件发生 if (selector.select(1000) == 0) { continue; } // 有事件发生,找到发生事件的 Channel 对应的 SelectionKey 的集合 Set<SelectionKey> selectionKeys = selector.selectedKeys(); //轮询选择器集合 Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); // 发生 OP_ACCEPT 事件,处理连接请求 if (selectionKey.isAcceptable()) { SocketChannel socketChannel = serverSocketChannel.accept(); // 将 socketChannel 也注册到 selector,关注 OP_READ // 事件,并给 socketChannel 关联 Buffer socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); } // 发生 OP_READ 事件,读客户端数据 if (selectionKey.isReadable()) { SocketChannel channel = (SocketChannel) selectionKey.channel(); ByteBuffer buffer = (ByteBuffer) selectionKey.attachment(); channel.read(buffer); System.out.println("msg form client: " + new String(buffer.array())); } //......其他事件 // 手动从集合中移除当前的 selectionKey,防止重复处理事件 iterator.remove(); } }}
NIO编程难度比BIO大很多,但是也有很大的优势:
-
通过Selector多路复用轮询机制,客户端的连接不会像之前那样被同步阻塞\
-
SocketChannel的读写操作都是异步的, 如果没有可读写的操作,不会同步等待,而是直接返回,这样I/O线程就可以去处理其他链路,不需要同步等待\
-
由于对线程模型的优化,使得一个Selector可以处理成千上万个客户端连接,并且性能不会线性下降,因此非常适合做高性能、高并发的网络服务器。\
AIO编程
异步I/O模型,简称为AIO,它的基本流程是:
-
用户线程通过系统调用,向内核注册某个IO操作
-
内核在整个I/O操作完成后,发送一个信号通知用户程序
-
用户收到信号后执行后续的业务操作
在AIO模型中,整个内核数据处理过程中,用户线程都不需要阻塞。
AIO,异步I/O模型,虽然AIO的编程比NIO更为简单,但是目前并没有得到很广泛的应用,主要原因有以下几个:
-
Linux对AIO的实现不够成熟
-
Linux下AIO相比NIO的性能提升不够明显。
基于以上几个因素,AIO的应用很少,Netty旧版本也支持AIO,但是后面的版本中仅仅支持NIO了。
二、为什么要使用Netty
NIO存在的问题
虽然NIO功能很强大,但是实际中使用起来并不容易:
-
NIO类库和API繁杂,使用麻烦
-
对开发者编程水平要求高,需要熟练掌握额外的技能,如多线程
-
开发工作量和工作难度很大,如:心跳检测、断线重连、粘包拆包、网络阻塞等
-
JDK NIO的BUG,如epoll可能会在某些情况下导致Selector空转,导致CPU 100%。
基于上述原因,在大多数场景下,并不适合直接使用NIO来进行开发,除非非常精通NIO或者有特殊的需求。因此在大多数场景下我们可以用一个基于NIO封装的Netty框架来代替原生的NIO进行开发。
Netty相比NIO的优点
Netty是基于NIO的框架之一,它的健壮性、功能、性能、可扩展性都是首屈一指的。
Netty有着如下优点:
-
API使用简单,开发门槛低
-
设计优雅
-
高性能、高吞吐量、可扩展性强
-
比较成熟稳定,社区活跃,更新比较快
三、Netty入门介绍
Netty是什么
Netty是一个基于NIO的异步的、事件驱动的高性能网络编程框架,它以高性能、高并发著称。使用Netty可以在保证性能的前提下轻松编写出网络应用程序。
关于Netty在网络中的地位,下图可以很好的表示出来:
Netty应用场景
Netty在各个行业都被广泛使用,常用于开发各种高性能的网络通信框架,如:
-
互联网行业:互联网行业的特点的并发量高,对性能要求高,Netty刚好满足行业的需求,在分布式系统中,各个节点之间都要进行通信,高性能的RPC框架必不可少,而Netty就是构建这些通信基础组件的基础。常见的应用有:阿里的RPC框架Dubbo
-
游戏行业:无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。
-
大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通信,它的 Netty Service 基于 Netty 框架二次封装实现。
Netty入门示例
我们将通过一个入门案例来演示第一个Netty开发的应用程序,在该程序中,服务端监听8080端口,客户端去连接本地的8080端口,连接成功后,会发送"Hello Netty!"给服务端,服务端接收到消息之后,也回复"Hello Netty!"给客户端。
Netty程序开发流程:
-
引入maven坐标
-
服务端启动程序开发
-
服务端业务处理handler开发
-
客户端启动程序开发
-
客户端业务处理handler开发
- 引入maven坐标
在该示例中,引入的是netty-all4.1版本
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.55.Final</version></dependency>
2. 服务端启动程序开发
import io.netty.bootstrap.ServerBootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.ChannelPipeline;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioServerSocketChannel;import io.netty.util.concurrent.Future;import io.netty.util.concurrent.GenericFutureListener;
/**
* Netty服务端
*/
public class HelloNettyServer { private static final Integer port = 8080; public static void main(String[] args) throws InterruptedException { //设置两个NioEventLoopGroup线程组:bossGroup和workerGroup NioEventLoopGroup bossGroup = new NioEventLoopGroup(); NioEventLoopGroup workerGroup = new NioEventLoopGroup(); try { //启动服务端引导 ServerBootstrap bootstrap = new ServerBootstrap(); // 设置线程组 bootstrap.group(workerGroup, workerGroup) //设置服务器端通道的实现类 .channel(NioServerSocketChannel.class) // handler()方法用于给 BossGroup 设置业务处理器 .handler(new MyChannelHandler()) // childHandler()方法用于给 WorkerGroup 设置业务处理器 .childHandler(new ChannelInitializer<SocketChannel>() { protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); //加入自定义的handler处理器 pipeline.addLast(new HelloNettyServerHandler()); } }); //绑定端口号 ChannelFuture future = bootstrap.bind(port).sync(); System.out.println("服务器启动成功,监听端口:" + port); //等待服务端监听端口关闭 future.channel().closeFuture().sync(); } finally { //优雅关闭资源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }}
服务端启动程序主要逻辑是:
-
创建两个NioEventLoopGroup线程组:bossGroup和workerGroup
-
创建服务端启动引导类:ServerBootstrap
-
ServerBootstrap绑定两个线程组
-
设置服务器端通道的实现类
-
设置其他可选参数
-
向pipeline中添加自定义的业务处理器:HelloNettyServerHandler
-
启动并监听8080端口
-
等待服务端监听端口关闭
在开发时,我们更多关注业务处理器:HelloNettyServerHandler
,因为业务内容多数都通过handler来实现。
3.服务端业务处理handler开发
import io.netty.buffer.ByteBuf;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.ChannelInboundHandlerAdapter;import io.netty.util.CharsetUtil;
/**
* 服务端业务处理器
*/
public class HelloNettyServerHandler extends ChannelInboundHandlerAdapter { /**
* 当通道有数据可读时执行
* @param ctx 通道连接上下文对象
* @param msg 接收到的消息
* @throws Exception
*/
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //接收客户端发送的数据(byteBuf),并输出 ByteBuf in = (ByteBuf) msg; System.out.println("服务端接收到一条消息:" + in.toString(CharsetUtil.UTF_8)); //将客户端发送的数据原样回复给客户端 ctx.writeAndFlush(in); } /**
* 发生异常时调用
* @param ctx 通道连接上下文对象
* @param cause 异常
* @throws Exception
*/
@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println("出现异常 : "); cause.printStackTrace(); }}
在该handler中,继承于ChannelInboundHandlerAdapter
,该类是负责处理入站和用户自定义事件处理接口ChannelInboundHandler
的实现类,在该类中实现channelRead
方法,在服务端接收到客户端发送的消息后会回调该方法。
4. 客户端启动程序开发
import io.netty.bootstrap.Bootstrap;import io.netty.channel.ChannelFuture;import io.netty.channel.ChannelInitializer;import io.netty.channel.nio.NioEventLoopGroup;import io.netty.channel.socket.SocketChannel;import io.netty.channel.socket.nio.NioSocketChannel;import java.net.InetSocketAddress;
/**
* Netty客户端
*/
public class HelloNettyClient { private static final String host = "127.0.0.1"; private static final Integer port = 8080; public static void main(String[] args) throws InterruptedException { //设置客户端NioEventLoopGroup线程组 NioEventLoopGroup group = new NioEventLoopGroup(); try { //设置客户端启动引导 Bootstrap bootstrap = new Bootstrap(); //绑定线程组 bootstrap.group(group) //设置客户端的socket通道实现类 .channel(NioSocketChannel.class) //指定连接的服务器IP和端口 .remoteAddress(new InetSocketAddress(host, port)) //handler方法用于设置业务处理器 .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel socketChannel) { //像pipeline添加业务处理器 socketChannel.pipeline().addLast(new HelloNettyClientHandler()); } }); //连接服务器 ChannelFuture sync = bootstrap.connect().sync(); System.out.println("客户端连接服务器成功,服务器地址:" + host + ", 端口号:" + port); //对通道关闭进行监听 sync.channel().closeFuture().sync(); } finally { //优雅关闭资源 group.shutdownGracefully(); } }}
客户端的流程和服务端类似,只是客户端只需要一个NioEventLoopGroup
线程组,启动引导类是Bootstrap
,对应的客户端通道实现类是NioSocketChannel
,然后根据指定的IP和端口连接服务器即可。
同服务端一样,也需要添加客户端业务处理器来完成业务处理。
5. 客户端业务处理handler开发
import io.netty.buffer.ByteBuf;import io.netty.buffer.Unpooled;import io.netty.channel.ChannelHandlerContext;import io.netty.channel.SimpleChannelInboundHandler;import io.netty.util.CharsetUtil;
/**
* 客户端自定义事件处理器
* 需要继承SimpleChannelInboundHandler,
* 用于处理数据流入本端(客户端)的IO事件
*/
public class HelloNettyClientHandler extends SimpleChannelInboundHandler<ByteBuf> { /**
* 通道建立后调用该方法
* @param ctx 通道连接上下文对象
* @throws Exception
*/
@Override public void channelActive(ChannelHandlerContext ctx) throws Exception { //像服务器发送数据,Unpooled是Netty提供的用来操作ByteBuf缓冲区的工具类 ctx.writeAndFlush(Unpooled.copiedBuffer("Hello Netty!", CharsetUtil.UTF_8)); System.out.println("客户端向服务端发送消息成功"); } /**
* 当通道有数据可读时执行
* @param channelHandlerContext 通道连接上下文对象
* @param byteBuf ByteBuf数据缓冲区
* @throws Exception
*/
@Override protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception { System.out.println("客户端接收到消息: " + byteBuf.toString(CharsetUtil.UTF_8)); } /**
* 发生异常时调用
* @param ctx 通道连接上下文对象
* @param cause 异常
* @throws Exception
*/
@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); }}
客户端业务处理器继承于客户端专用的SimpleChannelInboundHandler
,该类继承自ChannelInboundHandlerAdapter
,该类和其父类的主要区别是:
-
需要指定泛型类型,对应传输的数据类型
-
在
channelRead
方法完成后就自动释放指向保存该消息的ByteBuf的内存引用
实现以下三个方法完成业务处理:
-
channelActive
:通道建立后调用该方法,然后将消息"Hello Netty!"发送到服务端 -
channelRead0
:当通道有数据可读时调用该方法 -
exceptionCaught
:发生异常时调用该方法
6. 效果演示
先启动服务端,然后再启动客户端,可以看到服务端输出:
客户端输出:
通过上述代码的编写,就可以完成一个简单的Netty网络通信程序,虽然代码看着很复杂,但是性能却是很强大,而且相比NIO的代码量,Netty已经简化了太多编码工作了。
Netty架构设计
1. Netty整体架构
这是一张Netty官网关于Netty的核心架构图:
可以看到Netty由以下三个核心部分构成:
-
传输服务:传输服务层提供了网络传输能力的定义和实现方法。它支持 Socket、HTTP 隧道、虚拟机管道等传输方式。Netty 对 TCP、UDP 等数据传输做了抽象和封装,用户可以更聚焦在业务逻辑实现上,而不必关系底层数据传输的细节。
-
协议支持:Netty支持多种常见的数据传输协议,包括:HTTP、WebSocket、SSL、zlib/gzip、二进制、文本等,还支持自定义编解码实现的协议。Netty丰富的协议支持降低了开发成本,基于 Netty 我们可以快速开发 HTTP、WebSocket 等服务。
-
Core核心:Netty的核心,提供了底层网络通信的通用抽象和实现,包括:可扩展的事件驱动模型、通用的通信API、支持零拷贝的Buffer缓冲对象。
2. Netty逻辑架构
Netty采用典型的三层网络架构进行设计和开发,其逻辑架构如图:
2.1 网络通信层
网络通信层的职责主要是监听网络的读写和连接操作,负责将网络层的数据读取到内存缓冲区中,然后触发各种网络事件,如连接创建、连接激活、读事件、写事件等,这些网络事件会分发给事件调度层进行处理。
网络通信层的核心组件有:
- Bootstrap
启动引导,主要负责整个Netty程序的启动、初始化、服务器连接等过程,它相当于一条主线,把Netty的其他核心组件给串联起来。
Netty中的引导器分为两种:Bootstrap:客户端启动引导,另一种是ServerBootstrap:服务端启动引导。它们都继承自抽象类 AbstractBootstrap。
- Channel
网络通信的载体,提供了基本的用于I/O操作的API,如:register、bind、connect、read、write、flush等。
Netty的Channel是在JDK的NIO Channel基础上进行封装的,提供了更高层次的抽象,同时屏蔽了底层Socket的复杂性,赋予了Channel更加强大的功能。
- ByteBuf
当我们在进行数据传输的时候,往往需要使用到缓冲区。常用的缓冲区就是JDK NIO类库提供的java.nio.Buffer。
在NIO中,除了boolean类型外,其他JAVA基础数据类型都有自己的缓冲区实现,对于NIO来说,使用比较多的是ByteBuffer。但是ByteBuffer本身有一定的局限性:
-
长度固定,一旦分配完成, 不能扩容和缩容
-
只有一个标识位置的指针position,读写切换需要手动调用flip()方法
-
API封装的功能有限
因此,Netty提供了比BufferBuffer更为丰富强大的缓冲区实现-ByteBuf。
2.2 事件调度层
事件调度层的职责是通过 Reactor 线程模型对各类事件进行聚合处理,通过 Selector 主循环线程集成多种事件( I/O 事件、信号事件、定时事件等),实际的业务处理逻辑是交由服务编排层中相关的 Handler 完成。
事件调度层的核心组件有:
- EventLoopGroup
- EventLoop
EventLoopGroup本质上是一个线程池,主要负责接收I/O请求,并分配线程处理请求。一个EventLoopGroup包含一个或者多个EventLoop,EventLoop用于处理Channel生命周期内的所有I/O事件,如accept、read、write等。
EventLoop同一时间会与一个线程绑定,每个EventLoop负责处理多个Channel。
每新建一个Channel,EventLoopGroup会选择一个EventLoop与其绑定,该Channel生命周期内都可以对EventLoop进行多次绑定和解绑。
2.3 服务编排层
负责组装各类服务,它是Netty的核心业务处理链,用于实现网络事件的动态编排和有序传播。
服务编排层的核心组件有:
-
ChannelPipeline:handler处理器链列表,类似于容器,内部通过双向链表将不同的ChannelHandler串联在一起,当I/O读写事件发生时,ChannelPipeline会依次调用ChannelHandler列表对Channel的数据进行拦截处理。每个Channel创建时就会绑定一个新的ChannelPipeline,他们是一一对应的关系。
-
ChandlerHandler:事件业务处理器顶层接口,它不处理入站和出站事件,而是由其子类来实现,核心子类包括:
-
-
ChannelInboundHandler:负责处理入站事件,以及用户自定义事件
-
ChannelOutboundHandler:负责处理出站事件
-
ChannelDuplexHandler:同时实现了ChannelInboundHandler和ChannelOutboundHandler两个接口,既可以处理入站事件,也可以处理出站事件
-
-
ChannelHandlerContext:ChandlerHandler的上下文对象,在调用ChandlerHandler方法时,都会传入该上下文对象,ChannelHandlerContext 包含了 ChannelHandler 生命周期的所有事件,如 connect、bind、read、flush、write、close 等。
由于Netty的分层架构设计合理、优雅,因此,基于Netty开发的各种服务器程序才会越来越多。
四、Netty何以实现高性能
I/O模型
用什么样的通道将数据发送给对方,BIO、NIO 或者 AIO,I/O 模型在很大程度上决定了框架的性能。
Netty采用基于NIO的多路复用技术,用一个Selector单线程就可以接入成千上万的客户端连接,降低了线程资源消耗,并且可以避免多线程的切换带来的性能开销。
线程模型
Reactor线程模型是对于传统的I/O线程模型的一种优化。
传统的I/O线程模型采用阻塞I/O来获取输入流数据,并且每个连接都需要独立的线程完成数据的输入、业务处理、数据返回等一个完整的操作链路。这种模型在高并发场景下,有两个比较明显的缺点:
-
每个连接都需要创建一个对应线程,线程大量创建占用大量的服务器资源
-
线程没有数据可读情况下的阻塞会对性能造成很大的影响
Reactor线程模型为了解决这两个问题,提供了以下解决方案:
-
基于I/O多路复用:多个客户端连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,通过事件驱动通知应用程序,线程从阻塞状态返回,开始进行业务处理\
-
基于线程池技术减少线程创建:基于线程池,不必再为每一个连接创建线程,将连接完成后的业务处理分配给线程池进行调度。
三种Reactor线程模型
根据Reactor线程和handler处理线程的模型不同,有三种Reactor线程模型:
-
单Reactor单线程模型
-
单Reactor多线程模型
-
主从Reactor线程模型
1. 单Reactor单线程模型
单Reactor单线程模型是指用一个线程通过多路复用来完成所有的I/O操作(accept、read、write等)。
服务端处理流程:
-
Reactor对象通过select监听客户端请求,收到请求后通过Dispatch分发
-
如果是建立连接请求,则由Acceptor通过accept处理连接,然后创建一个handler对象处理完成连接后的各种事件
-
如果不是建立连接请求,则分发调用对应的Handler来完成数据读取、业务处理,并将结果返回给客户端
单Reactor单线程模型的优点在于模型简单,没有多线程、进程间通信、竞争的问题,全部都在一个线程中完成。
缺点是:
-
性能问题:只有一个线程去处理任务,在高并发情况下很容易阻塞
-
可靠性问题:一旦线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
为了解决上面的两个问题,演进了单Reactor多线程模型。
2. 单Reactor多线程模型
该模型和单Reactor单线程模型的最主要区别在于,Reactor主线程只负责监听、接收客户端请求以及派发任务,而比较耗时的I/O操作由另一个worker线程池来进行分配线程去处理。
服务端处理流程:
-
Reactor对象通过select监听客户端请求,收到请求后通过Dispatch分发\
-
如果是建立连接请求,则由Acceptor通过accept处理连接,然后创建一个handler对象处理完成连接后的各种事件
-
如果不是建立连接请求,则分发调用对应的Handler来处理\
-
Handler 只负责响应事件,不做具体的业务处理,通过 read 读取数据后,会分发给后面的 Worker 线程池的某个线程处理业务。
-
Worker线程池会分配独立线程完成真正的业务,并将结果返回给 Handler,Handler 收到响应后,通过 send 将结果返回给 客户端。
在绝大多数场景下,该模型性能表现优异,可以充分发挥多核CPU的处理能力。
但是在并发上百万的场景下,一个NIO线程负责监听和处理所有客户端连接可能存在性能问题。例如,某些场景下,会对客户端的请求进行安全认证等,这类请求非常耗时。在此场景下,单独一个Reactor线程可能存在性能不足的问题,为了解决这个问题,诞生了第三种线程模型:主从Reactor多线程模型。
3. 主从Reactor线程模型
该线程模型与单Reactor多线程模型主要在于Reactor线程分为了主从Reactor线程两部分,即下图中的Main Reactor和Sub Reactor。
服务端处理请求流程:
-
Reactor主线程对象通过seletct监听连接事件,收到事件后,通过Acceptor处理连接事件
-
当Acceptor处理连接事件后,主Reactor线程将连接分配给子Reactor线程
-
Reactor子线程将连接加入连接队列进行监听,并创建handler进行各种事件处理
-
当有新事件发生时,子Reactor线程会调用对应的handler进行处理
-
handler读取数据分发给worker线程池分配一个独立的线程进行业务处理,并返回结果给handler
-
handler收到响应的结果后,通过send将结果返回给客户端
该模型虽然编程复杂度高,但是其优势比较明显,体现在:
-
主从线程职责分明,主线程只需要接收新请求,子线程完成后续的业务处理
-
主从线程数据交互简单,主线程只需要把新连接传给子线程
因此该模型在许多项目中广泛使用,包括Nginx、Memcached、Netty等。
Netty线程模型
Netty的线程模型不是一成不变的,它实际取决于用户的启动参数配置。通过设置不同的启动参数,Netty可以同时支持Reactor单线程模型、多线程模型和主从Reactor多线程模型。
Netty官方推荐的是基于主从Reactor线程模型实现的NioEventLoopGroup线程模型:
NioEventLoopGroup bossGroup = new NioEventLoopGroup(); NioEventLoopGroup workerGroup = new NioEventLoopGroup();
NioEventLoopGroup相当于一个事件循环组,这个组里面有多个事件循环,称为NioEventLoop,类似于线程池和中间的线程的关系。
NioEventLoop表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上面的socket网络通信。
一个NioEventLoopGroup可以有多个NioEventLoop,可以通过构造函数指定,默认是CPU核心数x2。
Netty线程模型抽象出两组NioEventLoopGroup:
-
BossGroup:专门负责接收客户端的请求
-
WorkerGroup:专门负责网络的读写、业务处理
Boss NioEventLoop职责:
-
select:轮询注册在其上的ServerSocketChannel的accept事件(OP_ACCEPT)
-
processSelectedKeys:处理accept事件,与client事件建立连接,生成NioSocketChannel,并将其注册到某个worker NIoEventLoop上的selector上
-
runAllTasks:再循环处理任务队列的任务
Worker NioEventLoop职责:
-
select:轮询注册在其上的NioSocketChannel的read/write事件(OP_READ和OP_WRITE)、
-
processSelectedKeys:在对应的NIoSocketChannel 上进行I/O处理
-
runAllTasks:再循环处理任务队列的任务
每个Worker NioEventLoop处理业务时,会通过pipeline(管道),pipeline中包含了channel,管道中维护了很多的处理器,数据会在管道中的各个处理器间进行流转、处理。
序列化协议
影响序列化性能的关键因素总结如下:
-
序列化后的码流大小(网络带宽的占用);
-
序列化&反序列化的性能(CPU资源占用);
-
是否支持跨语言(异构系统的对接和开发语言切换)。
采用什么样的通信协议,对系统的性能极其重要,netty默认提供了对Google Protobuf的支持,也可以通过扩展Netty的编解码接口,用户可以实现其它的高性能序列化框架。
这是一张不同的序列化框架序列化后的字节数组对比图:
从上图可以看出,Protobuf序列化后的码流只有Java序列化的1/4左右。正是由于Java原生序列化性能表现太差(以及不能跨语言等),才催生出了各种高性能的开源序列化技术和框架。
其他
1. 零拷贝
Netty的零拷贝体现在三个方面:
-
Netty的接收和发送ByteBuf采用DIRECT BUFFERS,使用堆外内存进行socket读写,不需要进行自己缓冲区的二次拷贝。
-
Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。
-
Netty的文件传输采用了transfer()方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write发送导致的内存拷贝问题。
2. 内存池
由于ByteBuf分配的是堆外内存空间,其分配和回收过程不像JAVA实例内存分配和回收那么简单,为了尽量重用缓冲区,Netty提供了基于内存池的缓冲区重用机制。
基于内存池重用的ByteBuf性能比普通的ByteBuf高几十个数量级。
3. 无锁化串行设计
为了尽可能提升性能,Netty采用了串行无锁化设计,在I/O线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。
4. 高效并发编程
包括:
-
volatile关键字的大量、正确使用
-
CAS和原子类的广泛使用
-
线程安全容器的使用
-
通过读写锁提升并发性能
五、文章总结
鉴于篇幅原因, 本文只介绍了Netty的核心架构以及重要组件简单介绍,还有一些高级的知识点还未介绍,如高效序列化、心跳检测、TCP粘包拆包问题解决、多协议支持等。
总结一下文章的核心内容:
JAVA中的三种I/O模式:
-
BIO:传统同步阻塞IO
-
NIO:同步非阻塞IO
-
AIO:异步非阻塞IO
虽然NIO功能很强大,性能很好,但是由于其开发难度大,并且存在BUG,因此业界普通使用封装更完善、功能更强大、扩展性更好的Netty来实现高并发的网络编程。
Netty的整体架构分为:
-
传输服务
-
协议支持
-
Core核心
Netty的逻辑架构分为:
-
网络通信层
-
事件调度层
-
服务编排层
Netty的核心组件有:
-
Bootstrap:启动引导\
-
Channel:网络通信载体
-
ByteBuf:数据缓冲区,读写都是直接操作缓冲区
-
EventLoopGroup和EventLoop:线程模型,实现高并发的核心
-
ChannelPipeline:handler处理器链容器
-
ChandlerHandler:事件业务处理器顶层接口
-
ChannelHandlerContext:ChandlerHandler的上下文对象
Netty实现高性能的原因:
-
非阻塞I/O模型
-
良好的线程模型设计
-
高效序列化协议支持
-
其他:零拷贝、内存池、无锁化减少线程切换开销、高效并发编程等
六、参考资料
-
《Netty权威指南》:比较权威,知识点比较详细,有广度也有深度,但是有些内容不是很好理解
-
《Netty编程实战》:注重根据实战来讲解理论知识
-
《Netty、Redis、Zookeeper高并发实战》:对各种I/O模型中的同步、异步、阻塞、非阻塞等比较让人困惑的名词做了详细的解释
-
B站视频:尚硅谷Netty视频教程:如果没了解过netty,可以跟着视频教程学,不过比较耗时